aiden-runtime 4.7.0 → 4.8.1

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.
Files changed (36) hide show
  1. package/README.md +12 -1
  2. package/dist/cli/v4/aidenCLI.js +40 -5
  3. package/dist/cli/v4/callbacks.js +52 -31
  4. package/dist/cli/v4/chatSession.js +55 -8
  5. package/dist/cli/v4/commands/help.js +22 -11
  6. package/dist/cli/v4/commands/runs.js +42 -24
  7. package/dist/cli/v4/commands/skills.js +15 -17
  8. package/dist/cli/v4/commands/update.js +14 -2
  9. package/dist/cli/v4/commands/usage.js +17 -5
  10. package/dist/cli/v4/daemonAgentBuilder.js +1 -0
  11. package/dist/cli/v4/design/tokens.js +265 -0
  12. package/dist/cli/v4/display/framedPanel.js +116 -0
  13. package/dist/cli/v4/display/toolTrail.js +2 -2
  14. package/dist/cli/v4/display.js +489 -164
  15. package/dist/cli/v4/onboarding/disclaimer.js +42 -10
  16. package/dist/cli/v4/onboarding/loading.js +24 -1
  17. package/dist/cli/v4/onboarding/successScreen.js +17 -8
  18. package/dist/cli/v4/pasteIntercept.js +214 -70
  19. package/dist/cli/v4/replyRenderer.js +213 -58
  20. package/dist/cli/v4/setupWizard.js +19 -2
  21. package/dist/cli/v4/skinEngine.js +13 -0
  22. package/dist/cli/v4/table.js +65 -8
  23. package/dist/core/v4/aidenAgent.js +23 -0
  24. package/dist/core/v4/auxiliaryClient.js +46 -13
  25. package/dist/core/v4/daemon/dispatcher/realAgentRunner.js +13 -8
  26. package/dist/core/v4/promptBuilder.js +51 -0
  27. package/dist/core/v4/subagent/childBuilder.js +1 -0
  28. package/dist/core/v4/subagent/spawnSubAgent.js +7 -1
  29. package/dist/core/v4/ui/banner.js +16 -16
  30. package/dist/core/v4/update/executeInstall.js +10 -6
  31. package/dist/core/v4/update/installMethodDetect.js +7 -0
  32. package/dist/core/version.js +67 -2
  33. package/dist/moat/approvalEngine.js +14 -0
  34. package/dist/tools/v4/index.js +54 -0
  35. package/dist/tools/v4/subagent/spawnSubAgentTool.js +23 -0
  36. package/package.json +1 -3
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.usage = void 0;
4
4
  const modelCatalog_1 = require("../../../providers/v4/modelCatalog");
5
+ const table_1 = require("../table");
5
6
  exports.usage = {
6
7
  name: 'usage',
7
8
  description: 'Show token consumption and estimated cost.',
@@ -33,12 +34,23 @@ exports.usage = {
33
34
  const aux = ctx.auxiliaryClient.getUsage();
34
35
  const purposes = Object.keys(aux);
35
36
  if (purposes.length > 0) {
37
+ // v4.8.0 Slice 3 — framed auxiliary-calls table replaces the
38
+ // ad-hoc padEnd lines. Right-align numeric columns.
36
39
  ctx.display.write('\n');
37
- ctx.display.info('Auxiliary calls:');
38
- for (const p of purposes) {
39
- const u = aux[p];
40
- ctx.display.write(` ${p.padEnd(18)} calls=${u.calls} in=${u.inputTokens} out=${u.outputTokens}\n`);
41
- }
40
+ ctx.display.write((0, table_1.renderTable)(purposes.map((p) => ({
41
+ purpose: p,
42
+ calls: String(aux[p].calls),
43
+ in: String(aux[p].inputTokens),
44
+ out: String(aux[p].outputTokens),
45
+ })), [
46
+ { key: 'purpose', header: 'purpose', align: 'left' },
47
+ { key: 'calls', header: 'calls', align: 'right' },
48
+ { key: 'in', header: 'in', align: 'right' },
49
+ { key: 'out', header: 'out', align: 'right' },
50
+ ], {
51
+ title: 'Auxiliary calls',
52
+ totalCount: `${purposes.length} ${purposes.length === 1 ? 'purpose' : 'purposes'}`,
53
+ }));
42
54
  }
43
55
  }
44
56
  return {};
@@ -111,6 +111,7 @@ function buildDaemonAgentBuilder(deps) {
111
111
  resolveVerifiedFlag: deps.resolveVerifiedFlag,
112
112
  resolveToolset: deps.resolveToolset,
113
113
  resolveMutates: deps.resolveMutates,
114
+ resolveUiOnly: deps.resolveUiOnly,
114
115
  // Memory snapshot refresh — daemon agent doesn't track dirty
115
116
  // bits because each instance is short-lived; we provide the
116
117
  // refresh callback so honestyEnforcement (and any future
@@ -0,0 +1,265 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2026 Shiva Deore (Taracod).
4
+ * Licensed under AGPL-3.0. See LICENSE for details.
5
+ *
6
+ * Aiden — local-first agent.
7
+ */
8
+ /**
9
+ * cli/v4/design/tokens.ts — v4.8.0 Slice 2 design-system foundation.
10
+ *
11
+ * Color, glyph, and spacing tokens consumed by every subsequent slice
12
+ * (tables, panels, status bar, markdown, loading state). Hex strings
13
+ * here parallel the RGB tuples in `skinEngine.ts` — tokens.ts is the
14
+ * design intent, skinEngine is the runtime color authority for
15
+ * `applyColors(text, kind)`. Renderer slices (3+) consume these.
16
+ * isVerbose() reads env at call time (Ollama-options precedent).
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.VERBOSE_MODE_ENV = exports.spacing = exports.glyphs = exports.colors = exports.TRAIL_PIPE = void 0;
20
+ exports.isVerbose = isVerbose;
21
+ // Re-export so consumers can import the trail gutter from tokens
22
+ // alongside the rest of the design system. The original constant
23
+ // stays at its current home for backward compatibility with code
24
+ // that imports from `display/toolTrail` directly.
25
+ var toolTrail_1 = require("../display/toolTrail");
26
+ Object.defineProperty(exports, "TRAIL_PIPE", { enumerable: true, get: function () { return toolTrail_1.TRAIL_PIPE; } });
27
+ // ── Colors ────────────────────────────────────────────────────────────────
28
+ /**
29
+ * Hex color tokens. Mirrors skinEngine RGB tuples for existing kinds;
30
+ * adds `content.tertiary`, `semantic.info`, `surface.*` (renderer
31
+ * slices propagate these to skinEngine). Content primary lifted
32
+ * `#e0e0e0` → `#e8ebf0` for legibility on dark terminals.
33
+ */
34
+ exports.colors = {
35
+ brand: {
36
+ /** Aiden signature orange — already used by skinEngine's `brand`. */
37
+ primary: '#FF6B35',
38
+ /** 30%-luma brand orange for borders and dim brand surfaces. */
39
+ muted: '#7a3119',
40
+ },
41
+ content: {
42
+ /** Brightest text — primary reply content, headings on dark bg. */
43
+ primary: '#e8ebf0',
44
+ /** Warm muted — gutter, secondary detail, post-action timeline. */
45
+ secondary: '#b8a89a',
46
+ /** Least-important text — captions, dim status, deprecated rows. */
47
+ tertiary: '#6a6a6a',
48
+ },
49
+ semantic: {
50
+ success: '#7fc28b',
51
+ warn: '#e0a040',
52
+ error: '#e05a5a',
53
+ info: '#7da7c7',
54
+ },
55
+ /**
56
+ * v4.8.0 Slice 7 hotfix #2 — per-metric accent palette for the
57
+ * packed status footer. Each metric gets a stable colour so
58
+ * cross-glance reading stays consistent: cyan model, amber tokens,
59
+ * purple turn count, teal timer. Maps to skinEngine ColorKinds at
60
+ * runtime: tool / warn / metric_turn / success.
61
+ */
62
+ metrics: {
63
+ model: '#9cdcfe',
64
+ tokens: '#e0a040',
65
+ turnCount: '#a48be0',
66
+ timer: '#7fc28b',
67
+ },
68
+ surface: {
69
+ /** Terminal background reference; tokens never paint bg directly. */
70
+ bg: '#0d0e10',
71
+ /** Elevated panel fill (Slice 4 cards / boxed surfaces). */
72
+ elevated: '#16181b',
73
+ /** Panel borders — frames around tables, capability cards. */
74
+ border: '#2a2a2a',
75
+ /** Section dividers — between boot card and REPL, between events. */
76
+ divider: '#3a3a3a',
77
+ },
78
+ };
79
+ // ── Glyphs ────────────────────────────────────────────────────────────────
80
+ /**
81
+ * Centralized glyph vocabulary. Four namespaces — `event` (ui_* row
82
+ * glyphs), `status` (boot/footer chrome), `util` (bullets, dividers,
83
+ * checks), `trail` (tool-trail row prefix). Renderer slices (3+)
84
+ * replace inline literals with these references.
85
+ */
86
+ exports.glyphs = {
87
+ event: {
88
+ /** ui_task_update status:'running' */
89
+ running: '⟳',
90
+ /** ui_task_done status:'success' */
91
+ done: '✓',
92
+ /** ui_task_done status:'failure' */
93
+ fail: '✗',
94
+ /** ui_task_done status:'blocked' / hard-blocked */
95
+ blocked: '⊘',
96
+ /** ui_task_update status:'paused' */
97
+ paused: '⏸',
98
+ /** ui_task_update status:'blocked' (soft block, awaiting input) */
99
+ hardBlock: '⛔',
100
+ /** ui_approval_request leading glyph + ui_toast kind:'warning' */
101
+ warning: '⚠',
102
+ /** ui_toast kind:'info' */
103
+ info: 'ℹ',
104
+ /** ui_command_result header glyph */
105
+ cmd: '▸',
106
+ /** ui_artifact_created kind:'file' */
107
+ file: '📄',
108
+ /** ui_artifact_created kind:'skill' */
109
+ skill: '🛠',
110
+ /** ui_artifact_created kind:'directory' */
111
+ directory: '📁',
112
+ },
113
+ status: {
114
+ /** User-prompt prefix + Aiden brand triangle (activityIndicator). */
115
+ triangle: '▲',
116
+ /** Solid filled dot — used for `●` status markers. */
117
+ dot: '●',
118
+ /** Hollow dot — companion to `dot` for inactive states. */
119
+ dotOpen: '○',
120
+ /** Status footer column separator. */
121
+ sep: '│',
122
+ /** Slice 7 — turn counter prefix. Slice 9 hotfix: glyph dropped
123
+ * entirely. The bare colored number (purple `metric_turn` kind)
124
+ * carries the meaning, matching the timer pattern. */
125
+ turn: '',
126
+ /** Slice 7 — session timer prefix. Slice 9 hotfix: `⌛` (U+231B
127
+ * HOURGLASS WITH FLOWING SAND) restored. The font set Shiva
128
+ * confirmed renders ●/○/▲ also renders this widely-supported
129
+ * emoji-class hourglass. Trailing space owned by the segment
130
+ * composition site to keep the token minimal. */
131
+ timer: '⌛',
132
+ },
133
+ util: {
134
+ /** Section / row divider. */
135
+ divider: '─',
136
+ /** Solid bullet for list rows. */
137
+ bullet: '●',
138
+ /** Hollow bullet (inactive / unselected). */
139
+ bulletOpen: '○',
140
+ /** Mid-dot — existing skin bullet for compact lists. */
141
+ midDot: '•',
142
+ /** Check-success — heavier than ui-event `✓` for emphasis. Slice 8
143
+ * hotfix: VS16 (U+FE0F) appended to force emoji-presentation width
144
+ * = 2 cells so `✔` renders with the same visual heft as `●`/`○`
145
+ * bullets and doesn't sit flush against following text. Same fix
146
+ * pattern as Slice 5 for `✏`/`👁`. */
147
+ check: '✔️',
148
+ /** Inline arrow — submenu, breadcrumb. */
149
+ arrow: '›',
150
+ /** Trail-style horizontal arrow — ui_command_result header. */
151
+ triArrow: '▸',
152
+ },
153
+ trail: {
154
+ /** Tool-trail gutter character. Re-exported from toolTrail.ts. */
155
+ gutter: '┊',
156
+ },
157
+ /**
158
+ * Box-drawing chrome for table + panel surfaces (Slice 3+). Renders
159
+ * sharp ASCII so wide-display terminals and narrow ConPTYs both
160
+ * align cleanly. Slice 4 reuses `hLine` for the panel divider.
161
+ */
162
+ chrome: {
163
+ topLeft: '┌', topRight: '┐', botLeft: '└', botRight: '┘',
164
+ teeDown: '┬', teeUp: '┴', teeRight: '├', teeLeft: '┤',
165
+ cross: '┼', hLine: '─', vLine: '│',
166
+ },
167
+ /**
168
+ * Aiden-native framed-panel chrome (Slice 4). Left-edge accent bar
169
+ * gives panels brand identity without a closing box; asymmetric
170
+ * chrome (top + bottom dividers, no corners) reads as intentional
171
+ * Aiden signature rather than borrowed pattern.
172
+ *
173
+ * v4.8.0 Slice 11c — bar glyph swapped from `▎` (U+258E LEFT ONE
174
+ * QUARTER BLOCK) to `│` (U+2502 BOX DRAWING LIGHT VERTICAL).
175
+ * `▎` rendered as outline-tofu on Cascadia / common Windows
176
+ * ConPTY fonts; `│` is universally supported across Consolas,
177
+ * Cascadia, SF Mono, JetBrains Mono, etc. (same source as the
178
+ * existing `chrome.vLine`). Brand identity now carried by colour,
179
+ * not by an exotic codepoint. Propagates to all panel surfaces:
180
+ * /help, approval prompt, Aiden reply header, setup wizard,
181
+ * Built solo card, and the framed-panel renderer.
182
+ */
183
+ panel: {
184
+ bar: '│',
185
+ },
186
+ /**
187
+ * v4.8.0 Slice 10d — rounded heavy frame for identity / credits
188
+ * surfaces (the "Built solo" scrollFooter being the first consumer).
189
+ * Distinct from `glyphs.chrome` which uses SHARP corners (`┌ ┐ └ ┘`)
190
+ * for table chrome — rounded corners signal "identity card" rather
191
+ * than "data table". Both glyph sets share `─` and `│` for sides
192
+ * (those live on `glyphs.chrome.hLine` / `vLine` to avoid duplication).
193
+ */
194
+ box: {
195
+ topLeft: '╭',
196
+ topRight: '╮',
197
+ bottomLeft: '╰',
198
+ bottomRight: '╯',
199
+ },
200
+ /**
201
+ * Status footer progress bar pair. v4.8.0 Slice 7 hotfix #3 — moved
202
+ * from hex dots (⬢/⬡) back to the U+25CF/U+25CB circle pair: those
203
+ * codepoints render in every monospace font (Consolas, Cascadia,
204
+ * SF Mono, JetBrains Mono, etc.) whereas the hex dots required
205
+ * Nerd Font / Apple Symbols fallback chains and rendered as `□`
206
+ * boxes on many setups. Colour still carries the meaning; the
207
+ * glyph just needs to render at all.
208
+ */
209
+ bar: {
210
+ filled: '●',
211
+ empty: '○',
212
+ },
213
+ /**
214
+ * v4.8.0 Slice 11 — single-row activity-indicator shimmer track.
215
+ * Replaces the prior 2-row layout (verb row + `▓`/`░` wave bar) with
216
+ * one row whose leading glyph cluster is a 4-cell `█` (U+2588 FULL
217
+ * BLOCK) segment sliding left-to-right on a muted `─` track. Both
218
+ * glyphs are CP437-safe and already shipping in chrome elsewhere
219
+ * (`hLine`, panel `bar`). Colour, not glyph, signals state: the
220
+ * filled block paints `brand`, the track paints `muted`.
221
+ */
222
+ shimmer: {
223
+ block: '█',
224
+ track: '─',
225
+ },
226
+ };
227
+ // ── Spacing ───────────────────────────────────────────────────────────────
228
+ /**
229
+ * Integer spacing tokens. 0-indexed column counts; subagent depth
230
+ * indent is INSIDE the gutter (`┊` stays at col 0, indent shifts the
231
+ * glyph + content right).
232
+ */
233
+ exports.spacing = {
234
+ indent: {
235
+ /** Column where the trail gutter `┊` lands. */
236
+ gutter: 0,
237
+ /** Column where the event glyph lands (after `┊` + space). */
238
+ glyph: 2,
239
+ /** Column where primary content text lands (after glyph + space). */
240
+ content: 4,
241
+ /** Column where secondary detail text lands (after primary + space). */
242
+ detail: 6,
243
+ /** Spaces per subagent depth level (inside the gutter). */
244
+ subagentPerDepth: 2,
245
+ },
246
+ between: {
247
+ /** Blank rows between related events in the same group. */
248
+ sameGroup: 0,
249
+ /** Blank rows between event groups. */
250
+ groups: 1,
251
+ /** Blank rows between top-level sections (boot card → REPL → events). */
252
+ sections: 2,
253
+ },
254
+ };
255
+ // ── Verbose-mode env-gated flag ───────────────────────────────────────────
256
+ /** Env var name; Slice 5 consumes via isVerbose() to gate debug surfaces. */
257
+ exports.VERBOSE_MODE_ENV = 'AIDEN_VERBOSE';
258
+ /**
259
+ * Verbose flag, read at call time. Only literal `'1'` enables; any
260
+ * other value (`'true'`, `'yes'`, etc.) is off so the gate stays
261
+ * unambiguous. Matches the Ollama-options env-time-read precedent.
262
+ */
263
+ function isVerbose() {
264
+ return process.env[exports.VERBOSE_MODE_ENV] === '1';
265
+ }
@@ -0,0 +1,116 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2026 Shiva Deore (Taracod).
4
+ * Licensed under AGPL-3.0. See LICENSE for details.
5
+ *
6
+ * Aiden — local-first agent.
7
+ */
8
+ /**
9
+ * cli/v4/display/framedPanel.ts — v4.8.0 Slice 4. Aiden-native panel:
10
+ * left orange bar `▎`, top + bottom dividers (no corners), footer
11
+ * always present. Max 3 distinct colors per panel.
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.renderFramedPanel = renderFramedPanel;
15
+ const skinEngine_1 = require("../skinEngine");
16
+ const tokens_1 = require("../design/tokens");
17
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
18
+ const stringWidth = require('string-width');
19
+ function vWidth(s) {
20
+ try {
21
+ return stringWidth(s);
22
+ }
23
+ catch {
24
+ return s.length;
25
+ }
26
+ }
27
+ const BAR_COLOR = 'brand';
28
+ function statusKind(s) {
29
+ if (s === 'active')
30
+ return 'success';
31
+ if (s === 'running')
32
+ return 'tool';
33
+ if (s === 'disabled')
34
+ return 'muted';
35
+ if (s === 'installed' || s === 'available')
36
+ return 'session';
37
+ return 'muted';
38
+ }
39
+ /**
40
+ * Render the Aiden-native framed panel. Returns a multi-line string
41
+ * with trailing newline; caller writes via `display.write`.
42
+ */
43
+ /**
44
+ * v4.8.0 Slice 4 hotfix — word-boundary wrap. Returns the input split
45
+ * into chunks each ≤ `max` visible chars, preferring spaces. Used by
46
+ * the panel description column when the row's text exceeds the
47
+ * allocated width: subsequent chunks render on indented continuation
48
+ * lines instead of being truncated to `…`.
49
+ */
50
+ function smartWrap(s, max) {
51
+ if (max <= 0 || vWidth(s) <= max)
52
+ return [s];
53
+ const out = [];
54
+ let rest = s;
55
+ while (vWidth(rest) > max) {
56
+ let cut = max;
57
+ const lastSpace = rest.slice(0, max).lastIndexOf(' ');
58
+ if (lastSpace >= Math.floor(max * 0.5))
59
+ cut = lastSpace;
60
+ out.push(rest.slice(0, cut).trimEnd());
61
+ rest = rest.slice(cut).trimStart();
62
+ }
63
+ if (rest.length > 0)
64
+ out.push(rest);
65
+ return out;
66
+ }
67
+ function renderFramedPanel(opts) {
68
+ const sk = (0, skinEngine_1.getSkinEngine)();
69
+ // v4.8.0 Slice 4 hotfix — read terminal width like table.ts does so
70
+ // wide terminals get wide panels instead of a hardcoded 72-col cap.
71
+ // Indent every row by 2 cells (matches table's `indent` default) so
72
+ // the left bar sits at col 2 rather than col 0 — col-0 paint reads
73
+ // as "terminal-edge artifact" instead of "panel boundary".
74
+ const indent = ' ';
75
+ const termCols = process.stdout.columns ?? 100;
76
+ const innerW = Math.max(40, opts.width ?? Math.min(termCols - indent.length, 110));
77
+ const bar = sk.applyColors(tokens_1.glyphs.panel.bar, BAR_COLOR);
78
+ const line = (content) => `${indent}${bar} ${content}`;
79
+ const maxCmd = Math.max(...opts.rows.map(r => vWidth(r.command)), 4);
80
+ const maxArgs = Math.max(...opts.rows.map(r => vWidth(r.args ?? '')), 0);
81
+ const cmdCol = maxCmd + 2;
82
+ const argsCol = maxArgs > 0 ? maxArgs + 2 : 0;
83
+ const maxStat = Math.max(...opts.rows.map(r => vWidth(r.status ?? '')), 0);
84
+ const statCol = maxStat > 0 ? maxStat + 2 : 0;
85
+ const descCol = Math.max(8, innerW - 2 - cmdCol - argsCol - statCol);
86
+ const lines = [];
87
+ const titlePaint = sk.applyColors(opts.title, 'heading');
88
+ if (opts.subtitle) {
89
+ const subRight = ' '.repeat(Math.max(0, innerW - 1 - vWidth(opts.title) - vWidth(opts.subtitle)));
90
+ lines.push(line(` ${titlePaint}${subRight}${sk.applyColors(opts.subtitle, 'muted')}`));
91
+ }
92
+ else {
93
+ lines.push(line(` ${titlePaint}`));
94
+ }
95
+ lines.push(line(' ' + sk.applyColors(tokens_1.glyphs.chrome.hLine.repeat(innerW - 2), 'muted')));
96
+ // Body rows — wrap descriptions instead of truncating.
97
+ const descIndent = ' ' + ' '.repeat(cmdCol) + ' '.repeat(argsCol);
98
+ for (const row of opts.rows) {
99
+ const cmd = sk.applyColors(row.command.padEnd(cmdCol), 'agent');
100
+ const args = argsCol > 0
101
+ ? sk.applyColors((row.args ?? '').padEnd(argsCol), 'muted')
102
+ : '';
103
+ const stat = statCol > 0 && row.status
104
+ ? sk.applyColors(row.status.padStart(maxStat), statusKind(row.status))
105
+ : (statCol > 0 ? ' '.repeat(statCol) : '');
106
+ const wrapped = smartWrap(row.description, descCol);
107
+ const head = sk.applyColors(wrapped[0].padEnd(descCol), 'muted');
108
+ lines.push(line(` ${cmd}${args}${head}${stat ? ' ' + stat : ''}`));
109
+ for (let i = 1; i < wrapped.length; i++) {
110
+ lines.push(line(descIndent + sk.applyColors(wrapped[i].padEnd(descCol), 'muted')));
111
+ }
112
+ }
113
+ lines.push(line(' ' + sk.applyColors(tokens_1.glyphs.chrome.hLine.repeat(innerW - 2), 'muted')));
114
+ lines.push(line(' ' + sk.applyColors(opts.footer, 'muted')));
115
+ return lines.join('\n') + '\n';
116
+ }
@@ -49,12 +49,12 @@ const TRAIL_MAP = [
49
49
  'file_list', 'list_directory', 'list_directory_with_sizes',
50
50
  'directory_tree', 'file_info', 'get_file_info',
51
51
  'observe', 'read', 'list'],
52
- icon: '👁', verb: 'reading' },
52
+ icon: '👁️', verb: 'reading' },
53
53
  // ── Write / edit / create ────────────────────────────────────────────
54
54
  { keys: ['file_write', 'write_file', 'edit_file', 'move_file',
55
55
  'notebook_edit', 'create_directory',
56
56
  'write', 'edit', 'create', 'save'],
57
- icon: '', verb: 'writing' },
57
+ icon: '✏️', verb: 'writing' },
58
58
  // ── Execute / run / shell ────────────────────────────────────────────
59
59
  { keys: ['bash', 'powershell', 'execute_code', 'skill_view',
60
60
  'shortcuts_execute', 'javascript_tool',