ispbills-icli 8.7.5 → 8.7.6

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.6",
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,194 @@ 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 string into op type, type tag, and display parts.
303
+ * Returns { opType: 'read'|'edit'|'step'|'other', typeTag, verb, action, subject, done, ok }
304
+ */
305
+ function parseToolLine(lineStr) {
306
+ const s = String(lineStr).trim();
307
+ const done = /^[✓✗x]/.test(s);
308
+ const ok = /^✓/.test(s) || !/^[✗x]/.test(s);
309
+ const clean = s.replace(/^\s*[✓✗x]\s*/, '');
310
+
311
+ const em = clean.match(LEAD_EMOJI);
312
+ const body = em ? clean.slice(em[0].length).trim() : clean;
313
+ const ci = body.indexOf(':');
314
+ const action = (ci > -1 ? body.slice(0, ci) : body).trim();
315
+ const subject = (ci > -1 ? body.slice(ci + 1) : '').trim();
316
+ const combined = action + ' ' + subject;
317
+
318
+ // ── Op type ──────────────────────────────────────────────────────────────
319
+ let opType = 'other';
320
+ 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
+ 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 };
352
+ }
353
+
354
+ export function StatusLines({ lines = [], busy = false, navigable = false }) {
355
+ const [sel, setSel] = useState(-1);
356
+ const [expanded, setExpanded] = useState(new Set());
357
+
358
+ // Reset selection when lines change
359
+ useEffect(() => {
360
+ setSel(-1);
361
+ setExpanded(new Set());
362
+ }, [lines.length]);
363
+
364
+ useInput((input, key) => {
365
+ 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
+ }
374
+ 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
+ });
380
+ return;
381
+ }
382
+ // Esc clears selection
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
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
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;
405
+ const hostTrunc = host.length > 22 ? host.slice(0, 20) + '…' : host;
406
+ const resultTrunc = result.length > 30 ? result.slice(0, 28) + '…' : result;
318
407
  return html`
319
- <${Box} key=${'s' + i}>
320
- <${Text} color=${C.gray}> <//>
321
- <${Text} color=${C.accent}>${branch} <//>
322
- <${Text} color=${C.muted}>${idx}/${total} <//>
323
- <${Text} color=${C.white} bold>${hostTrunc}<//>
324
- <${Text} color=${C.muted}> ${dash()} <//>
325
- ${running
326
- ? 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}<//>`}
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
+ <//>
328
419
  <//>`;
329
420
  }
330
421
 
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}`;
422
+ // ── Regular tool status line ───────────────────────────────────────
423
+ const { opType, typeTag, verb, action, subject, done, ok } = parseToolLine(lineStr);
424
+
425
+ const opColor = opType === 'read' ? C.success
426
+ : opType === 'edit' ? C.error
427
+ : opType === 'step' ? C.brand
428
+ : C.muted;
429
+
430
+ // Dot glyph (colored bullet)
431
+ const dot = '●';
432
+
433
+ // Type tag display (fixed 3-char width)
434
+ const tag = (typeTag || '···').slice(0, 3).padEnd(3);
435
+
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;
439
+
440
+ // Status indicator on the right
441
+ const statusIcon = isRunning
442
+ ? 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}<//>`
445
+ : null;
446
+
447
+ // Expand toggle hint for navigable lines
448
+ const expHint = navigable && isSel
449
+ ? html`<${Text} color=${C.muted} dimColor> [${isExp ? '−' : '+'}]<//>` : null;
450
+
340
451
  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}<//>`}
452
+ <${Box} key=${'s' + i} flexDirection="column">
453
+ <!-- Main line -->
454
+ <${Box} backgroundColor=${isSel ? '#1a1a2e' : undefined}>
455
+ <${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}<//>` }
463
+ ${statusIcon}
464
+ ${expHint}
465
+ <//>
466
+
467
+ <!-- Expanded detail -->
468
+ ${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)}<//>
474
+ <//>
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)}<//>
481
+ <//>` : null}
349
482
  <//>`;
350
483
  })}
484
+ ${navigable && lines.length > 1
485
+ ? html`<${Box} paddingLeft=${2} marginTop=${0}>
486
+ <${Text} color=${C.muted} dimColor>j/k navigate ${midDot()} Enter expand ${midDot()} Esc dismiss<//>
487
+ <//>`
488
+ : null}
351
489
  <//>`;
352
490
  }
353
491