Registry of background processes and subagents: /procs command, Processes panel, counter in the status line.

Function-hook mods for Claude Code (terminal and desktop). Each one adds a small piece of UI or a command to your session.
| Mod | What it does |
|---|---|
usage-limits | A bar above the prompt: plan limit left (5 hours, week) and time to reset. |
proc-registry | /procs command and a Processes panel: background shells and subagents with ID, port and start time, plus a counter in the status line. |
context-weather | A bar above the prompt: context fill, session cost and plan limits, with a warning before compaction. |
claude plugin marketplace add Chronosauros/claude-mods
claude plugin install usage-limits@chronosaur-mods
claude plugin install proc-registry@chronosaur-mods
claude plugin install context-weather@chronosaur-mods
English by default. For Polish, set the language option per mod (/config, or in settings.json):
{
"pluginConfigs": {
"usage-limits@chronosaur-mods": { "options": { "language": "pl" } },
"proc-registry@chronosaur-mods": { "options": { "language": "pl" } },
"context-weather@chronosaur-mods": { "options": { "language": "pl" } }
}
}
MIT
hooks/register.tsx 236 lines1import type { EngineInterface, Register } from 'claude-code'
2
3import type { ProcRow } from '../types'
4
5const PANE = 'procs'
6const rows = { plugin: 'proc-registry', key: 'rows' } as const
7
8const TEXT = {
9 en: {
10 background: 'in background',
11 commandDescription: 'Background processes and subagents of this session (ID, port, start time)',
12 paneTitle: 'Processes',
13 nothing: 'Nothing running in the background.',
14 toClose: 'To close',
15 recent: 'Finished (recent)',
16 clear: 'Clear finished',
17 statuses: {
18 running: 'running',
19 done: 'done',
20 background: 'background',
21 stopped: 'stopped',
22 finished: 'finished',
23 } as Record<string, string>,
24 },
25 pl: {
26 background: 'w tle',
27 commandDescription: 'Procesy w tle i subagenci tej sesji (ID, port, czas startu)',
28 paneTitle: 'Procesy',
29 nothing: 'Nic nie działa w tle.',
30 toClose: 'Do zamknięcia',
31 recent: 'Zakończone (ostatnie)',
32 clear: 'Wyczyść zakończone',
33 statuses: {
34 running: 'działa',
35 done: 'koniec',
36 background: 'w tle',
37 stopped: 'zatrzymany',
38 finished: 'zakończony',
39 } as Record<string, string>,
40 },
41} as const
42
43type Text = (typeof TEXT)[keyof typeof TEXT]
44
45const getRows = async ($: EngineInterface): Promise<ProcRow[]> =>
46 ((await $.state.get(rows)).value as ProcRow[] | undefined) ?? []
47
48const setRows = async ($: EngineInterface, change: (list: ProcRow[]) => ProcRow[]) => {
49 await $.state.set(rows, change(await getRows($)))
50}
51
52const PORT = /(?:--port[ =]|-p[ =]|PORT=|localhost:|127\.0\.0\.1:|0\.0\.0\.0:)(\d{2,5})\b/
53const ENDED = /completed|failed|killed|stopped|exited/i
54const DONE = new Set(['completed', 'failed', 'killed', 'stopped', 'exited', 'cancelled'])
55
56const portOf = (command: string) => {
57 const hit = PORT.exec(command)
58
59 return hit === null ? null : Number(hit[1])
60}
61
62const clip = (text: string, max: number) =>
63 text.length > max ? `${text.slice(0, max - 1)}…` : text
64
65const age = (ms: number) => {
66 const min = Math.max(0, Math.round(ms / 60000))
67
68 return min < 60 ? `${min} min` : `${Math.floor(min / 60)} h ${min % 60} min`
69}
70
71const upsert = (list: ProcRow[], row: ProcRow) =>
72 list.some(one => one.id === row.id)
73 ? list.map(one => (one.id === row.id ? { ...one, ...row, startedAt: one.startedAt } : one))
74 : [...list, row].slice(-100)
75
76let last = ''
77
78async function refresh($: EngineInterface, T: Text) {
79 const agents = await $.agent.list()
80 const now = await $.clock.now()
81 await setRows($, (list: ProcRow[]) => {
82 let out = list
83 for (const one of agents) {
84 const prev = out.find(row => row.id === one.id)
85 out = upsert(out, {
86 id: one.id,
87 kind: 'agent',
88 label: clip(`${one.type}: ${one.description}`, 70),
89 port: null,
90 startedAt: prev?.startedAt ?? now,
91 status: one.status,
92 isOpen: !DONE.has(one.status),
93 })
94 }
95
96 return out
97 })
98
99 const all = await getRows($)
100 const open = all.filter((one: ProcRow) => one.isOpen).length
101 const key = JSON.stringify(all.map((one: ProcRow) => [one.id, one.status]))
102 if (key !== last) {
103 last = key
104 $.ui.status(open > 0 ? `${T.background}: ${open} (/procs)` : undefined)
105 }
106}
107
108async function track($: EngineInterface, T: Text, e: any, next: any) {
109 const now = await $.clock.now()
110 const command: string = e.command ?? ''
111 const id: string = e.tool_use_id ?? `${e.tool}-${now}`
112 const base = { kind: 'shell' as const, label: clip(command.replace(/\s+/g, ' '), 70), port: portOf(command) }
113 await setRows($, (list: ProcRow[]) =>
114 upsert(list, { ...base, id, startedAt: now, status: 'running', isOpen: true }),
115 )
116 const ran = await next(e)
117 const taskId: string | undefined = ran.result?.backgroundTaskId
118 await setRows($, (list: ProcRow[]) =>
119 taskId === undefined
120 ? upsert(list, { ...base, id, startedAt: now, status: 'done', isOpen: false })
121 : upsert(
122 list.filter(one => one.id !== id),
123 { ...base, id: taskId, startedAt: now, status: 'background', isOpen: true },
124 ),
125 )
126 await refresh($, T)
127
128 return ran
129}
130
131export const register: Register = (on, options) => {
132 const T = TEXT[options.language === 'pl' ? 'pl' : 'en']
133 const shown = (status: string) => T.statuses[status] ?? status
134
135 on('session.start', async ($, e, next) => {
136 await $.command.register({
137 name: 'procs',
138 description: T.commandDescription,
139 })
140 $.clock.every(5000, () => refresh($, T))
141
142 return next(e)
143 })
144
145 on('command.run', { command: 'procs' }, async $ => {
146 await refresh($, T)
147 const now = await $.clock.now()
148 const open = (await getRows($)).filter((one: ProcRow) => one.isOpen)
149 await $.ui.open({ id: PANE, title: T.paneTitle })
150
151 return {
152 text:
153 open.length === 0
154 ? T.nothing
155 : `${T.toClose} (${open.length}):\n${open
156 .map(
157 (one: ProcRow) =>
158 `- ${one.kind} ${one.id}${one.port === null ? '' : ` :${one.port}`} - ${one.label} (${age(now - one.startedAt)})`,
159 )
160 .join('\n')}`,
161 }
162 })
163
164 on('tool.call', { tool: 'Bash' }, ($, e, next) => track($, T, e, next))
165 on('tool.call', { tool: 'PowerShell' }, ($, e, next) => track($, T, e, next))
166
167 on('tool.call', { tool: 'TaskStop' }, async ($, e, next) => {
168 const ran = await next(e)
169 const id: string | undefined = (e as any).task_id ?? (e as any).shell_id
170 if (id !== undefined) {
171 await setRows($, (list: ProcRow[]) =>
172 list.map(one => (one.id === id ? { ...one, status: 'stopped', isOpen: false } : one)),
173 )
174 await refresh($, T)
175 }
176
177 return ran
178 })
179
180 // Background task completions arrive as deliveries that name the task id;
181 // this is a text heuristic, so the "background" status may linger if the
182 // format changes.
183 on('session.receive', async ($, e, next) => {
184 const text = e.text
185 if (ENDED.test(text)) {
186 const open = (await getRows($)).filter((one: ProcRow) => one.isOpen && text.includes(one.id))
187 if (open.length > 0) {
188 await setRows($, (list: ProcRow[]) =>
189 list.map(one =>
190 open.some((o: ProcRow) => o.id === one.id)
191 ? { ...one, status: 'finished', isOpen: false }
192 : one,
193 ),
194 )
195 await refresh($, T)
196 }
197 }
198
199 return next(e)
200 })
201
202 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
203 const { Box, Button, Text } = $.ui.resolve(e)
204 const list = await getRows($)
205 const now = await $.clock.now()
206 const open = list.filter((one: ProcRow) => one.isOpen)
207 const closed = list.filter((one: ProcRow) => !one.isOpen).slice(-10)
208 const room = Math.max(3, (e.viewport?.rows ?? 24) - 8)
209
210 return (
211 <Box flexDirection="column">
212 <Text bold>{T.toClose}: {open.length}</Text>
213 {open.length === 0 && <Text dimColor>{T.nothing}</Text>}
214 {open.slice(-room).map((one: ProcRow) => (
215 <Text color="yellow">
216 {one.kind} {one.id}
217 {one.port === null ? '' : ` :${one.port}`} · {shown(one.status)} · {age(now - one.startedAt)} ·{' '}
218 {one.label}
219 </Text>
220 ))}
221 {closed.length > 0 && <Text bold>{T.recent}</Text>}
222 {closed.map((one: ProcRow) => (
223 <Text dimColor>
224 {one.kind} {one.id} · {shown(one.status)} · {one.label}
225 </Text>
226 ))}
227 <Button
228 key="clear"
229 label={T.clear}
230 onPress={() => setRows($, (all: ProcRow[]) => all.filter(one => one.isOpen))}
231 />
232 </Box>
233 )
234 })
235}
236types/index.d.ts 18 lines1export type ProcKind = 'shell' | 'agent'
2
3export type ProcRow = {
4 id: string
5 kind: ProcKind
6 label: string
7 port: number | null
8 startedAt: number
9 status: string
10 isOpen: boolean
11}
12
13declare module 'claude-code' {
14 interface PluginState {
15 'proc-registry': { rows: ProcRow[] }
16 }
17}
18