SLOPSHOPPER

helpin-per

Pane listing open Helpin PER-* tasks; pick one to have Claude start working on it

newpanecommandtoast
v0.1.0no licenseupdated 2026-10-24jz-wilson/claude-mods/helpin-per
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · helpin-per
│ ┃ Helpin PER ✕ › fix the failing auth test and add an audit log call │ ┃ PER 0 open · never r: load │ ┃ ⏺ Read(src/auth.ts) │ ┃ Press load (r) to fetch tasks. ⎿ Read 6 lines │ ⏺ Update(src/auth.ts) │ ⎿ Added 2 lines, removed 1 line │ ⏺ Bash(bun test) │ ⎿ 3 pass, 1 fail │ │ ● Done. refresh now rejects expired claims and logs an audit event. │ │ ✻ Worked for 42s · done 4:20 PM │ │ › /per │ ⎿ helpin-per: Helpin PER pane opened. │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · Helpin PER
PER 0 open · never r: load Press load (r) to fetch tasks.
README

claude-mods

Mods for Claude Code.

ModWhat it does
cache-barStatus-line bar under your existing status line showing how long the prompt cache stays warm, with a handoff countdown: cache ▰▰▰▱▱▱▱▱ 23m 182k · handoff in 18m. Turns yellow at a quarter of the TTL, red at the handoff threshold, and follows the drop to a 5-minute TTL. /cache-bar:setup installs it.
filetreeFork of data-goblin/claude-code-filetree that shows only the files Claude touched this session; the header's ◆ button toggles to the whole tree.
helper-deskTask list above the prompt that follows Claude's to-do list, plus a side pane with one live card per subagent.
helpin-perPane listing open Helpin PER-* tasks; pick one to have Claude start working on it.
cache-clockCountdown above the prompt until the chat's cache goes cold, with a Keep warm button.
handoffWhen a chat gets very long, writes a handoff prompt and starts a fresh chat with it (/refresh <focus>; the handoff tells the new chat to run /pickup).
reflectWhen you correct Claude, offers to save the fix as a lasting rule in CLAUDE.md or a rules folder.
chat-inventorySide pane, In this chat, listing the to-do list, connectors and calls per connector, files and skills this chat used.
screen-share/screen-share hides names, emails, phone numbers and money amounts on screen while you record.
replay-theater/replay steps through the file edits Claude made in the last turn, one diff at a time.
mods/mods opens a pane to turn each mod on or off, including mods installed from other marketplaces, and to install mods a marketplace offers that are not installed yet.
effort-optimizerBefore Claude starts on a prompt, a quick Sonnet check picks the effort level it needs (Low to Max) and offers a one-click switch, or sets it by itself with /effort-optimizer auto.
prompt-refinerA Refine button in the hint line under the prompt, any time in the chat. Press it and pick a rewrite style for the prompt in the box, like a phone keyboard's suggestions: Clearer, Shorter, Elaborate (fuller, same shape), Structured (the job, the why, the guardrails, done means), Ask me first (up to 3 questions on gaps), or My guidance. The rewrite shows above the prompt with what changed; Use it fills the box, Refine runs another pass. /refine <prompt> does the same.

The eight mods other than replay-theater, from helper-desk to effort-optimizer, come from Ben AI's benai-mods (benai.co) and are redistributed with permission, with Windows fixes added here. replay-theater (one Ben AI change: /mods switch support) is Anthropic's sample mod from anthropics/claude-code-playground, under the Apache 2.0 license in its LICENSE.

Reflect setting

Reflect saves a rule either to ~/.claude/CLAUDE.md (Save everywhere) or as its own note in a folder you choose (Save to vault). Set that folder as rules_folder when you install reflect, or later for reflect in /config. If it is empty, reflect asks for the folder the first time you press Save to vault.

Install

At the prompt of a terminal session:

/plugin install helper-desk --marketplace jz-wilson/claude-mods

Answer y to add the marketplace, then pick a scope.

From a shell:

claude plugin install helper-desk --marketplace jz-wilson/claude-mods
claude plugin install cache-bar --marketplace jz-wilson/claude-mods

In the desktop app, where /plugin does not run: click + next to the prompt box, then Plugins, Add marketplace with jz-wilson/claude-mods, then Add plugin.

cache-bar and claude-hud

cache-bar is a status-line script, so it takes over statusLine and runs the previous status line inside it. After installing, run /cache-bar:setup. It:

  • backs up settings.json to settings.json.bak-cache-bar;
  • moves the current statusLine.command (claude-hud's, for example) into inner in ~/.claude/plugins/cache-bar/config.json;
  • points statusLine.command at the newest installed cache-bar version, and sets refreshInterval: 60 so the countdown repaints while you are idle.

The previous status line prints first, unchanged, and the bar goes on its own line underneath. If the inner command fails, the bar still prints. Before the first request of a session there is no cache yet, so the bar is hidden; after expiry it shows a dim cold.

The same config file sets the handoff threshold and the context floor:

{ "inner": "…", "handoffMinutes": 5, "floorTokens": 80000 }

The threshold is capped at a fifth of the TTL, so it fits the 5-minute TTL too. Under floorTokens of context the bar says no handoff (<floor).

To undo, copy settings.json.bak-cache-bar back over settings.json, or set statusLine.command back to the inner value.

Development

Each mod is a folder with .claude-plugin/plugin.json. CI runs claude plugin validate on every mod for each push and pull request, then claude plugin test for mods with a hooks module and bun test for the rest. A merge to main that bumps a mod's version publishes a GitHub release tagged <mod>-v<version>.

claude plugin validate agent-tree
claude plugin test agent-tree
bun test cache-bar
Source 2 files
hooks/register.tsx 196 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { PerList, PerTask } from '../types'
5
6const PANE = 'helpin-per'
7const TITLE = 'Helpin PER'
8const SERVER = 'helpin'
9const PREFIX = 'PER-'
10const PRIORITY_RANK: Record<string, number> = { urgent: 0, high: 1, medium: 2, low: 3, none: 4 }
11
12const ACCENT = '#06b6d4'
13const HEALTHY = '#22c55e'
14const DANGER = '#ef4444'
15const PRIORITY_COLOR: Record<string, string | undefined> = {
16  urgent: DANGER,
17  high: '#f59e0b',
18  medium: ACCENT,
19}
20
21function clockTime(ms: number): string {
22  const d = new Date(ms)
23  return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
24}
25
26const list = atom({ plugin: 'helpin-per', key: 'list' } as const, { tasks: [] } as PerList, {
27  shape: 'list-1',
28})
29
30type RawTask = {
31  task_key?: string
32  task_id: string
33  name: string
34  state_name?: string
35  priority?: string
36}
37
38let inflight: Promise<boolean> | undefined
39
40// Tasks load only when the person presses the pane's load button, never on a
41// timer, a turn or session start: a plugin's MCP call fires PreToolUse and
42// PostToolUse hooks, and Orca reads those as Claude working until the next Stop.
43function refresh($: EngineInterface): Promise<boolean> {
44  inflight ??= fetchTasks($)
45    .catch(error => {
46      $.ui.toast(`Helpin PER refresh failed: ${error instanceof Error ? error.message : String(error)}`)
47      return false
48    })
49    .finally(() => {
50      inflight = undefined
51    })
52  return inflight
53}
54
55async function fetchTasks($: EngineInterface): Promise<boolean> {
56  const tasks: PerTask[] = []
57  try {
58    let offset: number | null = 0
59    while (offset !== null) {
60      const result = await $.mcp.call(SERVER, 'list_tasks', {
61        open_only: true,
62        detail_level: 'summary',
63        limit: 100,
64        offset,
65      })
66      const text = result.content.find(block => block.type === 'text')?.text ?? ''
67      if (result.isError) throw new Error(text || 'list_tasks failed')
68      const data = JSON.parse(text).data as {
69        tasks: RawTask[]
70        has_more?: boolean
71        next_offset?: number | null
72      }
73      for (const raw of data.tasks) {
74        if (!raw.task_key?.startsWith(PREFIX)) continue
75        tasks.push({
76          key: raw.task_key,
77          id: raw.task_id,
78          name: raw.name,
79          state: raw.state_name ?? '',
80          priority: raw.priority ?? 'none',
81        })
82      }
83      const next = data.next_offset
84      offset = data.has_more && typeof next === 'number' && next > offset ? next : null
85    }
86  } catch (error) {
87    const message = error instanceof Error ? error.message : String(error)
88    await update($, list, old => ({ ...old, error: message }))
89    return false
90  }
91  tasks.sort(
92    (a, b) =>
93      (PRIORITY_RANK[a.priority] ?? 9) - (PRIORITY_RANK[b.priority] ?? 9) ||
94      Number(b.key.slice(PREFIX.length)) - Number(a.key.slice(PREFIX.length)),
95  )
96  await update($, list, () => ({ tasks, fetchedAt: Date.now() }))
97  return true
98}
99
100async function startWork($: EngineInterface, task: PerTask): Promise<void> {
101  const result = await $.prompt.submit({
102    asUser: true,
103    text:
104      `Start working on Helpin task ${task.key} "${task.name}" (task_id ${task.id}). ` +
105      `Read it in full with mcp__helpin__get_task_context, move it to In Progress, ` +
106      `then do the work and comment on the task with what you did.`,
107  })
108  $.ui.toast(
109    'drop' in result ? `${task.key} not queued: ${result.drop}` : `Queued ${task.key} for Claude`,
110  )
111}
112
113export const register: Register = on => {
114  on('session.start', async ($, e, next) => {
115    await $.command.register({
116      name: 'per',
117      description: 'Show open Helpin PER-* tasks in a pane and pick one to work on',
118    })
119    try {
120      const opened = await $.ui.open({ id: PANE, title: TITLE })
121      if (!opened.isPlaced) $.ui.log(`Helpin PER pane waiting: ${opened.reason}`)
122    } catch (error) {
123      $.ui.log(`Helpin PER pane failed to open: ${error instanceof Error ? error.message : String(error)}`)
124    }
125
126    return next(e)
127  })
128
129  on('command.run', { command: 'per' }, async $ => {
130    await $.ui.open({ id: PANE, title: TITLE })
131
132    return { text: 'Helpin PER pane opened.' }
133  })
134
135  // The pane scrolls its own body (arrows while focused), so every task is drawn.
136  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
137    const { Box, Text, Button } = $.ui.resolve(e)
138    const { tasks, error, fetchedAt } = await read($, list)
139    const at = fetchedAt ? clockTime(fetchedAt) : 'never'
140
141    return (
142      <Box flexDirection="column" gap={1}>
143        <Box flexDirection="row" justifyContent="space-between">
144          <Text wrap="truncate-end">
145            <Text color={ACCENT} weight="bold">
146              PER
147            </Text>
148            <Text dimColor>
149              {' '}
150              {tasks.length} open · {at}
151            </Text>
152          </Text>
153          <Button key="refresh" plain hotkey="r" onPress={() => refresh($)}>
154            {fetchedAt ? 'refresh' : 'load'}
155          </Button>
156        </Box>
157        {error && (
158          <Text color={DANGER} wrap="truncate-end">
159            Helpin: {error}
160          </Text>
161        )}
162        {tasks.length === 0 && !error && (
163          <Text dimColor>
164            {fetchedAt ? 'No open PER tasks.' : 'Press load (r) to fetch tasks.'}
165          </Text>
166        )}
167        {tasks.length > 0 && (
168          <Box flexDirection="column">
169            {tasks.map(task => (
170              <Button key={task.key} plain onPress={() => startWork($, task)}>
171                <Text color={task.state === 'In Progress' ? HEALTHY : 'gray'}>
172                  {task.state === 'In Progress' ? '● ' : '○ '}
173                </Text>
174                <Text color={PRIORITY_COLOR[task.priority]} weight="bold">
175                  {task.key}
176                </Text>{' '}
177                {task.name}{' '}
178                <Text dimColor>
179                  {task.state}
180                  {task.priority !== 'none' ? ` · ${task.priority}` : ''}
181                </Text>
182              </Button>
183            ))}
184          </Box>
185        )}
186        {!e.props.isFocused && tasks.length > 0 && (
187          <Text dimColor wrap="truncate-end">
188            {e.surface === 'terminal' ? 'ctrl+x tab or click to focus · ' : ''}press a task to
189            start work
190          </Text>
191        )}
192      </Box>
193    )
194  })
195}
196
types/index.d.ts 20 lines
1export type PerTask = {
2  key: string
3  id: string
4  name: string
5  state: string
6  priority: string
7}
8
9export type PerList = {
10  tasks: PerTask[]
11  error?: string
12  fetchedAt?: number
13}
14
15declare module 'claude-code' {
16  interface PluginState {
17    'helpin-per': { list: Shaped<PerList> }
18  }
19}
20