ispbills-icli 8.7.6 → 8.7.8

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.6",
3
+ "version": "8.7.8",
4
4
  "description": "iCli — IspBills AI network engineer in your terminal",
5
5
  "keywords": [
6
6
  "ispbills",
package/src/banner.js CHANGED
@@ -13,7 +13,7 @@ const LOGO = [
13
13
  const LOGO_WIDTH = Math.max(...LOGO.map((l) => l.length));
14
14
 
15
15
  /**
16
- * Full iCopilot banner. On narrow terminals falls back to a compact header.
16
+ * Full iPolot banner. On narrow terminals falls back to a compact header.
17
17
  * A full-width rule frames the header so it uses the whole window.
18
18
  *
19
19
  * @param {object} cfg Loaded config ({ url, user, _model }).
@@ -26,9 +26,9 @@ export function printBanner(cfg = {}, opts = {}) {
26
26
  console.log();
27
27
  if (width >= LOGO_WIDTH) {
28
28
  for (const line of LOGO) console.log(ACCENT + BOLD + line + RESET);
29
- console.log(` ${DIM}iCopilot — network engineer in your terminal${RESET}`);
29
+ console.log(` ${DIM}iPolot — network engineer in your terminal${RESET}`);
30
30
  } else {
31
- console.log(` ${BOLD}iCopilot${RESET} ${DIM}— IspBills AI terminal${RESET}`);
31
+ console.log(` ${BOLD}iPolot${RESET} ${DIM}— IspBills AI terminal${RESET}`);
32
32
  }
33
33
  console.log();
34
34
 
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());
@@ -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 iPolot 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
  }
@@ -1683,7 +1685,7 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, sh
1683
1685
  return;
1684
1686
  }
1685
1687
  if (q === '/app') {
1686
- pushNotice('iCopilot web app — open your IspBills instance in the browser with /o or by typing the URL.', C.muted);
1688
+ pushNotice('iPolot web app — open your IspBills instance in the browser with /o or by typing the URL.', C.muted);
1687
1689
  return;
1688
1690
  }
1689
1691
  if (q === '/user' || q.startsWith('/user ')) {
@@ -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 iPolot 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) {
@@ -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}
@@ -2185,7 +2194,7 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, sh
2185
2194
  />
2186
2195
  ${diffState.commentInput
2187
2196
  ? html`<${Choice}
2188
- title=${`Tell Copilot what stands out about ${diffState.commentInput.file}:${diffState.commentInput.lineNumber || '?'}`}
2197
+ title=${`Tell iPolot what stands out about ${diffState.commentInput.file}:${diffState.commentInput.lineNumber || '?'}`}
2189
2198
  note=${diffState.commentInput.content}
2190
2199
  sel=${0}
2191
2200
  allowText=${true}
@@ -2258,6 +2267,7 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, sh
2258
2267
  clearToken=${composerClearToken}
2259
2268
  />`}
2260
2269
 
2270
+ <${Text} color=${C.separator}>${'─'.repeat(Math.max(1, cols))}<//>
2261
2271
  <${Box} paddingX=${1} width=${cols}>
2262
2272
  <${Box} flexGrow=${1}>
2263
2273
  <${Text} color=${footerStatusColor} bold=${busy} wrap="truncate-end">${footerStatusLeft}<//>
@@ -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
  <//>`;
@@ -168,7 +168,7 @@ export function Banner({ cfg = {}, model, width = 80, compact = false, streamerM
168
168
  if (compact) {
169
169
  return html`
170
170
  <${Box} marginBottom=${1}>
171
- <${Text} color=${C.accent} bold>${glyphs.ring} iCopilot <//>
171
+ <${Text} color=${C.accent} bold>${glyphs.ring} iPolot <//>
172
172
  <${Text} color=${C.muted}>${midDot()}<//>
173
173
  <${Text} color=${C.muted}>${m}<//>
174
174
  ${cfg.user?.name ? html`<${Text} color=${C.muted}>${midDot()}<//>` : null}
@@ -183,7 +183,7 @@ export function Banner({ cfg = {}, model, width = 80, compact = false, streamerM
183
183
  <//>
184
184
 
185
185
  <${Box} marginTop=${1} justifyContent="center">
186
- <${Text} color=${C.accent} bold>${glyphs.ring} iCopilot<//>
186
+ <${Text} color=${C.accent} bold>${glyphs.ring} iPolot<//>
187
187
  <${Text} color=${C.muted}> ${dash()} IspBills network engineer in your terminal<//>
188
188
  <//>
189
189
 
@@ -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>iPolot${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
  }
@@ -299,8 +306,9 @@ const SUBAGENT = /^\s*[✓✗x]?\s*\[(\d+)\/(\d+)\]\s*(.+?)\s*[::]\s*(.+?)\s*$
299
306
  const LEAD_EMOJI = /^\s*([\u{1F000}-\u{1FAFF}\u{2600}-\u{27BF}\u{2B00}-\u{2BFF}\u{2190}-\u{21FF}\u{FE0F}\u{2049}\u{203C}]+)\s*/u;
300
307
 
301
308
  /**
302
- * Classify a status line string into op type, type tag, and display parts.
303
- * Returns { opType: 'read'|'edit'|'step'|'other', typeTag, verb, action, subject, done, ok }
309
+ * Classify a status line into op type, a dim type-tag, and display parts.
310
+ * Tag is metadata only (file ext, device protocol, service) never colored.
311
+ * Op type drives color: read=green, edit=red, step=blue.
304
312
  */
305
313
  function parseToolLine(lineStr) {
306
314
  const s = String(lineStr).trim();
@@ -313,177 +321,164 @@ function parseToolLine(lineStr) {
313
321
  const ci = body.indexOf(':');
314
322
  const action = (ci > -1 ? body.slice(0, ci) : body).trim();
315
323
  const subject = (ci > -1 ? body.slice(ci + 1) : '').trim();
316
- const combined = action + ' ' + subject;
324
+ const aL = action.toLowerCase();
325
+ const sL = subject.toLowerCase();
317
326
 
318
- // ── Op type ──────────────────────────────────────────────────────────────
327
+ // ── Op type + descriptive verb (from action keywords) ────────────────────
319
328
  let opType = 'other';
320
329
  let verb = '';
321
- if (/search|lookup|look.?up|reading|read|fetch|check|analys|correlat|simulat|predict|searching|looking|checking|analysing|correlating|predicting|simulating|retrieve/i.test(action)) {
322
- opType = 'read'; verb = 'Read';
323
- } else if (/edit|write|save|saving|generat|rollback|running on|exec|apply|creat|update/i.test(action)) {
324
- opType = 'edit'; verb = 'Edit';
325
- } else if (/assign|spawn|sub.?agent|delegat|step|next/i.test(action)) {
326
- opType = 'step'; verb = 'Step';
327
- }
328
-
329
- // ── Type tag from subject/action ─────────────────────────────────────────
330
+ if (/running on\b/i.test(aL)) { opType = 'edit'; verb = 'Exec'; }
331
+ else if (/\b(save|saving|generat|rollback|write|creat|update|apply)\b/i.test(aL)) { opType = 'edit'; verb = aL.includes('save') || aL.includes('saving') ? 'Save' : aL.includes('generat') ? 'Generate' : aL.includes('rollback') ? 'Rollback' : 'Edit'; }
332
+ else if (/\b(search|searching)\b/i.test(aL)) { opType = 'read'; verb = 'Search'; }
333
+ else if (/\b(lookup|look.?up|looking)\b/i.test(aL)) { opType = 'read'; verb = 'Lookup'; }
334
+ else if (/\b(check|checking|analys|correlat|simulat|predict|retrieve)\b/i.test(aL)) { opType = 'read'; verb = 'Check'; }
335
+ else if (/\b(fetch|reading|read)\b/i.test(aL)) { opType = 'read'; verb = 'Read'; }
336
+ else if (/\b(assign|spawn|sub.?agent|delegat)\b/i.test(aL)) { opType = 'step'; verb = 'Spawn'; }
337
+
338
+ // ── Type tag — ONLY from explicit context; never inherit device from subject ──
339
+ // Tag identifies the protocol/format involved, not the device mentioned.
330
340
  let typeTag = '';
331
- const hay = combined.toLowerCase();
332
- if (/\.(js|jsx|mjs|cjs)\b/.test(hay)) typeTag = 'JS';
333
- else if (/\.(ts|tsx)\b/.test(hay)) typeTag = 'TS';
334
- else if (/\.php\b/.test(hay)) typeTag = 'PHP';
335
- else if (/\.py\b/.test(hay)) typeTag = 'PY';
336
- else if (/\.(rb)\b/.test(hay)) typeTag = 'RB';
337
- else if (/\.(go)\b/.test(hay)) typeTag = 'GO';
338
- else if (/\.(sh|bash)\b/.test(hay)) typeTag = 'SH';
339
- else if (/nas#|mikrotik|routeros|\/export|\/ip |\/ppp|\/interface|\/system|\/queue/.test(hay)) typeTag = 'ROS';
340
- else if (/olt#|optical|onu|gpon|epon/.test(hay)) typeTag = 'OLT';
341
- else if (/github/.test(hay)) typeTag = 'GIT';
342
- else if (/http|url|web page|web search/.test(hay)) typeTag = 'WEB';
343
- else if (/customer|subscriber|user/.test(hay)) typeTag = 'CRM';
344
- else if (/firmware|version|upgrade/.test(hay)) typeTag = 'FW';
345
- else if (/sub.?agent|specialist|role/.test(hay)) typeTag = 'AI';
346
- else if (/shell|command|script/.test(hay)) typeTag = 'SH';
347
- else if (/checkpoint|backup|config/.test(hay)) typeTag = 'CFG';
348
- else if (/docs|documentation/.test(hay)) typeTag = 'DOC';
349
- else if (/signal|optical|dbm/.test(hay)) typeTag = 'OPT';
350
-
351
- return { opType, typeTag, verb, action, subject, done, ok, full: s };
341
+ // File extensions in subject or action (most specific)
342
+ const allText = aL + ' ' + sL;
343
+ if (/\.(js|jsx|mjs|cjs)\b/.test(allText)) typeTag = 'JS';
344
+ else if (/\.(ts|tsx)\b/.test(allText)) typeTag = 'TS';
345
+ else if (/\.php\b/.test(allText)) typeTag = 'PHP';
346
+ else if (/\.py\b/.test(allText)) typeTag = 'PY';
347
+ else if (/\.(rb)\b/.test(allText)) typeTag = 'RB';
348
+ else if (/\.(go)\b/.test(allText)) typeTag = 'GO';
349
+ else if (/\.(sh|bash)\b/.test(allText)) typeTag = 'SH';
350
+ // Device protocol — ONLY when action is a device exec ("running on")
351
+ else if (/running on/i.test(aL)) {
352
+ if (/nas#|mikrotik|\/export|\/ip |\/ppp|\/interface|\/system|\/queue|\/routing/.test(allText)) typeTag = 'ROS';
353
+ else if (/olt#|gpon|epon|optical/.test(allText)) typeTag = 'OLT';
354
+ else typeTag = 'DEV';
355
+ }
356
+ // Action-based tags
357
+ else if (/github/i.test(aL)) typeTag = 'GIT';
358
+ else if (/web|url|page|http/i.test(aL)) typeTag = 'WEB';
359
+ else if (/customer|subscriber/i.test(aL)) typeTag = 'CRM';
360
+ else if (/firmware|version/i.test(aL)) typeTag = 'FW';
361
+ else if (/sub.?agent|specialist|assign/i.test(aL)) typeTag = 'AI';
362
+ else if (/shell|command/i.test(aL)) typeTag = 'SH';
363
+ else if (/checkpoint|backup/i.test(aL)) typeTag = 'CFG';
364
+ else if (/config/i.test(aL)) typeTag = 'CFG';
365
+ else if (/docs|documentation/i.test(aL)) typeTag = 'DOC';
366
+ else if (/signal|optical|dbm/i.test(aL)) typeTag = 'OPT';
367
+ else if (/device|router|nas|olt/i.test(aL)) typeTag = 'DEV';
368
+
369
+ // Display text: verb + detail
370
+ // When action has no colon split, subject is empty — show trimmed action as detail
371
+ const display = subject || action;
372
+
373
+ return { opType, typeTag, verb, action, subject, display, done, ok, full: s };
352
374
  }
353
375
 
354
376
  export function StatusLines({ lines = [], busy = false, navigable = false }) {
355
377
  const [sel, setSel] = useState(-1);
356
378
  const [expanded, setExpanded] = useState(new Set());
357
379
 
358
- // Reset selection when lines change
359
- useEffect(() => {
360
- setSel(-1);
361
- setExpanded(new Set());
362
- }, [lines.length]);
380
+ useEffect(() => { setSel(-1); setExpanded(new Set()); }, [lines.length]);
363
381
 
364
382
  useInput((input, key) => {
365
383
  if (!navigable) return;
366
- if (key.upArrow || input === 'k') {
367
- setSel((i) => Math.max(0, (i < 0 ? lines.length - 1 : i) - 1));
368
- return;
369
- }
370
- if (key.downArrow || input === 'j') {
371
- setSel((i) => Math.min(lines.length - 1, (i < 0 ? -1 : i) + 1));
372
- return;
373
- }
384
+ if (key.upArrow || input === 'k') { setSel((i) => Math.max(0, (i < 0 ? lines.length - 1 : i) - 1)); return; }
385
+ if (key.downArrow || input === 'j') { setSel((i) => Math.min(lines.length - 1, (i < 0 ? -1 : i) + 1)); return; }
374
386
  if ((key.return || input === ' ') && sel >= 0) {
375
- setExpanded((prev) => {
376
- const next = new Set(prev);
377
- next.has(sel) ? next.delete(sel) : next.add(sel);
378
- return next;
379
- });
387
+ setExpanded((prev) => { const n = new Set(prev); n.has(sel) ? n.delete(sel) : n.add(sel); return n; });
380
388
  return;
381
389
  }
382
- // Esc clears selection
383
390
  if (key.escape) { setSel(-1); return; }
384
391
  });
385
392
 
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) => {
392
- const lineStr = String(line);
400
+ const lineStr = String(line);
393
401
  const isRunning = busy && i === lastIdx;
394
- const isSel = navigable && sel === i;
395
- const isExp = expanded.has(i);
402
+ const isSel = navigable && sel === i;
403
+ const isExp = expanded.has(i);
396
404
 
397
- // ── Sub-agent tree line ────────────────────────────────────────────
405
+ // ── Sub-agent tree line ─────────────────────────────────────────
398
406
  const sm = lineStr.match(SUBAGENT);
399
407
  if (sm) {
400
408
  const [, idx, total, host, result] = sm;
401
409
  const isCompleted = /^\s*[✓✗x]/.test(lineStr);
402
- const running = busy && !isCompleted;
410
+ const running = busy && !isCompleted;
403
411
  const good = !/fail|error|down|offline|unreachable|timeout|deny/i.test(result);
404
412
  const branch = Number(idx) === Number(total) ? glyphs.branchEnd : glyphs.branchMid;
405
- const hostTrunc = host.length > 22 ? host.slice(0, 20) + '…' : host;
406
- const resultTrunc = result.length > 30 ? result.slice(0, 28) + '…' : result;
407
413
  return html`
408
- <${Box} key=${'s' + i} flexDirection="column">
409
- <${Box}>
410
- <${Text} color=${C.muted}> <//>
411
- <${Text} color=${C.accent}>${branch} <//>
412
- <${Text} color=${C.muted}>${idx}/${total} <//>
413
- <${Text} color=${C.white} bold>${hostTrunc}<//>
414
- <${Text} color=${C.muted}> ${dash()} <//>
415
- ${running
416
- ? html`<${Text} color=${C.warning}><${Spinner} type=${spinnerType()} /><//> <${Text} color=${C.muted}>running<//>`
417
- : html`<${Text} color=${good ? C.success : C.error} bold>${good ? glyphs.check : glyphs.cross} ${resultTrunc}<//>`}
418
- <//>
414
+ <${Box} key=${`sa${i}`}>
415
+ <${Text} color=${C.muted}> <//>
416
+ <${Text} color=${C.accent}>${branch} <//>
417
+ <${Text} color=${C.muted}>${idx}/${total} <//>
418
+ <${Text} color=${C.white} bold>${host.slice(0, 20)}<//>
419
+ <${Text} color=${C.muted}> ${dash()} <//>
420
+ ${running
421
+ ? html`<${Text} color=${C.warning}><${Spinner} type=${spinnerType()} /><//> <${Text} color=${C.muted}>running<//>`
422
+ : html`<${Text} color=${good ? C.success : C.error} bold>${good ? glyphs.check : glyphs.cross} ${result.slice(0, 32)}<///>`}
419
423
  <//>`;
420
424
  }
421
425
 
422
- // ── Regular tool status line ───────────────────────────────────────
423
- const { opType, typeTag, verb, action, subject, done, ok } = parseToolLine(lineStr);
426
+ // ── Regular tool status line ────────────────────────────────────
427
+ const { opType, typeTag, verb, display, done, ok, full } = parseToolLine(lineStr);
424
428
 
425
429
  const opColor = opType === 'read' ? C.success
426
- : opType === 'edit' ? C.error
427
- : opType === 'step' ? C.brand
430
+ : opType === 'edit' ? C.error
431
+ : opType === 'step' ? C.brand
428
432
  : C.muted;
429
433
 
430
- // Dot glyph (colored bullet)
431
- const dot = '';
434
+ // Type tag: always dim/muted — it's just metadata, never colored
435
+ const tag = typeTag ? typeTag.slice(0, 3).padEnd(3) : ' ';
432
436
 
433
- // Type tag display (fixed 3-char width)
434
- const tag = (typeTag || '···').slice(0, 3).padEnd(3);
437
+ // Verb label: padded to 8 chars for alignment
438
+ const verbLabel = (verb || 'Run').slice(0, 8).padEnd(8);
435
439
 
436
- // Subject truncation
437
- const subjectTrunc = subject.length > 36 ? subject.slice(0, 34) + '…' : subject;
438
- const actionTrunc = action.length > 28 ? action.slice(0, 26) + '…' : action;
440
+ // Detail text: as much as fits
441
+ const detail = display.length > 52 ? display.slice(0, 50) + '…' : display;
439
442
 
440
- // Status indicator on the right
443
+ // Status icon
441
444
  const statusIcon = isRunning
442
445
  ? html`<${Text} color=${C.warning}><${Spinner} type=${spinnerType()} /><//>`
443
- : done && ok ? html`<${Text} color=${C.success}> ${glyphs.check}<//>`
444
- : done && !ok ? html`<${Text} color=${C.error}> ${glyphs.cross}<//>`
446
+ : (done && ok) ? html`<${Text} color=${C.success}> ${glyphs.check}<//>`
447
+ : (done && !ok) ? html`<${Text} color=${C.error}> ${glyphs.cross}<//>`
445
448
  : null;
446
449
 
447
- // Expand toggle hint for navigable lines
448
- const expHint = navigable && isSel
449
- ? html`<${Text} color=${C.muted} dimColor> [${isExp ? '−' : '+'}]<//>` : null;
450
-
451
450
  return html`
452
- <${Box} key=${'s' + i} flexDirection="column">
453
- <!-- Main line -->
454
- <${Box} backgroundColor=${isSel ? '#1a1a2e' : undefined}>
451
+ <${Box} key=${`sl${i}`} flexDirection="column">
452
+ <${Box}>
455
453
  <${Text} color=${C.muted}> <//>
456
- <${Text} color=${opColor} dimColor>${tag}<//>
457
- <${Text} color=${C.muted}> <//>
458
- <${Text} color=${opColor}>${dot} <//>
459
- <${Text} color=${opColor} bold=${opType === 'read'}>${verb || action.slice(0, 12)}<//>
460
- ${subject
461
- ? html`<${Text} color=${C.white} bold=${opType === 'read'}> ${subjectTrunc.slice(0, 40)}<//>`
462
- : html`<${Text} color=${C.muted} bold=${opType === 'read'}> ${actionTrunc}<//>` }
454
+ <!-- Type tag: dim neutral — NOT colored by op type -->
455
+ <${Text} color=${C.muted} dimColor>${tag}<//>
456
+ <${Text} color=${C.muted}> <//>
457
+ <!-- Colored dot the ONLY colored part -->
458
+ <${Text} color=${opColor}>● <//>
459
+ <!-- Verb: bold for reads, normal otherwise -->
460
+ <${Text} color=${C.white} bold=${opType === 'read'}>${verbLabel}<//>
461
+ <!-- Detail: plain white -->
462
+ <${Text} color=${C.white} dimColor> ${detail}<//>
463
463
  ${statusIcon}
464
- ${expHint}
464
+ ${isSel ? html`<${Text} color=${C.muted} dimColor> [${isExp ? '−' : '+'}]<//>` : null}
465
465
  <//>
466
-
467
- <!-- Expanded detail -->
468
466
  ${isExp ? html`
469
- <${Box} flexDirection="column" marginLeft=${4}>
470
- <${Text} color=${C.separator}>${'┌' + '─'.repeat(50)}<//>
471
- <${Box}>
472
- <${Text} color=${C.separator}>│ <//>
473
- <${Text} color=${C.muted} italic>${lineStr.slice(0, 120)}<//>
467
+ <${Box} flexDirection="column" paddingLeft=${8}>
468
+ <${Text} color=${C.separator}>${'┌' + '─'.repeat(48)}<//>
469
+ <${Box}><${Text} color=${C.separator}>│ <//>
470
+ <${Text} color=${C.muted}>${full.slice(0, 100)}<//>
474
471
  <//>
475
- ${subject.length > 40 ? html`
476
- <${Box}>
477
- <${Text} color=${C.separator}>│ <//>
478
- <${Text} color=${C.muted} italic>${subject.slice(40)}<//>
479
- <//>` : null}
480
- <${Text} color=${C.separator}>${'└' + '─'.repeat(50)}<//>
472
+ ${full.length > 100 ? html`<${Box}><${Text} color=${C.separator}>│ <//>
473
+ <${Text} color=${C.muted}>${full.slice(100, 200)}<//>
474
+ <//>` : null}
475
+ <${Text} color=${C.separator}>${'└' + '─'.repeat(48)}<//>
481
476
  <//>` : null}
482
477
  <//>`;
483
478
  })}
484
479
  ${navigable && lines.length > 1
485
480
  ? html`<${Box} paddingLeft=${2} marginTop=${0}>
486
- <${Text} color=${C.muted} dimColor>j/k navigate ${midDot()} Enter expand ${midDot()} Esc dismiss<//>
481
+ <${Text} color=${C.muted} dimColor>j/k select Enter expand Esc dismiss<//>
487
482
  <//>`
488
483
  : null}
489
484
  <//>`;
@@ -667,15 +662,17 @@ export function Thinking({ label = 'Working', startedAt }) {
667
662
  }
668
663
 
669
664
  export function Choice({ title, note, options = [], sel = 0, allowText = false, text = '', focusText = false, isToolApproval = false, cols = 80 }) {
670
- // §13: tool approval uses exact spec format with Sep-style header
665
+ // §13: tool approval uses separator-line style matching TUI.md spec exactly
671
666
  if (isToolApproval) {
672
667
  return html`
673
668
  <${Box} flexDirection="column" marginTop=${1}>
674
- <${Box} paddingX=${2} marginTop=${1}>
675
- <${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<//>
676
672
  <//>
677
- ${note ? html`<${Box} paddingX=${2} marginTop=${1}><${Text} color=${C.muted}>${note}<//><//>` : null}
678
- <${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}>
679
676
  ${options.map((o, i) => {
680
677
  const on = !focusText && i === sel;
681
678
  const label = o.label ?? o.value ?? String(o);
@@ -689,13 +686,13 @@ export function Choice({ title, note, options = [], sel = 0, allowText = false,
689
686
  ${allowText
690
687
  ? html`<${Box} key="freetext" marginTop=${1}>
691
688
  <${Text} color=${focusText ? C.accent : C.muted}>${focusText ? glyphs.prompt + ' ' : ' '}<//>
692
- <${Text} color=${C.muted}>Tell Copilot what to do differently: <//>
689
+ <${Text} color=${C.muted}>Tell iPolot what to do differently: <//>
693
690
  <${Text} color=${C.white}>${text}<//>${focusText ? html`<${Text} inverse> <//>` : null}
694
691
  <//>`
695
692
  : null}
696
693
  <//>
697
- <${Box} marginTop=${1} paddingX=${2}>
698
- <${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<//>
699
696
  <//>
700
697
  <//>`;
701
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.