SLOPSHOPPER

agent-status

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

newpanebandguardpromptmodel
★ 9v0.1.0MITupdated 2026-10-09ysoftman/myenv/.claude/skills/agent-status
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · agent-status
│ ┃ summary ✕ › fix the failing auth test and add an audit log call │ ┃ ╭─────────────────────────────────────────── │ ┃ │ ◇ summary ⏺ Read(src/auth.ts) │ ┃ ╰─────────────────────────────────────────── ⎿ Read 6 lines │ ┃ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ⏺ Update(src/auth.ts) │ ┃ ╭─────────────────────────────────────────── ⎿ Added 2 lines, removed 1 line │ ┃ │ alerts ⏺ Bash(bun test) │ ┃ │ ✗ 0s ago Bash src/auth.test.ts: ⎿ 3 pass, 1 fail │ ┃ ╰─────────────────────────────────────────── │ ┃ ╭─────────────────────────────────────────── ● Done. refresh now rejects expired claims and logs an audit event. │ ┃ │ skills │ ┃ │ no skill yet ✻ Worked for 42s · done 4:20 PM │ ┃ ╰─────────────────────────────────────────── │ ┃ ╭─────────────────────────────────────────── │ ┃ │ agents │ ┃ │ no agent running │ ┃ ╰─────────────────────────────────────────── │ ┃ ╭─────────────────────────────────────────── │ ┃ │ files 3 edited · 0 uncommitted │ ┃ │ · 1× src/cache.ts │ ┃ │ · 1× src/audit.ts │ ┃ │ · 1× src/auth.ts │ ┃ ╰─────────────────────────────────────────── │ ┃ ╭─────────────────────────────────────────── │ ┃ │ recent │ ┃ │ ⠋ 0s ago 42s 99k summarizing… toggle ⟨Claude Code's own drawing⟩ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
toggle ⟨Claude Code's own drawing⟩
Pane · summary
╭──────────────────────────────────────────────────────╮ │ ◇ summary │ ╰──────────────────────────────────────────────────────╯ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ╭──────────────────────────────────────────────────────╮ │ alerts │ │ ✗ 0s ago Bash src/auth.test.ts: │ ╰──────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────╮ │ skills │ │ no skill yet │ ╰──────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────╮ │ agents │ │ no agent running │ ╰──────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────╮ │ files 3 edited · 0 uncommitted │ │ · 1× src/cache.ts │ │ · 1× src/audit.ts │ │ · 1× src/auth.ts │ ╰──────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────╮ │ recent │ │ ⠋ 0s ago 42s 99k summarizing… │ ╰──────────────────────────────────────────────────────╯ toggle ctrl+x tab focus
README

myenv

개인 환경 백업 및 복구 자동화 스크립트~ :smile:

  • myenv 는 디폴트로 ~/workspace/myenv 에 위치(클론)해야 한다. 변경하려면 .zshrc/.bashrc > myenv_path 값을 수정해야 한다.
  • 설치 & 백업
# 설정 및 프로그램 목록등 백업
bash ./backupmysetting.sh

# 전체 설치
bash ./installall.sh

# 필요한 환경/프로그램은 installxxx.sh 사용
  • mac iterm2 사용시
  • iterm2 > general > selection > application in terminal may access clipboard 활성화(tmux 환경에서 클립보드를 사용하기 위해)
  • iterm2 > profiles > colors > color presets 선택
  • iterm2 > profiles > text > font : 18, font-hack-nerd-font(installbrew.sh 에서 설치함)
  • iterm2 에서 tmux 사용시 더블 클릭은 alt + double click(triple click) 로 사용해야 한다.
  • windows alacritty 사용시
  • 설정 파일은 다른 OS 도 같이 사용하니 윈도우 전용으로 수정하지 않는다. 대신 기본 powershell 에서 wsl 실행하면 된다.
  • windows terminal 사용시
  • 폰트 다운로드 받아 설치
  • <https://github.com/ryanoasis/nerd-fonts/releases/download/v3.5.1/Hack.zip>
  • <https://github.com/ryanoasis/nerd-fonts/releases/download/v3.5.1/FiraCode.zip>
  • terminal 에서 ctrl + , 로 설정 파일을 오픈 후 settings.json 내용 복사 & 붙여넣기로 설정 적용
  • 터미널앱 사용(창 선택)시 배경색이 바뀌는 경우, 윈도우 투명도(settings > transparency effects > off)를 비활성화 해야 한다.
  • Ubuntu, sh > dash 쉘로 링크되어 있어 bash 로 쉘스크립트를 실행하려면 상단에 #!/usr/bin/env bash shebang 을 추가하거나, 아래처럼 전역 sh 링크를 bash로 변경한다.
sudo unlink /bin/sh
sudo ln -s /bin/bash /bin/sh
Source 2 files
hooks/register.tsx 641 lines
1import 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}
641
types/index.d.ts 49 lines
1export 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