SLOPSHOPPER

quiet-items

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

newrowsguardcommand
v0.3.1MITupdated 2026-10-09niksavis/handily/mods/quiet-items
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · quiet-items
› fix the failing auth test and add an audit log call ● quiet-items: quiet-items: no row for toolu_06; the command check failed: TypeError: undefined is not an object (evaluating '$.workitems.classify') ● quiet-items: quiet-items: no row for toolu_07; the command check failed: TypeError: undefined is not an object (evaluating '$.workitems.classify') ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /quiet-items ⎿ quiet-items: quiet-items off for this session. Tracker commands draw in full. ● quiet-items: quiet-items: no row for toolu_08; the command check failed: TypeError: undefined is not an object (evaluating '$.workitems.classify') ● quiet-items: quiet-items: no row for toolu_09; the command check failed: TypeError: undefined is not an object (evaluating '$.workitems.classify') ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

quiet-items

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.

What it matches

ToolMatchRow
BashA tracker CLI write: br, bd, basicly tracker, .basicly/core/kit/tracker/cli.pyOne row per item of the refresh diff
Write, EditA tracker file at the workitems root: .beads/issues.jsonl, .basicly/ledger/ (pending-*.jsonl, events-*.jsonl, snapshot.jsonl), .beans/**/<id>--<slug>.mdOne 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.
  • The write verbs are those of $.workitems.writeVerbs(). bd uses the verbs of br.
  • basicly tracker write -- <verb> uses the verbs of .basicly/core/kit/tracker/cli.py.
  • A command with --help, -h or --dry-run is not a write.

Which commands go quiet

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:

  • Each segment starts with one of these programs:
  • br, bd or basicly;
  • python3 or python, directly followed by .basicly/core/kit/tracker/cli.py;
  • .basicly/core/kit/tracker/cli.py alone.
  • Each segment is a tracker write or cd with one word.
  • No cd comes before a segment that runs the kit path. A cd before br, bd or basicly is allowed.
  • Only && joins two segments. A trailing status echo is the one exception (see below).
  • The command has at most 8192 characters.
  • Every word is one of these:
  • a bare word of the characters A-Z a-z 0-9 . _ / : = @ , + % -;
  • a single-quoted string;
  • a double-quoted string without $, a backtick or a backslash.
CommandRowWhy
br close a, br update a --title "Fix the parser"QuietAn allowed shape
br update a --title '$(id)'QuietThe shell does not expand text in single quotes
cd x && br close a && br update b --status openQuiet&& stops at the first failure, so the exit status shows it
br close a && echo okQuietThe same as above
br close a; echo "exit=$?"Quiet only when the last output line is exit=0The echo prints the exit status of the tracker command
br close a; echo okEngineThe echo hides the exit status of the tracker command
br close a; br close b, `br close a \\br close b`EngineThe second write hides the exit status of the first
br close a; git log, `br close a \tail -1`EngineA segment that is not a tracker write can hide a failure
br update a --title "$(id)", $'…', a backtick, \EngineThe shell expands or runs it
br close x-*, br close {a,b}, ~/notes, $HOMEEngineThe shell expands it
br close a > out.txt, 2>&1, < in.txt, <(…)EngineA redirection, also to /dev/null, changes where the input or output goes
br close a & id, (br close a), br close a # noteEngineThe parser does not read a background job, a subshell or a comment
FOO=1 br close a, env …, sudo …EngineOnly a bare tracker program may start a segment
uvx br close a, npx br close aEngineThe wrapper fetches a package from a registry
uv run python3 .basicly/core/kit/tracker/cli.py close aEngineuv run syncs the project first, and a build backend in the tree can run any code
python3 -I …, python3 -c…EngineNo option of python may appear
/tmp/br close a, ./br close a, an absolute kit pathEngineOnly a bare tracker name or the exact relative kit path is allowed
cd x && python3 .basicly/core/kit/tracker/cli.py close aEngineAfter cd, the relative kit path names a script in another directory
A command longer than 8192 charactersEngineThe 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.

How it finds the items

  1. Before the call runs, the mod calls $.workitems.refresh() and keeps the version it reached. A change from before the call is then inside that version.
  2. After the call succeeds, it calls $.workitems.refresh({ since: version }).
  3. It keeps the rows of the diff in $.state, under the tool_use_id of the call.
  4. The ToolUse row draws the rows. The ToolResult block under it draws empty.
  5. When the engine folds the call into a 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.

When it draws the engine row

The engine draws its own row (next(e)) in each of these cases:

  • The call is still running, errored or was interrupted, or the command wrote to stderr.
  • The command does not have an allowed shape (see the section above), or no write matched.
  • The last output line of a status echo does not show the exit status 0.
  • The workitems state is not ok.
  • The refresh diff is empty, or a refresh rejected.
  • The mode is off.
  • The simple-view mode is off.

Settings and the command

SettingDefaultUse
modeonThe mode when a session starts: on or off
titleLength60The 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.

Follow simple-view

/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.

  • The mod reads the 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.

Develop

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.

Source 3 files
hooks/register.tsx 205 lines
1import 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}
205
hooks/row.tsx 109 lines
1import 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}
109
types/index.d.ts 33 lines
1export 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