SLOPSHOPPER

program-status

Reports the session state to the terminal (OSC 7501) through the program-status command.

newguardprocess
A shopper browsing a rack in a slop shop
README

Chaosteil's Dotfiles

Description

Just various config files with some setup files for quickly setting up a new dev environment on another computer.

The primary for this is going to be nix, where the instructions on how to install everything is outlined in nix/flake.nix. This is definitely the fastest way.

The configurations are both valid for OSX (either nix-darwin or home-manager) and a Linux system where home-manager is primarily used. Additionally the configs are actually also stow-compatible if so desired.

Screenshot

environment!

Source 1 files
hooks/register.ts 232 lines
1import type { EngineInterface, Register } from 'claude-code'
2
3// Reports the session to the terminal through the program-status command.
4// https://www.superlogical.com/rex/docs/build/program-status
5// The session is the record "claude". Each subagent is a record below it.
6
7type State = 'idle' | 'working' | 'done' | 'blocked' | 'error' | 'clear'
8type Kind = 'permission' | 'question' | 'auth'
9type Report = { state: State; kind?: Kind; msg?: string }
10type Input = Record<string, unknown>
11
12const ROOT = 'claude'
13
14const clip = (text: string, max = 200) => {
15  const line = text.trim().split('\n')[0] ?? ''
16  return line.length > max ? `${line.slice(0, max - 1)}…` : line
17}
18
19const basename = (path: string) => path.split('/').filter(Boolean).pop() ?? path
20
21const host = (url: string) => {
22  try {
23    return new URL(url).host
24  } catch {
25    return url
26  }
27}
28
29const str = (input: Input, key: string) => (typeof input[key] === 'string' ? (input[key] as string) : '')
30
31// One line about what a tool call does.
32const activity = (tool: string, input: Input): string => {
33  switch (tool) {
34    case 'Bash':
35      return clip(str(input, 'description') || str(input, 'command'))
36    case 'Read':
37    case 'Edit':
38    case 'MultiEdit':
39    case 'Write':
40      return `${tool} ${basename(str(input, 'file_path'))}`
41    case 'NotebookEdit':
42      return `Edit ${basename(str(input, 'notebook_path'))}`
43    case 'Grep':
44    case 'Glob':
45      return clip(`Search ${str(input, 'pattern')}`)
46    case 'WebFetch':
47      return `Fetch ${host(str(input, 'url'))}`
48    case 'WebSearch':
49      return clip(`Search the web: ${str(input, 'query')}`)
50    case 'Agent':
51    case 'Task':
52      return clip(`Agent: ${str(input, 'description')}`)
53    case 'Skill':
54      return `Skill ${str(input, 'skill')}`
55    case 'AskUserQuestion': {
56      const questions = input.questions as { question?: string }[] | undefined
57      return clip(questions?.[0]?.question ?? 'Question')
58    }
59    case 'ExitPlanMode':
60      return 'Review the plan'
61  }
62  if (tool.startsWith('mcp__')) {
63    const [, server, name] = tool.split('__')
64    return `${server}: ${name}`
65  }
66  return tool
67}
68
69// The tools that wait for an answer, not for permission.
70const QUESTIONS = new Set(['AskUserQuestion', 'ExitPlanMode'])
71
72const state = {
73  isEnabled: false,
74  title: '',
75  main: undefined as Report | undefined,
76  // Subagent record ids and their agent types.
77  agents: new Map<string, string>(),
78  tail: Promise.resolve(),
79}
80
81// Runs program-status. The calls run in order, so the last report wins.
82function send($: EngineInterface, id: string, report: Report, extra: string[] = []): Promise<void> {
83  if (!state.isEnabled) return Promise.resolve()
84  const argv = ['program-status', report.state, `id=${id}`, ...extra]
85  if (report.kind) argv.push(`kind=${report.kind}`)
86  if (report.msg) argv.push(`msg=${report.msg}`)
87  state.tail = state.tail
88    .then(() => $.process.run(argv, { timeoutMs: 5000 }))
89    .then(
90      () => undefined,
91      () => undefined,
92    )
93  return state.tail
94}
95
96// Reports the session record when it changes.
97function report($: EngineInterface, next: Report): Promise<void> {
98  const { main } = state
99  if (main?.state === next.state && main.kind === next.kind && main.msg === next.msg) {
100    return Promise.resolve()
101  }
102  state.main = next
103  return send($, ROOT, next, ['app=claude', `title=${state.title}`])
104}
105
106const agentId = (id: string) => `${ROOT}/${id.replace(/[^A-Za-z0-9_.-]/g, '').slice(0, 32)}`
107
108export const register: Register = on => {
109  on('session.start', async ($, e, next) => {
110    state.isEnabled = e.isInteractive
111    state.title = basename(e.cwd)
112    state.main = undefined
113    await report($, { state: 'idle' })
114    return next(e)
115  })
116
117  on('session.end', async ($, e, next) => {
118    state.main = undefined
119    state.agents.clear()
120    await send($, ROOT, { state: 'clear' })
121    // A /clear goes on with a new conversation in the same process.
122    if (e.reason === 'clear') await report($, { state: 'idle' })
123    return next(e)
124  })
125
126  on('turn.start', async ($, e, next) => {
127    await report($, { state: 'working', msg: 'Thinking' })
128    return next(e)
129  })
130
131  on('turn.complete', async ($, e, next) => {
132    // Subagents and the engine's own forks have an agentId.
133    if (e.agentId === undefined) {
134      // No answer: the person dismissed a question or a permission prompt.
135      if (e.reason === 'answer') await report($, e.answer.trim() ? { state: 'done', msg: clip(e.answer) } : { state: 'idle' })
136      else if (e.reason === 'aborted') await report($, { state: 'idle', msg: 'Interrupted' })
137      else if (e.reason === 'refusal') await report($, { state: 'error', msg: 'The model refused' })
138      else if (state.main?.state !== 'error') await report($, { state: 'error', msg: 'The turn failed' })
139    }
140    return next(e)
141  })
142
143  on('classic.StopFailure', async ($, e, next) => {
144    await report($, { state: 'error', msg: clip(e.error_details ?? String(e.error)) })
145    return next(e)
146  })
147
148  on('tool.call', async ($, e, next) => {
149    const input = e as unknown as Input
150    const msg = activity(e.tool, input)
151    let started: Promise<void>
152    if (e.agentId !== undefined) {
153      const id = agentId(e.agentId)
154      const type = state.agents.get(id)
155      started = type === undefined ? Promise.resolve() : send($, id, { state: 'working', msg }, [`title=${type}`])
156    } else if (QUESTIONS.has(e.tool)) {
157      started = report($, { state: 'blocked', kind: 'question', msg })
158    } else {
159      started = report($, { state: 'working', msg })
160    }
161    const result = await next(e)
162    await started
163    if (state.main?.state === 'blocked') {
164      await report($, { state: 'working', msg: 'Thinking' })
165    }
166    return result
167  })
168
169  on('classic.PermissionRequest', async ($, e, next) => {
170    const input = (e.tool_input ?? {}) as Input
171    const kind: Kind = QUESTIONS.has(e.tool_name) ? 'question' : 'permission'
172    const msg = kind === 'question' ? activity(e.tool_name, input) : clip(`Allow ${activity(e.tool_name, input)}?`)
173    await report($, { state: 'blocked', kind, msg })
174    return next(e)
175  })
176
177  // The background hint shows only while an approved command runs.
178  on('ui.render', { component: 'ToolProgress' }, async ($, e, next) => {
179    if (state.main?.state === 'blocked' && state.main.kind === 'permission') {
180      await report($, { state: 'working', msg: 'Running' })
181    }
182    return next(e)
183  })
184
185  on('classic.Notification', async ($, e, next) => {
186    if (state.main?.state !== 'blocked') {
187      if (e.notification_type === 'permission_prompt') {
188        await report($, { state: 'blocked', kind: 'permission', msg: clip(e.message) })
189      } else if (e.notification_type === 'elicitation_dialog') {
190        await report($, { state: 'blocked', kind: 'question', msg: clip(e.message) })
191      }
192    }
193    return next(e)
194  })
195
196  on('classic.Elicitation', async ($, e, next) => {
197    const kind: Kind = e.mode === 'url' ? 'auth' : 'question'
198    await report($, { state: 'blocked', kind, msg: clip(`${e.mcp_server_name}: ${e.message}`) })
199    return next(e)
200  })
201
202  on('classic.ElicitationResult', async ($, e, next) => {
203    await report($, { state: 'working', msg: 'Thinking' })
204    return next(e)
205  })
206
207  on('classic.PreCompact', async ($, e, next) => {
208    await report($, { state: 'working', msg: 'Compacting' })
209    return next(e)
210  })
211
212  on('classic.PostCompact', async ($, e, next) => {
213    // A manual /compact runs at the prompt. An automatic one runs in a turn.
214    await report($, e.trigger === 'manual' ? { state: 'idle' } : { state: 'working', msg: 'Thinking' })
215    return next(e)
216  })
217
218  on('classic.SubagentStart', async ($, e, next) => {
219    const id = agentId(e.agent_id)
220    state.agents.set(id, e.agent_type)
221    await send($, id, { state: 'working' }, [`title=${e.agent_type}`])
222    return next(e)
223  })
224
225  on('classic.SubagentStop', async ($, e, next) => {
226    const id = agentId(e.agent_id)
227    state.agents.delete(id)
228    await send($, id, { state: 'clear' })
229    return next(e)
230  })
231}
232