ispbills-icli 8.0.0 → 8.1.1

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/README.md CHANGED
@@ -108,26 +108,65 @@ icli --reasoning "audit the config on olt#1" # stream reasoning inline
108
108
 
109
109
  ### Slash commands & keys (REPL)
110
110
 
111
- Inside the REPL, type `/` to see the menu. Shortcuts:
111
+ Type `/` to open the command menu. It filters as you type, shows an inline
112
+ suggestion (accept with **Tab** or **→**), and runs the highlighted command on
113
+ **Enter**.
114
+
115
+ **Devices & diagnostics**
116
+
117
+ | Slash | Description |
118
+ |---|---|
119
+ | `/check [device]` | Run full diagnostics on a device |
120
+ | `/status [device]` | Health summary across the fleet |
121
+ | `/reachable` | Which devices are up / unreachable |
122
+ | `/uptime [device]` | Device uptime and last reboot |
123
+ | `/cpu [device]` | CPU, memory and temperature load |
124
+ | `/interfaces [device]` | Interface / port status and errors |
125
+ | `/config [device]` | Show the running configuration |
126
+ | `/audit [device]` | Audit config and suggest fixes |
127
+ | `/backup [device]` | Back up the device configuration |
128
+ | `/reboot [device]` | Reboot a device (asks to confirm) |
129
+ | `/alarms` | Active alarms and recent faults |
130
+
131
+ **Access network**
112
132
 
113
133
  | Slash | Description |
114
134
  |---|---|
115
- | `/check`, `/vlan`, `/onu`, `/customer`, `/ping`, `/online` | Common NOC queries |
116
- | `/new` | Start a fresh conversation |
117
- | `/history` | Show conversation history |
118
- | `/clear` | Clear the screen |
119
- | `/update` | Update iCli |
120
- | `/logout` | Log out |
121
- | `/help` | Show help |
122
- | `/exit` | Quit |
135
+ | `/onu [id]` | ONU status and signal levels |
136
+ | `/optical [device]` | Optical RX/TX power (dBm) |
137
+ | `/vlan [id]` | VLAN usage free or in use |
138
+ | `/pppoe [user]` | PPPoE session detail |
139
+ | `/online` · `/offline` | Online / offline customers & ONUs |
140
+ | `/bandwidth [device]` | Traffic / bandwidth usage |
141
+
142
+ **Lookups**
143
+
144
+ | Slash | Description |
145
+ |---|---|
146
+ | `/mac [addr]` | Locate a MAC address on the network |
147
+ | `/arp [device]` · `/route [device]` | ARP / routing tables |
148
+ | `/dhcp` | DHCP leases and pool usage |
149
+ | `/ping [host]` · `/trace [host]` | Ping / traceroute a host |
150
+ | `/customer [name/IP]` | Look up a customer |
151
+
152
+ **Session**
153
+
154
+ | Slash | Description |
155
+ |---|---|
156
+ | `/autopilot` | Toggle autopilot — proposed fixes are applied automatically (also **Shift+Tab**) |
157
+ | `/model [name]` | Show or set the AI model |
158
+ | `/reasoning` | Toggle reasoning display (also **Ctrl+T**) |
159
+ | `/new` · `/history` · `/clear` | Reset conversation · show history · clear screen |
160
+ | `/update` · `/logout` · `/help` · `/exit` | Update · log out · help · quit |
123
161
 
124
162
  **Keys**
125
163
 
126
164
  | Key | Action |
127
165
  |---|---|
128
166
  | **Enter** | Send message (or run the highlighted slash command) |
129
- | **/** then **↑/↓** | Open the slash menu and move the selection; **Tab** completes, **Enter** runs |
167
+ | **/** then **↑/↓** | Open the slash menu and move the selection; **Tab** / **→** completes, **Enter** runs |
130
168
  | **↑/↓** (empty input) | Navigate input history |
169
+ | **Shift+Tab** | Toggle autopilot mode |
131
170
  | **Ctrl+T** | Toggle reasoning display |
132
171
  | **Ctrl+L** | Clear the transcript |
133
172
  | **Esc** | Cancel the in-flight request |
@@ -135,6 +174,10 @@ Inside the REPL, type `/` to see the menu. Shortcuts:
135
174
  | **Ctrl+A/E** | Jump to start / end of the line |
136
175
  | **Ctrl+U/K/W** | Delete to start / to end / previous word |
137
176
 
177
+ **Autopilot** — when on (⚡ shown in the footer), any fix the AI proposes as a
178
+ plan is applied automatically without waiting for you to type `apply fix`.
179
+ Toggle it with **Shift+Tab** or `/autopilot`.
180
+
138
181
  The REPL runs as a full-height app in the terminal's alternate screen: the banner
139
182
  stays pinned at the top, the transcript fills the middle, and the input box is
140
183
  pinned to the bottom — the same layout as GitHub Copilot CLI. On exit, the session
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ispbills-icli",
3
- "version": "8.0.0",
3
+ "version": "8.1.1",
4
4
  "description": "iCli — IspBills AI network engineer in your terminal",
5
5
  "keywords": [
6
6
  "ispbills",
package/src/commands.js CHANGED
@@ -1,23 +1,58 @@
1
1
  import { RESET, BOLD, DIM, GRAY, WHITE, ACCENT, GREEN } from './ui.js';
2
2
 
3
- /** All slash commands, used for the menu, help and Tab completion. */
3
+ /**
4
+ * All slash commands, used for the menu, help and Tab completion.
5
+ *
6
+ * `group` is only used to organise the /help output. `local: true` marks
7
+ * commands the REPL handles itself (they never hit the AI backend).
8
+ */
4
9
  export const SLASH_COMMANDS = [
5
- { cmd: '/check', hint: '[device]', desc: 'Run diagnostics on a device' },
6
- { cmd: '/vlan', hint: '[id]', desc: 'Check VLAN usage' },
7
- { cmd: '/onu', hint: '[id]', desc: 'Show ONU status and signal' },
8
- { cmd: '/customer', hint: '[name/IP]', desc: 'Look up a customer' },
9
- { cmd: '/ping', hint: '[host]', desc: 'Ping a host' },
10
- { cmd: '/online', hint: '', desc: 'List online sessions' },
11
- { cmd: '/new', hint: '', desc: 'Start a fresh conversation' },
12
- { cmd: '/history', hint: '', desc: 'Show conversation history' },
13
- { cmd: '/clear', hint: '', desc: 'Clear the screen' },
14
- { cmd: '/update', hint: '', desc: 'Update iCli to the latest version' },
15
- { cmd: '/logout', hint: '', desc: 'Log out and clear credentials' },
16
- { cmd: '/help', hint: '', desc: 'Show help' },
17
- { cmd: '/exit', hint: '', desc: 'Exit iCli' },
10
+ // ── Devices & diagnostics ──
11
+ { cmd: '/check', hint: '[device]', desc: 'Run full diagnostics on a device', group: 'Devices' },
12
+ { cmd: '/status', hint: '[device]', desc: 'Health summary across the fleet', group: 'Devices' },
13
+ { cmd: '/reachable', hint: '', desc: 'Which devices are up / unreachable', group: 'Devices' },
14
+ { cmd: '/uptime', hint: '[device]', desc: 'Device uptime and last reboot', group: 'Devices' },
15
+ { cmd: '/cpu', hint: '[device]', desc: 'CPU, memory and temperature load', group: 'Devices' },
16
+ { cmd: '/interfaces', hint: '[device]', desc: 'Interface / port status and errors', group: 'Devices' },
17
+ { cmd: '/config', hint: '[device]', desc: 'Show the running configuration', group: 'Devices' },
18
+ { cmd: '/audit', hint: '[device]', desc: 'Audit config and suggest fixes', group: 'Devices' },
19
+ { cmd: '/backup', hint: '[device]', desc: 'Back up the device configuration', group: 'Devices' },
20
+ { cmd: '/reboot', hint: '[device]', desc: 'Reboot a device (asks to confirm)', group: 'Devices' },
21
+ { cmd: '/alarms', hint: '', desc: 'Active alarms and recent faults', group: 'Devices' },
22
+
23
+ // ── Optical / access network ──
24
+ { cmd: '/onu', hint: '[id]', desc: 'ONU status and signal levels', group: 'Access' },
25
+ { cmd: '/optical', hint: '[device]', desc: 'Optical RX/TX power (dBm)', group: 'Access' },
26
+ { cmd: '/vlan', hint: '[id]', desc: 'VLAN usage — free or in use', group: 'Access' },
27
+ { cmd: '/pppoe', hint: '[user]', desc: 'PPPoE session detail', group: 'Access' },
28
+ { cmd: '/online', hint: '', desc: 'List online PPPoE sessions', group: 'Access' },
29
+ { cmd: '/offline', hint: '[olt/router]', desc: 'List offline OLTs, routers or ONUs', group: 'Access' },
30
+ { cmd: '/down', hint: '[circuit]', desc: 'List down circuits / uplinks', group: 'Access' },
31
+ { cmd: '/bandwidth', hint: '[device]', desc: 'Traffic / bandwidth usage', group: 'Access' },
32
+
33
+ // ── Layer 2/3 lookups ──
34
+ { cmd: '/mac', hint: '[addr]', desc: 'Locate a MAC address on the network', group: 'Lookup' },
35
+ { cmd: '/arp', hint: '[device]', desc: 'ARP table for a device', group: 'Lookup' },
36
+ { cmd: '/route', hint: '[device]', desc: 'Routing table for a device', group: 'Lookup' },
37
+ { cmd: '/dhcp', hint: '', desc: 'DHCP leases and pool usage', group: 'Lookup' },
38
+ { cmd: '/ping', hint: '[host]', desc: 'Ping a host through the network', group: 'Lookup' },
39
+ { cmd: '/trace', hint: '[host]', desc: 'Traceroute the path to a host', group: 'Lookup' },
40
+ { cmd: '/customer', hint: '[name/IP]', desc: 'Look up a customer', group: 'Lookup' },
41
+
42
+ // ── Session & app ──
43
+ { cmd: '/autopilot', hint: '', desc: 'Toggle autopilot (auto-apply fixes)', group: 'Session', local: true },
44
+ { cmd: '/model', hint: '[name]', desc: 'Show or set the AI model', group: 'Session', local: true },
45
+ { cmd: '/reasoning', hint: '', desc: 'Toggle reasoning display (Ctrl+T)', group: 'Session', local: true },
46
+ { cmd: '/new', hint: '', desc: 'Start a fresh conversation', group: 'Session', local: true },
47
+ { cmd: '/history', hint: '', desc: 'Show conversation history', group: 'Session', local: true },
48
+ { cmd: '/clear', hint: '', desc: 'Clear the screen', group: 'Session', local: true },
49
+ { cmd: '/update', hint: '', desc: 'Update iCli to the latest version', group: 'Session', local: true },
50
+ { cmd: '/logout', hint: '', desc: 'Log out and clear credentials', group: 'Session', local: true },
51
+ { cmd: '/help', hint: '', desc: 'Show help', group: 'Session', local: true },
52
+ { cmd: '/exit', hint: '', desc: 'Exit iCli', group: 'Session', local: true },
18
53
  ];
19
54
 
20
- /** readline completer for slash commands. */
55
+ /** readline completer for slash commands (used by the non-Ink fallback REPL). */
21
56
  export function slashCompleter(line) {
22
57
  if (line.startsWith('/')) {
23
58
  const hits = SLASH_COMMANDS.map((s) => s.cmd + ' ').filter((s) => s.startsWith(line));
@@ -27,28 +62,66 @@ export function slashCompleter(line) {
27
62
  }
28
63
 
29
64
  /**
30
- * Map an IspBills AI shortcut command to a natural-language question.
65
+ * Map a NOC shortcut command to a natural-language question for the AI.
31
66
  * Returns null for commands that are handled locally by the REPL.
32
67
  */
33
68
  export function slashToQuestion(line) {
34
69
  const [sc, ...rest] = line.trim().split(/\s+/);
35
70
  const arg = rest.join(' ');
71
+ const on = (withArg, without) => (arg ? withArg : without);
36
72
  switch (sc) {
37
- case '/check': return arg ? `run diagnostics on ${arg}` : 'check status of all devices';
38
- case '/vlan': return arg ? `is vlan ${arg} in use or free?` : 'list all vlans in use';
39
- case '/onu': return arg ? `show ONU ${arg} status and signal level` : 'list all ONUs';
40
- case '/customer': return arg ? `look up customer ${arg}` : 'show recent customers';
41
- case '/ping': return arg ? `ping ${arg} through the network` : 'which devices are reachable?';
42
- case '/online': return 'list currently online PPPoE sessions';
43
- default: return null;
73
+ // Devices & diagnostics
74
+ case '/check': return on(`run full diagnostics on ${arg} and summarise any problems`, 'run diagnostics across all devices and flag anything unhealthy');
75
+ case '/status': return on(`give a health summary for ${arg}`, 'give a health summary of every device in the fleet');
76
+ case '/reachable': return 'which devices are reachable right now and which are down?';
77
+ case '/uptime': return on(`what is the uptime and last reboot time of ${arg}?`, 'show uptime for all devices and flag any that rebooted recently');
78
+ case '/cpu': return on(`show CPU, memory and temperature load on ${arg}`, 'show CPU and memory load across all devices and flag anything overloaded');
79
+ case '/interfaces': return on(`show interface/port status and error counters on ${arg}`, 'show interface status across devices and flag ports with errors or that are down');
80
+ case '/config': return on(`show the running configuration of ${arg}`, 'which device config do you want to see? list the devices');
81
+ case '/audit': return on(`audit the configuration of ${arg} and suggest concrete fixes`, 'audit the fleet configuration and suggest concrete improvements');
82
+ case '/backup': return on(`back up the running configuration of ${arg}`, 'back up the running configuration of all devices');
83
+ case '/reboot': return on(`reboot ${arg} safely`, 'which device should I reboot? list the devices first');
84
+ case '/alarms': return 'show active alarms and recent faults across the network';
85
+
86
+ // Optical / access
87
+ case '/onu': return on(`show ONU ${arg} status and optical signal level`, 'list all ONUs with their status and signal levels');
88
+ case '/optical': return on(`show optical RX/TX power in dBm for ${arg} and flag weak signals`, 'show optical RX/TX power across ONUs and flag any weak or out-of-range signals');
89
+ case '/vlan': return on(`is VLAN ${arg} in use or free across the network?`, 'list all VLANs and show which are in use or free');
90
+ case '/pppoe': return on(`show PPPoE session detail for ${arg}`, 'summarise current PPPoE sessions');
91
+ case '/online': return 'list currently online PPPoE sessions';
92
+ case '/offline': {
93
+ const t = arg.toLowerCase();
94
+ if (/olt/.test(t)) return 'list all OLTs that are currently offline or unreachable';
95
+ if (/rout|nas|mikrotik|router/.test(t)) return 'list all routers / NAS devices that are currently offline or unreachable';
96
+ if (/onu|ont/.test(t)) return 'list all ONUs that are currently offline';
97
+ if (/cust|client|user/.test(t)) return 'list all customers that are currently offline';
98
+ return arg
99
+ ? `list all ${arg} that are currently offline`
100
+ : 'list everything that is currently offline — OLTs, routers and ONUs';
101
+ }
102
+ case '/down': return on(`is the ${arg} circuit / uplink down? show its status`, 'list all down circuits, uplinks or backbone links across the network');
103
+ case '/bandwidth': return on(`show traffic and bandwidth usage on ${arg}`, 'show current bandwidth usage across the network and flag the top talkers');
104
+
105
+ // Layer 2/3 lookups
106
+ case '/mac': return on(`locate MAC address ${arg} — which device and port is it on?`, 'how do I locate a MAC address? ask me for the address');
107
+ case '/arp': return on(`show the ARP table for ${arg}`, 'show ARP tables and flag any duplicate or suspicious entries');
108
+ case '/route': return on(`show the routing table for ${arg}`, 'show routing tables across the network');
109
+ case '/dhcp': return 'show DHCP leases and pool usage, and flag pools that are nearly exhausted';
110
+ case '/ping': return on(`ping ${arg} through the network and report latency and loss`, 'which hosts should I ping? list reachable devices');
111
+ case '/trace': return on(`traceroute the path to ${arg} and show each hop`, 'what host should I traceroute?');
112
+ case '/customer': return on(`look up customer ${arg} with their device, plan and session status`, 'show recently active customers');
113
+
114
+ default: return null; // locally handled command
44
115
  }
45
116
  }
46
117
 
47
118
  export function printSlashMenu() {
48
119
  console.log(`\n ${BOLD}Slash commands${RESET}`);
49
- for (const { cmd, hint, desc } of SLASH_COMMANDS) {
50
- const key = (cmd + (hint ? ' ' + hint : '')).padEnd(20);
51
- console.log(` ${ACCENT}${key}${RESET}${DIM}${desc}${RESET}`);
120
+ let group = null;
121
+ for (const { cmd, hint, desc, group: g } of SLASH_COMMANDS) {
122
+ if (g !== group) { group = g; console.log(` ${DIM}${group}${RESET}`); }
123
+ const key = (cmd + (hint ? ' ' + hint : '')).padEnd(22);
124
+ console.log(` ${ACCENT}${key}${RESET}${DIM}${desc}${RESET}`);
52
125
  }
53
126
  console.log();
54
127
  }
package/src/tui/app.js CHANGED
@@ -4,7 +4,7 @@
4
4
  // IspBills backend.
5
5
  import { html, useState, useEffect, useRef, useCallback } from './dom.js';
6
6
  import { Box, Text, useStdout, useApp, useInput } from 'ink';
7
- import { C, glyphs, midDot } from './theme.js';
7
+ import { C, glyphs, midDot, dash } from './theme.js';
8
8
  import {
9
9
  Banner, UserMessage, AssistantMessage, StatusLines, Reasoning,
10
10
  Plan, Connect, Notice, Thinking,
@@ -12,6 +12,7 @@ import {
12
12
  import { Composer } from './composer.js';
13
13
  import { streamChat } from '../client.js';
14
14
  import { slashToQuestion, SLASH_COMMANDS } from '../commands.js';
15
+ import { saveConfig } from '../config.js';
15
16
  import { shortModel } from '../ui.js';
16
17
 
17
18
  function useTerminalSize() {
@@ -71,17 +72,25 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion })
71
72
  const [busy, setBusy] = useState(false);
72
73
  const [model, setModel] = useState(cfg._model);
73
74
  const [showReasoning, setShowReasoning] = useState(!!display.reasoning);
75
+ const [autopilot, setAutopilot] = useState(false);
74
76
  const [hint, setHint] = useState('');
75
77
 
76
78
  const convo = useRef([]); // [{role, content}] for the backend
77
79
  const plain = useRef([]); // plain-text transcript for exit reprint
78
80
  const abortRef = useRef(null); // AbortController of the in-flight turn
79
81
  const ctrlC = useRef(0); // timestamp of the last lone Ctrl+C
82
+ const apRef = useRef(false); // live autopilot flag (read inside runTurn)
83
+ const modelRef = useRef(cfg._model); // preferred model to request
84
+
85
+ const setAutopilotMode = useCallback((next) => {
86
+ apRef.current = next;
87
+ setAutopilot(next);
88
+ }, []);
80
89
 
81
90
  const push = useCallback((item) => setItems((xs) => [...xs, { id: nextId(), ...item }]), []);
82
91
  const log = (line) => plain.current.push(line);
83
92
 
84
- const runTurn = useCallback(async (question) => {
93
+ const runTurn = useCallback(async (question, depth = 0) => {
85
94
  push({ type: 'user', text: question });
86
95
  log(`\n${glyphs.prompt} ${question}\n`);
87
96
  convo.current.push({ role: 'user', content: question });
@@ -93,9 +102,13 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion })
93
102
  const abort = new AbortController();
94
103
  abortRef.current = abort;
95
104
 
105
+ const context = { autopilot: apRef.current };
106
+ if (modelRef.current) context.model = modelRef.current;
107
+
96
108
  try {
97
109
  const { reply, text, meta } = await streamChat(cfg, [...convo.current], {
98
110
  signal: abort.signal,
111
+ context,
99
112
  onEvent: (ev) => {
100
113
  if (ev.type === 'status') s.status = [...s.status, ev.line];
101
114
  else if (ev.type === 'reasoning') s.reasoning += ev.delta;
@@ -109,6 +122,15 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion })
109
122
  else if (type === 'connect') { push({ type: 'connect', reply }); log('[connect] ' + (reply.device?.kind || '')); }
110
123
  else { push({ type: 'assistant', text: text || '(no response)' }); log(text || ''); }
111
124
  if (text) convo.current.push({ role: 'assistant', content: text });
125
+
126
+ // Autopilot: auto-confirm a proposed plan without waiting for the user.
127
+ if (type === 'plan' && apRef.current && depth < 3) {
128
+ push({ type: 'notice', text: `${glyphs.bolt} autopilot ${dash()} applying fix…`, color: C.yellow });
129
+ setLive(null);
130
+ setBusy(false);
131
+ abortRef.current = null;
132
+ return runTurn('apply fix', depth + 1);
133
+ }
112
134
  } catch (e) {
113
135
  if (abort.signal.aborted) push({ type: 'notice', text: `${glyphs.cross} Cancelled.`, color: C.yellow });
114
136
  else push({ type: 'notice', text: `${glyphs.cross} ${e.message}`, color: C.red });
@@ -138,9 +160,36 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion })
138
160
  setShowReasoning((v) => { push({ type: 'notice', text: `Reasoning display ${!v ? 'on' : 'off'}.` }); return !v; });
139
161
  return;
140
162
  }
163
+ if (q === '/autopilot' || q === '/auto') {
164
+ const next = !apRef.current;
165
+ setAutopilotMode(next);
166
+ push({ type: 'notice',
167
+ text: next
168
+ ? `${glyphs.bolt} Autopilot ON ${dash()} proposed fixes will be applied automatically. Toggle with Shift+Tab.`
169
+ : `Autopilot OFF ${dash()} fixes will wait for your confirmation.`,
170
+ color: next ? C.yellow : C.gray });
171
+ return;
172
+ }
173
+ if (q === '/model' || q.startsWith('/model ')) {
174
+ const name = q.slice('/model'.length).trim();
175
+ if (!name) {
176
+ push({ type: 'notice', text: `Current model: ${model || modelRef.current || 'server default'}.\nUse /model <name> to request a different model.` });
177
+ } else {
178
+ modelRef.current = name;
179
+ setModel(name);
180
+ cfg._model = name;
181
+ try { saveConfig(cfg); } catch {}
182
+ push({ type: 'notice', text: `${glyphs.check} Model set to ${name}. It applies to your next message.`, color: C.green });
183
+ }
184
+ return;
185
+ }
141
186
  if (q === '/help' || q === 'help') {
142
- const lines = SLASH_COMMANDS.map((c) => ` ${(c.cmd + (c.hint ? ' ' + c.hint : '')).padEnd(20)}${c.desc}`).join('\n');
143
- push({ type: 'notice', text: 'Commands:\n' + lines + '\n\n Keys: Ctrl+T reasoning · Ctrl+L clear · Ctrl+C cancel · Ctrl+C twice to exit' });
187
+ let group = null;
188
+ const lines = SLASH_COMMANDS.map((c) => {
189
+ const head = c.group !== group ? ((group = c.group), `\n ${c.group}\n`) : '';
190
+ return head + ` ${(c.cmd + (c.hint ? ' ' + c.hint : '')).padEnd(22)}${c.desc}`;
191
+ }).join('\n');
192
+ push({ type: 'notice', text: 'Commands:' + lines + '\n\n Keys: Shift+Tab autopilot · Ctrl+T reasoning · Ctrl+L clear · Ctrl+C twice to exit' });
144
193
  return;
145
194
  }
146
195
  if (q === '/history') {
@@ -158,7 +207,7 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion })
158
207
  else { push({ type: 'notice', text: `Unknown command: ${q}. Type /help.` }); return; }
159
208
  }
160
209
  runTurn(question);
161
- }, [doExit, push, runTurn, onExternal]);
210
+ }, [doExit, push, runTurn, onExternal, setAutopilotMode, model, cfg]);
162
211
 
163
212
  // Global keys (Composer handles text editing; here we handle app-level keys).
164
213
  useInput((input, key) => {
@@ -171,6 +220,12 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion })
171
220
  return;
172
221
  }
173
222
  if (key.escape && busy) { abortRef.current?.abort(); return; }
223
+ if (key.tab && key.shift && !busy) {
224
+ const next = !apRef.current;
225
+ setAutopilotMode(next);
226
+ setHint(next ? 'Autopilot ON' : 'Autopilot OFF');
227
+ return;
228
+ }
174
229
  if (key.ctrl && input === 't') { setShowReasoning((v) => !v); return; }
175
230
  if (key.ctrl && input === 'l' && !busy) { setItems([]); return; }
176
231
  });
@@ -193,7 +248,8 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion })
193
248
  };
194
249
 
195
250
  const ctx = [model ? shortModel(model) : null, cfg.user?.name, cfg.user?.role].filter(Boolean).join(midDot());
196
- const footerRight = hint || `/help ${midDot()} Ctrl+T reasoning ${midDot()} Ctrl+C exit`;
251
+ const modeTag = autopilot ? `${glyphs.bolt} autopilot ${midDot()} ` : '';
252
+ const footerRight = hint || `${modeTag}/help ${midDot()} Shift+Tab autopilot ${midDot()} Ctrl+C exit`;
197
253
 
198
254
  // Tail-slice the transcript to what fits the middle region so nothing is
199
255
  // clipped mid-line on small terminals.
@@ -215,7 +271,7 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion })
215
271
 
216
272
  return html`
217
273
  <${Box} flexDirection="column" width=${cols} height=${rows}>
218
- <${Box} flexShrink=${0} flexDirection="column">
274
+ <${Box} flexShrink=${0} flexDirection="column" alignItems="center">
219
275
  <${Banner} cfg=${cfg} model=${model} width=${cols} compact=${compact} />
220
276
  <//>
221
277
 
@@ -236,7 +292,7 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion })
236
292
  <${Composer} onSubmit=${onSubmit} busy=${busy} width=${cols} />
237
293
  <${Box} paddingX=${1} width=${cols}>
238
294
  <${Box} flexGrow=${1}><${Text} color=${C.gray} wrap="truncate-end">${ctx}<//><//>
239
- <${Text} color=${hint ? C.yellow : C.gray} wrap="truncate-end">${footerRight}<//>
295
+ <${Text} color=${hint || autopilot ? C.yellow : C.gray} wrap="truncate-end">${footerRight}<//>
240
296
  <//>
241
297
  <//>
242
298
  <//>`;
@@ -36,11 +36,11 @@ export function Banner({ cfg = {}, model, width = 80, compact = false }) {
36
36
  }
37
37
 
38
38
  return html`
39
- <${Box} flexDirection="column" marginBottom=${1}>
39
+ <${Box} flexDirection="column" marginBottom=${1} alignItems="center">
40
40
  ${wide
41
- ? html`<${Box} flexDirection="column">
41
+ ? html`<${Box} flexDirection="column" alignItems="center">
42
42
  ${LOGO.map((l, i) => html`<${Text} key=${'l' + i} color=${C.accent} bold>${l}<//>`)}
43
- <${Text} color=${C.gray}> iCopilot ${dash()} network engineer in your terminal<//>
43
+ <${Text} color=${C.gray}>iCopilot ${dash()} network engineer in your terminal<//>
44
44
  <//>`
45
45
  : html`<${Text} color=${C.accent} bold>iCopilot ${html`<${Text} color=${C.gray}>${dash()} IspBills AI terminal<//>`}<//>`}
46
46
  <${Box} marginTop=${1}>
@@ -51,7 +51,7 @@ export function Banner({ cfg = {}, model, width = 80, compact = false }) {
51
51
  <${Text} color=${label === 'model' ? C.cyan : C.white}>${val}<//>
52
52
  <//>`)}
53
53
  <//>
54
- ${cfg.url ? html`<${Text} color=${C.gray}>url ${html`<${Text} color=${C.accent}>${cfg.url}<//>`}<//>` : null}
54
+ ${cfg.url ? html`<${Text} color=${C.gray}>${cfg.url}<//>` : null}
55
55
  <//>`;
56
56
  }
57
57
 
@@ -46,7 +46,7 @@ export function Composer({ onSubmit, busy, width = 80 }) {
46
46
  submit(value.slice(0, cursor) + first + value.slice(cursor));
47
47
  return;
48
48
  }
49
- if (key.tab) {
49
+ if (key.tab && !key.shift) {
50
50
  if (menuOpen) { const c = matches[sel].cmd + ' '; setText(c, c.length); }
51
51
  return;
52
52
  }
@@ -75,7 +75,12 @@ export function Composer({ onSubmit, busy, width = 80 }) {
75
75
 
76
76
  // ── Cursor movement ──
77
77
  if (key.leftArrow) { setCursor((c) => Math.max(0, c - 1)); return; }
78
- if (key.rightArrow) { setCursor((c) => Math.min(value.length, c + 1)); return; }
78
+ if (key.rightArrow) {
79
+ // At end of a slash token, Right accepts the highlighted completion.
80
+ if (menuOpen && cursor >= value.length) { const c = matches[sel].cmd + ' '; setText(c, c.length); return; }
81
+ setCursor((c) => Math.min(value.length, c + 1));
82
+ return;
83
+ }
79
84
  if (key.ctrl && input === 'a') { setCursor(0); return; } // line start
80
85
  if (key.ctrl && input === 'e') { setCursor(value.length); return; } // line end
81
86
 
@@ -107,6 +112,8 @@ export function Composer({ onSubmit, busy, width = 80 }) {
107
112
  const at = value[cursor] ?? ' ';
108
113
  const after = value.slice(cursor + 1);
109
114
  const empty = value.length === 0;
115
+ const sug = menuOpen && matches[sel] ? matches[sel].cmd : '';
116
+ const ghost = sug && sug.startsWith(value) && cursor >= value.length ? sug.slice(value.length) : '';
110
117
 
111
118
  return html`
112
119
  <${Box} flexDirection="column" width=${width}>
@@ -139,7 +146,7 @@ export function Composer({ onSubmit, busy, width = 80 }) {
139
146
  <${Text} color=${C.gray}>${busy ? 'Working…' : 'Ask about your network, or type / for commands'}<//>
140
147
  <//>`
141
148
  : html`<${Box} flexGrow=${1}>
142
- <${Text}>${before}<//><${Text} inverse>${at}<//><${Text}>${after}<//>
149
+ <${Text}>${before}<//><${Text} inverse>${at}<//><${Text}>${after}<//><${Text} color=${C.gray}>${ghost}<//>
143
150
  <//>`}
144
151
  <//>
145
152
  <//>`;