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

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에 적용하지 않습니다.
계정 공유 한도는 실제 제공된 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의 상표입니다. 사용자용 커스텀 테마입니다.
hooks/register.tsx 340 lines1import { 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}
340hooks/view.tsx 209 lines1import 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}
209hooks/pet.tsx 52 lines1import 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)
52hooks/logo.ts 4 lines1// 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 = [" ⣿⣆ ⢰⡆ ⣀ ", " ⢶⣦⣘⢿⣆⣿⣣⣾⡟⠁ ", "⣀⣀⣈⣛⣿⣿⣿⣿⣯⣤⣤⠦", "⠈⠉⢉⣩⣽⣿⣿⣿⡟⠶⠶⡦", " ⠰⠛⣩⡾⢹⡏⢻⣟⠷⡄ ", " ⠐⠋ ⢾⠇ ⠙⠁ "]
4hooks/cat-sprites.ts 511 lines1// 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
511types/index.d.ts 57 lines1export 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