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 +1 -1
- package/src/tui/app.js +5 -2
- package/src/tui/components.js +153 -28
package/package.json
CHANGED
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(
|
|
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} />`;
|
package/src/tui/components.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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=${
|
|
320
|
-
<${Text} color=${C.
|
|
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>${
|
|
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} ${
|
|
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
|
-
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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=${
|
|
342
|
-
<${
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
|