command-code 1.82.5 → 1.84.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/dist/bundled/command-code-knowledge/live-models.ts +73 -16
- package/dist/bundled/command-code-knowledge/reference/custom-slash-commands.md +125 -0
- package/dist/bundled/command-code-knowledge/reference/product-help.md +3 -0
- package/dist/bundled/mod-builder/SKILL.md +1 -0
- package/dist/bundled/mod-builder/examples/model-router.ts +43 -0
- package/dist/bundled/mod-builder/examples/pane-and-buttons.ts +60 -0
- package/dist/bundled/mod-builder/examples/pane-form.ts +68 -0
- package/dist/bundled/mod-builder/examples/permission-policy.ts +34 -0
- package/dist/bundled/mod-builder/examples/redact-secrets.ts +46 -0
- package/dist/bundled/mod-builder/examples/spinner-count.ts +47 -0
- package/dist/bundled/mod-builder/examples/stream-filter.ts +70 -0
- package/dist/bundled/mod-builder/reference/api.md +40 -5
- package/dist/bundled/mod-builder/reference/hooks-and-events.md +37 -1
- package/dist/bundled/mod-builder/reference/overview.md +19 -4
- package/dist/bundled/mod-builder/reference/packaging.md +14 -1
- package/dist/bundled/mod-builder/reference/ui.md +192 -20
- package/dist/bundled/mod-builder/reference/verify.md +7 -3
- package/dist/cli.mjs +5 -5
- package/package.json +1 -1
- package/vsix/commandcode-vscode.vsix +0 -0
|
@@ -2,19 +2,45 @@
|
|
|
2
2
|
|
|
3
3
|
# UI surface
|
|
4
4
|
|
|
5
|
-
Everything a mod can put on screen rides `cmd.ui`, `cmd.addRenderer`, and `cmd.showEntry`. All of it is
|
|
5
|
+
Everything a mod can put on screen rides `cmd.ui`, `cmd.addRenderer`, and `cmd.showEntry`. All of it is data, never components: a mod returns strings or a small element tree, the TUI and the desktop app draw it, headless runs degrade to deterministic defaults, and a crashing render becomes a one-line warning - never a broken screen.
|
|
6
6
|
|
|
7
7
|
## Notifications and dialogs
|
|
8
8
|
|
|
9
9
|
- `cmd.ui.notify(message, level?)` - a `notice` feed row, styled by `level`: `'info'` (default) is a dim bullet, `'warning'` an amber ⚠, `'error'` a red ⚠. Rendered during a turn (the per-turn feed projector); a notice raised at factory time flushes once the session binds.
|
|
10
|
+
- `cmd.ui.toast(text, {level?, durationMs?})` - a short-lived line above the prompt that clears itself (default 4s). `level` is `info`, `success`, `warning`, or `error`. Where nothing draws toasts (headless), it falls back to a notice row so the message is never lost.
|
|
10
11
|
- `cmd.ui.confirm({title, message?})` / `cmd.ui.select({title, options})` / `cmd.ui.input({title, placeholder?})` - the Interaction question modal in the TUI. Headless, each resolves its deterministic default: confirm → `false`, select/input → `undefined` - never auto-approved.
|
|
11
12
|
- `cmd.ui.setTitle(text | null)` - the terminal tab title. The text replaces the session title on the tab (and survives the host's own title re-assertions) until `null` hands the tab back. Headless: no-op.
|
|
12
13
|
|
|
13
|
-
##
|
|
14
|
+
## Clipboard and sound
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
- `cmd.ui.copy(text)` - write `text` to the system clipboard through the same helper `/copy` uses. It never throws: `{isCopied: true}`, or `{isCopied: false, reason}` where `reason` is `headless` (no interactive host), `no-clipboard` (the machine's clipboard is unreachable, for example over SSH without X11), or `error`.
|
|
17
|
+
- `cmd.audio.play({file, timeoutMs?})` - play a sound file with the platform player: `afplay` on macOS, `paplay` then `aplay` on Linux, PowerShell `Media.SoundPlayer` on Windows. A relative `file` resolves against the directory of the mod's own file, so ship the sound next to it; an absolute path works too. It runs through the runtime shell and is killed after `timeoutMs` (default 10s). It never throws: `{isPlayed: true}`, or `{isPlayed: false, reason}` where `reason` is `headless`, `not-found`, `no-player`, `timeout`, or `error`.
|
|
18
|
+
- `cmd.audio.bell()` - ring the terminal bell. Returns `{isPlayed: true}`, or `{isPlayed: false, reason: 'headless'}`.
|
|
19
|
+
- Where they work today. In the desktop app, copy, bell, and play all answer `headless` for now. Over SSH without a display, copy answers `no-clipboard`. On Windows, and on Linux with only ALSA (`aplay`, no PulseAudio or PipeWire), play handles WAV files only. Ship a `.wav` if the mod should sound everywhere.
|
|
20
|
+
- Bad arguments never throw either. A `file` that is not a string answers `error`. A `timeoutMs` that is not a positive whole number falls back to the 10s default. A hot reload stops any sound the old code started.
|
|
16
21
|
|
|
17
|
-
|
|
22
|
+
Sound is a side effect, so do not `await` it in a hook unless the turn should wait for it:
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
cmd.hooks({
|
|
26
|
+
onStop: () => {
|
|
27
|
+
void cmd.audio.play({file: './done.wav'});
|
|
28
|
+
return undefined;
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
cmd.addCommand({
|
|
32
|
+
name: 'copy-branch',
|
|
33
|
+
handler: async ({exec, ui}) => {
|
|
34
|
+
const {stdout} = await exec({command: 'git', args: ['branch', '--show-current']});
|
|
35
|
+
const copied = await ui.copy(stdout.trim());
|
|
36
|
+
return {message: copied.isCopied ? 'Copied.' : `Not copied: ${copied.reason}`};
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Footer status segments
|
|
42
|
+
|
|
43
|
+
`cmd.ui.setStatus(text | null)` - a persistent per-mod segment on the TUI's status row (under the input). One segment per mod: a new call replaces the text, `null` (or whitespace-only text) clears it, and the returned `Disposable` clears it too - but only while it still owns the segment, so a stale handle disposing late can't wipe a newer status. Segments from multiple mods render after the row's built-in items in load order, and a call from async code repaints on its own. Style text with ansi colors (SGR) and OSC 8 links; every other escape - cursor moves, screen clears, OSC titles and clipboard writes - is stripped, and newlines and tabs become spaces. Mod segments share the user's one status line, after the built-ins and before the right-pinned phase model; the line is cut with `…` at the terminal edge, so trailing segments can be cut off. `/statusline off` hides the whole row (`cmd.ui.capabilities.status` stays true). Headless: stored, rendered nowhere. For a complete example built by asking Command Code, see [Add your own items with mods](https://commandcode.ai/docs/reference/slash-commands#add-your-own-items-with-mods).
|
|
18
44
|
|
|
19
45
|
```ts
|
|
20
46
|
cmd.hooks({
|
|
@@ -23,20 +49,46 @@ cmd.hooks({
|
|
|
23
49
|
});
|
|
24
50
|
```
|
|
25
51
|
|
|
26
|
-
`cmd.ui.capabilities.status` - whether this host renders footer segments (true in the interactive TUI, false headless). Use
|
|
52
|
+
`cmd.ui.capabilities.status` / `cmd.ui.capabilities.surface` - whether this host renders footer segments, and widgets / panes / toasts (true in the interactive TUI and the desktop app, false headless). Use them to pick a surface instead of shipping a permanent fallback:
|
|
27
53
|
|
|
28
54
|
```ts
|
|
29
55
|
if (cmd.ui.capabilities.status) cmd.ui.setStatus(text);
|
|
30
56
|
else cmd.ui.notify(text);
|
|
31
57
|
```
|
|
32
58
|
|
|
33
|
-
|
|
59
|
+
## Elements
|
|
60
|
+
|
|
61
|
+
A widget's or pane's `render({width})` returns a list of nodes. A node is a plain string (one line) or one of:
|
|
62
|
+
|
|
63
|
+
| Element | Shape | Draws |
|
|
64
|
+
|---|---|---|
|
|
65
|
+
| text | `{type: 'text', text, color?, bold?, dim?}` | styled text; `color` is `red` `green` `yellow` `blue` `magenta` `cyan` `white` `gray` |
|
|
66
|
+
| row | `{type: 'row', children}` | children on one line, space-separated |
|
|
67
|
+
| box | `{type: 'box', title?, children}` | an optional bold title, then each child on its own line |
|
|
68
|
+
| button | `{type: 'button', label, hotkey?, key?, onPress}` | `[ r: Reset ]`; `hotkey` is one letter or digit, `key` is its identity (defaults to the label) |
|
|
69
|
+
| progress | `{type: 'progress', value, width?, label?}` | `label ██████░░░░ 60%`, `value` 0..1 |
|
|
70
|
+
| divider | `{type: 'divider'}` | a rule across `width` |
|
|
71
|
+
| input | `{type: 'input', key, placeholder?, value?, onSubmit(text)}` | a one-line text field `[ value ]`; editable only while its pane has focus, Enter submits |
|
|
72
|
+
| select | `{type: 'select', key, options: [{label, value}], value?, onSelect(value)}` | `○ Small ◉ Medium ○ Large` with `value` marked; while focused, ←/→ move the highlight and Enter picks |
|
|
73
|
+
| markdown | `{type: 'markdown', text}` | markdown drawn the way the feed draws replies (headings, lists, code, tables); a host without a markdown renderer falls back to plain lines |
|
|
74
|
+
|
|
75
|
+
`width` is the columns the surface draws into, so a mod can size its own layout. Mods still own their styling: a plain string is printed as written, ansi colors and styles included. Other escape sequences (cursor moves, screen clears) and control bytes other than newline and tab are dropped from all mod text, so a mod can never take over the terminal.
|
|
76
|
+
|
|
77
|
+
`input` and `select` need a `key` (their identity for focus and handlers); a button's `key` defaults to its label. Their handlers run in the mod like a hook: a throw becomes `mod_error {hook: 'ui.submit'}` or `{hook: 'ui.select'}`, and the surface repaints after every submit or pick. Keep what they set in `cmd.state` and read it back as `value`, so the field shows it after a repaint or a hot reload. Images and raster graphics are not elements: terminal graphics protocols (kitty, iTerm2, sixel) differ per terminal and do not survive the TUI's line-based redraw.
|
|
78
|
+
|
|
79
|
+
## Widgets: the band above the prompt
|
|
80
|
+
|
|
81
|
+
`cmd.ui.widget({placement: 'above-editor' | 'below-editor', render})` - lines drawn around the input panel; `above-editor` is the band right above the prompt, `below-editor` sits under it with the status row. `render` re-runs on every repaint; call `cmd.ui.refreshWidgets()` after the data it reads changed (or keep that data in `cmd.state`, whose `set` repaints for you). A throwing render shows a one-line warning and reports a `mod_error` so siblings keep drawing. The returned `Disposable` removes the widget. Headless: stored, drawn nowhere.
|
|
34
82
|
|
|
35
83
|
```ts
|
|
36
84
|
let failing = 0;
|
|
37
85
|
cmd.ui.widget({
|
|
38
86
|
placement: 'above-editor',
|
|
39
|
-
render: () => [
|
|
87
|
+
render: () => [
|
|
88
|
+
failing > 0
|
|
89
|
+
? {type: 'text', text: `✗ ${failing} tests failing`, color: 'red'}
|
|
90
|
+
: {type: 'text', text: '✓ tests green', color: 'green'},
|
|
91
|
+
],
|
|
40
92
|
});
|
|
41
93
|
cmd.on('tool_completed', () => {
|
|
42
94
|
failing = readFailingCount();
|
|
@@ -44,24 +96,144 @@ cmd.on('tool_completed', () => {
|
|
|
44
96
|
});
|
|
45
97
|
```
|
|
46
98
|
|
|
99
|
+
## Panes
|
|
100
|
+
|
|
101
|
+
`cmd.ui.pane({id, title, render})` - a titled, bordered panel above the prompt that the person opens and closes. Registering does not open it: `cmd.ui.openPane(id)` does (usually from a command), `cmd.ui.closePane(id)` closes it, and so do `/mods open <pane>` / `/mods close <pane>`. While a pane is open, **Alt+<hotkey>** presses its buttons (the pane's last line lists them) and **Alt+0** closes the newest one; typing still goes to the prompt. Hotkeys the prompt already owns (`p`, `v`, `w`, `u`, `m`) are drawn but never pressed by the chord. A pane's id is `<mod>/<id>`, so two mods can both have a `stats` pane; whether it is open survives a hot reload.
|
|
102
|
+
|
|
103
|
+
**Docked on wide terminals.** From 140 columns up, open panes draw in a column to the right of the prompt (about 40% of the width, 40 to 72 columns) instead of stacking above it, and the prompt narrows to fit beside them. The feed above is scrollback, so panes dock beside the prompt, not beside the feed. Below 140 columns they stack above the prompt.
|
|
104
|
+
|
|
105
|
+
### Focus
|
|
106
|
+
|
|
107
|
+
The prompt keeps the keyboard until a pane takes it, so typing never reaches a pane by accident. **Alt+↑** moves focus into the newest open pane (its border lights up and the prompt steps aside, keeping what you typed), and `cmd.ui.openPane(id, {focus: true})` opens a pane with focus, like a dialog. While a pane has focus:
|
|
108
|
+
|
|
109
|
+
| Key | Does |
|
|
110
|
+
|---|---|
|
|
111
|
+
| Tab / ↓ | next button, input, or select (wraps) |
|
|
112
|
+
| Shift+Tab / ↑ | previous one |
|
|
113
|
+
| Enter | press the button, submit the input, or pick the select's highlighted option |
|
|
114
|
+
| ←/→ | move a select's highlight |
|
|
115
|
+
| typing, Backspace | edit the focused input (it starts from its `value`) |
|
|
116
|
+
| Esc | give the keyboard back to the prompt. The pane stays open, and what you typed in it is still there when it takes focus again. |
|
|
117
|
+
| Alt+0 | close the newest pane (focus returns to the prompt) |
|
|
118
|
+
|
|
119
|
+
Ctrl+C still reaches the prompt's interrupt and exit keys while a pane has focus. Closing the focused pane, or a dialog taking the screen, hands the keyboard back too.
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
cmd.ui.pane({
|
|
123
|
+
id: 'standup',
|
|
124
|
+
title: 'Standup',
|
|
125
|
+
render: () => [
|
|
126
|
+
{type: 'markdown', text: '**Today**: one line, then pick how it is going.'},
|
|
127
|
+
{
|
|
128
|
+
type: 'input',
|
|
129
|
+
key: 'note',
|
|
130
|
+
placeholder: 'what are you working on?',
|
|
131
|
+
value: cmd.state.get<string>('note') ?? '',
|
|
132
|
+
onSubmit: text => cmd.state.set('note', text),
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
type: 'select',
|
|
136
|
+
key: 'mood',
|
|
137
|
+
options: [
|
|
138
|
+
{label: 'Great', value: 'great'},
|
|
139
|
+
{label: 'Blocked', value: 'blocked'},
|
|
140
|
+
],
|
|
141
|
+
value: cmd.state.get<string>('mood'),
|
|
142
|
+
onSelect: value => cmd.state.set('mood', value),
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
});
|
|
146
|
+
cmd.addCommand({
|
|
147
|
+
name: 'standup',
|
|
148
|
+
handler: () => cmd.ui.openPane('standup', {focus: true}),
|
|
149
|
+
});
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
cmd.ui.pane({
|
|
154
|
+
id: 'calls',
|
|
155
|
+
title: 'Tool calls',
|
|
156
|
+
render: () => [
|
|
157
|
+
{type: 'progress', label: 'budget', value: calls() / 50},
|
|
158
|
+
{
|
|
159
|
+
type: 'button',
|
|
160
|
+
label: 'Reset',
|
|
161
|
+
hotkey: 'r',
|
|
162
|
+
onPress: () => cmd.state.set('calls', 0),
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
});
|
|
166
|
+
cmd.addCommand({name: 'calls', handler: () => cmd.ui.openPane('calls')});
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
A button's `onPress` runs in the mod like a hook: a throw becomes `mod_error {hook: 'ui.press'}`, and the surface repaints after every press.
|
|
170
|
+
|
|
47
171
|
## Custom feed rendering
|
|
48
172
|
|
|
49
173
|
- `cmd.addRenderer(customType, data => lines)` - a renderer for a custom entry type; returns the lines to print (style them with ansi escapes - picocolors, `@commandcode/tui` helpers, or raw codes). First registration per type wins across mods.
|
|
50
174
|
- `cmd.showEntry(customType, data)` - render a custom entry into the live feed through the renderer registered for that type (unrendered types pretty-print as JSON). The TUI wires the sink; headless runs drop entries. Pair with `cmd.session.appendCustomEntry` when the data should also persist.
|
|
51
175
|
|
|
52
|
-
|
|
176
|
+
## Restyle built-in rows
|
|
53
177
|
|
|
54
|
-
|
|
178
|
+
`cmd.ui.override(component, render)` - redraw a row Command Code draws itself, with the same [elements](#elements) widgets use. `render(props, next)` gets the row's data and returns nodes, or `undefined` to keep the built-in row. The returned `Disposable` removes the override, and a hot reload drops it with every other registration.
|
|
55
179
|
|
|
56
|
-
|
|
|
180
|
+
| Component | `props` | What the nodes do |
|
|
57
181
|
|---|---|---|
|
|
58
|
-
| `
|
|
59
|
-
| `
|
|
60
|
-
| `
|
|
61
|
-
| `
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
182
|
+
| `spinner` | `{status, elapsedMs, tokens}` | appended after the working status line, on the same line |
|
|
183
|
+
| `tool_row` | `{toolName, displayName, input, status, result?}` | drawn instead of the tool's row; `status` is `pending`, `success`, or `error`, `result` is set once it finished |
|
|
184
|
+
| `user_message` | `{text}` | drawn instead of the person's message |
|
|
185
|
+
| `command_output` | `{message, details?, isError}` | drawn instead of a slash command's result row |
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
// "tool calls: 3" beside the spinner.
|
|
189
|
+
cmd.ui.override('spinner', (_props, next) => [
|
|
190
|
+
{type: 'text', text: `tool calls: ${calls()}`, color: 'cyan'},
|
|
191
|
+
...(next() ?? []),
|
|
192
|
+
]);
|
|
193
|
+
|
|
194
|
+
// Shell rows as one line; every other tool keeps its built-in row.
|
|
195
|
+
cmd.ui.override('tool_row', ({toolName, input}) =>
|
|
196
|
+
toolName === 'shell_command' ? [`$ ${input}`] : undefined,
|
|
197
|
+
);
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
How overrides compose and fail:
|
|
201
|
+
|
|
202
|
+
- **Load order, first answer wins.** Several mods can override the same component. The first mod to return nodes wins; `undefined` passes to the next mod, and past the last one to the built-in row.
|
|
203
|
+
- **`next()` is the rest of the chain.** It returns what the mods loaded after this one drew, or `undefined` when the built-in row would draw. Wrap it to add to another mod's drawing instead of replacing it (`[...mine, ...(next() ?? [])]`). It runs the later mods once, however often you call it.
|
|
204
|
+
- **A broken override falls back.** An override that throws, returns something other than an array or `undefined` (a string, or the Promise of an `async` render), or draws a malformed tree is skipped as if it returned `undefined`. It reports one `mod_error {hook: 'ui.override:<component>'}` until the mod registers it again, so a broken spinner override does not report on every frame.
|
|
205
|
+
- **Rows redraw with the row.** The spinner redraws about 12 times a second; a tool row redraws while it is live and is final once it scrolls into the transcript. An override applies to rows drawn after it registered.
|
|
206
|
+
- **Tool and command rows are a trust surface.** A `tool_row` or `command_output` override changes what the transcript shows for a tool call or a command. Install only mods you trust to show those rows honestly.
|
|
207
|
+
- **Never the permission prompt.** Only the four components above are overridable; any other name throws at registration.
|
|
208
|
+
|
|
209
|
+
## In the desktop app
|
|
210
|
+
|
|
211
|
+
The desktop app draws the same surface around the chat composer. Mod code still runs only in the app's main process: it renders each widget and pane there at a fixed 80 columns and sends the window plain lines and button ids, never code.
|
|
212
|
+
|
|
213
|
+
- **Widgets**: `above-editor` sits right above the composer, `below-editor` right under it, with the status row below that.
|
|
214
|
+
- **Panes**: an open pane is a card above the composer, titled, with a close button. Buttons are clickable where the mod drew them (`[ r: Reset ]`); Alt+hotkey chords are a TUI thing.
|
|
215
|
+
- **Toasts**: the app's own toasts, titled with the mod's name.
|
|
216
|
+
- **Styling**: ansi bold, dim, and the eight element colors map to the app theme; other escape codes (cursor moves, links, 256-color) are dropped.
|
|
217
|
+
|
|
218
|
+
## Headless behavior at a glance
|
|
219
|
+
|
|
220
|
+
| Surface | Interactive TUI | Desktop app | Headless (`-p`) |
|
|
221
|
+
|---|---|---|---|
|
|
222
|
+
| `notify` | notice feed row | notice feed row | printed notice |
|
|
223
|
+
| `toast` | line above the prompt, clears itself | app toast, clears itself | notice row |
|
|
224
|
+
| `confirm` | modal | question in the composer | resolves `false` |
|
|
225
|
+
| `select` / `input` | modal | question in the composer | resolves `undefined` |
|
|
226
|
+
| `setStatus` | footer segment under the input | status row under the composer | stored, rendered nowhere |
|
|
227
|
+
| `widget` | band above / below the prompt | band above / below the composer | stored, drawn nowhere |
|
|
228
|
+
| `pane` | bordered panel above the prompt (docked right from 140 columns), Alt+key buttons, Alt+↑ focus | card above the composer, clickable buttons | stored, drawn nowhere |
|
|
229
|
+
| `override` | redraws the spinner / tool / message / command rows | not drawn | stored, drawn nowhere |
|
|
230
|
+
| `setTitle` | terminal tab title | no-op | no-op |
|
|
231
|
+
| `showEntry` | rendered feed row | dropped | dropped |
|
|
232
|
+
| `addShortcut` | dispatched after host bindings | never fires | never fires |
|
|
233
|
+
| `sendUserMessage` | real user turn (queued while busy) | falls back to `queueMessage` | falls back to `queueMessage` |
|
|
234
|
+
| `prompt.submit` | starts a turn when idle, queues when busy | follow-up queue | follow-up queue |
|
|
235
|
+
| `shutdown` | exits after the turn settles | no-op | no-op |
|
|
236
|
+
| `ui.copy` | system clipboard | `{isCopied: false, reason: 'headless'}` | `{isCopied: false, reason: 'headless'}` |
|
|
237
|
+
| `audio.play` / `audio.bell` | platform player / terminal bell | no sound, `reason: 'headless'` | no sound, `reason: 'headless'` |
|
|
238
|
+
|
|
239
|
+
**TUI only for now.** `cmd.ui.override`, `cmd.ui.setTitle`, `cmd.ui.copy`, `cmd.audio`, `cmd.addShortcut`, `cmd.shutdown`, pane focus, and typing into `input` or picking in `select` elements are handled by the interactive TUI only. The desktop app draws an `input` or `select` element as plain text it cannot focus, and a `markdown` element as styled lines.
|
|
@@ -27,7 +27,11 @@ Your mod must appear, with no load warnings. If it does not appear, the warning
|
|
|
27
27
|
- **Hook** - trigger the behavior it guards (for a `beforeToolCall` blocker, ask for the blocked action and watch the block reason land as the tool result).
|
|
28
28
|
- **Input interception** - type the pattern `transformInput` matches and confirm the rewrite/consume happened.
|
|
29
29
|
- **Status** - `cmd.ui.setStatus('text')` must show up as a segment on the row under the input, replace itself on the next call, and disappear on `setStatus(null)`.
|
|
30
|
-
- **Widget** -
|
|
30
|
+
- **Widget** - an `above-editor` widget draws right above the prompt, a `below-editor` one under it; `cmd.ui.refreshWidgets()` (or a `cmd.state.set`) repaints it.
|
|
31
|
+
- **Pane** - open it (`/mods open <pane>`, or the mod's own command): it draws above the prompt with its buttons' Alt+keys on its last line; `Alt+<key>` presses one, `Alt+0` closes it. For inputs and selects, `Alt+↑` focuses it: Tab through the fields, type, press Enter, and check the pane redraws with the new `value`.
|
|
32
|
+
- **Toast** - `cmd.ui.toast('text')` shows above the prompt and clears itself.
|
|
33
|
+
- **Override** - send a prompt that runs a tool: a `spinner` override shows beside the working line, a `tool_row` override replaces that tool's row. If a row still looks built-in, the override returned `undefined` or threw (look for `mod_error` with `ui.override:` in its hook).
|
|
34
|
+
- **Permission answers** - with an `onPermissionRequest` hook, trigger a call that would prompt: an answered prompt never appears. `cmd mods inspect` lists the hook so reviewers see it too.
|
|
31
35
|
- **Renderer** - `cmd.showEntry` rows render styled; an unregistered type pretty-prints as JSON.
|
|
32
36
|
|
|
33
37
|
### Test: block-dangerous-commands guards rm -rf
|
|
@@ -42,9 +46,9 @@ Expected result:
|
|
|
42
46
|
- Declining blocks the tool - the model sees the block reason and adapts
|
|
43
47
|
- No `tool_running` fires for the blocked call
|
|
44
48
|
|
|
45
|
-
## 4. Iterate
|
|
49
|
+
## 4. Iterate: save, and it reloads
|
|
46
50
|
|
|
47
|
-
|
|
51
|
+
In the interactive TUI, saving a mod's file reloads it in place, with no restart: the feed says `[mods] ✓ reloaded <name>` (or why it did not). The old code gets `onSessionEnd` with `reason: 'reload'`, then every registration it made is disposed, except tools added with `addTool`. Those keep the old code until the next session. `cmd.state` survives, and `onSessionStart` fires again with `source: 'reload'`. A save that throws while loading leaves the old code running. `/mods reload [name]` reloads on demand. Two things need more than a reload: tools (the tool set is fixed per session, so start a new session or `/reload`) and new mod files (`/reload` re-discovers). `COMMANDCODE_MODS_HOT_RELOAD=0` turns the watcher off.
|
|
48
52
|
|
|
49
53
|
## 5. Headless check (CI)
|
|
50
54
|
|