ispbills-icli 8.7.7 → 8.7.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ispbills-icli",
3
- "version": "8.7.7",
3
+ "version": "8.7.9",
4
4
  "description": "iCli — IspBills AI network engineer in your terminal",
5
5
  "keywords": [
6
6
  "ispbills",
package/src/commands.js CHANGED
@@ -88,7 +88,7 @@ export const SLASH_COMMANDS = [
88
88
  { cmd: '/list-dirs', hint: '', desc: 'Show all allowed directories', group: 'Permissions', local: true },
89
89
  { cmd: '/sandbox', hint: '[enable|disable]', desc: 'Enable/disable local sandboxing', group: 'Permissions', local: true },
90
90
  { cmd: '/experimental', hint: '[on|off|show]', desc: 'Toggle experimental features', group: 'Permissions', local: true },
91
- { cmd: '/init', hint: '', desc: 'Initialize Copilot instructions for this repo', group: 'Permissions', local: true },
91
+ { cmd: '/init', hint: '', desc: 'Initialize .github/copilot-instructions.md for this repo', group: 'Permissions', local: true },
92
92
 
93
93
  // ── Agents & models ──
94
94
  { cmd: '/agent', hint: '', desc: 'Browse and select agents', group: 'Agents', local: true },
@@ -121,9 +121,9 @@ export const SLASH_COMMANDS = [
121
121
  { cmd: '/bug', hint: '', desc: 'Report a bug (alias for /feedback)', group: 'Help', local: true },
122
122
  { cmd: '/update', hint: '', desc: 'Update iCli to the latest version', group: 'Help', local: true },
123
123
  { cmd: '/downgrade', hint: 'VERSION', desc: 'Roll back to a specific CLI version', group: 'Help', local: true },
124
- { cmd: '/app', hint: '', desc: 'Launch the GitHub Copilot desktop app', group: 'Help', local: true },
124
+ { cmd: '/app', hint: '', desc: 'Show instructions to open the iPolot web app', group: 'Help', local: true },
125
125
  { cmd: '/user', hint: '', desc: 'Manage GitHub user list', group: 'Help', local: true },
126
- { cmd: '/login', hint: '', desc: 'Log in to Copilot', group: 'Help', local: true },
126
+ { cmd: '/login', hint: '', desc: 'Log in to iPolot', group: 'Help', local: true },
127
127
  { cmd: '/whoami', hint: '', desc: 'Show current user info', group: 'Help', local: true },
128
128
  { cmd: '/clikit', hint: '[component]', desc: 'Preview CLI UI components', group: 'Help', local: true },
129
129
  { cmd: '/extensions', hint: '[manage|mode]', desc: 'Manage CLI extensions', group: 'Help', local: true },
package/src/tui/app.js CHANGED
@@ -69,6 +69,8 @@ function approxTokens(messages) {
69
69
 
70
70
  const TOKEN_LIMIT = 100000;
71
71
  const SESSION_SORTS = ['relevance', 'created', 'name', 'last used'];
72
+ const TOOL_APPROVAL_ONCE = 'yes, proceed with this tool call';
73
+ const TOOL_APPROVAL_SESSION = '__session_allow__';
72
74
 
73
75
  function sessionAutoName(messages = []) {
74
76
  const firstUser = messages.find((m) => m?.role === 'user' && String(m.content || '').trim());
@@ -437,7 +439,7 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, sh
437
439
  const [deviceError, setDeviceError] = useState(null);
438
440
  const chipsRows = (!busy && chips.length && !choice && !search && !sessionPicker && !diffState) ? 1 : 0;
439
441
  const composerRows = 2;
440
- const chromeRows = 2 + composerRows + 1 + chipsRows + 2; // +2 for two footer rows
442
+ const chromeRows = 2 + composerRows + 1 + chipsRows + 1; // tabBar + composer + sep + chips + 1 footer row
441
443
  const viewportRows = Math.max(3, rows - chromeRows);
442
444
  const scrollPage = Math.max(4, viewportRows - 2);
443
445
 
@@ -899,9 +901,9 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, sh
899
901
  const toolArgs = reply.toolCall?.args || reply.toolCall?.command || '';
900
902
  const toolDesc = toolArgs ? `${toolName}: ${toolArgs}` : (reply.note || toolName);
901
903
  choiceActionRef.current = (value) => {
902
- if (value === '__session_allow__') {
904
+ if (value === TOOL_APPROVAL_SESSION) {
903
905
  cfg.allowedTools = [...(cfg.allowedTools || []), toolName];
904
- runTurn('yes, proceed with this tool call');
906
+ runTurn(TOOL_APPROVAL_ONCE);
905
907
  return;
906
908
  }
907
909
  if (value != null) runTurn(String(value));
@@ -912,9 +914,9 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, sh
912
914
  sel: 0, text: '', focusText: false, allowText: true,
913
915
  isToolApproval: true,
914
916
  options: [
915
- { label: '1. Yes', value: 'yes, proceed with this tool call' },
916
- { label: '2. Yes, and approve shell for the rest of the session', value: '__session_allow__' },
917
- { label: '3. No, and tell Copilot what to do differently (Esc)', value: null },
917
+ { label: '1. Yes', value: TOOL_APPROVAL_ONCE },
918
+ { label: `2. Yes, and approve ${toolName} for the rest of the session`, value: TOOL_APPROVAL_SESSION },
919
+ { label: '3. No, and tell iCopilot what to do differently (Esc)', value: null },
918
920
  ],
919
921
  });
920
922
  } else if (type === 'prompt' || type === 'choice') {
@@ -1449,7 +1451,7 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, sh
1449
1451
  if (q === '/init') {
1450
1452
  const fp = resolve(process.cwd(), '.github/copilot-instructions.md');
1451
1453
  if (existsSync(fp)) {
1452
- pushNotice(`Copilot instructions already exist at ${shortenPath(fp)}. Edit it directly.`);
1454
+ pushNotice(`.github/copilot-instructions.md already exists at ${shortenPath(fp)}. Edit it directly.`);
1453
1455
  } else {
1454
1456
  runTurn('Create a .github/copilot-instructions.md file for this repository with sensible defaults for an ISP NOC tool.');
1455
1457
  }
@@ -1774,7 +1776,7 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, sh
1774
1776
  if (value == null) {
1775
1777
  if (currentChoice?.isToolApproval || /⚙ Tool request|tool request|approval|shell:/i.test(`${currentChoice?.title || ''} ${currentChoice?.note || ''}`)) {
1776
1778
  setChoice({
1777
- title: 'Tell Copilot what to do differently:',
1779
+ title: 'Tell iCopilot what to do differently:',
1778
1780
  note: '',
1779
1781
  sel: 0,
1780
1782
  text: '',
@@ -1836,6 +1838,13 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, sh
1836
1838
  if (!choice) return;
1837
1839
  if (sessionPicker || diffState) return;
1838
1840
  if (key.escape || (key.ctrl && input === 'c')) { setChoice(null); return; }
1841
+ if (choice.isToolApproval && !choice.focusText) {
1842
+ const yesValue = choice.options.find((o) => o?.value === TOOL_APPROVAL_ONCE)?.value;
1843
+ const alwaysValue = choice.options.find((o) => o?.value === TOOL_APPROVAL_SESSION)?.value;
1844
+ if (input === 'y') { pickChoice(yesValue); return; }
1845
+ if (input === 'a') { pickChoice(alwaysValue); return; }
1846
+ if (input === 'n') { pickChoice(null); return; }
1847
+ }
1839
1848
  if (key.tab && choice.allowText) { setChoice((c) => ({ ...c, focusText: !c.focusText })); return; }
1840
1849
  if (key.upArrow) { setChoice((c) => ({ ...c, focusText: false, sel: Math.max(0, c.sel - 1) })); return; }
1841
1850
  if (key.downArrow) {
@@ -2124,23 +2133,23 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, sh
2124
2133
  const tokenCount = approxTokens(convo.current);
2125
2134
  const tokenLabel = contextUsageLabel(tokenCount);
2126
2135
  const modelLabel = streamerMode ? '' : shortModel(model || modelRef.current || 'server default');
2127
- // §8: progressive path shorteningreserve space for right section
2128
- const footerRightEstLen = (modelLabel.length || 0) + (tokenLabel.length || 0) + 8;
2136
+ // §8: single footer row (GH Copilot CLI style) cwd·branch on left, mode·model·tokens on right
2137
+ const modeLabel = mode === 'autopilot' ? 'AUTOPILOT' : mode === 'plan' ? 'PLAN' : null;
2138
+ const footerRightEstLen = (modeLabel ? modeLabel.length + 3 : 0) + (modelLabel.length || 0) + (tokenLabel.length || 0) + 8;
2129
2139
  const cwdMaxLen = Math.max(12, cols - footerRightEstLen - (branch ? branch.length + 6 : 0) - 4);
2130
2140
  const cwd = shortenPath(process.cwd(), cwdMaxLen);
2131
- const footerLeft = [cwd, branch ? `${glyphs.gitBranch}${branch}` : null].filter(Boolean).join(midDot());
2132
- const footerInfoRight = streamerMode ? '' : [modelLabel, tokenLabel].filter(Boolean).join(midDot());
2133
- // Status row: working indicator when busy, key hints when idle, hint message when set
2134
- const footerStatusLeft = busy
2141
+ const footerLeft = busy
2135
2142
  ? `${glyphs.spinner} Working\u2026`
2136
2143
  : hint
2137
2144
  ? hint
2138
- : activeTab !== 'session'
2139
- ? `Enter Session ${midDot()} r refresh ${midDot()} Tab next tab ${midDot()} 1-9 insert command`
2140
- : `? help ${midDot()} / commands ${midDot()} @ mention ${midDot()} \u2191\u2193 history ${midDot()} Tab mode`;
2141
- const footerStatusColor = busy ? C.warning : hint ? C.accent : C.brand;
2142
- const footerStatusRight = busy ? `Ctrl+C cancel` : mode === 'autopilot' ? 'AUTOPILOT' : mode === 'plan' ? 'PLAN' : '';
2143
- const footerStatusRightColor = busy ? C.warning : mode === 'autopilot' ? C.warning : mode === 'plan' ? C.accent : C.muted;
2145
+ : [cwd, branch ? `${glyphs.gitBranch}${branch}` : null].filter(Boolean).join(midDot());
2146
+ const footerLeftColor = busy ? C.warning : hint ? C.accent : C.muted;
2147
+ const footerRight = busy
2148
+ ? `Ctrl+C cancel`
2149
+ : streamerMode
2150
+ ? ''
2151
+ : [modeLabel, modelLabel, tokenLabel].filter(Boolean).join(midDot());
2152
+ const footerRightColor = busy ? C.warning : mode === 'autopilot' ? C.warning : mode === 'plan' ? C.accent : C.muted;
2144
2153
  const composerMode = shellMode ? 'shell' : mode;
2145
2154
 
2146
2155
  // ── Viewport slicing ───────────────────────────────────────────────────────
@@ -2156,7 +2165,7 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, sh
2156
2165
 
2157
2166
  const liveArea = live
2158
2167
  ? html`<${Box} flexDirection="column">
2159
- <${StatusLines} lines=${live.status} busy=${busy} />
2168
+ <${StatusLines} lines=${live.status} busy=${busy} cols=${cols} />
2160
2169
  ${live.reasoning ? html`<${Reasoning} text=${live.reasoning} show=${showReasoning} />` : null}
2161
2170
  ${live.text ? html`<${AssistantMessage} text=${live.text} cols=${cols} ts=${startedAt || Date.now()} streaming=${true} />` : null}
2162
2171
  ${live.reply?.steps ? html`<${Plan} reply=${live.reply} live=${true} />` : null}
@@ -2166,9 +2175,8 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, sh
2166
2175
 
2167
2176
  const scrollPct = maxScrollRef.current > 0 ? Math.round((scrollOffset / maxScrollRef.current) * 100) : 100;
2168
2177
  const scrollIndicator = hasAbove
2169
- ? html`<${Box} paddingX=${1} justifyContent="space-between" width=${cols}>
2170
- <${Text} color=${C.muted} dimColor>${glyphs.up} ${aboveCount} above ${midDot()} PgUp/PgDn ${midDot()} scroll<//>
2171
- <${Text} color=${C.muted} dimColor>${scrollPct}%<//>
2178
+ ? html`<${Box} paddingX=${1} justifyContent="flex-end" width=${cols}>
2179
+ <${Text} color=${C.muted} dimColor>${glyphs.up} ${aboveCount} above ${midDot()} ${scrollPct}%<//>
2172
2180
  <//>`
2173
2181
  : null;
2174
2182
 
@@ -2185,7 +2193,7 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, sh
2185
2193
  />
2186
2194
  ${diffState.commentInput
2187
2195
  ? html`<${Choice}
2188
- title=${`Tell Copilot what stands out about ${diffState.commentInput.file}:${diffState.commentInput.lineNumber || '?'}`}
2196
+ title=${`Tell iCopilot what stands out about ${diffState.commentInput.file}:${diffState.commentInput.lineNumber || '?'}`}
2189
2197
  note=${diffState.commentInput.content}
2190
2198
  sel=${0}
2191
2199
  allowText=${true}
@@ -2258,18 +2266,12 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, sh
2258
2266
  clearToken=${composerClearToken}
2259
2267
  />`}
2260
2268
 
2269
+ <${Text} color=${C.separator}>${'─'.repeat(Math.max(1, cols))}<//>
2261
2270
  <${Box} paddingX=${1} width=${cols}>
2262
2271
  <${Box} flexGrow=${1}>
2263
- <${Text} color=${footerStatusColor} bold=${busy} wrap="truncate-end">${footerStatusLeft}<//>
2264
- <//>
2265
- <${Text} color=${footerStatusRightColor} bold wrap="truncate-end">${footerStatusRight}<//>
2266
- <//>
2267
-
2268
- <${Box} paddingX=${1} width=${cols}>
2269
- <${Box} flexGrow=${1}>
2270
- <${Text} color=${C.muted} dimColor wrap="truncate-end">${footerLeft}<//>
2272
+ <${Text} color=${footerLeftColor} bold=${busy} dimColor=${!busy && !hint} wrap="truncate-end">${footerLeft}<//>
2271
2273
  <//>
2272
- <${Text} color=${C.muted} dimColor wrap="truncate-end">${footerInfoRight}<//>
2274
+ <${Text} color=${footerRightColor} bold=${mode === 'autopilot' || mode === 'plan'} dimColor=${!busy && !modeLabel} wrap="truncate-end">${footerRight}<//>
2273
2275
  <//>
2274
2276
  <//>
2275
2277
  <//>`;
@@ -124,7 +124,7 @@ export function TabBar({ active = 'session', cols = 80 }) {
124
124
  const label = ` ${i + 1} ${tab.label} `.padEnd(TAB_W);
125
125
  return html`
126
126
  <${Box} key=${tab.id} width=${TAB_W}>
127
- <${Text} bold=${on} color=${on ? 'cyan' : 'white'} dimColor=${!on} inverse=${on}>
127
+ <${Text} bold=${on} color=${on ? C.brand : C.muted} dimColor=${!on} inverse=${on}>
128
128
  ${label}
129
129
  <//>
130
130
  <//>`;
@@ -229,8 +229,9 @@ export function UserMessage({ text, cols = 80, ts }) {
229
229
  const stamp = ts ? new Date(ts).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }) : '';
230
230
  return html`
231
231
  <${Box} flexDirection="column" marginBottom=${1}>
232
+ <${Sep} cols=${cols} />
232
233
  <${Box} flexDirection="row" justifyContent="space-between" width=${cols} paddingX=${1}>
233
- <${Text} bold color="cyan">You<//>
234
+ <${Text} bold color=${C.brand}>You<//>
234
235
  <${Text} color=${C.muted}>${stamp}<//>
235
236
  <//>
236
237
  <${Box} paddingX=${1} marginLeft=${2} flexWrap="wrap">
@@ -264,12 +265,14 @@ function StreamingText({ text = '' }) {
264
265
 
265
266
  export function AssistantMessage({ text, cols = 80, ts, label, streaming = false }) {
266
267
  const stamp = ts ? new Date(ts).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }) : '';
267
- const speakerLabel = streaming ? 'iCopilot ●' : 'iCopilot';
268
268
  const bodyWidth = Math.min(cols, 100);
269
269
  return html`
270
270
  <${Box} flexDirection="column" marginBottom=${1}>
271
+ <${Sep} cols=${cols} />
271
272
  <${Box} flexDirection="row" justifyContent="space-between" width=${cols} paddingX=${1}>
272
- <${Text} bold color=${C.success}>${speakerLabel}<//>
273
+ <${Box}>
274
+ <${Text} color=${C.accent} bold>${glyphs.copilotDot} <//><${Text} bold>iCopilot${streaming ? ' ●' : ''}<//>
275
+ <//>
273
276
  <${Text} color=${C.muted}>${stamp}<//>
274
277
  <//>
275
278
  <${Box} paddingX=${1} marginLeft=${2} flexDirection="column" width=${bodyWidth}>
@@ -279,18 +282,22 @@ export function AssistantMessage({ text, cols = 80, ts, label, streaming = false
279
282
  }
280
283
 
281
284
  export function ShellOutput({ command, output, cols = 80, ts, code = 0 }) {
285
+ const ok = code === 0;
286
+ const tone = ok ? C.success : C.error;
282
287
  return html`
283
288
  <${Box} flexDirection="column" marginTop=${1}>
289
+ <${Sep} cols=${cols} />
284
290
  <${SpeakerLine}
285
291
  label=${"$ Shell"}
286
- color=${code === 0 ? C.warning : C.error}
292
+ color=${tone}
287
293
  cols=${cols}
288
294
  ts=${ts}
289
- rightLabel=${code === 0 ? '' : `exit ${code}`}
295
+ rightLabel=${ok ? '' : `exit ${code}`}
290
296
  />
291
297
  <${Box} paddingX=${1} flexDirection="column">
292
298
  <${Text} color=${C.warning}>$ ${command}<//>
293
- <${Text}>${output || '(no output)'}<//>
299
+ <${Text} color=${tone}>${ok ? glyphs.check : `${glyphs.cross} exit ${code}` }<//>
300
+ <${Text} color=${ok ? C.white : tone}>${output || '(no output)'}<//>
294
301
  <//>
295
302
  <//>`;
296
303
  }
@@ -386,6 +393,7 @@ export function StatusLines({ lines = [], busy = false, navigable = false }) {
386
393
  if (!lines.length) return null;
387
394
  const lastIdx = lines.length - 1;
388
395
 
396
+
389
397
  return html`
390
398
  <${Box} flexDirection="column" marginTop=${1}>
391
399
  ${lines.map((line, i) => {
@@ -654,15 +662,17 @@ export function Thinking({ label = 'Working', startedAt }) {
654
662
  }
655
663
 
656
664
  export function Choice({ title, note, options = [], sel = 0, allowText = false, text = '', focusText = false, isToolApproval = false, cols = 80 }) {
657
- // §13: tool approval uses exact spec format with Sep-style header
665
+ // §13: tool approval uses separator-line style matching TUI.md spec exactly
658
666
  if (isToolApproval) {
659
667
  return html`
660
668
  <${Box} flexDirection="column" marginTop=${1}>
661
- <${Box} paddingX=${2} marginTop=${1}>
662
- <${Text} color=${C.accent} bold>${glyphs.bolt} Tool request<//>
669
+ <${Sep} cols=${cols} />
670
+ <${Box} paddingX=${1} marginBottom=${1}>
671
+ <${Text} color=${C.accent} bold>${glyphs.settings} Tool request<//>
663
672
  <//>
664
- ${note ? html`<${Box} paddingX=${2} marginTop=${1}><${Text} color=${C.muted}>${note}<//><//>` : null}
665
- <${Box} flexDirection="column" marginTop=${1} paddingX=${2}>
673
+ <${Sep} cols=${cols} />
674
+ ${note ? html`<${Box} paddingX=${1} marginTop=${1} marginBottom=${1}><${Text} color=${C.white}>${note}<//><//>` : null}
675
+ <${Box} flexDirection="column" marginTop=${1} paddingX=${1}>
666
676
  ${options.map((o, i) => {
667
677
  const on = !focusText && i === sel;
668
678
  const label = o.label ?? o.value ?? String(o);
@@ -676,13 +686,13 @@ export function Choice({ title, note, options = [], sel = 0, allowText = false,
676
686
  ${allowText
677
687
  ? html`<${Box} key="freetext" marginTop=${1}>
678
688
  <${Text} color=${focusText ? C.accent : C.muted}>${focusText ? glyphs.prompt + ' ' : ' '}<//>
679
- <${Text} color=${C.muted}>Tell Copilot what to do differently: <//>
689
+ <${Text} color=${C.muted}>Tell iCopilot what to do differently: <//>
680
690
  <${Text} color=${C.white}>${text}<//>${focusText ? html`<${Text} inverse> <//>` : null}
681
691
  <//>`
682
692
  : null}
683
693
  <//>
684
- <${Box} marginTop=${1} paddingX=${2}>
685
- <${Text} color=${C.muted}>${glyphs.up}${glyphs.down} choose ${midDot()}Enter confirm ${midDot()}Esc cancel<//>
694
+ <${Box} marginTop=${1} paddingX=${1}>
695
+ <${Text} color=${C.muted}>y / a / n ${midDot()} ${glyphs.up}${glyphs.down} move ${midDot()} Enter confirm ${midDot()} Esc cancel<//>
686
696
  <//>
687
697
  <//>`;
688
698
  }
@@ -512,7 +512,7 @@ export function Composer({
512
512
  <//>`
513
513
  : null}
514
514
 
515
- <${Text} color=${C.muted}>${''.repeat(Math.max(1, width))}<//>
515
+ <${Text} color=${C.accent}>${'_'.repeat(Math.max(1, width))}<//>
516
516
 
517
517
  <${Box} width=${width} paddingX=${1} justifyContent="space-between">
518
518
  <${Box} flexGrow=${1}>
package/src/tui/run.js CHANGED
@@ -93,7 +93,7 @@ function exitAltScreen() {
93
93
  altScreenActive = false;
94
94
  }
95
95
 
96
- // iCopilot wordmark in block letters
96
+ // iPolot wordmark in block letters
97
97
  const LOGO = [
98
98
  ' ██╗ ██████╗ ██████╗ ██████╗ ██╗██╗ ██████╗ ████████╗',
99
99
  ' ██║██╔════╝██╔═══██╗██╔══██╗██║██║ ██╔═══██╗╚══██╔══╝',
@@ -145,8 +145,8 @@ async function animateSplash(cfg = {}) {
145
145
  const R = '\x1b[0m';
146
146
 
147
147
  const hr = `${SEP}${'─'.repeat(cols)}${R}`;
148
- const subtitle = `${ACC}◈ iCopilot${R} ${DIM}IspBills network engineer in your terminal${R}`;
149
- const subtitleLen = 10 + 2 + 46; // approx visual length
148
+ const subtitle = `${ACC}◈ iPolot${R} ${DIM}IspBills network engineer in your terminal${R}`;
149
+ const subtitleLen = subtitle.replace(/\x1b\[[0-9;]*m/g, '').length;
150
150
  const subPad = ' '.repeat(Math.max(0, Math.floor((cols - subtitleLen) / 2)));
151
151
 
152
152
  process.stdout.write(`\n${hr}\n`);
package/src/tui/theme.js CHANGED
@@ -46,6 +46,7 @@ const ASCII_EXTRA = {
46
46
  chip: '|', // chip separator
47
47
  expand: '+', // expand indicator
48
48
  collapse: '-', // collapse indicator
49
+ settings: '#', // tool-approval header (ASCII safe)
49
50
  };
50
51
  const UNICODE_EXTRA = {
51
52
  copilotDot: '◈', // spec §6: ◈ Copilot speaker label (◈ in accent/purple)
@@ -56,6 +57,7 @@ const UNICODE_EXTRA = {
56
57
  chip: '·',
57
58
  expand: '▸',
58
59
  collapse: '▾',
60
+ settings: '⚙', // spec §13: tool request header glyph
59
61
  };
60
62
 
61
63
  // Merged glyph proxy: own extras first, then ui.js base glyphs.