Toggle a summary pane (ctrl+x s) of the running skills and each subagent/teammate's status, name, model and elapsed time

개인 환경 백업 및 복구 자동화 스크립트~ :smile:
# 설정 및 프로그램 목록등 백업
bash ./backupmysetting.sh
# 전체 설치
bash ./installall.sh
# 필요한 환경/프로그램은 installxxx.sh 사용
#!/usr/bin/env bash shebang 을 추가하거나, 아래처럼 전역 sh 링크를 bash로 변경한다.sudo unlink /bin/sh
sudo ln -s /bin/bash /bin/shhooks/register.tsx 641 lines1import type {
2 AgentStatus,
3 EngineInterface,
4 PluginState,
5 Register,
6 TurnCompleteReason,
7} from 'claude-code'
8import { atom, read, update } from 'claude-code'
9
10import type { AgentRow, Alert, WorkItem } from '../types'
11
12type SkillRun = PluginState['skill-badge']['runs'][number]
13
14const agents = atom({ plugin: 'agent-status', key: 'agents' } as const, [])
15const now = atom({ plugin: 'agent-status', key: 'now' } as const, 0)
16const recent = atom({ plugin: 'agent-status', key: 'recent' } as const, [])
17const files = atom({ plugin: 'agent-status', key: 'files' } as const, [])
18const dirty = atom({ plugin: 'agent-status', key: 'dirty' } as const, 0)
19const alerts = atom({ plugin: 'agent-status', key: 'alerts' } as const, [])
20const skillRuns = atom({ plugin: 'skill-badge', key: 'runs' } as const, [])
21const mainModel = atom({ plugin: 'skill-badge', key: 'model' } as const, '')
22
23const PANE = 'summary'
24const TOGGLE_ACTION = 'app:toggleDiffPreSession'
25const TOGGLE_KEY = 'ctrl+x s'
26const RECENT_MAX = 5
27const FILES_MAX = 20
28const FILES_SHOWN = 6
29const ALERTS_MAX = 4
30const EDIT_TOOLS = new Set(['Edit', 'MultiEdit', 'Write', 'NotebookEdit'])
31const SUMMARY =
32 'You write one-line entries for a log of finished work. Given what the user asked and the ' +
33 "coding assistant's final reply, say what the assistant did in at most 40 characters, in the " +
34 'language of the reply, as a terse phrase. Describe the action taken, not the content it ' +
35 'reported. No quotes, no markdown, no trailing period.'
36
37const C = {
38 text: '#cdd6f4',
39 subtext: '#a6adc8',
40 overlay: '#7f849c',
41 surface2: '#585b70',
42 surface1: '#45475a',
43 surface0: '#313244',
44 mauve: '#cba6f7',
45 pink: '#f5c2e7',
46 lavender: '#b4befe',
47 blue: '#89b4fa',
48 sky: '#89dceb',
49 teal: '#94e2d5',
50 green: '#a6e3a1',
51 yellow: '#f9e2af',
52 peach: '#fab387',
53 red: '#f38ba8',
54}
55
56const SPINNER = '⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'
57const PULSE = '◇◈◆◈'
58const GLOW = [C.mauve, C.pink, C.lavender, C.blue, C.sky, C.blue, C.lavender, C.pink]
59
60const ICON: Record<AgentRow['status'], string> = {
61 running: '●',
62 idle: '○',
63 done: '✓',
64 failed: '✗',
65 killed: '■',
66}
67const COLOR: Record<AgentRow['status'], string> = {
68 running: C.yellow,
69 idle: C.overlay,
70 done: C.green,
71 failed: C.red,
72 killed: C.red,
73}
74const GIT_COLOR: Record<string, string> = {
75 M: C.yellow,
76 A: C.green,
77 D: C.red,
78 R: C.blue,
79 '??': C.teal,
80}
81const RUN: Record<NonNullable<SkillRun['result']>, { icon: string; color: string }> = {
82 done: { icon: '✓', color: C.green },
83 failed: { icon: '✗', color: C.red },
84 aborted: { icon: '■', color: C.overlay },
85}
86
87const STATUS: Record<AgentStatus, AgentRow['status']> = {
88 pending: 'running',
89 running: 'running',
90 waiting: 'running',
91 idle: 'idle',
92 completed: 'done',
93 failed: 'failed',
94 killed: 'killed',
95}
96const TURN_END: Record<TurnCompleteReason, AgentRow['status']> = {
97 answer: 'done',
98 aborted: 'killed',
99 refusal: 'failed',
100 error: 'failed',
101}
102
103const ORDER: AgentRow['status'][] = ['running', 'idle', 'done', 'failed', 'killed']
104
105const modelColor = (model: string) =>
106 model.startsWith('opus') ? C.peach : model.startsWith('haiku') ? C.teal : C.blue
107
108const LABEL_MAX = 20
109
110const elapsed = (ms: number) => {
111 const s = Math.max(0, Math.round(ms / 1000))
112 return s < 60 ? `${s}s` : `${Math.floor(s / 60)}m${String(s % 60).padStart(2, '0')}s`
113}
114
115const clip = (s: string) => (s.length > LABEL_MAX ? `${s.slice(0, LABEL_MAX - 1)}…` : s)
116
117const isLive = (a: AgentRow) => a.status === 'running' || a.status === 'idle'
118
119const end = (a: AgentRow, t: number) => (a.status === 'running' ? t : (a.endedAt ?? t))
120
121const sync = (a: AgentRow, live: AgentRow['status'] | undefined, t: number): AgentRow => {
122 if (!isLive(a) || (live === undefined && !a.listed) || (live === a.status && a.listed)) {
123 return a
124 }
125 const status = live ?? 'done'
126 return { ...a, listed: true, status, ...(status === 'running' ? {} : { endedAt: end(a, t) }) }
127}
128
129const patch = async ($: EngineInterface, id: string, fn: (a: AgentRow) => AgentRow | undefined) => {
130 if ((await read($, agents)).some(a => a.id === id && fn(a) !== undefined)) {
131 await update($, agents, list => list.map(a => (a.id === id ? (fn(a) ?? a) : a)))
132 }
133}
134
135const cut = (s: string, n: number) => (s.length > n ? `${s.slice(0, n - 1)}…` : s)
136
137const firstLine = (answer: string) =>
138 cut(
139 answer
140 .split('\n')
141 .map(l =>
142 l
143 .replace(/^[#>*\-\s]+/, '')
144 .replace(/[*`]/g, '')
145 .trim(),
146 )
147 .find(l => l !== '') ?? 'done',
148 60,
149 )
150
151const ago = (ms: number) => {
152 const s = Math.max(0, Math.round(ms / 1000))
153 return s < 60 ? `${s}s` : s < 3600 ? `${Math.floor(s / 60)}m` : `${Math.floor(s / 3600)}h`
154}
155
156const summarize = async ($: EngineInterface, ask: string, answer: string) => {
157 try {
158 const done = await $.model.complete({
159 model: 'haiku',
160 system: SUMMARY,
161 prompt: `User asked:\n${ask}\n\nAssistant replied:\n${answer}`,
162 maxTokens: 60,
163 timeoutMs: 20_000,
164 })
165 const line = done.isAnswered ? done.text.split('\n')[0]?.trim() : undefined
166 return line ? cut(line, 60) : firstLine(answer)
167 } catch {
168 return firstLine(answer)
169 }
170}
171
172const kilo = (n: number) => (n < 1000 ? `${n}` : `${Math.round(n / 1000)}k`)
173
174const tail = (s: string, n: number) => (s.length > n ? `…${s.slice(s.length - n + 1)}` : s)
175
176const pathOf = (e: object) => {
177 const { file_path, notebook_path } = e as { file_path?: unknown; notebook_path?: unknown }
178 const path = file_path ?? notebook_path
179 return typeof path === 'string' ? path : undefined
180}
181
182const refreshGit = async ($: EngineInterface, root: string) => {
183 const run = await $.process
184 .run(['git', '-C', root, 'status', '--porcelain=v1', '--untracked-files=all'], {
185 timeoutMs: 5_000,
186 })
187 .catch(() => undefined)
188 if (run === undefined || run.exitCode !== 0) {
189 return
190 }
191 const status = new Map(
192 run.stdout
193 .split('\n')
194 .filter(l => l.length > 3)
195 .map(l => [l.slice(3).split(' -> ').at(-1) ?? '', l.slice(0, 2).trim()] as const),
196 )
197 if ((await read($, dirty)) !== status.size) {
198 await update($, dirty, () => status.size)
199 }
200 const list = await read($, files)
201 const next = list.map(({ status: _, ...f }) => {
202 const s = f.path.startsWith(`${root}/`) ? status.get(f.path.slice(root.length + 1)) : undefined
203 return s === undefined ? f : { ...f, status: s }
204 })
205 if (JSON.stringify(next) !== JSON.stringify(list)) {
206 await update($, files, () => next)
207 }
208}
209
210const record = async (
211 $: EngineInterface,
212 tool: string,
213 agentId: string | undefined,
214 path: string | undefined,
215 ran: { deny?: string; isError?: true; text?: string },
216) => {
217 const failed = ran.deny !== undefined || ran.isError === true
218 if (!failed && EDIT_TOOLS.has(tool) && path !== undefined) {
219 const stat = await $.fs.stat(path, { resolve: true }).catch(() => undefined)
220 const real = stat?.realPath ?? path
221 await update($, files, list => {
222 const hit = list.find(f => f.path === real)
223 return [
224 { ...hit, path: real, edits: (hit?.edits ?? 0) + 1 },
225 ...list.filter(f => f.path !== real),
226 ].slice(0, FILES_MAX)
227 })
228 }
229 if (failed && tool !== 'SubagentHandback') {
230 const agent =
231 agentId === undefined ? undefined : (await read($, agents)).find(a => a.id === agentId)?.label
232 const alert: Alert = {
233 at: await $.clock.now(),
234 tool,
235 reason: cut(firstLine((ran.deny ?? ran.text ?? '').trim() || 'error'), 120),
236 ...(agent === undefined ? {} : { agent }),
237 }
238 await update($, alerts, list => [alert, ...list].slice(0, ALERTS_MAX))
239 }
240}
241
242const isPaneOpen = async ($: EngineInterface) => (await $.ui.panes()).some(p => p.id === PANE)
243
244const toggle = async ($: EngineInterface) => {
245 if (await isPaneOpen($)) {
246 await $.ui.close({ id: PANE })
247 } else {
248 await $.ui.open({ id: PANE, title: 'summary', columns: 60 })
249 }
250}
251
252const isBusy = (list: AgentRow[], runs: SkillRun[], work: WorkItem[]) =>
253 list.some(a => a.status === 'running') ||
254 (runs.length > 0 && runs.at(-1)?.result === undefined) ||
255 work.some(w => w.text === undefined)
256
257export const register: Register = on => {
258 let worked = false
259 let ask = ''
260 let draining = false
261 let tick = 0
262 let root: string | undefined
263
264 on('session.start', async ($, e, next) => {
265 const top = await $.process
266 .run(['git', 'rev-parse', '--show-toplevel'], { timeoutMs: 5_000 })
267 .catch(() => undefined)
268 root = top?.exitCode === 0 ? top.stdout.trim() : undefined
269 $.clock.every(100, async () => {
270 tick++
271 const busy = isBusy(await read($, agents), await read($, skillRuns), await read($, recent))
272 if ((busy || tick % 100 === 0) && (await isPaneOpen($))) {
273 $.ui.invalidate('ui.render')
274 }
275 })
276 $.clock.every(1500, async () => {
277 const item = (await read($, recent)).find(w => w.text === undefined)
278 if (draining || item === undefined) {
279 return
280 }
281 draining = true
282 try {
283 const text = await summarize($, item.ask ?? '', item.answer ?? '')
284 await update($, recent, list =>
285 list.map(({ ask: _ask, answer: _answer, ...w }) =>
286 w.id === item.id ? { ...w, text } : w,
287 ),
288 )
289 } finally {
290 draining = false
291 }
292 })
293 $.clock.every(1000, async () => {
294 const rows = await read($, agents)
295 if (!rows.some(isLive)) {
296 return
297 }
298 const t = await $.clock.now()
299 const live = new Map((await $.agent.list()).map(a => [a.id, STATUS[a.status]]))
300 if (rows.some(a => sync(a, live.get(a.id), t) !== a)) {
301 await update($, agents, list => list.map(a => sync(a, live.get(a.id), t)))
302 }
303 if ((await read($, agents)).some(a => a.status === 'running')) {
304 await update($, now, () => t)
305 }
306 })
307
308 return next(e)
309 })
310
311 on('agent.spawn', async ($, e, next) => {
312 const ran = await next(e)
313 if (ran.agentId === undefined) {
314 return ran
315 }
316 const row: AgentRow = {
317 id: ran.agentId,
318 label: clip(e.name ?? e.subagentType),
319 model: ran.model.replace(/^.*claude-/, ''),
320 description: e.description,
321 startedAt: await $.clock.now(),
322 status: 'running',
323 ...(e.name === undefined ? {} : { name: e.name }),
324 }
325 await update($, agents, list => [...list.filter(a => a.id !== row.id), row])
326 await update($, now, () => row.startedAt)
327
328 return ran
329 })
330
331 on('turn.step', async function* ($, e, next) {
332 const id = e.agentId
333 if (id !== undefined && !(await read($, agents)).some(a => a.id === id)) {
334 const info = (await $.agent.list()).find(a => a.id === id)
335 if (info !== undefined) {
336 const row: AgentRow = {
337 id,
338 label: clip(info.name ?? info.type),
339 model: e.model.replace(/^.*claude-/, ''),
340 description: info.description,
341 startedAt: await $.clock.now(),
342 status: 'running',
343 listed: true,
344 ...(info.name === undefined ? {} : { name: info.name }),
345 }
346 await update($, agents, list => (list.some(a => a.id === id) ? list : [...list, row]))
347 await update($, now, () => row.startedAt)
348 }
349 }
350
351 return yield* next(e)
352 })
353
354 on('tool.call', async ($, e, next) => {
355 const tool = String(e.tool)
356 const id = e.agentId
357 worked ||= id === undefined
358 const [, ran] = await Promise.all([
359 id === undefined || tool === 'SubagentHandback'
360 ? undefined
361 : patch($, id, a =>
362 a.status === 'running' && a.tool === tool
363 ? undefined
364 : { ...a, status: 'running', tool },
365 ),
366 next(e),
367 ])
368 await record($, tool, id, pathOf(e), ran)
369
370 return ran
371 })
372
373 on('tool.call', { tool: 'TaskStop' }, async ($, e, next) => {
374 const ran = await next(e)
375 const task = e.task_id ?? e.shell_id
376 if (ran.deny !== undefined || ran.isError === true || task === undefined) {
377 return ran
378 }
379 const t = await $.clock.now()
380 const hit = (a: AgentRow) => a.status !== 'killed' && (a.id === task || a.name === task)
381 if ((await read($, agents)).some(hit)) {
382 await update($, agents, list =>
383 list.map(a => (hit(a) ? { ...a, status: 'killed', endedAt: end(a, t) } : a)),
384 )
385 }
386
387 return ran
388 })
389
390 on('turn.complete', async ($, e, next) => {
391 const id = e.agentId
392 if (id !== undefined && !(await $.agent.list()).some(a => a.id === id)) {
393 const endedAt = await $.clock.now()
394 await patch($, id, a =>
395 isLive(a) && !a.listed ? { ...a, status: TURN_END[e.reason], endedAt } : undefined,
396 )
397 }
398 if (id === undefined) {
399 if (worked && e.reason === 'answer' && e.answer.trim() !== '') {
400 const item: WorkItem = {
401 id: e.turnId,
402 at: await $.clock.now(),
403 durationMs: e.durationMs,
404 ...(e.usage === undefined
405 ? {}
406 : {
407 tokens:
408 e.usage.input_tokens +
409 e.usage.output_tokens +
410 e.usage.cache_read_input_tokens +
411 e.usage.cache_creation_input_tokens,
412 }),
413 ask,
414 answer: e.answer.slice(0, 4000),
415 }
416 await update($, recent, list =>
417 [item, ...list.filter(w => w.id !== item.id)].slice(0, RECENT_MAX),
418 )
419 }
420 if (worked && root !== undefined) {
421 await refreshGit($, root)
422 }
423 worked = false
424 }
425
426 return next(e)
427 })
428
429 on('prompt.submit', async ($, e, next) => {
430 if (e.turnId === undefined) {
431 ask = e.text.slice(0, 1000)
432 }
433 if (e.origin.kind === 'composer' || e.origin.kind === 'bridge') {
434 await update($, agents, list => list.filter(isLive))
435 if ((await read($, alerts)).length > 0) {
436 await update($, alerts, () => [])
437 }
438 }
439
440 return next(e)
441 })
442
443 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
444 if (e.props.hasSurvey) {
445 return next(e)
446 }
447 const below = await next(e)
448 const { Box, Button, Text } = $.ui.resolve(e)
449
450 return (
451 <Box flexDirection="column">
452 <Button plain key="toggle" action={TOGGLE_ACTION} onPress={() => toggle($)}>
453 <Text backgroundColor={C.surface0} color={C.lavender}>{` ${TOGGLE_KEY} `}</Text>
454 <Text color={C.overlay}> summary</Text>
455 </Button>
456 {below}
457 </Box>
458 )
459 })
460
461 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
462 const { Box, Button, Text } = $.ui.resolve(e)
463 const list = await read($, agents)
464 const runs = await read($, skillRuns)
465 const work = await read($, recent)
466 const touched = await read($, files)
467 const uncommitted = await read($, dirty)
468 const warnings = await read($, alerts)
469 const tag = await read($, mainModel)
470 const t = await $.clock.now()
471 const frame = Math.floor(t / 100)
472 const spin = SPINNER[frame % SPINNER.length]
473 const busy = isBusy(list, runs, work)
474 const glow = busy ? (GLOW[frame % GLOW.length] ?? C.mauve) : C.mauve
475 const width = Math.max(30, e.props.bodyColumns)
476 const rule = width - 2
477 const at = busy ? (frame % (rule + 6)) - 3 : -3
478 const lit = [at, at + 3].map(n => Math.min(rule, Math.max(0, n))) as [number, number]
479 const labelWidth = Math.max(0, ...list.map(a => a.label.length))
480 const modelWidth = Math.max(0, ...list.map(a => a.model.length))
481 const pathWidth = width - 4 - 3 - 4
482 const shortPath = (p: string) =>
483 root !== undefined && p.startsWith(`${root}/`) ? p.slice(root.length + 1) : p
484 const counts = ORDER.map(s => [s, list.filter(a => a.status === s).length] as const)
485 const keycap = (k: string) => (
486 <Text backgroundColor={C.surface0} color={C.lavender}>{` ${k} `}</Text>
487 )
488
489 return (
490 <Box flexDirection="column" width={width}>
491 <Box borderStyle="round" borderColor={glow} paddingX={1} justifyContent="space-between">
492 <Text bold color={C.mauve}>
493 <Text color={glow}>{busy ? PULSE[frame % PULSE.length] : '◆'}</Text> summary
494 </Text>
495 <Text color={modelColor(tag)}>{tag}</Text>
496 </Box>
497 <Box paddingX={1}>
498 <Text>
499 <Text color={C.surface0}>{'━'.repeat(lit[0])}</Text>
500 <Text color={C.lavender}>{'━'.repeat(lit[1] - lit[0])}</Text>
501 <Text color={C.surface0}>{'━'.repeat(rule - lit[1])}</Text>
502 </Text>
503 </Box>
504 {warnings.length > 0 && (
505 <Box flexDirection="column" borderStyle="round" borderColor={C.red} paddingX={1}>
506 <Text bold color={C.red}>
507 alerts
508 </Text>
509 {warnings.map(a => (
510 <Text wrap="truncate-end">
511 <Text color={C.red}>✗</Text>{' '}
512 <Text color={C.overlay}>{`${ago(t - a.at)} ago`.padEnd(7)}</Text>{' '}
513 <Text color={C.peach}>{a.tool}</Text>
514 {a.agent !== undefined && <Text color={C.overlay}>{` @${a.agent}`}</Text>}
515 <Text color={C.subtext}>{` ${a.reason}`}</Text>
516 </Text>
517 ))}
518 </Box>
519 )}
520 <Box flexDirection="column" borderStyle="round" borderColor={C.surface1} paddingX={1}>
521 <Text bold color={C.blue}>
522 skills
523 </Text>
524 {runs.length === 0 ? (
525 <Text color={C.overlay}>no skill yet</Text>
526 ) : (
527 [...runs].reverse().map(r => (
528 <Text wrap="truncate-end">
529 {r.result === undefined ? (
530 <Text color={C.peach}>{spin}</Text>
531 ) : (
532 <Text color={RUN[r.result].color}>{RUN[r.result].icon}</Text>
533 )}{' '}
534 <Text
535 bold={r.result === undefined}
536 color={r.result === undefined ? C.text : C.subtext}
537 >
538 {r.skills.join(', ')}
539 </Text>{' '}
540 <Text color={modelColor(r.model)}>
541 {[r.model, r.effort].filter(v => v !== undefined).join(' · ')}
542 </Text>{' '}
543 <Text color={C.overlay}>{elapsed((r.endedAt ?? t) - r.startedAt)}</Text>
544 </Text>
545 ))
546 )}
547 </Box>
548 <Box flexDirection="column" borderStyle="round" borderColor={C.surface1} paddingX={1}>
549 <Text>
550 <Text bold color={C.mauve}>
551 agents
552 </Text>
553 {counts
554 .filter(([, n]) => n > 0)
555 .map(([s, n]) => (
556 <Text color={COLOR[s]}>{` ${ICON[s]} ${n} ${s}`}</Text>
557 ))}
558 </Text>
559 {list.length === 0 && <Text color={C.overlay}>no agent running</Text>}
560 {list.map(a => (
561 <Text wrap="truncate-end">
562 <Text color={COLOR[a.status]}>{a.status === 'running' ? spin : ICON[a.status]}</Text>{' '}
563 <Text color={C.text} dimColor={a.status !== 'running'}>
564 {a.label.padEnd(labelWidth)}
565 </Text>{' '}
566 <Text color={modelColor(a.model)} dimColor={a.status !== 'running'}>
567 {a.model.padEnd(modelWidth)}
568 </Text>{' '}
569 <Text color={C.overlay} dimColor={a.status !== 'running'}>
570 {elapsed(end(a, t) - a.startedAt).padStart(6)}
571 </Text>{' '}
572 <Text color={C.subtext} dimColor>
573 {a.description}
574 {a.status === 'running' && a.tool !== undefined ? ` · ${a.tool}` : ''}
575 </Text>
576 </Text>
577 ))}
578 </Box>
579 <Box flexDirection="column" borderStyle="round" borderColor={C.surface1} paddingX={1}>
580 <Text>
581 <Text bold color={C.yellow}>
582 files
583 </Text>
584 <Text color={C.overlay}>
585 {` ${touched.length} edited · ${uncommitted} uncommitted`}
586 </Text>
587 </Text>
588 {touched.length === 0 && <Text color={C.overlay}>no file edited yet</Text>}
589 {touched.slice(0, FILES_SHOWN).map(f => (
590 <Text>
591 <Text color={f.status === undefined ? C.overlay : (GIT_COLOR[f.status] ?? C.peach)}>
592 {(f.status ?? '·').padEnd(2)}
593 </Text>{' '}
594 <Text color={C.overlay}>{`${f.edits}×`.padStart(3)}</Text>{' '}
595 <Text color={C.text}>{tail(shortPath(f.path), pathWidth)}</Text>
596 </Text>
597 ))}
598 {touched.length > FILES_SHOWN && (
599 <Text color={C.overlay}>{`+${touched.length - FILES_SHOWN} more`}</Text>
600 )}
601 </Box>
602 <Box flexDirection="column" borderStyle="round" borderColor={C.surface1} paddingX={1}>
603 <Text bold color={C.green}>
604 recent
605 </Text>
606 {work.length === 0 && <Text color={C.overlay}>no finished work yet</Text>}
607 {work.map(w => (
608 <Text wrap="truncate-end">
609 {w.text === undefined ? (
610 <Text color={C.peach}>{spin}</Text>
611 ) : (
612 <Text color={C.green}>✓</Text>
613 )}{' '}
614 <Text color={C.overlay}>{`${ago(t - w.at)} ago`.padEnd(7)}</Text>{' '}
615 <Text color={C.overlay} dimColor>
616 {elapsed(w.durationMs).padStart(5)}
617 </Text>{' '}
618 <Text color={C.overlay} dimColor>
619 {(w.tokens === undefined ? '' : kilo(w.tokens)).padStart(4)}
620 </Text>{' '}
621 <Text color={w.text === undefined ? C.subtext : C.text}>
622 {w.text ?? 'summarizing…'}
623 </Text>
624 </Text>
625 ))}
626 </Box>
627 <Box paddingX={1} gap={2}>
628 <Button plain key="toggle" action={TOGGLE_ACTION} onPress={() => toggle($)}>
629 {keycap(TOGGLE_KEY)}
630 <Text color={C.subtext}> close</Text>
631 </Button>
632 <Text>
633 {keycap('ctrl+x tab')}
634 <Text color={C.subtext}> focus</Text>
635 </Text>
636 </Box>
637 </Box>
638 )
639 })
640}
641types/index.d.ts 49 lines1export type AgentRow = {
2 id: string
3 label: string
4 name?: string
5 model: string
6 description: string
7 startedAt: number
8 endedAt?: number
9 status: 'running' | 'idle' | 'done' | 'failed' | 'killed'
10 tool?: string
11 listed?: boolean
12}
13
14export type WorkItem = {
15 id: string
16 at: number
17 durationMs: number
18 tokens?: number
19 text?: string
20 ask?: string
21 answer?: string
22}
23
24export type FileTouch = {
25 path: string
26 edits: number
27 status?: string
28}
29
30export type Alert = {
31 at: number
32 tool: string
33 reason: string
34 agent?: string
35}
36
37declare module 'claude-code' {
38 interface PluginState {
39 'agent-status': {
40 agents: AgentRow[]
41 now: number
42 recent: WorkItem[]
43 files: FileTouch[]
44 dirty: number
45 alerts: Alert[]
46 }
47 }
48}
49