ispbills-icli 8.0.0 → 8.1.0
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 +53 -10
- package/package.json +1 -1
- package/src/commands.js +88 -26
- package/src/tui/app.js +64 -8
- package/src/tui/components.js +4 -4
- package/src/tui/composer.js +10 -3
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
|
-
|
|
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
|
-
| `/
|
|
116
|
-
| `/
|
|
117
|
-
| `/
|
|
118
|
-
| `/
|
|
119
|
-
| `/
|
|
120
|
-
| `/
|
|
121
|
-
|
|
122
|
-
|
|
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
package/src/commands.js
CHANGED
|
@@ -1,23 +1,57 @@
|
|
|
1
1
|
import { RESET, BOLD, DIM, GRAY, WHITE, ACCENT, GREEN } from './ui.js';
|
|
2
2
|
|
|
3
|
-
/**
|
|
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
|
-
|
|
6
|
-
{ cmd: '/
|
|
7
|
-
{ cmd: '/
|
|
8
|
-
{ cmd: '/
|
|
9
|
-
{ cmd: '/
|
|
10
|
-
{ cmd: '/
|
|
11
|
-
{ cmd: '/
|
|
12
|
-
{ cmd: '/
|
|
13
|
-
{ cmd: '/
|
|
14
|
-
{ cmd: '/
|
|
15
|
-
{ cmd: '/
|
|
16
|
-
{ cmd: '/
|
|
17
|
-
|
|
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: '', desc: 'List offline customers / ONUs', group: 'Access' },
|
|
30
|
+
{ cmd: '/bandwidth', hint: '[device]', desc: 'Traffic / bandwidth usage', group: 'Access' },
|
|
31
|
+
|
|
32
|
+
// ── Layer 2/3 lookups ──
|
|
33
|
+
{ cmd: '/mac', hint: '[addr]', desc: 'Locate a MAC address on the network', group: 'Lookup' },
|
|
34
|
+
{ cmd: '/arp', hint: '[device]', desc: 'ARP table for a device', group: 'Lookup' },
|
|
35
|
+
{ cmd: '/route', hint: '[device]', desc: 'Routing table for a device', group: 'Lookup' },
|
|
36
|
+
{ cmd: '/dhcp', hint: '', desc: 'DHCP leases and pool usage', group: 'Lookup' },
|
|
37
|
+
{ cmd: '/ping', hint: '[host]', desc: 'Ping a host through the network', group: 'Lookup' },
|
|
38
|
+
{ cmd: '/trace', hint: '[host]', desc: 'Traceroute the path to a host', group: 'Lookup' },
|
|
39
|
+
{ cmd: '/customer', hint: '[name/IP]', desc: 'Look up a customer', group: 'Lookup' },
|
|
40
|
+
|
|
41
|
+
// ── Session & app ──
|
|
42
|
+
{ cmd: '/autopilot', hint: '', desc: 'Toggle autopilot (auto-apply fixes)', group: 'Session', local: true },
|
|
43
|
+
{ cmd: '/model', hint: '[name]', desc: 'Show or set the AI model', group: 'Session', local: true },
|
|
44
|
+
{ cmd: '/reasoning', hint: '', desc: 'Toggle reasoning display (Ctrl+T)', group: 'Session', local: true },
|
|
45
|
+
{ cmd: '/new', hint: '', desc: 'Start a fresh conversation', group: 'Session', local: true },
|
|
46
|
+
{ cmd: '/history', hint: '', desc: 'Show conversation history', group: 'Session', local: true },
|
|
47
|
+
{ cmd: '/clear', hint: '', desc: 'Clear the screen', group: 'Session', local: true },
|
|
48
|
+
{ cmd: '/update', hint: '', desc: 'Update iCli to the latest version', group: 'Session', local: true },
|
|
49
|
+
{ cmd: '/logout', hint: '', desc: 'Log out and clear credentials', group: 'Session', local: true },
|
|
50
|
+
{ cmd: '/help', hint: '', desc: 'Show help', group: 'Session', local: true },
|
|
51
|
+
{ cmd: '/exit', hint: '', desc: 'Exit iCli', group: 'Session', local: true },
|
|
18
52
|
];
|
|
19
53
|
|
|
20
|
-
/** readline completer for slash commands. */
|
|
54
|
+
/** readline completer for slash commands (used by the non-Ink fallback REPL). */
|
|
21
55
|
export function slashCompleter(line) {
|
|
22
56
|
if (line.startsWith('/')) {
|
|
23
57
|
const hits = SLASH_COMMANDS.map((s) => s.cmd + ' ').filter((s) => s.startsWith(line));
|
|
@@ -27,28 +61,56 @@ export function slashCompleter(line) {
|
|
|
27
61
|
}
|
|
28
62
|
|
|
29
63
|
/**
|
|
30
|
-
* Map
|
|
64
|
+
* Map a NOC shortcut command to a natural-language question for the AI.
|
|
31
65
|
* Returns null for commands that are handled locally by the REPL.
|
|
32
66
|
*/
|
|
33
67
|
export function slashToQuestion(line) {
|
|
34
68
|
const [sc, ...rest] = line.trim().split(/\s+/);
|
|
35
69
|
const arg = rest.join(' ');
|
|
70
|
+
const on = (withArg, without) => (arg ? withArg : without);
|
|
36
71
|
switch (sc) {
|
|
37
|
-
|
|
38
|
-
case '/
|
|
39
|
-
case '/
|
|
40
|
-
case '/
|
|
41
|
-
case '/
|
|
42
|
-
case '/
|
|
43
|
-
|
|
72
|
+
// Devices & diagnostics
|
|
73
|
+
case '/check': return on(`run full diagnostics on ${arg} and summarise any problems`, 'run diagnostics across all devices and flag anything unhealthy');
|
|
74
|
+
case '/status': return on(`give a health summary for ${arg}`, 'give a health summary of every device in the fleet');
|
|
75
|
+
case '/reachable': return 'which devices are reachable right now and which are down?';
|
|
76
|
+
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');
|
|
77
|
+
case '/cpu': return on(`show CPU, memory and temperature load on ${arg}`, 'show CPU and memory load across all devices and flag anything overloaded');
|
|
78
|
+
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');
|
|
79
|
+
case '/config': return on(`show the running configuration of ${arg}`, 'which device config do you want to see? list the devices');
|
|
80
|
+
case '/audit': return on(`audit the configuration of ${arg} and suggest concrete fixes`, 'audit the fleet configuration and suggest concrete improvements');
|
|
81
|
+
case '/backup': return on(`back up the running configuration of ${arg}`, 'back up the running configuration of all devices');
|
|
82
|
+
case '/reboot': return on(`reboot ${arg} safely`, 'which device should I reboot? list the devices first');
|
|
83
|
+
case '/alarms': return 'show active alarms and recent faults across the network';
|
|
84
|
+
|
|
85
|
+
// Optical / access
|
|
86
|
+
case '/onu': return on(`show ONU ${arg} status and optical signal level`, 'list all ONUs with their status and signal levels');
|
|
87
|
+
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');
|
|
88
|
+
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');
|
|
89
|
+
case '/pppoe': return on(`show PPPoE session detail for ${arg}`, 'summarise current PPPoE sessions');
|
|
90
|
+
case '/online': return 'list currently online PPPoE sessions';
|
|
91
|
+
case '/offline': return 'list customers or ONUs that are currently offline';
|
|
92
|
+
case '/bandwidth': return on(`show traffic and bandwidth usage on ${arg}`, 'show current bandwidth usage across the network and flag the top talkers');
|
|
93
|
+
|
|
94
|
+
// Layer 2/3 lookups
|
|
95
|
+
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');
|
|
96
|
+
case '/arp': return on(`show the ARP table for ${arg}`, 'show ARP tables and flag any duplicate or suspicious entries');
|
|
97
|
+
case '/route': return on(`show the routing table for ${arg}`, 'show routing tables across the network');
|
|
98
|
+
case '/dhcp': return 'show DHCP leases and pool usage, and flag pools that are nearly exhausted';
|
|
99
|
+
case '/ping': return on(`ping ${arg} through the network and report latency and loss`, 'which hosts should I ping? list reachable devices');
|
|
100
|
+
case '/trace': return on(`traceroute the path to ${arg} and show each hop`, 'what host should I traceroute?');
|
|
101
|
+
case '/customer': return on(`look up customer ${arg} with their device, plan and session status`, 'show recently active customers');
|
|
102
|
+
|
|
103
|
+
default: return null; // locally handled command
|
|
44
104
|
}
|
|
45
105
|
}
|
|
46
106
|
|
|
47
107
|
export function printSlashMenu() {
|
|
48
108
|
console.log(`\n ${BOLD}Slash commands${RESET}`);
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
console.log(` ${
|
|
109
|
+
let group = null;
|
|
110
|
+
for (const { cmd, hint, desc, group: g } of SLASH_COMMANDS) {
|
|
111
|
+
if (g !== group) { group = g; console.log(` ${DIM}${group}${RESET}`); }
|
|
112
|
+
const key = (cmd + (hint ? ' ' + hint : '')).padEnd(22);
|
|
113
|
+
console.log(` ${ACCENT}${key}${RESET}${DIM}${desc}${RESET}`);
|
|
52
114
|
}
|
|
53
115
|
console.log();
|
|
54
116
|
}
|
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
|
-
|
|
143
|
-
|
|
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
|
|
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
|
<//>`;
|
package/src/tui/components.js
CHANGED
|
@@ -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}>
|
|
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}
|
|
54
|
+
${cfg.url ? html`<${Text} color=${C.gray}>${cfg.url}<//>` : null}
|
|
55
55
|
<//>`;
|
|
56
56
|
}
|
|
57
57
|
|
package/src/tui/composer.js
CHANGED
|
@@ -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) {
|
|
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
|
<//>`;
|