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 +1 -1
- package/src/tui/app.js +5 -2
- package/src/tui/components.js +169 -31
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,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
|
-
|
|
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 >
|
|
317
|
-
const resultTrunc = result.length >
|
|
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
|
-
<${
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
-
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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}
|
|
342
|
-
|
|
343
|
-
<${
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
|