SLOPSHOPPER

agent-board

이 세션의 서브에이전트(모델 포함), 스킬, MCP, 백그라운드 작업 현황을 한 패널에 보여줍니다. 세션을 열면 자동으로 열리고, /agent-board로 열고 닫습니다.

newpanespinnerguardcommandprompt
v0.7.0Apache-2.0updated 2026-10-12akaz01/claude-mods/plugins/agent-board
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · agent-board
│ ┃ agent-board ✕ › fix the failing auth test and add an audit log call │ ┃ │ ┃ ⏺ Read(src/auth.ts) │ ┃ ⎿ 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 │ ┃ │ ┃ › /agent-board │ ┃ 메인 세션 1 ────────────────────────────── ⎿ agent-board: Agent Board를 닫았습니다. │ ┃ ○ main · 대기 │ ┃ claude-opus-5-5 │ ┃ 마지막 도구 · Bash │ ┃ │ ┃ 서브 에이전트 0 ────────────────────────── │ ┃ 작업 중인 서브 에이전트 없음 │ ┃ │ ┃ 백그라운드 작업 0 ──────────────────────── │ ┃ 조회된 실행 명령 없음 │ ┃ │ ┃ 스킬 0 ─────────────────────────────────── ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ✳ Anthropic · Claude Code ⟨Claude Code's own drawing⟩

Draws

Pane · agent-board
메인 세션 1 ──────────────────────────────────────── ○ main · 대기 claude-opus-5-5 마지막 도구 · Bash 서브 에이전트 0 ──────────────────────────────────── 작업 중인 서브 에이전트 없음 백그라운드 작업 0 ────────────────────────────────── 조회된 실행 명령 없음 스킬 0 ───────────────────────────────────────────── 이 세션에서 사용한 스킬 · 실행 중 여부와 구분 이 세션에서 사용한 스킬 없음 MCP 서버 0 ───────────────────────────────────────── 현재 세션 · 사용 가능한 도구 서버와 호출 기록 현재 확인된 서버 없음 ⣿⣆ ⢰⡆ ⣀ Anthropic ⢶⣦⣘⢿⣆⣿⣣⣾⡟⠁ CLAUDE / LOCAL MONITOR ⣀⣀⣈⣛⣿⣿⣿⣿⣯⣤⣤⠦ claude-opus-5-5 ⠈⠉⢉⣩⣽⣿⣿⣿⡟⠶⠶⡦ Git feat/auth-refresh ⠰⠛⣩⡾⢹⡏⢻⣟⠷⡄ 5h 69% left ⠐⠋ ⢾⠇ ⠙⠁ ━━━━━━━━──── 0 작업 중 · 서브 0 · 작업 0 · MCP 0 1: H 2: A 3: L 4: M 5: P 6: S ──────────────────────────────────────────────────── ────────────────────────────────────────────────────── ▄ ▄▀█ ██▀▀▀█▀██████ ▀█▀▀▀▀█▀▀▀▀██ ▀▀▀▀▀▀██▀██▀█▀▀▀▀ ▄▀▀▀▀▀▀▀▀▀█▀▀▄ █ ▀ ▀██▀██████████ ▄▀ ▀ ▀▀▀▀▀▀█▀▀▀▀▀██▀▀▀▀▀▀▀ 문제를 살펴보는 중
Prompt hint
✳ Anthropic · Claude Code ⟨Claude Code's own drawing⟩
README

Agent Board 0.7.0 — Codex와 같은 대시보드 UI

Anthropic/Claude 마크와 현재 모델·Git 브랜치·작은 5h/7d 잔량 바는 유지하고, Codex Terminal Monitor처럼 고정 헤더·가로 구분선·시안 모델명·작업 상태 색상·세 줄 에이전트 행·고정 고양이를 사용합니다. Home 순서는 메인 세션 → 서브 에이전트 → 백그라운드 작업 → 스킬 → MCP 서버입니다.

오른쪽 패널을 클릭하거나 Claude의 패널 포커스 단축키로 이동한 뒤 1~6 또는 탭 버튼을 사용합니다. Home / Agents / Log / MCP / Plug / Skills가 Codex와 같은 순서로 표시됩니다. 스크롤은 Claude의 화살표·Page Up/Down·마우스 휠을 사용합니다. /agent-board로 열고 닫습니다. 헤더와 고양이는 스크롤해도 고정되고, 긴 내용은 한 줄로 잘립니다. Codex 전용 tmux 단축키는 Claude에 적용하지 않습니다.

  • Agents: 작업 중인 서브 에이전트만 표시하며 모델과 실제 관측 작업을 함께 보여줍니다.
  • Log: 최근 도구 24개의 이름·소유 에이전트·완료/오류 상태. 도구 인수·대화·비공개 추론은 저장하지 않습니다.
  • 스킬: 현재 세션에서 사용한 스킬과 호출 횟수. 실시간 실행 중 목록과 구분합니다.
  • MCP: 도구 목록에 있는 서버와 현재 세션에서 관측한 호출 기록. 실행 상태를 추측하지 않습니다.
  • Plug: 엔진이 알려준 현재 로드 그룹의 모드 이름입니다. 재로드 직후에는 재로드된 그룹만 표시될 수 있으며 전체 설치 목록을 의미하지 않습니다.

계정 공유 한도는 실제 제공된 5h/7d 값만 표시하며 잔량 바는 최대 12칸입니다. 모델·브랜치·한도는 시작 시, session.measure 이벤트, 약 15초마다 읽습니다. 이 조회와 고양이 애니메이션은 모델을 호출하지 않습니다.

고양이는 검정·옅은 크림색·흰색 삼색 무늬, 비대칭 얼굴과 연녹색 눈을 유지합니다. 휴식·관찰·작업·분주·확인 대기·오류·완료의 7가지 동작이며 각각 4프레임입니다. 높이 26줄 이상이면 하단 10줄을 고양이와 여백에 사용하고, 작은 창에서는 두 줄로 줄입니다.

Claude 하단 context는 함께 포함된 token-weather 0.2.0을 사용합니다. Codex와 같은 기준으로 초록 Go(<50%), 노랑 Caution(50~74%), 빨강 Stop(75~89%), 빨강 Compact soon(90% 이상)을 표시합니다. 누적 비용/토큰 총합이 아닌 현재 context 사용률입니다. 값이 없으면 0으로 만들지 않고, 갱신 실패 시 마지막 값에 STALE을 표시합니다.

설치와 갱신

claude plugin install agent-board@my-mods 또는 기존 설치는 claude plugin update agent-board@my-mods를 사용합니다. 최신 파일을 적용하려면 새 Claude Code 세션을 열거나, 작업이 끝난 현재 세션에서 /reload-plugins를 실행하세요.

개발 검증은 claude plugin test .와 claude plugin validate .입니다. 배포 ZIP에는 테스트 파일이 제외됩니다. Claude Code 2.1.296에서 검증했습니다. Mods API 참고: https://code.claude.com/docs/en/plugins/mods/reference

Anthropic과 Claude 명칭 및 마크는 Anthropic의 상표입니다. 사용자용 커스텀 테마입니다.

Source 6 files
hooks/register.tsx 340 lines
1import { atom, read, update } from 'claude-code'
2import type { AgentInfo, EngineInterface, Register } from 'claude-code'
3
4import { renderBoard } from './view'
5import { emptyPet, petState } from './pet'
6import type { Activity, AgentRow, HeaderData, LogRow, MainRow, Models, TaskRow, UsageRow } from '../types'
7
8const pet = atom({ plugin: 'agent-board', key: 'pet' } as const, emptyPet())
9
10const PANE = 'agent-board'
11const TITLE = 'Agent Board'
12const POLL_MS = 2000
13const MAX_ROWS = 50
14const UNKNOWN_MODEL = '모델 미확인'
15
16const agents = atom({ plugin: 'agent-board', key: 'agents' } as const, [] as AgentRow[])
17const skills = atom({ plugin: 'agent-board', key: 'skills' } as const, [] as UsageRow[])
18const mcp = atom({ plugin: 'agent-board', key: 'mcp' } as const, [] as UsageRow[])
19const servers = atom({ plugin: 'agent-board', key: 'servers' } as const, [] as string[])
20const tasks = atom({ plugin: 'agent-board', key: 'tasks' } as const, [] as TaskRow[])
21const activity = atom({ plugin: 'agent-board', key: 'activity' } as const, {} as Activity)
22const models = atom({ plugin: 'agent-board', key: 'models' } as const, {} as Models)
23const main = atom({ plugin: 'agent-board', key: 'main' } as const, { status: '대기', lastAt: 0 } as MainRow)
24const isOpen = atom({ plugin: 'agent-board', key: 'isOpen' } as const, false)
25const header = atom({ plugin: 'agent-board', key: 'header' } as const, { limits: [], updatedAt: 0 } as HeaderData)
26const tab = atom({ plugin: 'agent-board', key: 'tab' } as const, 0)
27let engineMods: string[] = []
28let lastViewportColumns = 0
29const log = atom({ plugin: 'agent-board', key: 'log' } as const, [] as LogRow[])
30
31const STATUS_KO: Record<string, string> = {
32  pending: '대기',
33  waiting: '대기',
34  idle: '대기',
35  running: '작업 중',
36  in_progress: '작업 중',
37  completed: '완료',
38  failed: '오류',
39  killed: '오류',
40}
41
42export const statusKo = (status: string): string => STATUS_KO[status] ?? status
43
44export const shortModel = (model: string | undefined): string =>
45  model ? model.replace(/^claude-/, '') : UNKNOWN_MODEL
46
47export const bump = (rows: UsageRow[], name: string, at: number): UsageRow[] => {
48  const found = rows.some(row => row.name === name)
49  const next = found
50    ? rows.map(row => (row.name === name ? { ...row, count: row.count + 1, lastAt: at } : row))
51    : [...rows, { name, count: 1, lastAt: at }]
52  return next.slice(-MAX_ROWS)
53}
54
55export const mergeAgents = (
56  previous: AgentRow[],
57  listed: AgentInfo[],
58  seen: Activity,
59  spawned: Models,
60  now: number,
61): AgentRow[] =>
62  listed.map(info => {
63    const old = previous.find(row => row.id === info.id)
64    const status = statusKo(info.status)
65    const changed = old === undefined || old.status !== status
66    const act = seen[info.id]
67    const lastAt = Math.max(act?.at ?? 0, changed ? now : old.lastAt)
68    return {
69      id: info.id,
70      name: info.name ?? info.id.slice(0, 8),
71      type: info.type,
72      model: shortModel(spawned[info.id]),
73      task: act ? `${info.description} · ${act.tool}` : info.description,
74      status,
75      lastAt,
76    }
77  })
78
79export const mcpServer = (tool: string): string | undefined =>
80  tool.startsWith('mcp__') ? tool.slice(5).split('__')[0] : undefined
81
82const TASK_ID = /<task-id>([^<]+)<\/task-id>/
83const TASK_STATUS = /<status>([^<]+)<\/status>/
84
85export const parseNotification = (text: string): { id: string; status: string } | undefined => {
86  const id = TASK_ID.exec(text)?.[1]?.trim()
87  const status = TASK_STATUS.exec(text)?.[1]?.trim()
88  return id && status ? { id, status } : undefined
89}
90
91// Bookkeeping must never block the call it observes.
92const quietly = async ($: EngineInterface, work: () => Promise<unknown>): Promise<void> => {
93  try {
94    await work()
95  } catch (error) {
96    $.ui.log(`agent-board: 기록 실패 (${String(error)})`)
97  }
98}
99
100const openBoard = async ($: EngineInterface) => {
101  const opened = await $.ui.open({ id: PANE, title: TITLE })
102  await update($, isOpen, () => true)
103  return opened
104}
105
106// Plain usage() reads the host's cached figures; never ask for a token breakdown.
107const refreshHeader = async ($: EngineInterface) => {
108  try {
109    const [model, cwd, usage] = await Promise.all([$.session.model(), $.session.cwd(), $.session.usage()])
110    let branch = '—'
111    try {
112      const git = await $.process.run(['git', 'symbolic-ref', '--quiet', '--short', 'HEAD'], { cwd, timeoutMs: 1000 })
113      if (git.stdout.trim()) branch = git.stdout.trim()
114      else {
115        const head = await $.process.run(['git', 'rev-parse', '--short', 'HEAD'], { cwd, timeoutMs: 1000 })
116        if (head.stdout.trim()) branch = head.stdout.trim() + ' (detached)'
117      }
118    } catch { /* Not a repository, or git is unavailable. */ }
119    await update($, header, () => ({ model, branch, limits: usage.rateLimits ?? [], updatedAt: Date.now(), stale: false }))
120  } catch {
121    await update($, header, row => ({ ...row, stale: true }))
122  }
123}
124
125export const register: Register = on => {
126  on('engine.create', async ($, e, next) => {
127    const built = await next(e)
128    engineMods = [...new Set(e.plugins)].sort()
129    return built
130  })
131  // Observe permission decisions without changing their result. The call's
132  // finally block clears its wait flag when it resolves or is cancelled.
133  on('tool.check', async ($, e, next) => {
134    const verdict = await next(e)
135    if (e.tool_use_id && verdict.decision === 'ask') {
136      await quietly($, () => update($, pet, row => ({ ...row, waiting: [...new Set([...row.waiting, e.tool_use_id!])] })))
137    }
138    return verdict
139  })
140
141  on('session.end', async ($, e, next) => {
142    await quietly($, () => update($, pet, () => emptyPet()))
143    return next(e)
144  })
145  on('session.start', async ($, e, next) => {
146    await $.command.register({
147      name: 'agent-board',
148      description: '에이전트·스킬·MCP·백그라운드 작업 현황 패널을 열고 닫습니다',
149    })
150    const refresh = async () => {
151      try {
152        const listed = await $.agent.list()
153        const seen = await read($, activity)
154        const spawned = await read($, models)
155        const previous = await read($, agents)
156        const merged = mergeAgents(previous, listed, seen, spawned, Date.now())
157        await update($, agents, () => merged)
158        if (previous.some(row => row.status === '작업 중') && !merged.some(row => row.status === '작업 중')) {
159          if (merged.some(row => row.status === '오류')) {
160            await update($, pet, row => ({ ...row, errorAt: Date.now() }))
161          } else if (merged.some(row => row.status === '완료')) {
162            await update($, pet, row => ({ ...row, doneAt: Date.now() }))
163          }
164        }
165        const tools = await $.tool.list()
166        const names = [...new Set(tools.filter(t => t.mcp).map(t => mcpServer(t.name) ?? t.name))]
167        await update($, servers, () => names.sort())
168      } catch (error) {
169        $.ui.log(`agent-board: 현황을 읽지 못했습니다 (${String(error)})`)
170      }
171    }
172    await refresh()
173    $.clock.every(POLL_MS, () => void refresh())
174    $.clock.every(250, async () => {
175      if (await read($, isOpen)) {
176        await update($, pet, row => ({ ...row, tick: row.tick + 1 }))
177        $.ui.invalidate('ui.render')
178      }
179    })
180    const started = await next(e)
181    await refreshHeader($)
182    $.clock.every(15000, () => void refreshHeader($))
183    // Open the board with every interactive session; /agent-board still closes it.
184    if (e.isInteractive) await quietly($, () => openBoard($))
185    return started
186  })
187
188  on('session.measure', async ($, e, next) => {
189    const result = await next(e)
190    await refreshHeader($)
191    return result
192  })
193
194  // Keep the engine's shortcut hint and any other mod's rendering alongside the mark.
195  on('ui.render', { component: 'PromptHint', surface: 'terminal' }, async ($, e, next) => {
196    const original = await next(e)
197    const { Box, Text } = $.ui.resolve(e)
198    return <Box flexDirection="row" gap={2}>
199      <Box flexShrink={0}><Text wrap="truncate-end" color="claude">✳ Anthropic · Claude Code</Text></Box>
200      <Box flexShrink={1}>{original}</Box>
201    </Box>
202  })
203
204  on('command.run', { command: 'agent-board' }, async $ => {
205    if (await read($, isOpen)) {
206      await $.ui.close({ id: PANE })
207      await update($, isOpen, () => false)
208      return { text: 'Agent Board를 닫았습니다.' }
209    }
210    const opened = await openBoard($)
211    return { text: opened.isPlaced ? 'Agent Board를 열었습니다.' : 'Agent Board를 열었습니다. 터미널 폭이 좁으면 넓혀 주세요.' }
212  })
213
214  on('ui.close', async ($, e, next) => {
215    if (e.id === PANE) await quietly($, () => update($, isOpen, () => false))
216    return next(e)
217  })
218
219  // agent.list carries no model, so remember the one each spawn resolved to.
220  on('agent.spawn', async ($, e, next) => {
221    const started = await next(e)
222    const id = started.agentId
223    if (id) await quietly($, () => update($, models, seen => ({ ...seen, [id]: started.model })))
224    return started
225  })
226
227  // agent.list holds subagents and teammates only; the main loop is tracked here.
228  on('turn.step', async function* ($, e, next) {
229    if (!e.agentId) {
230      await quietly($, () => update($, main, row => ({ ...row, status: '작업 중', model: e.model, lastAt: Date.now() })))
231      await quietly($, () => update($, pet, row => ({ ...row, doneAt: 0, errorAt: 0 })))
232    }
233    return yield* next(e)
234  })
235
236  on('turn.complete', async ($, e, next) => {
237    if (!e.agentId) {
238      await quietly($, () => update($, main, row => ({ ...row, status: '대기', lastAt: Date.now() })))
239      await quietly($, () => update($, pet, row => ({ ...row, waiting: [], inFlight: [],
240        doneAt: e.reason === 'answer' ? Date.now() : 0,
241        errorAt: e.reason === 'error' || e.reason === 'refusal' ? Date.now() : row.errorAt })))
242    }
243    return next(e)
244  })
245
246  on('skill.prompt', async ($, e, next) => {
247    await quietly($, () => update($, skills, rows => bump(rows, e.skill, Date.now())))
248    return next(e)
249  })
250
251  on('tool.call', async ($, e, next) => {
252    const now = Date.now()
253    const id = e.agentId
254    if (id) await quietly($, () => update($, activity, seen => ({ ...seen, [id]: { at: now, tool: e.tool } })))
255    else await quietly($, () => update($, main, row => ({ ...row, tool: e.tool, lastAt: now })))
256    const server = mcpServer(e.tool)
257    if (server) await quietly($, () => update($, mcp, rows => bump(rows, server, now)))
258    const key = e.tool_use_id || `${e.agentId ?? 'main'}:${e.tool}`
259    await quietly($, () => update($, log, rows => [{ id: key, owner: id ?? 'main', tool: e.tool, status: '작업 중', at: now }, ...rows].slice(0, 24)))
260    await quietly($, () => update($, pet, row => ({ ...row,
261      calls: [...row.calls.filter(at => now - at < 10000), now].slice(-50),
262      inFlight: [...new Set([...row.inFlight, key])],
263      waiting: e.tool === 'AskUserQuestion' ? [...new Set([...row.waiting, key])] : row.waiting,
264    })))
265    const ran = await next(e).catch(async error => {
266      await quietly($, () => update($, pet, row => ({ ...row, errorAt: Date.now() })))
267      await quietly($, () => update($, log, rows => rows.map(row => row.id === key ? { ...row, status: '오류' } : row)))
268      throw error
269    }).finally(() => quietly($, () => update($, pet, row => ({ ...row,
270      inFlight: row.inFlight.filter(id => id !== key), waiting: row.waiting.filter(id => id !== key),
271    }))))
272    if (ran.isError || ran.deny) await quietly($, () => update($, pet, row => ({ ...row, errorAt: Date.now() })))
273    await quietly($, () => update($, log, rows => rows.map(row => row.id === key ? { ...row, status: ran.isError || ran.deny ? '오류' : '완료' } : row)))
274    const taskId = e.tool === 'Bash' ? (ran.result as { backgroundTaskId?: string } | undefined)?.backgroundTaskId : undefined
275    if (e.tool === 'Bash' && taskId) {
276      const row: TaskRow = {
277        id: taskId,
278        label: e.description ?? e.command.slice(0, 60),
279        status: '작업 중',
280        startedAt: now,
281        lastAt: now,
282      }
283      await quietly($, () => update($, tasks, rows => [...rows, row].slice(-MAX_ROWS)))
284    }
285    return ran
286  })
287
288  on('prompt.submit', async ($, e, next) => {
289    const done = e.origin.kind === 'task-notification' ? parseNotification(e.text) : undefined
290    if (done) {
291      const now = Date.now()
292      if (done.status === 'completed') await quietly($, () => update($, pet, row => ({ ...row, doneAt: now })))
293      if (done.status === 'failed' || done.status === 'killed') await quietly($, () => update($, pet, row => ({ ...row, errorAt: now })))
294      await quietly($, () =>
295        update($, tasks, rows =>
296          rows.map(row => (row.id === done.id ? { ...row, status: statusKo(done.status), lastAt: now } : row)),
297        ),
298      )
299    }
300    return next(e)
301  })
302
303  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
304    // In the terminal dock the viewport describes the remaining transcript;
305    // include this pane's span to avoid shrinking it again on each resize.
306    const totalColumns = (e.viewport?.columns ?? 0) + e.props.bodyColumns + 1
307    if (e.props.placement === 'dock' && e.viewport?.columns && totalColumns !== lastViewportColumns) {
308      lastViewportColumns = totalColumns
309      await quietly($, () => $.ui.open({ id: PANE, title: TITLE, columns: Math.max(24, Math.floor(totalColumns * 0.4)) }))
310    }
311    const elements = $.ui.resolve(e)
312    const [mainRow, agentRows, skillRows, mcpRows, serverNames, taskRows, headerRow, petRow, tabRow, modsRow, logRows] = await Promise.all([
313      read($, main),
314      read($, agents),
315      read($, skills),
316      read($, mcp),
317      read($, servers),
318      read($, tasks),
319      read($, header),
320      read($, pet),
321      read($, tab), Promise.resolve(engineMods), read($, log),
322    ])
323    const data = { main: mainRow, agents: agentRows, tasks: taskRows, skills: skillRows, mcp: mcpRows, servers: serverNames, header: headerRow,
324      tab: tabRow, plugins: modsRow, log: logRows,
325      onTab: (index: number) => { void quietly($, async () => {
326        await update($, tab, () => index)
327        await $.ui.scroll({ in: PANE, to: 'start' })
328        $.ui.invalidate('ui.render')
329      }) },
330    }
331    const working = [mainRow, ...agentRows, ...taskRows].filter(row => row.status === '작업 중').length
332    const taskErrorAt = Math.max(0, ...[...taskRows, ...agentRows].filter(row => row.status === '오류').map(row => row.lastAt))
333    const state = petState({ ...petRow, errorAt: Math.max(petRow.errorAt, taskErrorAt) }, working, Date.now(), taskRows.filter(row => row.status === '작업 중').length)
334    const bodyRows = e.props.scroll.bodyRows
335    return renderBoard(elements, data, e.props.bodyColumns, {
336      bodyRows, offset: e.props.scroll.offset, state, tick: petRow.tick,
337    })
338  })
339}
340
hooks/view.tsx 209 lines
1import type { BoxProps, ButtonProps, Color, ElementConstructor, TextProps } from 'claude-code'
2
3import type { AgentRow, HeaderData, LogRow, MainRow, TaskRow, UsageRow } from '../types'
4import { CLAUDE_MARK } from './logo'
5import { renderPet, type PetState } from './pet'
6import { CAT } from './cat-sprites'
7
8type Elements = {
9  Box: ElementConstructor<BoxProps>
10  Text: ElementConstructor<TextProps>
11  Button?: ElementConstructor<ButtonProps>
12}
13
14export type BoardData = {
15  main: MainRow
16  agents: AgentRow[]
17  tasks: TaskRow[]
18  skills: UsageRow[]
19  mcp: UsageRow[]
20  servers: string[]
21  header?: HeaderData
22  tab?: number
23  plugins?: string[]
24  log?: LogRow[]
25  onTab?: (index: number) => void
26}
27
28type Mark = { icon: string; color: Color }
29
30const MARKS: Record<string, Mark> = {
31  '작업 중': { icon: '●', color: 'success' },
32  '대기': { icon: '○', color: 'inactive' },
33  '완료': { icon: '✓', color: 'subtle' },
34  '오류': { icon: '✗', color: 'error' },
35}
36const OTHER_MARK: Mark = { icon: '·', color: 'inactive' }
37const ACCENT: Color = 'cyan'
38const RULE = '─'
39
40export const markFor = (status: string): Mark => MARKS[status] ?? OTHER_MARK
41
42export const clock = (at: number): string =>
43  at > 0 ? new Date(at).toTimeString().slice(0, 8) : '-'
44
45export const isWorking = (status: string): boolean => status === '작업 중'
46
47export const remainingLimits = (limits: HeaderData['limits']) =>
48  limits.filter(row => Number.isFinite(row.percentUsed)).map(row => ({
49    label: row.kind === 'five_hour' ? '5h' : row.kind === 'seven_day' ? '7d' : row.kind,
50    remaining: Math.max(0, Math.min(100, 100 - row.percentUsed)),
51  }))
52
53export const summary = (data: BoardData): string => {
54  const working = [data.main, ...data.agents, ...data.tasks].filter(row => isWorking(row.status)).length
55  return `${working} 작업 중 · 서브 ${data.agents.length} · 작업 ${data.tasks.length} · MCP ${data.servers.length}`
56}
57
58export const splitServers = (servers: string[], used: UsageRow[]): { used: UsageRow[]; idle: number } => {
59  const connected = used.filter(row => servers.includes(row.name))
60  const extra = used.filter(row => !servers.includes(row.name))
61  return {
62    used: [...connected, ...extra].sort((a, b) => b.lastAt - a.lastAt),
63    idle: servers.filter(name => !used.some(row => row.name === name)).length,
64  }
65}
66
67export const renderBoard = ({ Box, Text, Button }: Elements, data: BoardData, columns: number,
68  pet?: { bodyRows: number; offset: number; state: PetState; tick: number }) => {
69  const width = Math.max(1, columns)
70  const ruleWidth = Math.max(1, columns - 4)
71  const tab = data.tab ?? 0
72  const header = data.header
73  const limits = remainingLimits(header?.limits ?? []).filter(row => row.label === '5h' || row.label === '7d')
74  const headerWidth = Math.max(1, columns - (columns >= 44 ? 18 : 4))
75  const quotaWidth = Math.max(1, Math.min(13, Math.floor((headerWidth - Math.max(0, limits.length - 1) * 2) / Math.max(1, limits.length))))
76  const barWidth = Math.max(1, Math.min(12, quotaWidth))
77  const tabNames = ['Home', 'Agents', 'Log', 'MCP', 'Plug', 'Skills']
78  const activeAgents = data.agents.filter(row => isWorking(row.status))
79  const activeTasks = data.tasks.filter(row => isWorking(row.status))
80  const connectedServers = [...new Set([...data.servers, ...data.mcp.map(row => row.name)])].sort()
81  const headerRows = 12
82
83  const Section = ({ title, count }: { title: string; count?: string }) => (
84    <Box flexDirection="row" marginTop={1} gap={1}>
85      <Box flexShrink={0}><Text wrap="truncate-end" bold>{title}</Text></Box>
86      {count !== undefined && <Box flexShrink={0}><Text wrap="truncate-end" bold>{count}</Text></Box>}
87      <Box flexShrink={1} flexGrow={1}><Text wrap="truncate-end" bold>{RULE.repeat(Math.max(1, ruleWidth - [...title].reduce((n, c) => n + (c.charCodeAt(0) > 255 ? 2 : 1), 0) - (count === undefined ? 1 : count.length + 2)))}</Text></Box>
88    </Box>
89  )
90  const Empty = ({ text = '없음' }: { text?: string }) => <Text wrap="truncate-end" dimColor>{text}</Text>
91  const Row = (props: { status: string; name: string; model?: string; task?: string }) => {
92    const mark = markFor(props.status)
93    return <Box flexDirection="column">
94      <Text wrap="truncate-end" color={mark.color}>{`${mark.icon} ${props.name}  ·  ${props.status}`}</Text>
95      {props.model !== undefined && <Text wrap="truncate-end" color={ACCENT}>{`    ${props.model}`}</Text>}
96      {props.task !== undefined && <Text wrap="truncate-end">{`    ${props.task}`}</Text>}
97    </Box>
98  }
99  const Main = () => <Box flexDirection="column">
100    <Section title="메인 세션" count="1" />
101    <Row status={data.main.status} name="main" model={data.main.model ?? header?.model ?? '모델 미확인'}
102      task={data.main.tool ? `마지막 도구 · ${data.main.tool}` : '공개 작업 기록을 기다리는 중'} />
103  </Box>
104  const Agents = () => <Box flexDirection="column">
105    <Section title="서브 에이전트" count={`${activeAgents.length}`} />
106    {!activeAgents.length && <Empty text="작업 중인 서브 에이전트 없음" />}
107    {activeAgents.map(row => <Row status={row.status} name={row.name} model={row.model} task={row.task} />)}
108  </Box>
109  const Background = () => <Box flexDirection="column">
110    <Section title="백그라운드 작업" count={`${activeTasks.length}`} />
111    {!activeTasks.length && <Empty text="조회된 실행 명령 없음" />}
112    {activeTasks.map(row => <Box flexDirection="column">
113      <Text color="success" wrap="truncate-end">{`● ${row.label}`}</Text>
114      <Text dimColor wrap="truncate-end">{`  ${row.id.slice(0, 8)} · 시작 ${clock(row.startedAt)}`}</Text>
115    </Box>)}
116  </Box>
117  const Skills = () => <Box flexDirection="column">
118    <Section title="스킬" count={`${data.skills.length}`} />
119    <Text dimColor wrap="truncate-end">이 세션에서 사용한 스킬 · 실행 중 여부와 구분</Text>
120    {!data.skills.length && <Empty text="이 세션에서 사용한 스킬 없음" />}
121    {data.skills.map(row => <Text color={ACCENT} wrap="truncate-end">{`● ${row.name}  ·  ×${row.count}`}</Text>)}
122  </Box>
123  const MCP = () => <Box flexDirection="column">
124    <Section title="MCP 서버" count={`${connectedServers.length}`} />
125    <Text dimColor wrap="truncate-end">현재 세션 · 사용 가능한 도구 서버와 호출 기록</Text>
126    {!connectedServers.length && <Empty text="현재 확인된 서버 없음" />}
127    {connectedServers.map(name => {
128      const row = data.mcp.find(item => item.name === name)
129      return <Text color="success" wrap="truncate-end">{`● ${name}${row ? `  ·  ×${row.count}` : ''}`}</Text>
130    })}
131  </Box>
132  const body = <Box key="board-content" flexDirection="column" paddingX={2} flexShrink={0}>
133    {tab === 0 && <Box flexDirection="column"><Main /><Agents /><Background /><Skills /><MCP /></Box>}
134    {tab === 1 && <Box flexDirection="column"><Main /><Agents /></Box>}
135    {tab === 2 && <Box flexDirection="column">
136      <Section title="ACTIVITY" />
137      <Text dimColor wrap="truncate-end">최근 도구 24개 · 인수·대화·비공개 추론 저장 안 함</Text>
138      {!data.log?.length && <Empty text="아직 표시할 활동이 없습니다." />}
139      {data.log?.map(row => <Text wrap="truncate-end" color={markFor(row.status).color}>{`${clock(row.at)}  ${row.owner === 'main' ? 'main' : row.owner.slice(0, 8)}  ${row.status}  ${row.tool}`}</Text>)}
140      <Background />
141    </Box>}
142    {tab === 3 && <MCP />}
143    {tab === 4 && <Box flexDirection="column">
144      <Section title="PLUGINS" count={`${data.plugins?.length ?? 0}`} />
145      <Text dimColor wrap="truncate-end">현재 엔진 로드 그룹의 모드 · 설치 목록과 구분</Text>
146      {!data.plugins?.length && <Empty text="로드 정보 없음" />}
147      {data.plugins?.map(name => <Text wrap="truncate-end" color="success">{`● ${name}`}</Text>)}
148    </Box>}
149    {tab === 5 && <Skills />}
150  </Box>
151  const top = <Box key="board-header" flexDirection="column" height={headerRows} paddingX={2} paddingTop={1} flexShrink={0}>
152    <Box flexDirection="row" gap={2} height={7} flexShrink={0}>
153      {columns >= 44 && <Box flexDirection="column" width={12} flexShrink={0}>
154        {CLAUDE_MARK.map(line => <Text wrap="truncate-end" color="claude">{line}</Text>)}
155      </Box>}
156      <Box flexDirection="column" flexGrow={1} flexShrink={1}>
157        <Box flexDirection="row" justifyContent="space-between">
158          <Text wrap="truncate-end" bold>Anthropic</Text>
159          {columns >= 54 && <Text wrap="truncate-end" color={header?.stale ? 'warning' : header?.updatedAt ? 'success' : 'subtle'}>{header?.stale ? '● STALE' : header?.updatedAt ? '● LIVE' : '● WAIT'}</Text>}
160        </Box>
161        <Text dimColor wrap="truncate-end">CLAUDE / LOCAL MONITOR</Text>
162        <Text color={ACCENT} wrap="truncate-end">{header?.model ?? data.main.model ?? '모델 미확인'}</Text>
163        <Text dimColor wrap="truncate-end">Git  {header?.branch ?? '—'}</Text>
164        {limits.length > 0 && <Box key="quota-windows" flexDirection="row" alignSelf="flex-start">
165          {limits.map((row, index) => {
166            const filled = Math.round(barWidth * row.remaining / 100)
167            const color: Color = row.remaining <= 15 ? 'error' : row.remaining <= 40 ? 'warning' : 'success'
168            return <Box key={`quota-${row.label}`} flexDirection="row" flexShrink={0}>
169              <Box flexDirection="column" width={quotaWidth}>
170                <Text wrap="truncate-end" color={color}>{`${row.label} ${Math.round(row.remaining * 10) / 10}% left`}</Text>
171                <Text wrap="truncate-end" color={color}>{'━'.repeat(filled) + '─'.repeat(barWidth - filled)}</Text>
172              </Box>
173              {index < limits.length - 1 && <Box width={2} flexDirection="column" alignItems="center" flexShrink={0}>
174                <Text dimColor wrap="truncate-end">│</Text><Text dimColor wrap="truncate-end">│</Text>
175              </Box>}
176            </Box>
177          })}
178        </Box>}
179        {!limits.length && <Text wrap="truncate-end" dimColor>한도 미제공 / 조회 중</Text>}
180        {!limits.length && <Text wrap="truncate-end" dimColor>읽기 전용 · 모델 호출 없음</Text>}
181      </Box>
182    </Box>
183    <Text dimColor wrap="truncate-end">{summary(data)}</Text>
184    <Text wrap="truncate-end"> </Text>
185    <Box flexDirection="row" gap={1}>
186      {tabNames.map((name, index) => Button ? <Button key={`tab-${index}`} plain hotkey={`${index + 1}`} label={columns >= 58 ? name : name.slice(0, 1)}
187        dimColor={index !== tab} onPress={() => data.onTab?.(index)} /> :
188        <Text wrap="truncate-end" color={index === tab ? ACCENT : 'subtle'}>{`${index + 1} ${name}`}</Text>)}
189    </Box>
190    <Text wrap="truncate-end" dimColor>{RULE.repeat(ruleWidth)}</Text>
191  </Box>
192  if (!pet) return <Box flexDirection="column" width={width}>{top}{body}</Box>
193  const footerRows = pet.bodyRows >= 26 ? 10 : 2
194  return <Box key="board-with-cat" position="relative" flexDirection="column" minHeight={Math.max(1, pet.bodyRows)} width={width}>
195    <Box key="board-window" flexDirection="column" flexShrink={0}>
196      <Box height={headerRows} flexShrink={0} />{body}
197    </Box>
198    <Box height={footerRows} flexShrink={0} />
199    <Box key="fixed-header" position="absolute" top={pet.offset} left={0} width={width} height={headerRows} backgroundColor="#171d27">{top}</Box>
200    <Box key="cat-footer" position="absolute" top={pet.offset + Math.max(0, pet.bodyRows - footerRows)} left={0} width={width} height={footerRows} backgroundColor="#171d27" flexDirection="column">
201      {footerRows === 10 ? renderPet({ Box, Text }, pet.state, pet.tick, columns) :
202        <Box key="family-cat-compact" flexDirection="column" height={2} paddingX={2} flexShrink={0}>
203          <Text wrap="truncate-end" dimColor>{RULE.repeat(ruleWidth)}</Text>
204          <Text wrap="truncate-end" dimColor>{String.raw`/\_/\ · ${CAT.states[pet.state].label}`}</Text>
205        </Box>}
206    </Box>
207  </Box>
208}
209
hooks/pet.tsx 52 lines
1import type { BoxProps, ElementConstructor, TextProps } from 'claude-code'
2import { CAT } from './cat-sprites'
3import type { PetActivity } from '../types'
4
5export type PetState = keyof typeof CAT.states
6export const emptyPet = (): PetActivity => ({ tick: 0, calls: [], inFlight: [], waiting: [], doneAt: 0, errorAt: 0 })
7export const petState = (pet: PetActivity, working: number, now: number, background = 0): PetState => {
8  if (pet.waiting.length) return 'waiting'
9  if (pet.errorAt > 0 && now - pet.errorAt < 8000) return 'error'
10  if (working > 0 || pet.inFlight.length > 0) {
11    if (working >= 3 || pet.calls.filter(at => now - at < 10000).length >= 4) return 'busy'
12    return pet.inFlight.length > 0 || background > 0 ? 'working' : 'watching'
13  }
14  if (pet.doneAt > 0 && now - pet.doneAt < 8000) return 'done'
15  return 'idle'
16}
17
18type Pixel = keyof typeof CAT.palette
19export const catCells = (state: PetState, elapsedMs: number, width: number) => {
20  const animation = CAT.states[state]
21  const frame = animation.frames[Math.floor(elapsedMs / animation.periodMs) % animation.frames.length]
22  const crop = Math.max(0, Math.floor((CAT.width - width) / 2))
23  const rows: { glyph: string; foreground: string | undefined; background: string | undefined }[][] = []
24  for (let y = 0; y < CAT.height; y += 2) {
25    const row = []
26    for (let x = crop; x < Math.min(CAT.width, crop + Math.max(0, width)); x++) {
27      const top = frame[y][x] as Pixel, bottom = frame[y + 1][x] as Pixel
28      const foreground = CAT.palette[top === '.' ? bottom : top] ?? undefined
29      row.push({ glyph: top === bottom ? top === '.' ? ' ' : '█' : top === '.' ? '▄' : '▀',
30        foreground, background: top !== bottom && top !== '.' ? CAT.palette[bottom] ?? undefined : undefined })
31    }
32    rows.push(row)
33  }
34  return rows
35}
36
37export const renderPet = ({ Box, Text }: { Box: ElementConstructor<BoxProps>; Text: ElementConstructor<TextProps> },
38  state: PetState, tick: number, columns: number) => (
39  <Box key="family-cat" flexDirection="column" flexShrink={0} height={10} paddingX={1}>
40    <Text dimColor wrap="truncate-end">{'─'.repeat(Math.max(1, columns - 2))}</Text>
41    <Text wrap="truncate-end"> </Text>
42    <Box flexDirection="column" alignItems="center">
43      {catCells(state, tick * 250, Math.max(1, columns - 2)).map((row, y) => (
44        <Text key={`cat-row-${y}`} wrap="truncate-end">{row.map((cell, x) => (
45          <Text key={`pixel-${x}`} color={cell.foreground} backgroundColor={cell.background}>{cell.glyph}</Text>
46        ))}</Text>
47      ))}
48      <Text key="cat-status" color={state === 'error' ? 'error' : state === 'waiting' ? 'warning' : state === 'done' ? 'success' : 'subtle'} wrap="truncate-end">{CAT.states[state].label}</Text>
49    </Box>
50  </Box>
51)
52
hooks/logo.ts 4 lines
1// Terminal rendering of the official installed Claude app tray mark.
2// The Claude mark is a trademark of Anthropic; not affiliated with or endorsed by Anthropic.
3export const CLAUDE_MARK = ["   ⣿⣆ ⢰⡆ ⣀  ", " ⢶⣦⣘⢿⣆⣿⣣⣾⡟⠁ ", "⣀⣀⣈⣛⣿⣿⣿⣿⣯⣤⣤⠦", "⠈⠉⢉⣩⣽⣿⣿⣿⡟⠶⠶⡦", " ⠰⠛⣩⡾⢹⡏⢻⣟⠷⡄ ", "  ⠐⠋ ⢾⠇ ⠙⠁  "]
4
hooks/cat-sprites.ts 511 lines
1// Shared terminal sprite data. Black / pale cream / white; no orange.
2export const CAT = {
3  "width": 32,
4  "height": 14,
5  "palette": {
6    ".": null,
7    "k": "#45474f",
8    "s": "#65676d",
9    "d": "#24272d",
10    "c": "#ded3b8",
11    "w": "#f3f0e8",
12    "e": "#a4b697",
13    "p": "#9f8b91",
14    "a": "#83cbd7",
15    "r": "#d68888"
16  },
17  "states": {
18    "idle": {
19      "label": "잠깐 쉬는 중",
20      "frames": [
21        [
22          "................................",
23          "................................",
24          ".............................aa.",
25          ".............................aa.",
26          "...........................aak..",
27          "..........kkkkkkkkpk.......aak..",
28          ".......kkccckkkkkkpkkkkkkkkkpk..",
29          "......kkccccckkkkkkccckwkkkpkk..",
30          ".....kkkkccckkkkkkcdddwwwdddkk..",
31          ".....kkksskkkkkkkkccccwwwwkkkk..",
32          ".....sskkkkkkkkwwwwcccwdwwwkwwww",
33          "....ssskkkkkkkkkkkkkkwwpwwwkk...",
34          ".....ssssskkkkkkkkwwwwwkkkkk....",
35          "........sssssssssssss..........."
36        ],
37        [
38          "................................",
39          ".............................aa.",
40          ".............................aa.",
41          "................................",
42          "..........kkkkkkkkkk.......aak..",
43          "........kkkkkkkkkkpkkk.....aak..",
44          "......kkkccckkkkkkpkkkkkkkkkpk..",
45          ".....kkkccccckkkkkkccckwkkkpkk..",
46          ".....kkkkccckkkkkkcdddwwwdddkk..",
47          ".....kkksskkkkkkkkccccwwwwkkkk..",
48          ".....sskkkkkkkkwwwwcccwdwwwkwwww",
49          "....ssskkkkkkkkkkkkkkwwpwwwkk...",
50          ".....ssssskkkkkkkkwwwwwkkkkk....",
51          "........sssssssssssss..........."
52        ],
53        [
54          "................................",
55          "................................",
56          ".............................aa.",
57          ".............................aa.",
58          "...........................aak..",
59          "..........kkkkkkkkpk.......aak..",
60          ".......kkccckkkkkkpkkkkkkkkkpk..",
61          "......kkccccckkkkkkccckwkkkpkk..",
62          ".....kkkkccckkkkkkcdddwwwdddkk..",
63          ".....kkksskkkkkkkkccccwwwwkkkk..",
64          ".....sskkkkkkkkwwwwcccwdwwwkwwww",
65          "....ssskkkkkkkkkkkkkkwwpwwwkk...",
66          ".....ssssskkkkkkkkwwwwwkkkkk....",
67          "........sssssssssssss..........."
68        ],
69        [
70          "................................",
71          ".............................aa.",
72          ".............................aa.",
73          "................................",
74          "...........................aak..",
75          "..........kkkkkkkkpk.......aak..",
76          ".......kkccckkkkkkpkkkkkkkkkpk..",
77          "......kkccccckkkkkkccckwkkkpkk..",
78          ".....kkkkccckkkkkkcdddwwwdddkk..",
79          ".....kkksskkkkkkkkccccwwwwkkkk..",
80          ".....sskkkkkkkkwwwwcccwdwwwkwwww",
81          "....ssskkkkkkkkkkkkkkwwpwwwkk...",
82          ".....ssssskkkkkkkkwwwwwkkkkk....",
83          "........sssssssssssss..........."
84        ]
85      ],
86      "periodMs": 800
87    },
88    "watching": {
89      "label": "작업을 지켜보는 중",
90      "frames": [
91        [
92          "..........kk.........kk.........",
93          "..........kpk.......kpk.........",
94          "..........kkpkkkkkkkpkk.........",
95          "..........kkccckwkkkpkk.........",
96          "..........kcedecwkedekk.........",
97          "...........ccccwwwwkkkk.........",
98          "........wwwwccwwdwwkkwwww.......",
99          "............kkwwpwwwkk..........",
100          "..........kkkkkkkkkkkk......s...",
101          ".........kccwwwwwwwkccc.....s...",
102          ".........ccccwwwwwwkccc.....s...",
103          ".........kcwwwwwwwwwwwc....k....",
104          "..........kwwwwwwwkwwwkkkkk.....",
105          "............kkkkkkkk............"
106        ],
107        [
108          "..........kk.........kk.........",
109          "..........kpk.......kpk.........",
110          "..........kkpkkkkkkkpkk.........",
111          "..........kkccckwkkkpkk.........",
112          "..........kcedecwkedekk.........",
113          "...........ccccwwwwkkkk.........",
114          "........wwwwccwwdwwkkwwww.......",
115          "............kkwwpwwwkk.......s..",
116          "..........kkkkkkkkkkkk.......s..",
117          ".........kccwwwwwwwkccc.....s...",
118          ".........ccccwwwwwwkccc.....s...",
119          ".........kcwwwwwwwwwwwc....k....",
120          "..........kwwwwwwwkwwwkkkkk.....",
121          "............kkkkkkkk............"
122        ],
123        [
124          "..........kk.........kk.........",
125          "..........kpk.......kpk.........",
126          "..........kkpkkkkkkkpkk.........",
127          "..........kkccckwkkkpkk.........",
128          "..........kcedecwkedekk.........",
129          "...........ccccwwwwkkkk.........",
130          "........wwwwccwwdwwkkwwww.......",
131          "............kkwwpwwwkk..........",
132          "..........kkkkkkkkkkkk......s...",
133          ".........kccwwwwwwwkccc.....s...",
134          ".........ccccwwwwwwkccc.....s...",
135          ".........kcwwwwwwwwwwwc....k....",
136          "..........kwwwwwwwkwwwkkkkk.....",
137          "............kkkkkkkk............"
138        ],
139        [
140          "..........kk.........kk.........",
141          "..........kpk.......kpk.........",
142          "..........kkpkkkkkkkpkk.........",
143          "..........kkccckwkkkpkk.........",
144          "..........kcdddcwkdddkk.........",
145          "...........ccccwwwwkkkk.........",
146          "........wwwwccwwdwwkkwwww.......",
147          "............kkwwpwwwkk.......s..",
148          "..........kkkkkkkkkkkk.......s..",
149          ".........kccwwwwwwwkccc.....s...",
150          ".........ccccwwwwwwkccc.....s...",
151          ".........kcwwwwwwwwwwwc....k....",
152          "..........kwwwwwwwkwwwkkkkk.....",
153          "............kkkkkkkk............"
154        ]
155      ],
156      "periodMs": 800
157    },
158    "working": {
159      "label": "함께 작업 중",
160      "frames": [
161        [
162          "..........kk.........kk.........",
163          "..........kpk.......kpk.........",
164          "..........kkpkkkkkkkpkk.........",
165          "..........kkccckwkkkpkk.........",
166          "..........kcedecwkedekk.........",
167          "...........ccccwwwwkkkk.........",
168          "........wwwwccwwdwwkkwwww.......",
169          "............kkwwpwwwkk..........",
170          "..........kkkkkkkkkkkk......s...",
171          ".........kccwwwwwwwkccc.....s...",
172          ".........ccccwwwwwwwwwc.....s...",
173          ".........kcwwwwwwwwwwwc....k....",
174          ".........dddddddddddddddkkk.....",
175          "........asasasasasasasasa......."
176        ],
177        [
178          "..........kk.........kk.........",
179          "..........kpk.......kpk.........",
180          "..........kkpkkkkkkkpkk.........",
181          "..........kkccckwkkkpkk.........",
182          "..........kcedecwkedekk.........",
183          "...........ccccwwwwkkkk.........",
184          "........wwwwccwwdwwkkwwww.......",
185          "............kkwwpwwwkk.......s..",
186          "..........kkkkkkkkkkkk.......s..",
187          ".........kccwwwwwwwkccc.....s...",
188          ".........ccwwwwwwwwkccc.....s...",
189          ".........kcwwwwwwwwwwwc....k....",
190          ".........dddddddddddddddkkk.....",
191          "........asasasasasasasasa......."
192        ],
193        [
194          "..........kk.........kk.........",
195          "..........kpk.......kpk.........",
196          "..........kkpkkkkkkkpkk.........",
197          "..........kkccckwkkkpkk.........",
198          "..........kcedecwkedekk.........",
199          "...........ccccwwwwkkkk.........",
200          "........wwwwccwwdwwkkwwww.......",
201          "............kkwwpwwwkk..........",
202          "..........kkkkkkkkkkkk......s...",
203          ".........kccwwwwwwwkccc.....s...",
204          ".........ccccwwwwwwwwwc.....s...",
205          ".........kcwwwwwwwwwwwc....k....",
206          ".........dddddddddddddddkkk.....",
207          "........asasasasasasasasa......."
208        ],
209        [
210          "..........kk.........kk.........",
211          "..........kpk.......kpk.........",
212          "..........kkpkkkkkkkpkk.........",
213          "..........kkccckwkkkpkk.........",
214          "..........kcdddcwkdddkk.........",
215          "...........ccccwwwwkkkk.........",
216          "........wwwwccwwdwwkkwwww.......",
217          "............kkwwpwwwkk.......s..",
218          "..........kkkkkkkkkkkk.......s..",
219          ".........kccwwwwwwwkccc.....s...",
220          ".........ccwwwwwwwwkccc.....s...",
221          ".........kcwwwwwwwwwwwc....k....",
222          ".........dddddddddddddddkkk.....",
223          "........asasasasasasasasa......."
224        ]
225      ],
226      "periodMs": 450
227    },
228    "busy": {
229      "label": "바쁘게 작업 중",
230      "frames": [
231        [
232          "..........kk.........kk.........",
233          "..........kpk.......kpk.........",
234          "..........kkpkkkkkkkpkk.........",
235          "..........kkccckwkkkpkk.........",
236          ".....aa...kcedecwkedekk....a....",
237          "....a......ccccwwwwkkkk.........",
238          "........wwwwccwwdwwkkwwww.......",
239          "............kkwwpwwwkk..........",
240          "..........kkkkkkkkkkkk......s...",
241          ".........kccwwwwwwwkccc.....s...",
242          ".........ccccwwwwwwwwwc.....s...",
243          ".........kcwwwwwwwwwwwc....k....",
244          ".........dddddddddddddddkkk.....",
245          "........asasasasasasasasa......."
246        ],
247        [
248          "...........kk.........kk........",
249          "...........kpk.......kpk........",
250          "...........kpkkkkkkkkkpk........",
251          "...........kkccckwkkkpkk........",
252          "......aa...kcedewwwedekk........",
253          ".....a......ccccwwwwkkkk...a....",
254          ".........wwwwcccwdwwwkwwww......",
255          "............kkkwwpwwwkk......s..",
256          "..........kkkwkkkkkkkk.......s..",
257          ".........kccwwwwwwwkccc.....s...",
258          ".........ccwwwwwwwwkccc.....s...",
259          ".........kcwwwwwwwwwwwc....k....",
260          ".........dddddddddddddddkkk.....",
261          "........asasasasasasasasa......."
262        ],
263        [
264          "..........kk.........kk.........",
265          "..........kpk.......kpk.........",
266          "..........kkpkkkkkkkpkk.........",
267          "..........kkccckwkkkpkk.........",
268          ".....aa...kcedecwkedekk....a....",
269          "....a......ccccwwwwkkkk.........",
270          "........wwwwccwwdwwkkwwww.......",
271          "............kkwwpwwwkk..........",
272          "..........kkkkkkkkkkkk......s...",
273          ".........kccwwwwwwwkccc.....s...",
274          ".........ccccwwwwwwwwwc.....s...",
275          ".........kcwwwwwwwwwwwc....k....",
276          ".........dddddddddddddddkkk.....",
277          "........asasasasasasasasa......."
278        ],
279        [
280          "...........kk.........kk........",
281          "...........kpk.......kpk........",
282          "...........kpkkkkkkkkkpk........",
283          "...........kkccckwkkkpkk........",
284          "......aa...kcedewwwedekk........",
285          ".....a......ccccwwwwkkkk...a....",
286          ".........wwwwcccwdwwwkwwww......",
287          "............kkkwwpwwwkk......s..",
288          "..........kkkwkkkkkkkk.......s..",
289          ".........kccwwwwwwwkccc.....s...",
290          ".........ccwwwwwwwwkccc.....s...",
291          ".........kcwwwwwwwwwwwc....k....",
292          ".........dddddddddddddddkkk.....",
293          "........asasasasasasasasa......."
294        ]
295      ],
296      "periodMs": 220
297    },
298    "waiting": {
299      "label": "확인을 기다리는 중",
300      "frames": [
301        [
302          "..........kk.........kk.........",
303          "..........kpk.......kpk....a....",
304          "..........kkpkkkkkkkpkk...a.a...",
305          "..........kkccckwkkkpkk....a....",
306          "..........kcedecwkkkkkk.........",
307          "...........ccccwwwedekk....a....",
308          "........wwwwccwwdwwkkwwww.......",
309          "............kkwwpwwwkk..........",
310          ".......wwwkkkkkkkkkkkk......s...",
311          ".......cccccwwwwwwwkccc.....s...",
312          "........cccccwwwwwwkccc.....s...",
313          ".........kccwwwwwwwkccc....k....",
314          "..........kkkwwwwwkkkkkkkkk.....",
315          "............kkkkkkkk............"
316        ],
317        [
318          "..........kk.........kk.........",
319          "..........kpk.......kpk....a....",
320          "..........kkpkkkkkkkpkk...a.a...",
321          "..........kkccckwkkkpkk....a....",
322          "..........kcedecwkkkkkk.........",
323          "...........ccccwwwedekk....a....",
324          "........wwwwccwwdwwkkwwww.......",
325          "............kkwwpwwwkk.......s..",
326          "..........kkkkkkkkkkkk.......s..",
327          ".......wwwccwwwwwwwkccc.....s...",
328          ".......ccccccwwwwwwkccc.....s...",
329          "........ccccwwwwwwwkccc....k....",
330          "..........kkkwwwwwkkkkkkkkk.....",
331          "............kkkkkkkk............"
332        ],
333        [
334          "..........kk.........kk.........",
335          "..........kpk.......kpk....a....",
336          "..........kkpkkkkkkkpkk...a.a...",
337          "..........kkccckwkkkpkk....a....",
338          "..........kcedecwkkkkkk.........",
339          "...........ccccwwwedekk....a....",
340          "........wwwwccwwdwwkkwwww.......",
341          "............kkwwpwwwkk..........",
342          ".......wwwkkkkkkkkkkkk......s...",
343          ".......cccccwwwwwwwkccc.....s...",
344          "........cccccwwwwwwkccc.....s...",
345          ".........kccwwwwwwwkccc....k....",
346          "..........kkkwwwwwkkkkkkkkk.....",
347          "............kkkkkkkk............"
348        ],
349        [
350          "..........kk.........kk.........",
351          "..........kpk.......kpk....a....",
352          "..........kkpkkkkkkkpkk...a.a...",
353          "..........kkccckwkkkpkk....a....",
354          "..........kcdddcwkkkkkk.........",
355          "...........ccccwwwdddkk....a....",
356          "........wwwwccwwdwwkkwwww.......",
357          "............kkwwpwwwkk.......s..",
358          "..........kkkkkkkkkkkk.......s..",
359          ".......wwwccwwwwwwwkccc.....s...",
360          ".......ccccccwwwwwwkccc.....s...",
361          "........ccccwwwwwwwkccc....k....",
362          "..........kkkwwwwwkkkkkkkkk.....",
363          "............kkkkkkkk............"
364        ]
365      ],
366      "periodMs": 800
367    },
368    "error": {
369      "label": "문제를 살펴보는 중",
370      "frames": [
371        [
372          ".....................kk.........",
373          "...........p........kpk.........",
374          "..........kkpkkkkkkkpkk.........",
375          "..........kkccckwkkkpkk.........",
376          "..........kcedecwkedekk.........",
377          "...........ccccwwwwkkkk.........",
378          "........wwwwccwwdwwkkwwww.......",
379          "............kkwwpwwwkk..........",
380          "..........kkkkkkkkkkkk......s...",
381          ".........kccwwwwwwwkccc.....s...",
382          "....r....ccccwwwwwwkccc.....s...",
383          ".........kccwwwwwwwkccc....k....",
384          "....r.wwwwkkkwwwwwkkkkkkkkk.....",
385          "............kkkkkkkk............"
386        ],
387        [
388          ".....................kk.........",
389          "...........p........kpk.........",
390          "..........kkpkkkkkkkpkk.........",
391          "..........kkccckwkkkpkk.........",
392          "..........kcedecwkedekk.........",
393          "...........ccccwwwwkkkk.........",
394          "........wwwwccwwdwwkkwwww.......",
395          "............kkwwpwwwkk.......s..",
396          "..........kkkkkkkkkkkk.......s..",
397          ".........kccwwwwwwwkccc.....s...",
398          "....r....ccccwwwwwwkccc.....s...",
399          ".........kccwwwwwwwkccc....k....",
400          "....r.wwwwkkkwwwwwkkkkkkkkk.....",
401          "............kkkkkkkk............"
402        ],
403        [
404          ".....................kk.........",
405          "...........p........kpk.........",
406          "..........kkpkkkkkkkpkk.........",
407          "..........kkccckwkkkpkk.........",
408          "..........kcedecwkedekk.........",
409          "...........ccccwwwwkkkk.........",
410          "........wwwwccwwdwwkkwwww.......",
411          "............kkwwpwwwkk..........",
412          "..........kkkkkkkkkkkk......s...",
413          ".........kccwwwwwwwkccc.....s...",
414          "....r....ccccwwwwwwkccc.....s...",
415          ".........kccwwwwwwwkccc....k....",
416          "....r.wwwwkkkwwwwwkkkkkkkkk.....",
417          "............kkkkkkkk............"
418        ],
419        [
420          ".....................kk.........",
421          "...........p........kpk.........",
422          "..........kkpkkkkkkkpkk.........",
423          "..........kkccckwkkkpkk.........",
424          "..........kcdddcwkdddkk.........",
425          "...........ccccwwwwkkkk.........",
426          "........wwwwccwwdwwkkwwww.......",
427          "............kkwwpwwwkk.......s..",
428          "..........kkkkkkkkkkkk.......s..",
429          ".........kccwwwwwwwkccc.....s...",
430          "....r....ccccwwwwwwkccc.....s...",
431          ".........kccwwwwwwwkccc....k....",
432          "....r.wwwwkkkwwwwwkkkkkkkkk.....",
433          "............kkkkkkkk............"
434        ]
435      ],
436      "periodMs": 800
437    },
438    "done": {
439      "label": "작업 끝! 기지개",
440      "frames": [
441        [
442          "................................",
443          "..k.............................",
444          "...k............................",
445          "....k........................e..",
446          ".....sss..kkkkkkkkk.k.......e...",
447          "........skkkkcckkkkkkkk..k......",
448          ".......kkkkkcccckkkckkkkkk....e.",
449          ".......kkkkkkcckkkcckkccckk.....",
450          "........kckkkkkkkkkcckccdkkk....",
451          "........c.kkkkkkkkk..kccwwkdk...",
452          "........c.........c...kkkkwwk...",
453          ".......c...........cc...ckkk....",
454          "......wwww...........cc..wwwww..",
455          ".......................wwwww...."
456        ],
457        [
458          "................................",
459          "..k.............................",
460          "...kk...........................",
461          ".....s.......................e..",
462          "......ss............k.......e...",
463          "........s.kkkcckkkk.kkk..k......",
464          "........kkkkcccckkkckkkkkk....e.",
465          ".......kkkkkkcckkkcckkccckk.....",
466          "........kckkkkkkkkkcckccdkkk....",
467          "........c.kkkkkkkkk..kccwwkdk...",
468          "........c.........c...kkkkwwk...",
469          ".......c...........cc...ckkk....",
470          "......wwww...........cc..wwwww..",
471          ".......................wwwww...."
472        ],
473        [
474          "................................",
475          "..k.............................",
476          "...k............................",
477          "....k........................e..",
478          ".....sss..kkkkkkkkk.k.......e...",
479          "........skkkkcckkkkkkkk..k......",
480          ".......kkkkkcccckkkckkkkkk....e.",
481          ".......kkkkkkcckkkcckkccckk.....",
482          "........kckkkkkkkkkcckccdkkk....",
483          "........c.kkkkkkkkk..kccwwkdk...",
484          "........c.........c...kkkkwwk...",
485          ".......c...........cc...ckkk....",
486          "......wwww...........cc..wwwww..",
487          ".......................wwwww...."
488        ],
489        [
490          "................................",
491          "..k.............................",
492          "...kk...........................",
493          ".....s.......................e..",
494          "......ss............k.......e...",
495          "........s.kkkcckkkk.kkk..k......",
496          "........kkkkcccckkkckkkkkk....e.",
497          ".......kkkkkkcckkkcckkccckk.....",
498          "........kckkkkkkkkkcckccdkkk....",
499          "........c.kkkkkkkkk..kccwwkdk...",
500          "........c.........c...kkkkwwk...",
501          ".......c...........cc...ckkk....",
502          "......wwww...........cc..wwwww..",
503          ".......................wwwww...."
504        ]
505      ],
506      "periodMs": 800
507    }
508  },
509  "description": "Black, pale cream, white family cat; asymmetric face, sage eyes, white bib and socks. No orange."
510} as const
511
types/index.d.ts 57 lines
1export type PetActivity = { tick: number; calls: number[]; inFlight: string[]; waiting: string[]; doneAt: number; errorAt: number }
2
3export type AgentRow = {
4  id: string
5  name: string
6  type: string
7  model: string
8  task: string
9  status: string
10  lastAt: number
11}
12
13export type UsageRow = { name: string; count: number; lastAt: number }
14
15export type TaskRow = {
16  id: string
17  label: string
18  status: string
19  startedAt: number
20  lastAt: number
21}
22
23export type Activity = Record<string, { at: number; tool: string }>
24
25export type Models = Record<string, string>
26export type LogRow = { id: string; owner: string; tool: string; status: string; at: number }
27
28export type MainRow = { status: string; model?: string; tool?: string; lastAt: number }
29
30export type HeaderData = {
31  model?: string
32  branch?: string
33  limits: { kind: string; percentUsed: number; resetsAt?: string }[]
34  updatedAt: number
35  stale?: boolean
36}
37
38declare module 'claude-code' {
39  interface PluginState {
40    'agent-board': {
41      agents: AgentRow[]
42      skills: UsageRow[]
43      mcp: UsageRow[]
44      servers: string[]
45      tasks: TaskRow[]
46      activity: Activity
47      models: Models
48      main: MainRow
49      isOpen: boolean
50      header: HeaderData
51      pet: PetActivity
52      tab: number
53      log: LogRow[]
54    }
55  }
56}
57