One short row in place of each tracker write in the transcript.

quiet-items draws one short row in place of each tracker write in the transcript. The model still reads the full tool result. Only the screen changes.
● work item created handily-ab12 Draw text mocks for the mods open P2
● !raw tracker edit! .beads/issues.jsonl ~Edit, not through the tracker CLI~
The approved mocks are in docs/mocks.md, section 1.
| Tool | Match | Row |
|---|---|---|
Bash | A tracker CLI write: br, bd, basicly tracker, .basicly/core/kit/tracker/cli.py | One row per item of the refresh diff |
Write, Edit | A tracker file at the workitems root: .beads/issues.jsonl, .basicly/ledger/ (pending-*.jsonl, events-*.jsonl, snapshot.jsonl), .beans/**/<id>--<slug>.md | One raw tracker edit row |
$.workitems.classify(command) reads a Bash command, and $.workitems.trackerFile names a tracker file. The rules are in the workitems mod, so item-toasts uses the same rules.$.workitems.writeVerbs(). bd uses the verbs of br.basicly tracker write -- <verb> uses the verbs of .basicly/core/kit/tracker/cli.py.--help, -h or --dry-run is not a write.The parser uses an allowlist. The mod goes quiet only when the command has one of the allowed shapes. Every other command draws the engine row.
A command goes quiet only when all of these hold:
br, bd or basicly;python3 or python, directly followed by .basicly/core/kit/tracker/cli.py;.basicly/core/kit/tracker/cli.py alone.cd with one word.cd comes before a segment that runs the kit path. A cd before br, bd or basicly is allowed.&& joins two segments. A trailing status echo is the one exception (see below).A-Z a-z 0-9 . _ / : = @ , + % -;$, a backtick or a backslash.| Command | Row | Why | ||
|---|---|---|---|---|
br close a, br update a --title "Fix the parser" | Quiet | An allowed shape | ||
br update a --title '$(id)' | Quiet | The shell does not expand text in single quotes | ||
cd x && br close a && br update b --status open | Quiet | && stops at the first failure, so the exit status shows it | ||
br close a && echo ok | Quiet | The same as above | ||
br close a; echo "exit=$?" | Quiet only when the last output line is exit=0 | The echo prints the exit status of the tracker command | ||
br close a; echo ok | Engine | The echo hides the exit status of the tracker command | ||
br close a; br close b, `br close a \ | \ | br close b` | Engine | The second write hides the exit status of the first |
br close a; git log, `br close a \ | tail -1` | Engine | A segment that is not a tracker write can hide a failure | |
br update a --title "$(id)", $'…', a backtick, \ | Engine | The shell expands or runs it | ||
br close x-*, br close {a,b}, ~/notes, $HOME | Engine | The shell expands it | ||
br close a > out.txt, 2>&1, < in.txt, <(…) | Engine | A redirection, also to /dev/null, changes where the input or output goes | ||
br close a & id, (br close a), br close a # note | Engine | The parser does not read a background job, a subshell or a comment | ||
FOO=1 br close a, env …, sudo … | Engine | Only a bare tracker program may start a segment | ||
uvx br close a, npx br close a | Engine | The wrapper fetches a package from a registry | ||
uv run python3 .basicly/core/kit/tracker/cli.py close a | Engine | uv run syncs the project first, and a build backend in the tree can run any code | ||
python3 -I …, python3 -c… | Engine | No option of python may appear | ||
/tmp/br close a, ./br close a, an absolute kit path | Engine | Only a bare tracker name or the exact relative kit path is allowed | ||
cd x && python3 .basicly/core/kit/tracker/cli.py close a | Engine | After cd, the relative kit path names a script in another directory | ||
| A command longer than 8192 characters | Engine | The parser does not read it. It gives opaque when a tracker name appears |
A trailing echo goes quiet only when it comes after ;, a newline or &&. Its words must pass the word rule, and $? is the one $ that it may hold. A word that starts with - keeps the engine row.
The parser gives one of three results:
write or echoed: the command has an allowed shape.opaque: the command names br, bd, basicly or the kit path, but it is not an allowed shape. When the parser cannot read the command, any mention of these names counts.none: the command names no tracker program and no kit path, for example npm test.The parser cannot see inside a script. bash close.sh gives none, even when the script runs a tracker write.
$.workitems.refresh() and keeps the version it reached. A change from before the call is then inside that version.$.workitems.refresh({ since: version }).$.state, under the tool_use_id of the call.ToolUse row draws the rows. The ToolResult block under it draws empty.ToolGroup line (Ran 1 shell command), the mod unfolds that group, so the row shows.The verb of a row is created, updated, closed or commented. commented replaces updated when every matched write is a comment verb.
The engine draws its own row (next(e)) in each of these cases:
workitems state is not ok.off.simple-view mode is off.| Setting | Default | Use |
|---|---|---|
mode | on | The mode when a session starts: on or off |
titleLength | 60 | The number of title characters before the cut (…) |
/quiet-items toggles the mode for this session only. It keeps the mode in $.state, so a change does not reach other sessions. It takes no argument.
When the session starts and the mode key in $.state is unset, the mod writes the mode setting to it. A mod that reads the key, such as simple-view, then sees the active mode. The mod does not overwrite a mode that /quiet-items set before a reload of the plugin.
/simple is one switch for the concise view. While the simple-view mode is off, this mod draws every row as the engine draws it: the ToolUse row, the ToolResult block and a folded ToolGroup line. While simple-view is not installed or its mode is not off, this mod follows its own mode.
mode key of simple-view from $.state. It cannot set that key, because only its owner writes it.simple-view lists quiet-items under dependencies. This mod does not list simple-view, because the two lists would make a cycle. The mod types the key as unknown and checks the value when it reads it. Only the value off turns the rows off./quiet-items still toggles its own mode. While /simple is off, /quiet-items on draws no quiet row until /simple is on again. Its reply says so: quiet-items on for this session, but /simple is off, so tracker writes draw in full.claude --plugin-dir mods
claude plugin test mods/quiet-items
quiet-items depends on workitems, so load the mods folder. The tests load a fake workitems provider, because claude plugin test loads only the mod under test.
hooks/register.tsx 205 lines1import type { EngineInterface, PluginState, Register } from 'claude-code'
2import type { QuietItemsMode, QuietItemsRow, QuietItemsSimpleViewMode } from '../types'
3import { drawEmpty, drawRows, rowsFromDiff } from './row'
4
5declare module 'claude-code' {
6 interface PluginState {
7 'simple-view': { mode: unknown }
8 }
9}
10
11type Snapshot = PluginState['workitems']['snapshot']
12
13const COMMAND = 'quiet-items'
14const DEFAULT_TITLE_LENGTH = 60
15const ON_TEXT = 'quiet-items on for this session. Tracker writes draw as one row.'
16const OFF_TEXT = 'quiet-items off for this session. Tracker commands draw in full.'
17const SIMPLE_VIEW_OFF_TEXT =
18 'quiet-items on for this session, but /simple is off, so tracker writes draw in full.'
19const NO_ARGUMENT_TEXT =
20 '/quiet-items takes no argument; it toggles this session. Set the default with the plugin\'s "mode" setting.'
21const FULL_TAIL = 'so tracker commands draw in full.'
22const EXIT_STATUS = '$?'
23const QUIET_ITEMS_MODE = { plugin: 'quiet-items', key: 'mode' } as const
24const SIMPLE_VIEW_MODE = { plugin: 'simple-view', key: 'mode' } as const
25const SIMPLE_VIEW_OFF: QuietItemsSimpleViewMode = 'off'
26
27type CallResult = Awaited<ReturnType<EngineInterface['tool']['call']>>
28type RefreshResult = Awaited<ReturnType<EngineInterface['workitems']['refresh']>>
29type Parsed = Awaited<ReturnType<EngineInterface['workitems']['classify']>>
30
31function hasSucceeded(result: CallResult): boolean {
32 if (result.deny !== undefined || result.isError === true) return false
33 const output: unknown = result.result
34 if (typeof output !== 'object' || output === null) return true
35 if ('interrupted' in output && output.interrupted === true) return false
36 return !('stderr' in output && typeof output.stderr === 'string' && output.stderr.trim() !== '')
37}
38
39function stdoutOf(result: CallResult): string {
40 const output: unknown = result.result
41 if (typeof output !== 'object' || output === null || !('stdout' in output)) return ''
42 return typeof output.stdout === 'string' ? output.stdout : ''
43}
44
45export function hasEchoedSuccess(line: string, stdout: string): boolean {
46 const printed = stdout.trimEnd().split(/\r?\n/).at(-1) ?? ''
47 return printed.trimEnd() === line.replaceAll(EXIT_STATUS, '0').trimEnd()
48}
49
50function shortReason(reason: string): string {
51 const [first = reason] = reason.split('. ')
52 return first.replace(/\.$/, '')
53}
54
55export function onReply(snapshot: Snapshot | undefined): string {
56 const prefix = 'quiet-items on for this session, but'
57 if (snapshot === undefined) return `${prefix} work items have not been read yet, ${FULL_TAIL}`
58 switch (snapshot.state) {
59 case 'ok':
60 return ON_TEXT
61 case 'failed':
62 return `${prefix} work items are unavailable (${shortReason(snapshot.reason)}), ${FULL_TAIL}`
63 case 'terminal-only':
64 return `${prefix} ${snapshot.sourceLabel} needs a terminal session here, ${FULL_TAIL}`
65 case 'stale':
66 return `${prefix} work items may be out of date (${shortReason(snapshot.reason)}), ${FULL_TAIL}`
67 case 'approval-needed':
68 return `${prefix} work items need your approval to run ${shortReason(snapshot.reason)}, ${FULL_TAIL}`
69 case 'no-tracker':
70 return `${prefix} no tracker was found at the repo root, ${FULL_TAIL}`
71 }
72}
73
74async function isSimpleViewOff($: EngineInterface): Promise<boolean> {
75 const { value: simpleViewMode } = await $.state.get(SIMPLE_VIEW_MODE)
76 return simpleViewMode === SIMPLE_VIEW_OFF
77}
78
79async function drawsQuietRows($: EngineInterface, defaultMode: QuietItemsMode): Promise<boolean> {
80 if (await isSimpleViewOff($)) return false
81 const { value: mode = defaultMode } = await $.state.get(QUIET_ITEMS_MODE)
82 return mode !== 'off'
83}
84
85export const register: Register = (on, options) => {
86 const defaultMode: QuietItemsMode = options.mode === 'off' ? 'off' : 'on'
87 const titleLength =
88 typeof options.titleLength === 'number' ? options.titleLength : DEFAULT_TITLE_LENGTH
89
90 on('session.start', async ($, e, next) => {
91 await $.command.register({
92 name: COMMAND,
93 description: 'Toggle quiet tracker rows for this session',
94 })
95 await $.state.set({ plugin: 'quiet-items', key: 'ready' }, { root: $.plugin.root })
96 const { value: mode, version } = await $.state.get(QUIET_ITEMS_MODE)
97 if (mode === undefined) await $.state.set(QUIET_ITEMS_MODE, defaultMode, { ifVersion: version })
98 return next(e)
99 })
100
101 on('command.run', { command: COMMAND }, async ($, e) => {
102 if (e.args.trim() !== '') return { text: NO_ARGUMENT_TEXT }
103 const { value: current = defaultMode } = await $.state.get(QUIET_ITEMS_MODE)
104 const mode: QuietItemsMode = current === 'on' ? 'off' : 'on'
105 await $.state.set(QUIET_ITEMS_MODE, mode)
106 if (mode === 'off') return { text: OFF_TEXT }
107 if (await isSimpleViewOff($)) return { text: SIMPLE_VIEW_OFF_TEXT }
108 const { value: snapshot } = await $.state.get({ plugin: 'workitems', key: 'snapshot' })
109 return { text: onReply(snapshot) }
110 })
111
112 on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
113 if (e.run_in_background === true) return next(e)
114 let parsed: Parsed
115 try {
116 parsed = await $.workitems.classify(e.command)
117 } catch (error) {
118 $.ui.log(
119 `quiet-items: no row for ${e.tool_use_id}; the command check failed: ${String(error)}`,
120 )
121 return next(e)
122 }
123 if (parsed.kind !== 'write' && parsed.kind !== 'echoed') return next(e)
124 const { value: before } = await $.state.get({ plugin: 'workitems', key: 'snapshot' })
125 if (before?.state !== 'ok') return next(e)
126 let reached: RefreshResult
127 try {
128 reached = await $.workitems.refresh()
129 } catch (error) {
130 $.ui.log(`quiet-items: no row for ${e.tool_use_id}; the refresh failed: ${String(error)}`)
131 return next(e)
132 }
133 const { version } = reached
134 const result = await next(e)
135 if (!hasSucceeded(result)) return result
136 if (parsed.kind === 'echoed' && !hasEchoedSuccess(parsed.line, stdoutOf(result))) return result
137 try {
138 const diff = await $.workitems.refresh({ since: version })
139 const rows: QuietItemsRow[] = rowsFromDiff(diff, parsed.writes)
140 if (rows.length > 0) {
141 await $.state.set({ plugin: 'quiet-items', key: 'rows', id: e.tool_use_id }, rows)
142 }
143 } catch (error) {
144 $.ui.log(`quiet-items: no row for ${e.tool_use_id}; the refresh failed: ${String(error)}`)
145 }
146 return result
147 })
148
149 on('tool.call', { tool: ['Write', 'Edit'] }, async ($, e, next) => {
150 const { value: snapshot } = await $.state.get({ plugin: 'workitems', key: 'snapshot' })
151 if (snapshot === undefined) return next(e)
152 let path: string | null
153 try {
154 path = await $.workitems.trackerFile({ path: e.file_path, root: snapshot.root })
155 } catch (error) {
156 $.ui.log(
157 `quiet-items: no row for ${e.tool_use_id}; the tracker file check failed: ${String(error)}`,
158 )
159 return next(e)
160 }
161 if (path === null) return next(e)
162 const result = await next(e)
163 if (!hasSucceeded(result)) return result
164 const rows: QuietItemsRow[] = [{ kind: 'raw-edit', path, tool: e.tool }]
165 try {
166 await $.state.set({ plugin: 'quiet-items', key: 'rows', id: e.tool_use_id }, rows)
167 } catch (error) {
168 $.ui.log(`quiet-items: no row for ${e.tool_use_id}: ${String(error)}`)
169 }
170 return result
171 })
172
173 on('ui.render', { component: 'ToolGroup' }, async ($, e, next) => {
174 if (e.props.isExpanded) return next(e)
175 if (!(await drawsQuietRows($, defaultMode))) return next(e)
176 for (const call of e.props.calls) {
177 if (call.tool_use_id === undefined || call.isRunning || call.isErrored) continue
178 const { value: rows } = await $.state.get({
179 plugin: 'quiet-items',
180 key: 'rows',
181 id: call.tool_use_id,
182 })
183 if (rows !== undefined && rows.length > 0) {
184 return next({ ...e, props: { ...e.props, isExpanded: true } })
185 }
186 }
187 return next(e)
188 })
189
190 on('ui.render', { component: ['ToolUse', 'ToolResult'] }, async ($, e, next) => {
191 if (e.props.isErrored) return next(e)
192 if (e.component === 'ToolUse' && (e.props.isRunning || e.props.isInterrupted)) return next(e)
193 if (!(await drawsQuietRows($, defaultMode))) return next(e)
194 const { value: rows } = await $.state.get({
195 plugin: 'quiet-items',
196 key: 'rows',
197 id: e.props.tool_use_id,
198 })
199 if (rows === undefined || rows.length === 0) return next(e)
200 const elements = $.ui.resolve(e)
201 if (e.component === 'ToolResult') return drawEmpty(elements)
202 return drawRows({ elements, hasToolMarker: e.surface === 'terminal' }, rows, titleLength)
203 })
204}
205hooks/row.tsx 109 lines1import type { Elements, EngineInterface, RenderElement, RenderSurface } from 'claude-code'
2import type { QuietItemsItemRow, QuietItemsRow, QuietItemsVerb } from '../types'
3
4type Diff = Awaited<ReturnType<EngineInterface['workitems']['refresh']>>
5type Parsed = Awaited<ReturnType<EngineInterface['workitems']['classify']>>
6type TrackerWrite = Exclude<Parsed, { kind: 'none' }>['writes'][number]
7type Item = Diff['created'][number]
8type RowElements = Pick<Elements[RenderSurface], 'Box' | 'Text'>
9
10const COMMENT_VERBS = new Set(['comments add', 'comment'])
11const VERB_COLUMN = 'work item commented'.length
12const STATUS_COLUMN = 'in_progress'.length
13const RAW_EDIT_NOTE = 'not through the tracker CLI'
14
15function itemRow(verb: QuietItemsVerb, item: Item): QuietItemsItemRow {
16 return {
17 kind: 'item',
18 verb,
19 id: item.id,
20 title: item.title,
21 status: item.rawStatus,
22 priority: item.priority,
23 }
24}
25
26export function rowsFromDiff(diff: Diff, writes: readonly TrackerWrite[]): QuietItemsItemRow[] {
27 const isComment = writes.length > 0 && writes.every((write) => COMMENT_VERBS.has(write.verb))
28 return [
29 ...diff.created.map((item) => itemRow('created', item)),
30 ...diff.updated.map((item) => itemRow(isComment ? 'commented' : 'updated', item)),
31 ...diff.closed.map((item) => itemRow('closed', item)),
32 ]
33}
34
35export function cutTitle(title: string, length: number): string {
36 const graphemes = Array.from(new Intl.Segmenter().segment(title), (part) => part.segment)
37 if (graphemes.length <= length) return title
38 return `${graphemes.slice(0, length).join('').trimEnd()}…`
39}
40
41type CellStyle = Omit<Parameters<RowElements['Text']>[0], 'children'>
42
43function cell(
44 { Box, Text }: RowElements,
45 text: string,
46 style: CellStyle = {},
47 canShrink = false,
48): RenderElement {
49 return Box({ flexShrink: canShrink ? 1 : 0, children: Text({ ...style, children: text }) })
50}
51
52export type RowLook = { elements: RowElements; hasToolMarker: boolean }
53
54function line({ elements, hasToolMarker }: RowLook, key: string, cells: RenderElement[]) {
55 const columns = elements.Box({ flexDirection: 'row', gap: 2, children: cells })
56 if (!hasToolMarker) return elements.Box({ key, children: columns })
57 const marker = elements.Box({
58 flexShrink: 0,
59 marginRight: 1,
60 children: elements.Text({ color: 'success', children: '●' }),
61 })
62 return elements.Box({ key, flexDirection: 'row', children: [marker, columns] })
63}
64
65function drawItem(
66 look: RowLook,
67 row: QuietItemsItemRow,
68 titleLength: number,
69 key: string,
70): RenderElement {
71 const { elements } = look
72 const title = cutTitle(row.title, titleLength).padEnd(titleLength + 1)
73 return line(look, key, [
74 cell(elements, `work item ${row.verb}`.padEnd(VERB_COLUMN)),
75 cell(elements, row.id, { bold: true }),
76 cell(elements, title, { wrap: 'truncate-end' }, true),
77 cell(elements, row.status.padEnd(STATUS_COLUMN), { dimColor: true }),
78 cell(elements, row.priority === null ? '' : `P${String(row.priority)}`, { dimColor: true }),
79 ])
80}
81
82function drawRawEdit(look: RowLook, path: string, tool: string, key: string): RenderElement {
83 const { elements } = look
84 return line(look, key, [
85 cell(elements, 'raw tracker edit'.padEnd(VERB_COLUMN), { color: 'warning' }),
86 cell(elements, path),
87 cell(elements, `${tool}, ${RAW_EDIT_NOTE}`, { dimColor: true, wrap: 'truncate-end' }, true),
88 ])
89}
90
91export function drawRows(
92 look: RowLook,
93 rows: readonly QuietItemsRow[],
94 titleLength: number,
95): RenderElement {
96 return look.elements.Box({
97 flexDirection: 'column',
98 children: rows.map((row, index) =>
99 row.kind === 'item'
100 ? drawItem(look, row, titleLength, `row-${String(index)}`)
101 : drawRawEdit(look, row.path, row.tool, `row-${String(index)}`),
102 ),
103 })
104}
105
106export function drawEmpty({ Box }: RowElements): RenderElement {
107 return Box({})
108}
109types/index.d.ts 33 lines1export type QuietItemsMode = 'on' | 'off'
2
3export type QuietItemsSimpleViewMode = 'on' | 'off'
4
5export type QuietItemsVerb = 'created' | 'updated' | 'commented' | 'closed'
6
7export type QuietItemsItemRow = {
8 kind: 'item'
9 verb: QuietItemsVerb
10 id: string
11 title: string
12 status: string
13 priority: number | null
14}
15
16export type QuietItemsRawEditRow = {
17 kind: 'raw-edit'
18 path: string
19 tool: 'Write' | 'Edit'
20}
21
22export type QuietItemsRow = QuietItemsItemRow | QuietItemsRawEditRow
23
24declare module 'claude-code' {
25 interface PluginState {
26 'quiet-items': {
27 mode: QuietItemsMode
28 rows: StateFamily<readonly QuietItemsRow[]>
29 ready: { root: string }
30 }
31 }
32}
33