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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ispbills-icli",
3
- "version": "8.6.8",
3
+ "version": "8.6.10",
4
4
  "description": "iCli — IspBills AI network engineer in your terminal",
5
5
  "keywords": [
6
6
  "ispbills",
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 3 + wrappedLines(it.text, w); // sep + speaker + margin + body
246
+ return 2 + wrappedLines(it.text, w); // marginTop + speaker + body
247
247
  case 'assistant':
248
- return 3 + wrappedLines(it.text, Math.min(w, 96));
248
+ return 2 + wrappedLines(it.text, Math.min(w, 96));
249
249
  case 'shell':
250
- return 4 + wrappedLines(it.output || '(no output)', w) + 1;
250
+ return 3 + wrappedLines(it.output || '(no output)', w) + 1;
251
251
  case 'plan':
252
- return 4 + (it.reply?.steps?.length || 0);
252
+ return 3 + (it.reply?.steps?.length || 0);
253
253
  case 'connect':
254
- return 5;
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 += 3 + wrappedLines(live.text, Math.min(w, 96));
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 + spacing + chips +
337
- // composer + footer). Everything else scrolls inside this region.
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
- pushNotice(`Current model: ${model || modelRef.current || 'server default'}.\nUse /model <name> to request a different model.`);
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
- ' customers Customer lookup and ticket integration (enabled)',
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', 'customers', 'alarms'];
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} ts=${it.ts} />`;
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.slash}>${c.cmd.padEnd(24)}<//>
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} · Press Enter to return to Session · Press c to start a #reference<//>
2032
+ \n<${Text} color=${C.muted}>${tabPanel.hint}<//>
1973
2033
  <//>`
1974
2034
  : html`<${Box} flexDirection="column">
1975
2035
  ${showEmptyBanner
@@ -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: 'customers', label: 'Customers' },
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.copilotDot} iCopilot`, streaming = false }) {
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
- <${Sep} cols=${cols} />
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) => {
@@ -533,7 +533,5 @@ export function Composer({
533
533
  ${paletteRows}
534
534
  <//>`
535
535
  : null}
536
-
537
- <${Text} color=${C.separator}>${(asciiSafe() ? '-' : '─').repeat(Math.max(1, width))}<//>
538
536
  <//>`;
539
537
  }