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

Mods for Claude Code.
| Mod | What it does |
|---|---|
| cache-bar | Status-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. |
| filetree | Fork 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-desk | Task list above the prompt that follows Claude's to-do list, plus a side pane with one live card per subagent. |
| helpin-per | Pane listing open Helpin PER-* tasks; pick one to have Claude start working on it. |
| cache-clock | Countdown above the prompt until the chat's cache goes cold, with a Keep warm button. |
| handoff | When 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). |
| reflect | When you correct Claude, offers to save the fix as a lasting rule in CLAUDE.md or a rules folder. |
| chat-inventory | Side 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-optimizer | Before 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-refiner | A 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 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.
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 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:
settings.json to settings.json.bak-cache-bar;statusLine.command (claude-hud's, for example) into inner in ~/.claude/plugins/cache-bar/config.json;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.
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-barhooks/register.tsx 196 lines1import { 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}
196types/index.d.ts 20 lines1export 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