ispbills-icli 8.6.8 → 8.6.9

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.9",
4
4
  "description": "iCli — IspBills AI network engineer in your terminal",
5
5
  "keywords": [
6
6
  "ispbills",
package/src/tui/app.js CHANGED
@@ -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);
@@ -984,7 +1028,42 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, sh
984
1028
  if (q === '/model' || q.startsWith('/model ')) {
985
1029
  const name = q.slice('/model'.length).trim();
986
1030
  if (!name) {
987
- pushNotice(`Current model: ${model || modelRef.current || 'server default'}.\nUse /model <name> to request a different model.`);
1031
+ // Show interactive model picker
1032
+ const cur = model || modelRef.current || '';
1033
+ const MODELS = [
1034
+ { id: 'gpt-4o-mini', desc: 'Fast & affordable (default)' },
1035
+ { id: 'openai/gpt-4o', desc: 'OpenAI GPT-4o — balanced' },
1036
+ { id: 'openai/gpt-4.1', desc: 'OpenAI GPT-4.1 — latest' },
1037
+ { id: 'openai/o3', desc: 'OpenAI o3 — reasoning' },
1038
+ { id: 'openai/o3-mini', desc: 'OpenAI o3-mini — fast reasoning' },
1039
+ { id: 'anthropic/claude-3-5-sonnet', desc: 'Claude 3.5 Sonnet — strong' },
1040
+ { id: 'anthropic/claude-3-opus', desc: 'Claude 3 Opus — advanced' },
1041
+ { id: 'anthropic/claude-3-haiku', desc: 'Claude 3 Haiku — fast' },
1042
+ { id: 'meta/llama-3.3-70b-instruct', desc: 'Llama 3.3 70B (open)' },
1043
+ { id: 'mistral/mistral-large', desc: 'Mistral Large' },
1044
+ ];
1045
+ choiceActionRef.current = (val) => {
1046
+ if (val) {
1047
+ modelRef.current = val;
1048
+ setModel(val);
1049
+ cfg._model = val;
1050
+ persistCfg();
1051
+ saveCurrentSession();
1052
+ pushNotice(`${glyphs.check} Model set to ${val}.`, C.green);
1053
+ }
1054
+ };
1055
+ setChoice({
1056
+ title: `Select AI Model (current: ${cur || 'server default'})`,
1057
+ note: 'Use ↑/↓ to move, Enter to confirm, Esc to cancel',
1058
+ sel: Math.max(0, MODELS.findIndex((m) => m.id === cur)),
1059
+ text: '',
1060
+ focusText: false,
1061
+ allowText: false,
1062
+ options: MODELS.map((m, i) => ({
1063
+ label: `${i + 1}. ${m.id.padEnd(36)} ${m.desc}`,
1064
+ value: m.id,
1065
+ })),
1066
+ });
988
1067
  } else {
989
1068
  modelRef.current = name;
990
1069
  setModel(name);
@@ -1318,7 +1397,7 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, sh
1318
1397
  ' optical — Optical power analysis and ONU diagnostics (enabled)',
1319
1398
  ' fleet-check — Parallel multi-device scanning (enabled)',
1320
1399
  ' backup — Automated config backup via git (enabled)',
1321
- ' customers Customer lookup and ticket integration (enabled)',
1400
+ ' maps Network topology maps and IP planning (enabled)',
1322
1401
  ' alarms — Real-time alarm monitoring (enabled)',
1323
1402
  '',
1324
1403
  'Use /experimental to unlock additional skills.',
@@ -1822,61 +1901,42 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, sh
1822
1901
  if (key.ctrl && input === 'p' && chips.length) { setActiveChip((i) => (i - 1 + chips.length) % chips.length); return; }
1823
1902
  if (key.tab && !key.shift && !busy && !composerText.trim()) {
1824
1903
  setActiveTab((t) => {
1825
- const tabs = ['session', 'devices', 'customers', 'alarms'];
1904
+ const tabs = ['session', 'devices', 'maps', 'alarms'];
1826
1905
  return tabs[(tabs.indexOf(t) + 1) % tabs.length];
1827
1906
  });
1828
1907
  }
1908
+ // Alt+↑/↓ for viewport scrolling (alternative to PgUp/PgDn)
1909
+ if (key.meta && key.upArrow && !choice && !search && !sessionPicker && !diffState) {
1910
+ setScrollOffset((n) => Math.min(maxScrollRef.current, n + Math.max(3, Math.floor(scrollPage / 4))));
1911
+ return;
1912
+ }
1913
+ if (key.meta && key.downArrow && !choice && !search && !sessionPicker && !diffState) {
1914
+ setScrollOffset((n) => Math.max(0, n - Math.max(3, Math.floor(scrollPage / 4))));
1915
+ return;
1916
+ }
1917
+ // Number keys on non-session tabs: 1-9 insert the panel's Nth command into composer
1918
+ if (activeTab !== 'session' && !choice && !search) {
1919
+ const n = parseInt(input, 10);
1920
+ if (!isNaN(n) && n >= 1) {
1921
+ const panel = TAB_CONTENT[activeTab];
1922
+ const cmd = panel?.commands[n - 1]?.cmd;
1923
+ if (cmd) {
1924
+ const insertCmd = cmd.replace(/\[.*?\]/g, '').trim();
1925
+ setActiveTab('session');
1926
+ setComposerPreset({ token: Date.now(), text: insertCmd });
1927
+ }
1928
+ return;
1929
+ }
1930
+ }
1829
1931
  });
1830
1932
 
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
1933
  const tabPanel = activeTab !== 'session' ? TAB_CONTENT[activeTab] : null;
1874
1934
 
1875
1935
  const renderItem = (it) => {
1876
1936
  const c = it.cols || 80;
1877
1937
  switch (it.type) {
1878
1938
  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} />`;
1939
+ case 'assistant': return html`<${AssistantMessage} key=${it.id} text=${it.text} cols=${c} />`;
1880
1940
  case 'plan': return html`<${Plan} key=${it.id} reply=${it.reply} />`;
1881
1941
  case 'connect': return html`<${Connect} key=${it.id} reply=${it.reply} />`;
1882
1942
  case 'activity': return html`<${StatusLines} key=${it.id} lines=${it.status} busy=${false} />`;
@@ -1929,7 +1989,7 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, sh
1929
1989
 
1930
1990
  const scrollIndicator = hasAbove
1931
1991
  ? html`<${Box} paddingX=${1}>
1932
- <${Text} color=${C.muted} dimColor>${glyphs.up} ${aboveCount} message${aboveCount === 1 ? '' : 's'} above ${midDot()} PgUp/PgDn to scroll<//>
1992
+ <${Text} color=${C.muted} dimColor>${glyphs.up} ${aboveCount} message${aboveCount === 1 ? '' : 's'} above ${midDot()} PgUp/PgDn or Alt+↑/↓ to scroll<//>
1933
1993
  <//>`
1934
1994
  : null;
1935
1995
 
@@ -1966,10 +2026,11 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, sh
1966
2026
  <${Text} color=${C.brand} bold>${tabPanel.title} — Quick Commands<//>\n
1967
2027
  ${tabPanel.commands.map((c, i) =>
1968
2028
  html`<${Box} key=${i}>
1969
- <${Text} color=${C.slash}>${c.cmd.padEnd(24)}<//>
2029
+ <${Text} color=${C.accent} bold>${String(i + 1)}. <//>
2030
+ <${Text} color=${C.slash}>${c.cmd.padEnd(22)}<//>
1970
2031
  <${Text} color=${C.muted}>${c.desc}<//>
1971
2032
  <//>`)}
1972
- \n<${Text} color=${C.muted}>${tabPanel.hint} · Press Enter to return to Session · Press c to start a #reference<//>
2033
+ \n<${Text} color=${C.muted}>${tabPanel.hint}<//>
1973
2034
  <//>`
1974
2035
  : html`<${Box} flexDirection="column">
1975
2036
  ${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
 
@@ -278,13 +278,13 @@ function StreamingText({ text = '' }) {
278
278
  return rendered;
279
279
  }
280
280
 
281
- export function AssistantMessage({ text, cols = 80, ts, label = `${glyphs.copilotDot} iCopilot`, streaming = false }) {
281
+ export function AssistantMessage({ text, cols = 80, ts, label = `${glyphs.ring} iCopilot`, streaming = false }) {
282
282
  // §11: response content box capped at 100 columns; separators stay full-width (§6).
283
283
  const bodyWidth = Math.min(cols, 100);
284
284
  return html`
285
285
  <${Box} flexDirection="column" marginTop=${1}>
286
286
  <${Sep} cols=${cols} />
287
- <${SpeakerLine} label=${label} color="white" glyphColor=${C.accent} cols=${cols} ts=${ts} />
287
+ <${SpeakerLine} label=${label} color="white" glyphColor=${C.accent} cols=${cols} />
288
288
  <${Box} paddingX=${1} flexDirection="column" width=${bodyWidth}>
289
289
  ${streaming ? html`<${StreamingText} text=${text} />` : html`<${Markdown} content=${text} />`}
290
290
  <//>