ispbills-icli 8.7.5 → 8.7.7

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.5",
3
+ "version": "8.7.7",
4
4
  "description": "iCli — IspBills AI network engineer in your terminal",
5
5
  "keywords": [
6
6
  "ispbills",
package/src/tui/app.js CHANGED
@@ -411,7 +411,7 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, sh
411
411
  const [live, setLive] = useState(null);
412
412
  const [busy, setBusy] = useState(false);
413
413
  const [model, setModel] = useState(initialSession?.model || cfg._model);
414
- const [showReasoning, setShowReasoning] = useState(false);
414
+ const [showReasoning, setShowReasoning] = useState(true);
415
415
  const [mode, setMode] = useState('ask');
416
416
  const [activeTab, setActiveTab] = useState('session');
417
417
  const [chips, setChips] = useState([]);
@@ -2099,6 +2099,9 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, sh
2099
2099
 
2100
2100
  const tabPanel = activeTab !== 'session' ? TAB_CONTENT[activeTab] : null;
2101
2101
 
2102
+ // ID of the last activity item — that one gets keyboard navigation (j/k + Enter to expand)
2103
+ const lastActivityId = [...items].reverse().find((it) => it.type === 'activity')?.id ?? null;
2104
+
2102
2105
  const renderItem = (it) => {
2103
2106
  const c = it.cols || 80;
2104
2107
  switch (it.type) {
@@ -2106,7 +2109,7 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, sh
2106
2109
  case 'assistant': return html`<${AssistantMessage} key=${it.id} text=${it.text} cols=${c} ts=${it.ts} />`;
2107
2110
  case 'plan': return html`<${Plan} key=${it.id} reply=${it.reply} />`;
2108
2111
  case 'connect': return html`<${Connect} key=${it.id} reply=${it.reply} />`;
2109
- case 'activity': return html`<${StatusLines} key=${it.id} lines=${it.status} busy=${false} />`;
2112
+ case 'activity': return html`<${StatusLines} key=${it.id} lines=${it.status} busy=${false} navigable=${!busy && it.id === lastActivityId} />`;
2110
2113
  case 'notice': return html`<${Notice} key=${it.id} text=${it.text} color=${it.color} />`;
2111
2114
  case 'info': return html`<${LabeledNotice} key=${it.id} label="ℹ Info" text=${it.text} color=${C.muted} bodyColor=${it.color || C.white} cols=${c} ts=${it.ts} />`;
2112
2115
  case 'error': return html`<${LabeledNotice} key=${it.id} label="✗ Error" text=${it.text} color=${C.error} bodyColor=${it.color || C.white} cols=${c} ts=${it.ts} />`;
@@ -1,6 +1,6 @@
1
1
  // Presentational components for the iCli Ink TUI.
2
2
  import { html, useState, useEffect } from './dom.js';
3
- import { Box, Text } from 'ink';
3
+ import { Box, Text, useInput } from 'ink';
4
4
  import Spinner from 'ink-spinner';
5
5
  import { C, glyphs, spinnerType, borderStyle, midDot, dash } from './theme.js';
6
6
  import { Markdown } from './markdown.js';
@@ -298,56 +298,181 @@ export function ShellOutput({ command, output, cols = 80, ts, code = 0 }) {
298
298
  const SUBAGENT = /^\s*[✓✗x]?\s*\[(\d+)\/(\d+)\]\s*(.+?)\s*[::]\s*(.+?)\s*$/;
299
299
  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
300
 
301
- export function StatusLines({ lines = [], busy = false }) {
301
+ /**
302
+ * Classify a status line into op type, a dim type-tag, and display parts.
303
+ * Tag is metadata only (file ext, device protocol, service) — never colored.
304
+ * Op type drives color: read=green, edit=red, step=blue.
305
+ */
306
+ function parseToolLine(lineStr) {
307
+ const s = String(lineStr).trim();
308
+ const done = /^[✓✗x]/.test(s);
309
+ const ok = /^✓/.test(s) || !/^[✗x]/.test(s);
310
+ const clean = s.replace(/^\s*[✓✗x]\s*/, '');
311
+
312
+ const em = clean.match(LEAD_EMOJI);
313
+ const body = em ? clean.slice(em[0].length).trim() : clean;
314
+ const ci = body.indexOf(':');
315
+ const action = (ci > -1 ? body.slice(0, ci) : body).trim();
316
+ const subject = (ci > -1 ? body.slice(ci + 1) : '').trim();
317
+ const aL = action.toLowerCase();
318
+ const sL = subject.toLowerCase();
319
+
320
+ // ── Op type + descriptive verb (from action keywords) ────────────────────
321
+ let opType = 'other';
322
+ let verb = '';
323
+ if (/running on\b/i.test(aL)) { opType = 'edit'; verb = 'Exec'; }
324
+ 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'; }
325
+ else if (/\b(search|searching)\b/i.test(aL)) { opType = 'read'; verb = 'Search'; }
326
+ else if (/\b(lookup|look.?up|looking)\b/i.test(aL)) { opType = 'read'; verb = 'Lookup'; }
327
+ else if (/\b(check|checking|analys|correlat|simulat|predict|retrieve)\b/i.test(aL)) { opType = 'read'; verb = 'Check'; }
328
+ else if (/\b(fetch|reading|read)\b/i.test(aL)) { opType = 'read'; verb = 'Read'; }
329
+ else if (/\b(assign|spawn|sub.?agent|delegat)\b/i.test(aL)) { opType = 'step'; verb = 'Spawn'; }
330
+
331
+ // ── Type tag — ONLY from explicit context; never inherit device from subject ──
332
+ // Tag identifies the protocol/format involved, not the device mentioned.
333
+ let typeTag = '';
334
+ // File extensions in subject or action (most specific)
335
+ const allText = aL + ' ' + sL;
336
+ if (/\.(js|jsx|mjs|cjs)\b/.test(allText)) typeTag = 'JS';
337
+ else if (/\.(ts|tsx)\b/.test(allText)) typeTag = 'TS';
338
+ else if (/\.php\b/.test(allText)) typeTag = 'PHP';
339
+ else if (/\.py\b/.test(allText)) typeTag = 'PY';
340
+ else if (/\.(rb)\b/.test(allText)) typeTag = 'RB';
341
+ else if (/\.(go)\b/.test(allText)) typeTag = 'GO';
342
+ else if (/\.(sh|bash)\b/.test(allText)) typeTag = 'SH';
343
+ // Device protocol — ONLY when action is a device exec ("running on")
344
+ else if (/running on/i.test(aL)) {
345
+ if (/nas#|mikrotik|\/export|\/ip |\/ppp|\/interface|\/system|\/queue|\/routing/.test(allText)) typeTag = 'ROS';
346
+ else if (/olt#|gpon|epon|optical/.test(allText)) typeTag = 'OLT';
347
+ else typeTag = 'DEV';
348
+ }
349
+ // Action-based tags
350
+ else if (/github/i.test(aL)) typeTag = 'GIT';
351
+ else if (/web|url|page|http/i.test(aL)) typeTag = 'WEB';
352
+ else if (/customer|subscriber/i.test(aL)) typeTag = 'CRM';
353
+ else if (/firmware|version/i.test(aL)) typeTag = 'FW';
354
+ else if (/sub.?agent|specialist|assign/i.test(aL)) typeTag = 'AI';
355
+ else if (/shell|command/i.test(aL)) typeTag = 'SH';
356
+ else if (/checkpoint|backup/i.test(aL)) typeTag = 'CFG';
357
+ else if (/config/i.test(aL)) typeTag = 'CFG';
358
+ else if (/docs|documentation/i.test(aL)) typeTag = 'DOC';
359
+ else if (/signal|optical|dbm/i.test(aL)) typeTag = 'OPT';
360
+ else if (/device|router|nas|olt/i.test(aL)) typeTag = 'DEV';
361
+
362
+ // Display text: verb + detail
363
+ // When action has no colon split, subject is empty — show trimmed action as detail
364
+ const display = subject || action;
365
+
366
+ return { opType, typeTag, verb, action, subject, display, done, ok, full: s };
367
+ }
368
+
369
+ export function StatusLines({ lines = [], busy = false, navigable = false }) {
370
+ const [sel, setSel] = useState(-1);
371
+ const [expanded, setExpanded] = useState(new Set());
372
+
373
+ useEffect(() => { setSel(-1); setExpanded(new Set()); }, [lines.length]);
374
+
375
+ useInput((input, key) => {
376
+ if (!navigable) return;
377
+ if (key.upArrow || input === 'k') { setSel((i) => Math.max(0, (i < 0 ? lines.length - 1 : i) - 1)); return; }
378
+ if (key.downArrow || input === 'j') { setSel((i) => Math.min(lines.length - 1, (i < 0 ? -1 : i) + 1)); return; }
379
+ if ((key.return || input === ' ') && sel >= 0) {
380
+ setExpanded((prev) => { const n = new Set(prev); n.has(sel) ? n.delete(sel) : n.add(sel); return n; });
381
+ return;
382
+ }
383
+ if (key.escape) { setSel(-1); return; }
384
+ });
385
+
302
386
  if (!lines.length) return null;
303
387
  const lastIdx = lines.length - 1;
388
+
304
389
  return html`
305
390
  <${Box} flexDirection="column" marginTop=${1}>
306
391
  ${lines.map((line, i) => {
307
- const lineStr = String(line);
392
+ const lineStr = String(line);
393
+ const isRunning = busy && i === lastIdx;
394
+ const isSel = navigable && sel === i;
395
+ const isExp = expanded.has(i);
396
+
397
+ // ── Sub-agent tree line ─────────────────────────────────────────
308
398
  const sm = lineStr.match(SUBAGENT);
309
399
  if (sm) {
310
400
  const [, idx, total, host, result] = sm;
311
- // A subagent line is still running when busy and hasn't got a ✓/✗ prefix yet
312
401
  const isCompleted = /^\s*[✓✗x]/.test(lineStr);
313
- const running = busy && !isCompleted;
402
+ const running = busy && !isCompleted;
314
403
  const good = !/fail|error|down|offline|unreachable|timeout|deny/i.test(result);
315
404
  const branch = Number(idx) === Number(total) ? glyphs.branchEnd : glyphs.branchMid;
316
- const hostTrunc = host.length > 24 ? host.slice(0, 22) + '…' : host;
317
- const resultTrunc = result.length > 32 ? result.slice(0, 30) + '…' : result;
318
405
  return html`
319
- <${Box} key=${'s' + i}>
320
- <${Text} color=${C.gray}> <//>
406
+ <${Box} key=${`sa${i}`}>
407
+ <${Text} color=${C.muted}> <//>
321
408
  <${Text} color=${C.accent}>${branch} <//>
322
409
  <${Text} color=${C.muted}>${idx}/${total} <//>
323
- <${Text} color=${C.white} bold>${hostTrunc}<//>
410
+ <${Text} color=${C.white} bold>${host.slice(0, 20)}<//>
324
411
  <${Text} color=${C.muted}> ${dash()} <//>
325
412
  ${running
326
413
  ? html`<${Text} color=${C.warning}><${Spinner} type=${spinnerType()} /><//> <${Text} color=${C.muted}>running<//>`
327
- : html`<${Text} color=${good ? C.success : C.error} bold>${good ? glyphs.check : glyphs.cross} ${resultTrunc}<//>`}
414
+ : html`<${Text} color=${good ? C.success : C.error} bold>${good ? glyphs.check : glyphs.cross} ${result.slice(0, 32)}<///>`}
328
415
  <//>`;
329
416
  }
330
417
 
331
- const running = busy && i === lastIdx;
332
- const em = lineStr.match(LEAD_EMOJI);
333
- const body = em ? lineStr.slice(em[0].length) : lineStr.replace(/^[\s◈◆●○◎⇢→>*✓✗×!⚠·-]+/u, '');
334
- const ci = body.indexOf(':');
335
- const action = ci > -1 ? body.slice(0, ci).trim() : body.trim();
336
- const detail = ci > -1 ? body.slice(ci + 1).trim() : '';
337
- const chipLabel = detail
338
- ? `${em ? em[1] : ''}${action}: ${detail.slice(0, 40)}`
339
- : `${em ? em[1] : ''}${action}`;
418
+ // ── Regular tool status line ────────────────────────────────────
419
+ const { opType, typeTag, verb, display, done, ok, full } = parseToolLine(lineStr);
420
+
421
+ const opColor = opType === 'read' ? C.success
422
+ : opType === 'edit' ? C.error
423
+ : opType === 'step' ? C.brand
424
+ : C.muted;
425
+
426
+ // Type tag: always dim/muted it's just metadata, never colored
427
+ const tag = typeTag ? typeTag.slice(0, 3).padEnd(3) : ' ';
428
+
429
+ // Verb label: padded to 8 chars for alignment
430
+ const verbLabel = (verb || 'Run').slice(0, 8).padEnd(8);
431
+
432
+ // Detail text: as much as fits
433
+ const detail = display.length > 52 ? display.slice(0, 50) + '…' : display;
434
+
435
+ // Status icon
436
+ const statusIcon = isRunning
437
+ ? html`<${Text} color=${C.warning}><${Spinner} type=${spinnerType()} /><//>`
438
+ : (done && ok) ? html`<${Text} color=${C.success}> ${glyphs.check}<//>`
439
+ : (done && !ok) ? html`<${Text} color=${C.error}> ${glyphs.cross}<//>`
440
+ : null;
441
+
340
442
  return html`
341
- <${Box} key=${'s' + i} marginTop=${0}>
342
- <${Text} color=${C.muted}> <//>
343
- <${Text} color=${running ? C.warning : C.muted}>[<//>
344
- <${Text} color=${running ? C.white : C.muted} bold=${running}>${chipLabel.trim().slice(0, 60)}<//><${Text} color=${running ? C.warning : C.muted}>]<//>
345
- <${Text}> <//>
346
- ${running
347
- ? html`<${Text} color=${C.accent}><${Spinner} type=${spinnerType()} /><//>`
348
- : html`<${Text} color=${C.success}>${glyphs.check}<//>`}
443
+ <${Box} key=${`sl${i}`} flexDirection="column">
444
+ <${Box}>
445
+ <${Text} color=${C.muted}> <//>
446
+ <!-- Type tag: dim neutral NOT colored by op type -->
447
+ <${Text} color=${C.muted} dimColor>${tag}<//>
448
+ <${Text} color=${C.muted}> <//>
449
+ <!-- Colored dot the ONLY colored part -->
450
+ <${Text} color=${opColor}>● <//>
451
+ <!-- Verb: bold for reads, normal otherwise -->
452
+ <${Text} color=${C.white} bold=${opType === 'read'}>${verbLabel}<//>
453
+ <!-- Detail: plain white -->
454
+ <${Text} color=${C.white} dimColor> ${detail}<//>
455
+ ${statusIcon}
456
+ ${isSel ? html`<${Text} color=${C.muted} dimColor> [${isExp ? '−' : '+'}]<//>` : null}
457
+ <//>
458
+ ${isExp ? html`
459
+ <${Box} flexDirection="column" paddingLeft=${8}>
460
+ <${Text} color=${C.separator}>${'┌' + '─'.repeat(48)}<//>
461
+ <${Box}><${Text} color=${C.separator}>│ <//>
462
+ <${Text} color=${C.muted}>${full.slice(0, 100)}<//>
463
+ <//>
464
+ ${full.length > 100 ? html`<${Box}><${Text} color=${C.separator}>│ <//>
465
+ <${Text} color=${C.muted}>${full.slice(100, 200)}<//>
466
+ <//>` : null}
467
+ <${Text} color=${C.separator}>${'└' + '─'.repeat(48)}<//>
468
+ <//>` : null}
349
469
  <//>`;
350
470
  })}
471
+ ${navigable && lines.length > 1
472
+ ? html`<${Box} paddingLeft=${2} marginTop=${0}>
473
+ <${Text} color=${C.muted} dimColor>j/k select Enter expand Esc dismiss<//>
474
+ <//>`
475
+ : null}
351
476
  <//>`;
352
477
  }
353
478