ispbills-icli 8.6.8 → 8.6.10
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 +118 -58
- package/src/tui/components.js +3 -9
- package/src/tui/composer.js +0 -2
package/package.json
CHANGED
package/src/tui/app.js
CHANGED
|
@@ -243,15 +243,15 @@ function itemHeight(it, cols = 80) {
|
|
|
243
243
|
const w = Math.max(1, cols - 4); // paddingX + body inset
|
|
244
244
|
switch (it.type) {
|
|
245
245
|
case 'user':
|
|
246
|
-
return
|
|
246
|
+
return 2 + wrappedLines(it.text, w); // marginTop + speaker + body
|
|
247
247
|
case 'assistant':
|
|
248
|
-
return
|
|
248
|
+
return 2 + wrappedLines(it.text, Math.min(w, 96));
|
|
249
249
|
case 'shell':
|
|
250
|
-
return
|
|
250
|
+
return 3 + wrappedLines(it.output || '(no output)', w) + 1;
|
|
251
251
|
case 'plan':
|
|
252
|
-
return
|
|
252
|
+
return 3 + (it.reply?.steps?.length || 0);
|
|
253
253
|
case 'connect':
|
|
254
|
-
return
|
|
254
|
+
return 4;
|
|
255
255
|
case 'activity':
|
|
256
256
|
return 1 + (it.status?.length || 0);
|
|
257
257
|
case 'notice':
|
|
@@ -295,7 +295,7 @@ function liveAreaHeight(live, cols = 80, showReasoning = false) {
|
|
|
295
295
|
if (!live) return 0;
|
|
296
296
|
const w = Math.max(1, cols - 4);
|
|
297
297
|
let h = 2; // thinking line + margin
|
|
298
|
-
if (live.text) h +=
|
|
298
|
+
if (live.text) h += 2 + wrappedLines(live.text, Math.min(w, 96));
|
|
299
299
|
if (live.reasoning) h += showReasoning ? 2 + wrappedLines(live.reasoning, w) : 1;
|
|
300
300
|
if (live.reply?.steps) h += 3 + live.reply.steps.length;
|
|
301
301
|
if (live.status?.length) h += 1 + live.status.length;
|
|
@@ -318,6 +318,50 @@ let itemId = 0;
|
|
|
318
318
|
const nextId = () => ++itemId;
|
|
319
319
|
const MODES = ['ask', 'plan', 'autopilot'];
|
|
320
320
|
|
|
321
|
+
// Tab-specific quick-command panels. Static — does not depend on component state.
|
|
322
|
+
const TAB_CONTENT = {
|
|
323
|
+
devices: {
|
|
324
|
+
title: 'Devices',
|
|
325
|
+
commands: [
|
|
326
|
+
{ cmd: '/status', desc: 'Fleet health summary' },
|
|
327
|
+
{ cmd: '/reachable', desc: 'Who is up / down' },
|
|
328
|
+
{ cmd: '/alarms', desc: 'Active alarms' },
|
|
329
|
+
{ cmd: '/check [device]', desc: 'Full device diagnostics' },
|
|
330
|
+
{ cmd: '/uptime [device]', desc: 'Uptime and last reboot' },
|
|
331
|
+
{ cmd: '/cpu [device]', desc: 'CPU / memory load' },
|
|
332
|
+
{ cmd: '/offline', desc: 'All offline devices' },
|
|
333
|
+
{ cmd: '/flapping [device]', desc: 'Flapping interfaces' },
|
|
334
|
+
],
|
|
335
|
+
hint: 'Press a number (1–8) to insert command · Enter to return to Session',
|
|
336
|
+
},
|
|
337
|
+
maps: {
|
|
338
|
+
title: 'Network Maps',
|
|
339
|
+
commands: [
|
|
340
|
+
{ cmd: '/vlan [id]', desc: 'VLAN topology & members' },
|
|
341
|
+
{ cmd: '/route [device]', desc: 'Routing table & next-hops' },
|
|
342
|
+
{ cmd: '/overlaps', desc: 'IP subnet overlap check' },
|
|
343
|
+
{ cmd: '/mac [addr]', desc: 'MAC address lookup' },
|
|
344
|
+
{ cmd: '/arp [device]', desc: 'ARP table & IP-MAC map' },
|
|
345
|
+
{ cmd: '/trace [host]', desc: 'Traceroute to host' },
|
|
346
|
+
{ cmd: '/topology', desc: 'Network topology diagram' },
|
|
347
|
+
{ cmd: '/segments', desc: 'Network segments & subnets' },
|
|
348
|
+
],
|
|
349
|
+
hint: 'Press a number (1–8) to insert command · Enter to return to Session',
|
|
350
|
+
},
|
|
351
|
+
alarms: {
|
|
352
|
+
title: 'Alarms',
|
|
353
|
+
commands: [
|
|
354
|
+
{ cmd: '/alarms', desc: 'Active alarms & recent faults' },
|
|
355
|
+
{ cmd: '/offline', desc: 'All offline devices' },
|
|
356
|
+
{ cmd: '/down [circuit]', desc: 'Down circuits / uplinks' },
|
|
357
|
+
{ cmd: '/flapping [device]', desc: 'Flapping interfaces' },
|
|
358
|
+
{ cmd: '/optical [device]', desc: 'Optical RX/TX (dBm)' },
|
|
359
|
+
{ cmd: '/reachable', desc: 'Reachability sweep' },
|
|
360
|
+
],
|
|
361
|
+
hint: 'Press a number (1–6) to insert command · Enter to return to Session',
|
|
362
|
+
},
|
|
363
|
+
};
|
|
364
|
+
|
|
321
365
|
export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, showBanner = true, agentName, resumeMode }) {
|
|
322
366
|
const { exit } = useApp();
|
|
323
367
|
const initialResumeRef = useRef(undefined);
|
|
@@ -333,9 +377,8 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, sh
|
|
|
333
377
|
const { cols, rows } = useTerminalSize();
|
|
334
378
|
const colsRef = useRef(cols);
|
|
335
379
|
useEffect(() => { colsRef.current = cols; }, [cols]);
|
|
336
|
-
// Fixed-height viewport: total rows minus chrome (tabbar
|
|
337
|
-
|
|
338
|
-
const viewportRows = Math.max(3, rows - 6);
|
|
380
|
+
// Fixed-height viewport: total rows minus chrome (tabbar=1, composer=2, footer=1, buffer=3).
|
|
381
|
+
const viewportRows = Math.max(3, rows - 7);
|
|
339
382
|
const scrollPage = Math.max(4, viewportRows - 2);
|
|
340
383
|
|
|
341
384
|
const [items, setItems] = useState(() => renderConversationItems(initialMessages, cols));
|
|
@@ -984,7 +1027,42 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, sh
|
|
|
984
1027
|
if (q === '/model' || q.startsWith('/model ')) {
|
|
985
1028
|
const name = q.slice('/model'.length).trim();
|
|
986
1029
|
if (!name) {
|
|
987
|
-
|
|
1030
|
+
// Show interactive model picker
|
|
1031
|
+
const cur = model || modelRef.current || '';
|
|
1032
|
+
const MODELS = [
|
|
1033
|
+
{ id: 'gpt-4o-mini', desc: 'Fast & affordable (default)' },
|
|
1034
|
+
{ id: 'openai/gpt-4o', desc: 'OpenAI GPT-4o — balanced' },
|
|
1035
|
+
{ id: 'openai/gpt-4.1', desc: 'OpenAI GPT-4.1 — latest' },
|
|
1036
|
+
{ id: 'openai/o3', desc: 'OpenAI o3 — reasoning' },
|
|
1037
|
+
{ id: 'openai/o3-mini', desc: 'OpenAI o3-mini — fast reasoning' },
|
|
1038
|
+
{ id: 'anthropic/claude-3-5-sonnet', desc: 'Claude 3.5 Sonnet — strong' },
|
|
1039
|
+
{ id: 'anthropic/claude-3-opus', desc: 'Claude 3 Opus — advanced' },
|
|
1040
|
+
{ id: 'anthropic/claude-3-haiku', desc: 'Claude 3 Haiku — fast' },
|
|
1041
|
+
{ id: 'meta/llama-3.3-70b-instruct', desc: 'Llama 3.3 70B (open)' },
|
|
1042
|
+
{ id: 'mistral/mistral-large', desc: 'Mistral Large' },
|
|
1043
|
+
];
|
|
1044
|
+
choiceActionRef.current = (val) => {
|
|
1045
|
+
if (val) {
|
|
1046
|
+
modelRef.current = val;
|
|
1047
|
+
setModel(val);
|
|
1048
|
+
cfg._model = val;
|
|
1049
|
+
persistCfg();
|
|
1050
|
+
saveCurrentSession();
|
|
1051
|
+
pushNotice(`${glyphs.check} Model set to ${val}.`, C.green);
|
|
1052
|
+
}
|
|
1053
|
+
};
|
|
1054
|
+
setChoice({
|
|
1055
|
+
title: `Select AI Model (current: ${cur || 'server default'})`,
|
|
1056
|
+
note: 'Use ↑/↓ to move, Enter to confirm, Esc to cancel',
|
|
1057
|
+
sel: Math.max(0, MODELS.findIndex((m) => m.id === cur)),
|
|
1058
|
+
text: '',
|
|
1059
|
+
focusText: false,
|
|
1060
|
+
allowText: false,
|
|
1061
|
+
options: MODELS.map((m, i) => ({
|
|
1062
|
+
label: `${i + 1}. ${m.id.padEnd(36)} ${m.desc}`,
|
|
1063
|
+
value: m.id,
|
|
1064
|
+
})),
|
|
1065
|
+
});
|
|
988
1066
|
} else {
|
|
989
1067
|
modelRef.current = name;
|
|
990
1068
|
setModel(name);
|
|
@@ -1318,7 +1396,7 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, sh
|
|
|
1318
1396
|
' optical — Optical power analysis and ONU diagnostics (enabled)',
|
|
1319
1397
|
' fleet-check — Parallel multi-device scanning (enabled)',
|
|
1320
1398
|
' backup — Automated config backup via git (enabled)',
|
|
1321
|
-
'
|
|
1399
|
+
' maps — Network topology maps and IP planning (enabled)',
|
|
1322
1400
|
' alarms — Real-time alarm monitoring (enabled)',
|
|
1323
1401
|
'',
|
|
1324
1402
|
'Use /experimental to unlock additional skills.',
|
|
@@ -1822,61 +1900,42 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, sh
|
|
|
1822
1900
|
if (key.ctrl && input === 'p' && chips.length) { setActiveChip((i) => (i - 1 + chips.length) % chips.length); return; }
|
|
1823
1901
|
if (key.tab && !key.shift && !busy && !composerText.trim()) {
|
|
1824
1902
|
setActiveTab((t) => {
|
|
1825
|
-
const tabs = ['session', 'devices', '
|
|
1903
|
+
const tabs = ['session', 'devices', 'maps', 'alarms'];
|
|
1826
1904
|
return tabs[(tabs.indexOf(t) + 1) % tabs.length];
|
|
1827
1905
|
});
|
|
1828
1906
|
}
|
|
1907
|
+
// Alt+↑/↓ for viewport scrolling (alternative to PgUp/PgDn)
|
|
1908
|
+
if (key.meta && key.upArrow && !choice && !search && !sessionPicker && !diffState) {
|
|
1909
|
+
setScrollOffset((n) => Math.min(maxScrollRef.current, n + Math.max(3, Math.floor(scrollPage / 4))));
|
|
1910
|
+
return;
|
|
1911
|
+
}
|
|
1912
|
+
if (key.meta && key.downArrow && !choice && !search && !sessionPicker && !diffState) {
|
|
1913
|
+
setScrollOffset((n) => Math.max(0, n - Math.max(3, Math.floor(scrollPage / 4))));
|
|
1914
|
+
return;
|
|
1915
|
+
}
|
|
1916
|
+
// Number keys on non-session tabs: 1-9 insert the panel's Nth command into composer
|
|
1917
|
+
if (activeTab !== 'session' && !choice && !search) {
|
|
1918
|
+
const n = parseInt(input, 10);
|
|
1919
|
+
if (!isNaN(n) && n >= 1) {
|
|
1920
|
+
const panel = TAB_CONTENT[activeTab];
|
|
1921
|
+
const cmd = panel?.commands[n - 1]?.cmd;
|
|
1922
|
+
if (cmd) {
|
|
1923
|
+
const insertCmd = cmd.replace(/\[.*?\]/g, '').trim();
|
|
1924
|
+
setActiveTab('session');
|
|
1925
|
+
setComposerPreset({ token: Date.now(), text: insertCmd });
|
|
1926
|
+
}
|
|
1927
|
+
return;
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1829
1930
|
});
|
|
1830
1931
|
|
|
1831
|
-
// Tab-specific content panels (shown when not on Session tab)
|
|
1832
|
-
const TAB_CONTENT = {
|
|
1833
|
-
devices: {
|
|
1834
|
-
title: 'Devices',
|
|
1835
|
-
commands: [
|
|
1836
|
-
{ cmd: '/status', desc: 'Fleet health summary' },
|
|
1837
|
-
{ cmd: '/reachable', desc: 'Who is up / down' },
|
|
1838
|
-
{ cmd: '/alarms', desc: 'Active alarms' },
|
|
1839
|
-
{ cmd: '/check [device]', desc: 'Full device diagnostics' },
|
|
1840
|
-
{ cmd: '/uptime [device]', desc: 'Uptime and last reboot' },
|
|
1841
|
-
{ cmd: '/cpu [device]', desc: 'CPU / memory load' },
|
|
1842
|
-
{ cmd: '/offline', desc: 'All offline devices' },
|
|
1843
|
-
{ cmd: '/flapping [device]', desc: 'Flapping interfaces' },
|
|
1844
|
-
],
|
|
1845
|
-
hint: 'Press Enter to switch back to Session and use the composer',
|
|
1846
|
-
},
|
|
1847
|
-
customers: {
|
|
1848
|
-
title: 'Customers',
|
|
1849
|
-
commands: [
|
|
1850
|
-
{ cmd: '/customer [name]', desc: 'Look up a customer' },
|
|
1851
|
-
{ cmd: '/online', desc: 'Online PPPoE sessions' },
|
|
1852
|
-
{ cmd: '/pppoe [user]', desc: 'PPPoE session detail' },
|
|
1853
|
-
{ cmd: '/dhcp', desc: 'DHCP leases & pool usage' },
|
|
1854
|
-
{ cmd: '/onu [id]', desc: 'ONU signal and status' },
|
|
1855
|
-
{ cmd: '/bandwidth [device]', desc: 'Traffic / bandwidth' },
|
|
1856
|
-
],
|
|
1857
|
-
hint: 'Press Enter to switch back to Session and send commands',
|
|
1858
|
-
},
|
|
1859
|
-
alarms: {
|
|
1860
|
-
title: 'Alarms',
|
|
1861
|
-
commands: [
|
|
1862
|
-
{ cmd: '/alarms', desc: 'Active alarms & recent faults' },
|
|
1863
|
-
{ cmd: '/offline', desc: 'All offline devices' },
|
|
1864
|
-
{ cmd: '/down [circuit]', desc: 'Down circuits / uplinks' },
|
|
1865
|
-
{ cmd: '/flapping [device]', desc: 'Flapping interfaces' },
|
|
1866
|
-
{ cmd: '/optical [device]', desc: 'Optical RX/TX (dBm)' },
|
|
1867
|
-
{ cmd: '/reachable', desc: 'Reachability sweep' },
|
|
1868
|
-
],
|
|
1869
|
-
hint: 'Press Enter to switch back to Session and type a command',
|
|
1870
|
-
},
|
|
1871
|
-
};
|
|
1872
|
-
|
|
1873
1932
|
const tabPanel = activeTab !== 'session' ? TAB_CONTENT[activeTab] : null;
|
|
1874
1933
|
|
|
1875
1934
|
const renderItem = (it) => {
|
|
1876
1935
|
const c = it.cols || 80;
|
|
1877
1936
|
switch (it.type) {
|
|
1878
1937
|
case 'user': return html`<${UserMessage} key=${it.id} text=${it.text} cols=${c} ts=${it.ts} />`;
|
|
1879
|
-
case 'assistant': return html`<${AssistantMessage} key=${it.id} text=${it.text} cols=${c}
|
|
1938
|
+
case 'assistant': return html`<${AssistantMessage} key=${it.id} text=${it.text} cols=${c} />`;
|
|
1880
1939
|
case 'plan': return html`<${Plan} key=${it.id} reply=${it.reply} />`;
|
|
1881
1940
|
case 'connect': return html`<${Connect} key=${it.id} reply=${it.reply} />`;
|
|
1882
1941
|
case 'activity': return html`<${StatusLines} key=${it.id} lines=${it.status} busy=${false} />`;
|
|
@@ -1929,7 +1988,7 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, sh
|
|
|
1929
1988
|
|
|
1930
1989
|
const scrollIndicator = hasAbove
|
|
1931
1990
|
? html`<${Box} paddingX=${1}>
|
|
1932
|
-
<${Text} color=${C.muted} dimColor>${glyphs.up} ${aboveCount} message${aboveCount === 1 ? '' : 's'} above ${midDot()} PgUp/PgDn to scroll<//>
|
|
1991
|
+
<${Text} color=${C.muted} dimColor>${glyphs.up} ${aboveCount} message${aboveCount === 1 ? '' : 's'} above ${midDot()} PgUp/PgDn or Alt+↑/↓ to scroll<//>
|
|
1933
1992
|
<//>`
|
|
1934
1993
|
: null;
|
|
1935
1994
|
|
|
@@ -1966,10 +2025,11 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, sh
|
|
|
1966
2025
|
<${Text} color=${C.brand} bold>${tabPanel.title} — Quick Commands<//>\n
|
|
1967
2026
|
${tabPanel.commands.map((c, i) =>
|
|
1968
2027
|
html`<${Box} key=${i}>
|
|
1969
|
-
<${Text} color=${C.
|
|
2028
|
+
<${Text} color=${C.accent} bold>${String(i + 1)}. <//>
|
|
2029
|
+
<${Text} color=${C.slash}>${c.cmd.padEnd(22)}<//>
|
|
1970
2030
|
<${Text} color=${C.muted}>${c.desc}<//>
|
|
1971
2031
|
<//>`)}
|
|
1972
|
-
\n<${Text} color=${C.muted}>${tabPanel.hint}
|
|
2032
|
+
\n<${Text} color=${C.muted}>${tabPanel.hint}<//>
|
|
1973
2033
|
<//>`
|
|
1974
2034
|
: html`<${Box} flexDirection="column">
|
|
1975
2035
|
${showEmptyBanner
|
package/src/tui/components.js
CHANGED
|
@@ -109,7 +109,7 @@ function SpeakerLine({ label, color, glyphColor, cols = 80, ts, rightLabel = ''
|
|
|
109
109
|
const TABS = [
|
|
110
110
|
{ id: 'session', label: 'Session' },
|
|
111
111
|
{ id: 'devices', label: 'Devices' },
|
|
112
|
-
{ id: '
|
|
112
|
+
{ id: 'maps', label: 'Network Maps' },
|
|
113
113
|
{ id: 'alarms', label: 'Alarms' },
|
|
114
114
|
];
|
|
115
115
|
|
|
@@ -247,7 +247,6 @@ function Sep({ cols = 80 }) {
|
|
|
247
247
|
export function UserMessage({ text, cols = 80, ts }) {
|
|
248
248
|
return html`
|
|
249
249
|
<${Box} flexDirection="column" marginTop=${1}>
|
|
250
|
-
<${Sep} cols=${cols} />
|
|
251
250
|
<${SpeakerLine} label="You" color=${C.brand} cols=${cols} ts=${ts} />
|
|
252
251
|
<${Box} paddingX=${1}>
|
|
253
252
|
<${Text}>${withMentions(text)}<//>
|
|
@@ -278,13 +277,12 @@ function StreamingText({ text = '' }) {
|
|
|
278
277
|
return rendered;
|
|
279
278
|
}
|
|
280
279
|
|
|
281
|
-
export function AssistantMessage({ text, cols = 80, ts, label = `${glyphs.
|
|
280
|
+
export function AssistantMessage({ text, cols = 80, ts, label = `${glyphs.ring} iCopilot`, streaming = false }) {
|
|
282
281
|
// §11: response content box capped at 100 columns; separators stay full-width (§6).
|
|
283
282
|
const bodyWidth = Math.min(cols, 100);
|
|
284
283
|
return html`
|
|
285
284
|
<${Box} flexDirection="column" marginTop=${1}>
|
|
286
|
-
<${
|
|
287
|
-
<${SpeakerLine} label=${label} color="white" glyphColor=${C.accent} cols=${cols} ts=${ts} />
|
|
285
|
+
<${SpeakerLine} label=${label} color="white" glyphColor=${C.accent} cols=${cols} />
|
|
288
286
|
<${Box} paddingX=${1} flexDirection="column" width=${bodyWidth}>
|
|
289
287
|
${streaming ? html`<${StreamingText} text=${text} />` : html`<${Markdown} content=${text} />`}
|
|
290
288
|
<//>
|
|
@@ -294,7 +292,6 @@ export function AssistantMessage({ text, cols = 80, ts, label = `${glyphs.copilo
|
|
|
294
292
|
export function ShellOutput({ command, output, cols = 80, ts, code = 0 }) {
|
|
295
293
|
return html`
|
|
296
294
|
<${Box} flexDirection="column" marginTop=${1}>
|
|
297
|
-
<${Sep} cols=${cols} />
|
|
298
295
|
<${SpeakerLine}
|
|
299
296
|
label="⌘ Shell"
|
|
300
297
|
color=${code === 0 ? C.warning : C.error}
|
|
@@ -429,7 +426,6 @@ export function Notice({ text, color = C.gray }) {
|
|
|
429
426
|
export function LabeledNotice({ label, text, color = C.gray, cols = 80, ts, bodyColor = C.white }) {
|
|
430
427
|
return html`
|
|
431
428
|
<${Box} flexDirection="column" marginTop=${1}>
|
|
432
|
-
<${Sep} cols=${cols} />
|
|
433
429
|
<${SpeakerLine} label=${label} color=${color} cols=${cols} ts=${ts} />
|
|
434
430
|
<${Box} paddingX=${1}>
|
|
435
431
|
<${Text} color=${bodyColor}>${text}<//>
|
|
@@ -543,11 +539,9 @@ export function Choice({ title, note, options = [], sel = 0, allowText = false,
|
|
|
543
539
|
if (isToolApproval) {
|
|
544
540
|
return html`
|
|
545
541
|
<${Box} flexDirection="column" marginTop=${1}>
|
|
546
|
-
<${Sep} cols=${cols} />
|
|
547
542
|
<${Box} paddingX=${2} marginTop=${1}>
|
|
548
543
|
<${Text} color=${C.accent} bold>⚙ Tool request<//>
|
|
549
544
|
<//>
|
|
550
|
-
<${Sep} cols=${cols} />
|
|
551
545
|
${note ? html`<${Box} paddingX=${2} marginTop=${1}><${Text} color=${C.muted}>${note}<//><//>` : null}
|
|
552
546
|
<${Box} flexDirection="column" marginTop=${1} paddingX=${2}>
|
|
553
547
|
${options.map((o, i) => {
|