SLOPSHOPPER

dash

A unified status dashboard pane: usage, subagents, and clm.

newpanetimeragents
★ 1v0.1.0no licenseupdated 2026-10-15RanolP/.dotfiles/nix/home/configs/claude/mods/dash
A shopper browsing a rack in a slop shop
README

.dotfiles

Installation

Windows CMD

Requirements:

  • Windows CMD
  • curl executable
  • Winget executable (>= v1.6.2631, follow instruction from winget repository if you don't have or have a lower version)
curl -L dotfiles.ranolp.dev/setup | cmd /Q

Windows PowerShell (TODO)

Requirements:

  • Windows PowerShell
  • Winget executable (>= v1.6.2631, follow instruction from winget repository if you don't have or have a lower version)
curl dotfiles.ranolp.dev/setup | iex

Windows ArchWSL (WIP)

Requirements:

  • ArchWSL Bash
  • curl executable
curl -L dotfiles.ranolp.dev/setup | sh

macOS (TODO)

Requirements:

  • macOS zsh
  • curl executable
curl -L dotfiles.ranolp.dev/setup | sh
Source 3 files
hooks/register.tsx 322 lines
1import { atom, read, update } from 'claude-code'
2import type {
3  DashDashboardState, EngineInterface, Register, SessionUsage, Timer,
4} from 'claude-code'
5import type { TrackedAgent, UsageSnapshot } from '../types'
6import { reconcileAgents, recordCompletion, recordMessage, recordSpawn } from './fleet'
7
8type Elements = ReturnType<EngineInterface['ui']['resolve']>
9type ClmStatus = Awaited<ReturnType<EngineInterface['clm']['status']>>
10
11const PANE = 'dash'
12const TITLE = 'Dash'
13const REFRESH_MS = 15_000
14const dashboard = atom({ plugin: 'dash', key: 'dashboard' } as const, {
15  now: 0, usage: null, agents: [], clm: null,
16} satisfies DashDashboardState)
17
18function errorText(error: unknown): string {
19  return error instanceof Error ? error.message : String(error)
20}
21
22async function log($: EngineInterface, operation: string, error: unknown): Promise<void> {
23  try {
24    await $.ui.log(`[dash] ${operation}: ${errorText(error)}`, { to: 'debug' })
25  } catch {
26    // The original operation's cause is more useful than a second logging failure.
27  }
28}
29
30async function now($: EngineInterface, operation: string): Promise<number | undefined> {
31  try {
32    return await $.clock.now()
33  } catch (error) {
34    await log($, operation, error)
35    return undefined
36  }
37}
38
39async function pass<T>($: EngineInterface, operation: string, run: () => Promise<T>): Promise<T> {
40  try {
41    return await run()
42  } catch (error) {
43    await log($, operation, error)
44    throw error
45  }
46}
47
48async function change($: EngineInterface, operation: string, fn: (state: DashDashboardState) => DashDashboardState): Promise<void> {
49  try {
50    await update($, dashboard, fn)
51  } catch (error) {
52    await log($, operation, error)
53  }
54}
55
56function snapshot(usage: Pick<SessionUsage, 'rateLimits' | 'cost'>): UsageSnapshot {
57  return {
58    rateLimits: usage.rateLimits.map(({ kind, percentUsed, resetsAt }) => ({ kind, percentUsed, ...(resetsAt ? { resetsAt } : {}) })),
59    ...(usage.cost ? { costUsd: usage.cost.usd } : {}),
60  }
61}
62
63async function initialize($: EngineInterface): Promise<void> {
64  const measuredAt = await now($, 'clock.now during startup')
65  if (measuredAt === undefined) return
66
67  const usage = await $.session.usage().then(
68    snapshot,
69    (error: unknown) => log($, 'session.usage during startup', error).then(() => undefined),
70  )
71  const listedAt = (await now($, 'clock.now before startup agent.list')) ?? measuredAt
72  const listed = await $.agent.list().catch(
73    (error: unknown) => log($, 'agent.list during startup', error).then(() => undefined),
74  )
75  const reconciledAt = (await now($, 'clock.now after startup agent.list')) ?? measuredAt
76  await change($, 'startup dashboard state update', state => ({
77    ...state,
78    now: reconciledAt,
79    ...(usage ? { usage } : {}),
80    ...(listed ? { agents: reconcileAgents(state.agents, listed, listedAt, reconciledAt) } : {}),
81  }))
82}
83
84async function refreshClm($: EngineInterface): Promise<void> {
85  try {
86    const status = await $.clm.status()
87    await change($, 'clm status state update', state => ({ ...state, clm: { status } }))
88  } catch (error) {
89    await log($, 'clm.status', error)
90    await change($, 'clm status error state update', state => ({ ...state, clm: { error: errorText(error) } }))
91  }
92}
93
94async function openDash($: EngineInterface, operation: string): Promise<void> {
95  try {
96    await $.ui.open({ id: PANE, title: TITLE })
97  } catch (error) {
98    await log($, operation, error)
99  }
100}
101
102function startTicker($: EngineInterface, isMainSession: boolean, timer?: Timer): Timer | undefined {
103  try {
104    timer?.cancel()
105    return $.clock.every(REFRESH_MS, () => {
106      void (async () => {
107        const current = await now($, 'clock.now for elapsed refresh')
108        if (current !== undefined) {
109          await change($, 'elapsed refresh state update', state => ({ ...state, now: current }))
110        }
111        await refreshClm($)
112        if (isMainSession) await openDash($, 'ui.open dash during refresh')
113      })().catch(error => log($, 'elapsed refresh', error))
114    })
115  } catch (error) {
116    void log($, 'clock.every for elapsed refresh', error)
117    return undefined
118  }
119}
120
121function number(value: number | undefined): string {
122  if (value === undefined || !Number.isFinite(value)) return '—'
123  return Number.isInteger(value) ? String(value) : value.toFixed(1)
124}
125
126function clip(text: string, columns: number): string {
127  const width = Math.max(1, columns)
128  return text.length > width ? `${text.slice(0, Math.max(0, width - 1))}…` : text
129}
130
131function resetTime(iso: string | undefined): string {
132  if (!iso) return ''
133  const time = iso.match(/T(\d{2}:\d{2})/)?.[1]
134  return time ? ` @${time}${iso.endsWith('Z') ? 'Z' : ''}` : ` @${iso.slice(0, 10)}`
135}
136
137function kindLabel(kind: string): string {
138  if (kind === 'five_hour') return '5h'
139  if (kind === 'seven_day') return '7d'
140  if (kind === 'spend_limit') return 'spend'
141  return kind.slice(0, 8)
142}
143
144function typeLabel(type: string): string {
145  return type.startsWith('codex-subagent:') ? `codex:${type.slice('codex-subagent:'.length)}` : type
146}
147
148function timeLabel(value: number, estimated: boolean): string {
149  const clock = new Date(value).toISOString().slice(11, 16)
150  return `${estimated ? '~' : ''}${clock}Z`
151}
152
153function elapsedLabel(agent: TrackedAgent, current: number): string {
154  const elapsed = Math.max(0, (agent.finishedAt ?? current) - agent.startedAt)
155  return durationLabel(elapsed)
156}
157
158function durationLabel(elapsed: number): string {
159  const totalMinutes = Math.floor(elapsed / 60_000)
160  return totalMinutes < 60 ? `${totalMinutes}m` : `${Math.floor(totalMinutes / 60)}h${totalMinutes % 60}m`
161}
162
163function usageRows({ Box, Text }: Elements, state: DashDashboardState, width: number) {
164  const usage = state.usage
165  if (!usage) return <Text dimColor>Usage figures unavailable.</Text>
166  return (
167    <Box flexDirection="column">
168      {usage.rateLimits.length === 0
169        ? <Text dimColor>No rate limits</Text>
170        : usage.rateLimits.map(limit => <Text key={limit.kind}>{clip(`${kindLabel(limit.kind)} ${number(limit.percentUsed)}%${resetTime(limit.resetsAt)}`, width)}</Text>)}
171      {usage.costUsd === undefined ? null : <Text>{clip(`Cost $${usage.costUsd.toFixed(2)}`, width)}</Text>}
172    </Box>
173  )
174}
175
176function fleetRows({ Box, Text }: Elements, state: DashDashboardState, width: number) {
177  if (state.agents.length === 0) return <Text dimColor>No subagents yet.</Text>
178  return (
179    <Box flexDirection="column">
180      {state.agents.map(agent => (
181        <Box key={agent.id} flexDirection="column">
182          <Text>{clip(`id ${agent.id}`, width)}</Text>
183          <Text>{clip(`${typeLabel(agent.type)} · ${timeLabel(agent.startedAt, agent.startEstimated)} · ${elapsedLabel(agent, state.now)} · ${agent.status}`, width)}</Text>
184          <Text dimColor>{clip(agent.lastMessage || 'No message yet', width)}</Text>
185        </Box>
186      ))}
187    </Box>
188  )
189}
190
191function clmStatusRows({ Box, Text }: Elements, status: ClmStatus, current: number, width: number) {
192  return (
193    <Box flexDirection="column">
194      <Text>{clip(status.focus
195        ? `Focus: ${status.focus.title} (${status.focus.status}) · depth ${status.stackDepth}`
196        : 'Focus: none', width)}</Text>
197      <Text>{clip(`Pending: ${status.pending.map(item => item.title).join(', ') || 'none'}`, width)}</Text>
198      <Text>Agents:</Text>
199      {status.agents.length === 0
200        ? <Text dimColor>none</Text>
201        : status.agents.map(agent => <Text key={agent.agentId}>{clip(`${agent.agentId.slice(0, 8)} → ${agent.title} (${agent.status})`, width)}</Text>)}
202      <Text>Later queue:</Text>
203      {status.later.length === 0
204        ? <Text dimColor>none</Text>
205        : status.later.map((item, index) => <Text key={`later-${index}`}>{clip(`${index + 1}. ${item.text}`, width)}</Text>)}
206      <Text>{clip(`Pending resume: ${status.resumeRequest?.text ?? 'none'}`, width)}</Text>
207      <Text>{clip(`Last fold: ${status.lastFoldAt === null ? 'never' : `${durationLabel(Math.max(0, current - status.lastFoldAt))} ago`}`, width)}</Text>
208      <Text>{clip(`Open issues: ${status.openIssues}`, width)}</Text>
209      <Text>{clip(`Ledger: ${status.ledgerPath}`, width)}</Text>
210    </Box>
211  )
212}
213
214function clmRows(elements: Elements, state: DashDashboardState, width: number) {
215  const { Text } = elements
216  if (state.clm === null) return <Text dimColor>{clip('Loading clm status…', width)}</Text>
217  if ('error' in state.clm) return <Text>{clip(`clm is unavailable: ${state.clm.error.replace(/\s+/g, ' ')}`, width)}</Text>
218  return clmStatusRows(elements, state.clm.status, state.now, width)
219}
220
221export const register: Register = on => {
222  let ticker: Timer | undefined
223  let isMainSession = false
224
225  on('session.start', async ($, e, next) => {
226    const started = await pass($, 'session.start', () => next(e))
227    await initialize($)
228    await refreshClm($)
229    isMainSession = !('agentId' in e)
230    ticker = startTicker($, isMainSession, ticker)
231    if (isMainSession) await openDash($, 'ui.open dash at session start')
232    return started
233  })
234
235  on('ui.close', { id: PANE }, async (_$, e, next) => {
236    if (e.origin.kind === 'unload') return next(e)
237    return { value: undefined }
238  })
239
240  on('session.measure', async ($, e, next) => {
241    const result = await pass($, 'session.measure', () => next(e))
242    const measuredAt = await now($, 'clock.now after session.measure')
243    if (measuredAt !== undefined) {
244      await change($, 'session.measure state update', state => ({
245        ...state,
246        now: measuredAt,
247        usage: snapshot({ rateLimits: e.rateLimits, ...(e.cost === undefined ? {} : { cost: e.cost }) }),
248      }))
249    }
250    return result
251  })
252
253  on('agent.spawn', async ($, e, next) => {
254    const spawned = await pass($, 'agent.spawn', () => next(e))
255    const agentId = 'agentId' in spawned ? spawned.agentId : undefined
256    if (agentId) {
257      const startedAt = await now($, `clock.now for agent ${agentId}`)
258      if (startedAt !== undefined) {
259        await change($, `track spawned agent ${agentId}`, state => ({
260          ...state,
261          agents: recordSpawn(state.agents, agentId, e.subagentType, startedAt),
262          now: startedAt,
263        }))
264      }
265    }
266    return spawned
267  })
268
269  on('session.receive', async ($, e, next) => {
270    const received = await pass($, 'session.receive', () => next(e))
271    const { agentId } = e
272    if (agentId) {
273      const receivedAt = await now($, `clock.now for message to agent ${agentId}`)
274      if (receivedAt !== undefined) {
275        await change($, `track message for agent ${agentId}`, state => ({
276          ...state,
277          agents: recordMessage(state.agents, agentId, e.text, receivedAt),
278          now: receivedAt,
279        }))
280      }
281    }
282    return received
283  })
284
285  on('turn.complete', async ($, e, next) => {
286    const completed = await pass($, 'turn.complete', () => next(e))
287    const { agentId } = e
288    if (agentId) {
289      const finishedAt = await now($, `clock.now for completed agent ${agentId}`)
290      if (finishedAt !== undefined) {
291        await change($, `complete agent ${agentId}`, state => ({
292          ...state,
293          agents: recordCompletion(state.agents, agentId, e.answer, finishedAt),
294          now: finishedAt,
295        }))
296      }
297    }
298    return completed
299  })
300
301  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
302    const elements = await pass($, 'ui.resolve for Pane', async () => $.ui.resolve(e))
303    const { Box, Text } = elements
304    const width = Math.max(8, e.props.bodyColumns - 1)
305    const state = await read($, dashboard).catch(
306      (error: unknown) => log($, 'read dashboard for ui.render', error).then(() => undefined),
307    )
308    if (state === undefined) return <Text dimColor>Dash state unavailable. See debug log.</Text>
309
310    return (
311      <Box flexDirection="column">
312        <Text bold>Usage</Text>
313        {usageRows(elements, state, width)}
314        <Text bold>Fleet</Text>
315        {fleetRows(elements, state, width)}
316        <Text bold>Clm</Text>
317        {clmRows(elements, state, width)}
318      </Box>
319    )
320  })
321}
322
hooks/fleet.ts 66 lines
1import type { AgentInfo } from 'claude-code'
2import type { TrackedAgent } from '../types'
3
4const RUNNING_STATUSES = new Set(['running', 'pending', 'queued', 'starting', 'waiting'])
5
6export function excerpt(text: string, limit = 112): string {
7  const flat = text.replace(/\s+/g, ' ').trim()
8  return flat.length > limit ? `${flat.slice(0, Math.max(0, limit - 1))}…` : flat
9}
10
11export function recordSpawn(agents: TrackedAgent[], id: string, type: string, now: number): TrackedAgent[] {
12  const existing = agents.find(agent => agent.id === id)
13  const tracked: TrackedAgent = existing
14    ? { ...existing, type, startedAt: now, startEstimated: false }
15    : { id, type, status: 'running', startedAt: now, startEstimated: false, lastMessage: '' }
16  return [...agents.filter(agent => agent.id !== id), tracked].sort((a, b) => a.startedAt - b.startedAt)
17}
18
19export function reconcileAgents(agents: TrackedAgent[], listed: readonly AgentInfo[], listedAt: number, now: number): TrackedAgent[] {
20  const previous = new Map(agents.map(agent => [agent.id, agent]))
21  const seen = new Set<string>()
22  const merged = listed.map(info => {
23    seen.add(info.id)
24    const prior = previous.get(info.id)
25    const completedAfterListing = prior?.status === 'done' && prior.finishedAt !== undefined && prior.finishedAt > listedAt
26    const isDone = completedAfterListing || !RUNNING_STATUSES.has(info.status.toLowerCase())
27    return {
28      id: info.id,
29      type: info.type || prior?.type || 'agent',
30      status: isDone ? 'done' as const : 'running' as const,
31      startedAt: prior?.startedAt ?? now,
32      startEstimated: prior?.startEstimated ?? true,
33      ...(isDone ? { finishedAt: prior?.finishedAt ?? now } : {}),
34      lastMessage: prior?.lastMessage ?? '',
35    }
36  })
37  for (const agent of agents) {
38    if (seen.has(agent.id)) continue
39    merged.push(agent.status === 'done' ? agent : { ...agent, status: 'done', finishedAt: now })
40  }
41  return merged.sort((a, b) => a.startedAt - b.startedAt)
42}
43
44export function recordMessage(agents: TrackedAgent[], id: string, text: string, now: number): TrackedAgent[] {
45  const existing = agents.find(agent => agent.id === id)
46  const tracked: TrackedAgent = existing
47    ? {
48      id: existing.id,
49      type: existing.type,
50      status: 'running',
51      startedAt: existing.startedAt,
52      startEstimated: existing.startEstimated,
53      lastMessage: excerpt(text),
54    }
55    : { id, type: 'agent', status: 'running', startedAt: now, startEstimated: true, lastMessage: excerpt(text) }
56  return [...agents.filter(agent => agent.id !== id), tracked].sort((a, b) => a.startedAt - b.startedAt)
57}
58
59export function recordCompletion(agents: TrackedAgent[], id: string, text: string, now: number): TrackedAgent[] {
60  const existing = agents.find(agent => agent.id === id)
61  const tracked: TrackedAgent = existing
62    ? { ...existing, status: 'done', finishedAt: now, lastMessage: excerpt(text) }
63    : { id, type: 'agent', status: 'done', startedAt: now, startEstimated: true, finishedAt: now, lastMessage: excerpt(text) }
64  return [...agents.filter(agent => agent.id !== id), tracked].sort((a, b) => a.startedAt - b.startedAt)
65}
66
types/index.d.ts 28 lines
1export type UsageSnapshot = {
2  rateLimits: { kind: string; percentUsed: number; resetsAt?: string }[]
3  costUsd?: number
4}
5
6export type TrackedAgent = {
7  id: string
8  type: string
9  status: 'running' | 'done'
10  startedAt: number
11  startEstimated: boolean
12  finishedAt?: number
13  lastMessage: string
14}
15
16declare module 'claude-code' {
17  export type DashDashboardState = {
18    now: number
19    usage: UsageSnapshot | null
20    agents: TrackedAgent[]
21    clm: { status: Awaited<ReturnType<EngineInterface['clm']['status']>> } | { error: string } | null
22  }
23
24  interface PluginState {
25    dash: { dashboard: DashDashboardState }
26  }
27}
28