ispbills-icli 4.0.5 → 5.0.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/bin/icli.js +102 -17
- package/package.json +1 -1
- package/src/chat.js +77 -37
- package/src/layout.js +55 -0
package/bin/icli.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { createInterface } from 'readline';
|
|
2
|
+
import { createInterface, emitKeypressEvents } from 'readline';
|
|
3
3
|
import { stdin as input, stdout as output, argv, exit } from 'process';
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import { loadConfig, configPath } from '../src/config.js';
|
|
6
6
|
import { loginFlow } from '../src/auth.js';
|
|
7
|
-
import { inner, onResize, boxTop, boxBot, boxMid, boxRow } from '../src/layout.js';
|
|
7
|
+
import { inner, onResize, boxTop, boxBot, boxMid, boxRow, bottomPalette } from '../src/layout.js';
|
|
8
8
|
import {
|
|
9
9
|
streamChat, printBanner, printSlashMenu, printUserBox,
|
|
10
10
|
SLASH_COMMANDS, slashCompleter,
|
|
@@ -25,6 +25,13 @@ const P = {
|
|
|
25
25
|
const args = argv.slice(2);
|
|
26
26
|
const cmd = args[0] ?? '';
|
|
27
27
|
|
|
28
|
+
// ── Session context (shown in top bar) ─────────────────────────────────────────
|
|
29
|
+
const ctx = {
|
|
30
|
+
device: '',
|
|
31
|
+
aiMode: 'MANUAL', // 'AUTOPILOT ON' | 'MANUAL'
|
|
32
|
+
operator: '',
|
|
33
|
+
};
|
|
34
|
+
|
|
28
35
|
// ── Help ───────────────────────────────────────────────────────────────────────
|
|
29
36
|
function printHelp() {
|
|
30
37
|
process.stdout.write('\n');
|
|
@@ -52,19 +59,23 @@ function printHelp() {
|
|
|
52
59
|
}
|
|
53
60
|
|
|
54
61
|
process.stdout.write(boxMid(P.border) + '\n');
|
|
55
|
-
process.stdout.write(boxRow(P.bold('
|
|
62
|
+
process.stdout.write(boxRow(P.bold('Function key shortcuts'), P.border) + '\n');
|
|
56
63
|
process.stdout.write(boxRow('', P.border) + '\n');
|
|
57
64
|
[
|
|
58
|
-
['
|
|
59
|
-
['
|
|
60
|
-
['
|
|
61
|
-
['
|
|
62
|
-
['
|
|
65
|
+
['F1', 'Open documentation'],
|
|
66
|
+
['F2', 'Show conversation history'],
|
|
67
|
+
['F3', 'Rollback last AI action'],
|
|
68
|
+
['F4', 'Run bulk device sweep'],
|
|
69
|
+
['Tab', 'Autocomplete slash commands'],
|
|
70
|
+
['↑ / ↓', 'Navigate input history'],
|
|
71
|
+
['Ctrl+L', 'Clear screen'],
|
|
72
|
+
['Ctrl+C', 'Cancel / exit'],
|
|
63
73
|
].forEach(([k, d]) =>
|
|
64
74
|
process.stdout.write(boxRow(P.warn(k.padEnd(14)) + P.dim(d), P.border) + '\n')
|
|
65
75
|
);
|
|
66
76
|
process.stdout.write(boxRow('', P.border) + '\n');
|
|
67
|
-
process.stdout.write(boxBot(P.border) + '\n
|
|
77
|
+
process.stdout.write(boxBot(P.border) + '\n');
|
|
78
|
+
process.stdout.write(bottomPalette() + '\n\n');
|
|
68
79
|
}
|
|
69
80
|
|
|
70
81
|
// ── Auth ───────────────────────────────────────────────────────────────────────
|
|
@@ -108,11 +119,16 @@ async function singleShot(cfg, question) {
|
|
|
108
119
|
|
|
109
120
|
// ── REPL ───────────────────────────────────────────────────────────────────────
|
|
110
121
|
async function interactiveRepl(cfg) {
|
|
111
|
-
const history
|
|
112
|
-
|
|
113
|
-
|
|
122
|
+
const history = [];
|
|
123
|
+
const PROMPT = () => ' ' + P.accentD('❯') + ' ';
|
|
124
|
+
|
|
125
|
+
let isProcessing = false;
|
|
126
|
+
|
|
127
|
+
// Populate operator from cfg
|
|
128
|
+
if (cfg.user?.name) ctx.operator = cfg.user.name;
|
|
129
|
+
if (cfg.user?.role) ctx.aiMode = cfg.user.role === 'noc' ? 'AUTOPILOT ON' : 'MANUAL';
|
|
114
130
|
|
|
115
|
-
printBanner(cfg);
|
|
131
|
+
printBanner(cfg, {}, ctx);
|
|
116
132
|
|
|
117
133
|
const rl = createInterface({
|
|
118
134
|
input, output,
|
|
@@ -122,6 +138,74 @@ async function interactiveRepl(cfg) {
|
|
|
122
138
|
});
|
|
123
139
|
|
|
124
140
|
onResize(() => rl.setPrompt(PROMPT()));
|
|
141
|
+
|
|
142
|
+
// ── F-key handling via keypress events ───────────────────────────────────────
|
|
143
|
+
emitKeypressEvents(process.stdin);
|
|
144
|
+
|
|
145
|
+
process.stdin.on('keypress', async (_ch, key) => {
|
|
146
|
+
if (!key || rl.closed || isProcessing) return;
|
|
147
|
+
|
|
148
|
+
if (key.name === 'f1') {
|
|
149
|
+
process.stdout.write('\n');
|
|
150
|
+
printHelp();
|
|
151
|
+
rl.prompt();
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (key.name === 'f2') {
|
|
156
|
+
// Show history
|
|
157
|
+
const turns = history.filter(m => m.role === 'user');
|
|
158
|
+
process.stdout.write('\n');
|
|
159
|
+
if (!turns.length) {
|
|
160
|
+
process.stdout.write(P.muted(' No history yet.\n\n'));
|
|
161
|
+
} else {
|
|
162
|
+
process.stdout.write(boxTop('History', P.border) + '\n');
|
|
163
|
+
turns.forEach((m, i) =>
|
|
164
|
+
process.stdout.write(
|
|
165
|
+
boxRow(P.dim(String(i + 1).padStart(2) + '.') + ' ' +
|
|
166
|
+
chalk.white(m.content.replace(/\s+/g, ' ').slice(0, inner() - 5)), P.border) + '\n',
|
|
167
|
+
);
|
|
168
|
+
process.stdout.write(boxBot(P.border) + '\n\n');
|
|
169
|
+
}
|
|
170
|
+
rl.prompt();
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (key.name === 'f3') {
|
|
175
|
+
rl.pause();
|
|
176
|
+
const q = 'rollback the last change you made';
|
|
177
|
+
process.stdout.write('\n ' + P.muted('→ ') + P.dim(q) + '\n');
|
|
178
|
+
printUserBox(q);
|
|
179
|
+
history.push({ role: 'user', content: q });
|
|
180
|
+
try {
|
|
181
|
+
const answer = await streamChat(cfg, [...history]);
|
|
182
|
+
if (answer) history.push({ role: 'assistant', content: answer });
|
|
183
|
+
} catch (e) {
|
|
184
|
+
process.stdout.write('\n ' + P.err('✖ ') + chalk.white(e.message) + '\n\n');
|
|
185
|
+
}
|
|
186
|
+
rl.resume();
|
|
187
|
+
rl.prompt();
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (key.name === 'f4') {
|
|
192
|
+
rl.pause();
|
|
193
|
+
const q = 'run a bulk sweep of all devices and report status';
|
|
194
|
+
process.stdout.write('\n ' + P.muted('→ ') + P.dim(q) + '\n');
|
|
195
|
+
printUserBox(q);
|
|
196
|
+
history.push({ role: 'user', content: q });
|
|
197
|
+
try {
|
|
198
|
+
const answer = await streamChat(cfg, [...history]);
|
|
199
|
+
if (answer) history.push({ role: 'assistant', content: answer });
|
|
200
|
+
} catch (e) {
|
|
201
|
+
process.stdout.write('\n ' + P.err('✖ ') + chalk.white(e.message) + '\n\n');
|
|
202
|
+
}
|
|
203
|
+
rl.resume();
|
|
204
|
+
rl.prompt();
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
125
209
|
rl.prompt();
|
|
126
210
|
|
|
127
211
|
rl.on('line', async (line) => {
|
|
@@ -132,7 +216,10 @@ async function interactiveRepl(cfg) {
|
|
|
132
216
|
process.stdout.write(P.muted('\n Goodbye.\n\n')); rl.close(); exit(0);
|
|
133
217
|
}
|
|
134
218
|
if (q === '/clear' || q === 'clear') {
|
|
135
|
-
process.stdout.write('\x1Bc');
|
|
219
|
+
process.stdout.write('\x1Bc');
|
|
220
|
+
printBanner(cfg, {}, ctx);
|
|
221
|
+
rl.prompt();
|
|
222
|
+
return;
|
|
136
223
|
}
|
|
137
224
|
if (q === '/help' || q === 'help') { printHelp(); rl.prompt(); return; }
|
|
138
225
|
if (q === '/history' || q === 'history') {
|
|
@@ -162,12 +249,10 @@ async function interactiveRepl(cfg) {
|
|
|
162
249
|
|
|
163
250
|
rl.pause();
|
|
164
251
|
history.push({ role: 'user', content: question });
|
|
165
|
-
|
|
166
|
-
// User message box
|
|
167
252
|
printUserBox(question);
|
|
168
253
|
|
|
169
254
|
try {
|
|
170
|
-
const answer = await streamChat(cfg, [...history]
|
|
255
|
+
const answer = await streamChat(cfg, [...history]);
|
|
171
256
|
if (answer) history.push({ role: 'assistant', content: answer });
|
|
172
257
|
} catch (e) {
|
|
173
258
|
process.stdout.write('\n ' + P.err('✖ ') + chalk.white(e.message) + '\n\n');
|
package/package.json
CHANGED
package/src/chat.js
CHANGED
|
@@ -5,7 +5,7 @@ import TerminalRenderer from 'marked-terminal';
|
|
|
5
5
|
import { refreshToken } from './auth.js';
|
|
6
6
|
import { cols, inner, visLen, padRight, stripAnsi,
|
|
7
7
|
boxTop, boxMid, boxBot, boxTopOpen, boxBotOpen,
|
|
8
|
-
boxRow, boxLine } from './layout.js';
|
|
8
|
+
boxRow, boxLine, bottomPalette, topBar } from './layout.js';
|
|
9
9
|
|
|
10
10
|
// ── Gemini-style palette ───────────────────────────────────────────────────────
|
|
11
11
|
const P = {
|
|
@@ -21,6 +21,7 @@ const P = {
|
|
|
21
21
|
warn: chalk.yellow,
|
|
22
22
|
cyan: chalk.cyan,
|
|
23
23
|
reasoning: chalk.hex('#9198A1').italic,
|
|
24
|
+
pill: chalk.bgHex('#1a2b47').hex('#8AB4F8'),
|
|
24
25
|
};
|
|
25
26
|
|
|
26
27
|
// ── Markdown ───────────────────────────────────────────────────────────────────
|
|
@@ -36,11 +37,9 @@ const termRenderer = new TerminalRenderer({
|
|
|
36
37
|
link: (_h, _t, t) => chalk.hex('#58a6ff').underline(t || _h),
|
|
37
38
|
});
|
|
38
39
|
|
|
39
|
-
// Override listitem on the instance — the options.listitem is a chalk color, not a renderer fn
|
|
40
40
|
const _origListitem = termRenderer.listitem.bind(termRenderer);
|
|
41
41
|
termRenderer.listitem = function(item) {
|
|
42
42
|
const raw = _origListitem(item);
|
|
43
|
-
// Replace the default ' * ' or ' - ' bullet with our accent '·'
|
|
44
43
|
return raw.replace(/^\s*[*\-]\s/, (m) => m.replace(/[*\-]/, P.accent('·')));
|
|
45
44
|
};
|
|
46
45
|
|
|
@@ -68,13 +67,21 @@ const shortModel = (m) =>
|
|
|
68
67
|
m.replace('openai/', '').replace(/^claude-/, '').replace(/^gpt-/, 'gpt-');
|
|
69
68
|
|
|
70
69
|
// ── Banner ────────────────────────────────────────────────────────────────────
|
|
71
|
-
export function printBanner(cfg, meta = {}) {
|
|
70
|
+
export function printBanner(cfg, meta = {}, ctx = {}) {
|
|
72
71
|
const model = meta.model ? shortModel(meta.model) : (cfg?._model ?? null);
|
|
73
72
|
|
|
74
73
|
process.stdout.write('\n');
|
|
75
74
|
process.stdout.write(boxTop('iCopilot', P.border) + '\n');
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
|
|
76
|
+
// Context bar (device · aiMode · operator) — delegates to topBar() in layout.js
|
|
77
|
+
const { device = '', aiMode = 'MANUAL', operator = '' } = ctx;
|
|
78
|
+
if (device || aiMode !== 'MANUAL' || operator) {
|
|
79
|
+
process.stdout.write(topBar(ctx) + '\n');
|
|
80
|
+
process.stdout.write(boxMid(P.border) + '\n');
|
|
81
|
+
} else {
|
|
82
|
+
process.stdout.write(boxRow(P.dim('Gemini-style terminal · iCopilot AI'), P.border) + '\n');
|
|
83
|
+
process.stdout.write(boxMid(P.border) + '\n');
|
|
84
|
+
}
|
|
78
85
|
|
|
79
86
|
const row1parts = [];
|
|
80
87
|
if (model) row1parts.push(P.dim('model ') + P.white(model));
|
|
@@ -98,7 +105,8 @@ export function printBanner(cfg, meta = {}) {
|
|
|
98
105
|
P.accentB('Ctrl+C') + P.muted(' exit'),
|
|
99
106
|
].join(P.muted(' '));
|
|
100
107
|
process.stdout.write(boxRow(hints, P.border) + '\n');
|
|
101
|
-
process.stdout.write(boxBot(P.border) + '\n
|
|
108
|
+
process.stdout.write(boxBot(P.border) + '\n');
|
|
109
|
+
process.stdout.write(bottomPalette() + '\n\n');
|
|
102
110
|
}
|
|
103
111
|
|
|
104
112
|
// ── Slash commands ─────────────────────────────────────────────────────────────
|
|
@@ -138,7 +146,6 @@ export function slashCompleter(line) {
|
|
|
138
146
|
export function printUserBox(question) {
|
|
139
147
|
process.stdout.write('\n');
|
|
140
148
|
process.stdout.write(boxTop('You', P.border) + '\n');
|
|
141
|
-
// Word-wrap the question to fit inside the box
|
|
142
149
|
const maxW = inner();
|
|
143
150
|
const words = question.split(' ');
|
|
144
151
|
let line = '';
|
|
@@ -155,8 +162,14 @@ export function printUserBox(question) {
|
|
|
155
162
|
}
|
|
156
163
|
|
|
157
164
|
// ── streamChat ────────────────────────────────────────────────────────────────
|
|
158
|
-
|
|
159
|
-
|
|
165
|
+
/**
|
|
166
|
+
* Stream a chat request. Reasoning and plan always flow inline — no keys to toggle.
|
|
167
|
+
*
|
|
168
|
+
* @param {object} cfg Loaded config (url, token, …)
|
|
169
|
+
* @param {Array} messages Conversation history
|
|
170
|
+
* @param {object} [context] Extra context passed to server
|
|
171
|
+
*/
|
|
172
|
+
export async function streamChat(cfg, messages, context = {}) {
|
|
160
173
|
const body = JSON.stringify({ messages, context });
|
|
161
174
|
|
|
162
175
|
async function doFetch(token) {
|
|
@@ -165,7 +178,7 @@ export async function streamChat(cfg, messages, context = {}, opts = {}) {
|
|
|
165
178
|
headers: {
|
|
166
179
|
'Content-Type': 'application/json',
|
|
167
180
|
'Accept': 'text/event-stream',
|
|
168
|
-
'Authorization':
|
|
181
|
+
'Authorization': 'Bearer ' + token,
|
|
169
182
|
},
|
|
170
183
|
body,
|
|
171
184
|
});
|
|
@@ -184,7 +197,7 @@ export async function streamChat(cfg, messages, context = {}, opts = {}) {
|
|
|
184
197
|
}
|
|
185
198
|
|
|
186
199
|
// ── Streaming state ─────────────────────────────────────────────────────────
|
|
187
|
-
let boxOpen = false;
|
|
200
|
+
let boxOpen = false;
|
|
188
201
|
let reasoningOpen = false;
|
|
189
202
|
let answerBuf = '';
|
|
190
203
|
let rawAnswer = '';
|
|
@@ -196,38 +209,42 @@ export async function streamChat(cfg, messages, context = {}, opts = {}) {
|
|
|
196
209
|
process.stdout.write('\n' + boxTopOpen('iCopilot', P.border) + '\n');
|
|
197
210
|
}
|
|
198
211
|
|
|
212
|
+
// Gemini-style thinking pill spinner
|
|
199
213
|
const spinner = ora({
|
|
200
|
-
text: P.
|
|
214
|
+
text: P.accent('Thinking…'),
|
|
201
215
|
color: 'blue',
|
|
202
|
-
spinner: '
|
|
203
|
-
prefixText: P.border('│') + ' ',
|
|
216
|
+
spinner: { interval: 80, frames: ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'] },
|
|
217
|
+
prefixText: P.border('│') + ' ' + P.pill(' ◆ '),
|
|
204
218
|
});
|
|
205
219
|
|
|
206
220
|
const stopSpinner = () => { if (spinner.isSpinning) spinner.stop(); };
|
|
207
221
|
|
|
222
|
+
// ── Reasoning section (always streams inline) ───────────────────────────────
|
|
208
223
|
function openReasoning() {
|
|
209
224
|
if (reasoningOpen) return;
|
|
210
225
|
stopSpinner();
|
|
211
226
|
reasoningOpen = true;
|
|
212
|
-
process.stdout.write(boxLine(
|
|
213
|
-
process.stdout.write(boxLine(P.
|
|
227
|
+
process.stdout.write(boxLine('', P.border) + '\n');
|
|
228
|
+
process.stdout.write(boxLine(P.accentB('◆ ') + P.dim('Reasoning'), P.border) + '\n');
|
|
229
|
+
process.stdout.write(boxLine(P.border(' │ '), P.border));
|
|
214
230
|
}
|
|
215
231
|
|
|
216
232
|
function writeReasonDelta(delta) {
|
|
217
|
-
if (!reasoningOpen)
|
|
233
|
+
if (!reasoningOpen) openReasoning();
|
|
218
234
|
const parts = delta.split('\n');
|
|
219
235
|
for (let i = 0; i < parts.length; i++) {
|
|
220
|
-
if (i > 0) process.stdout.write('\n' + boxLine(P.border('│
|
|
236
|
+
if (i > 0) process.stdout.write('\n' + boxLine(P.border(' │'), P.border));
|
|
221
237
|
if (parts[i]) process.stdout.write(P.reasoning(parts[i]));
|
|
222
238
|
}
|
|
223
239
|
}
|
|
224
240
|
|
|
225
241
|
function closeReasoning() {
|
|
226
242
|
if (!reasoningOpen) return;
|
|
227
|
-
process.stdout.write('\n');
|
|
243
|
+
process.stdout.write('\n' + boxLine('', P.border) + '\n');
|
|
228
244
|
reasoningOpen = false;
|
|
229
245
|
}
|
|
230
246
|
|
|
247
|
+
// ── Status delta ────────────────────────────────────────────────────────────
|
|
231
248
|
function printStatus(line) {
|
|
232
249
|
ensureBoxOpen();
|
|
233
250
|
if (reasoningOpen) closeReasoning();
|
|
@@ -236,7 +253,8 @@ export async function streamChat(cfg, messages, context = {}, opts = {}) {
|
|
|
236
253
|
const text = line.trimStart();
|
|
237
254
|
const indent = /^[\s·•⌁]/.test(line) ? ' ' : '';
|
|
238
255
|
process.stdout.write(boxLine(indent + icon + ' ' + P.dim(text), P.border) + '\n');
|
|
239
|
-
|
|
256
|
+
// Restart spinner after each status line
|
|
257
|
+
spinner.start();
|
|
240
258
|
}
|
|
241
259
|
|
|
242
260
|
function parseStreamText(raw) {
|
|
@@ -247,7 +265,8 @@ export async function streamChat(cfg, messages, context = {}, opts = {}) {
|
|
|
247
265
|
}
|
|
248
266
|
|
|
249
267
|
// ── SSE reader ─────────────────────────────────────────────────────────────
|
|
250
|
-
|
|
268
|
+
ensureBoxOpen();
|
|
269
|
+
spinner.start();
|
|
251
270
|
|
|
252
271
|
const decoder = new TextDecoder();
|
|
253
272
|
let sseBuffer = '';
|
|
@@ -266,7 +285,6 @@ export async function streamChat(cfg, messages, context = {}, opts = {}) {
|
|
|
266
285
|
printStatus(ev.status_delta);
|
|
267
286
|
} else if (ev.reason_delta !== undefined) {
|
|
268
287
|
ensureBoxOpen();
|
|
269
|
-
if (!reasoningOpen) openReasoning();
|
|
270
288
|
writeReasonDelta(ev.reason_delta);
|
|
271
289
|
} else if (ev.delta !== undefined) {
|
|
272
290
|
if (reasoningOpen) closeReasoning();
|
|
@@ -291,7 +309,6 @@ export async function streamChat(cfg, messages, context = {}, opts = {}) {
|
|
|
291
309
|
|
|
292
310
|
if (meta.model && cfg) cfg._model = shortModel(meta.model);
|
|
293
311
|
|
|
294
|
-
// Blank separator before content
|
|
295
312
|
process.stdout.write(boxLine('', P.border) + '\n');
|
|
296
313
|
|
|
297
314
|
if (replyType === 'message') {
|
|
@@ -302,36 +319,59 @@ export async function streamChat(cfg, messages, context = {}, opts = {}) {
|
|
|
302
319
|
}
|
|
303
320
|
|
|
304
321
|
} else if (replyType === 'plan') {
|
|
305
|
-
|
|
306
|
-
|
|
322
|
+
// Plan always rendered inline — no toggle key
|
|
323
|
+
process.stdout.write(boxLine(P.accentB('◆ ') + P.bold('Plan'), P.border) + '\n');
|
|
324
|
+
if (reply.summary) {
|
|
325
|
+
process.stdout.write(boxLine(P.dim(reply.summary), P.border) + '\n');
|
|
326
|
+
}
|
|
307
327
|
process.stdout.write(boxLine('', P.border) + '\n');
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
328
|
+
|
|
329
|
+
const steps = reply.steps || [];
|
|
330
|
+
steps.forEach((s, i) => {
|
|
331
|
+
const isLast = i === steps.length - 1;
|
|
332
|
+
const prefix = isLast ? '└─ ' : '├─ ';
|
|
333
|
+
const risk = s.risk === 'high' ? ' ' + P.err('⚠ high risk') :
|
|
334
|
+
s.risk === 'medium' ? ' ' + P.warn('⚡') : '';
|
|
335
|
+
process.stdout.write(
|
|
336
|
+
boxLine(
|
|
337
|
+
P.border(' ' + prefix) + P.dim(String(i + 1) + '.') + ' ' + P.warn(s.cmd || '') + risk,
|
|
338
|
+
P.border,
|
|
339
|
+
) + '\n',
|
|
340
|
+
);
|
|
341
|
+
if (s.purpose) {
|
|
342
|
+
process.stdout.write(boxLine(' ' + P.muted(s.purpose), P.border) + '\n');
|
|
343
|
+
}
|
|
313
344
|
});
|
|
345
|
+
|
|
314
346
|
process.stdout.write(boxLine('', P.border) + '\n');
|
|
315
|
-
process.stdout.write(
|
|
347
|
+
process.stdout.write(
|
|
348
|
+
boxLine(P.dim('Type ') + P.white('apply fix') + P.dim(' to confirm.'), P.border) + '\n',
|
|
349
|
+
);
|
|
316
350
|
|
|
317
351
|
} else if (replyType === 'connect') {
|
|
318
352
|
const dev = reply.device ?? {};
|
|
319
353
|
process.stdout.write(
|
|
320
|
-
boxLine(
|
|
321
|
-
' ' + P.
|
|
354
|
+
boxLine(
|
|
355
|
+
P.ok('🔌 ') + P.bold(`${dev.kind ?? '?'}#${dev.id ?? '?'}`) +
|
|
356
|
+
' ' + P.white(dev.name ?? '') + ' ' + P.dim(`(${dev.host ?? ''})`),
|
|
357
|
+
P.border,
|
|
358
|
+
) + '\n',
|
|
322
359
|
);
|
|
323
360
|
if (reply.note) process.stdout.write(boxLine(P.dim(reply.note), P.border) + '\n');
|
|
324
361
|
}
|
|
325
362
|
|
|
326
|
-
//
|
|
363
|
+
// Footer inside box
|
|
327
364
|
process.stdout.write(boxLine('', P.border) + '\n');
|
|
328
365
|
const foot = [meta.model ? shortModel(meta.model) : null, meta.user, meta.role].filter(Boolean);
|
|
329
366
|
if (foot.length) {
|
|
330
367
|
process.stdout.write(boxLine(P.muted(foot.join(' · ')), P.border) + '\n');
|
|
331
368
|
}
|
|
332
369
|
|
|
333
|
-
// Close the box
|
|
334
|
-
process.stdout.write(boxBotOpen(P.border) + '\n
|
|
370
|
+
// Close the open box
|
|
371
|
+
process.stdout.write(boxBotOpen(P.border) + '\n');
|
|
372
|
+
|
|
373
|
+
// Bottom palette after every AI reply
|
|
374
|
+
process.stdout.write(bottomPalette() + '\n\n');
|
|
335
375
|
|
|
336
376
|
return reply.text || answerBuf || '';
|
|
337
377
|
}
|
package/src/layout.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { execSync } from 'node:child_process';
|
|
2
|
+
import chalk from 'chalk';
|
|
2
3
|
|
|
3
4
|
// ── Terminal width ─────────────────────────────────────────────────────────────
|
|
4
5
|
let cachedCols = 0;
|
|
@@ -152,3 +153,57 @@ export function boxRow(content, borderFn) {
|
|
|
152
153
|
export function boxLine(content, borderFn) {
|
|
153
154
|
return borderFn('│') + ' ' + content;
|
|
154
155
|
}
|
|
156
|
+
|
|
157
|
+
// ── Top context bar ───────────────────────────────────────────────────────────
|
|
158
|
+
/**
|
|
159
|
+
* Renders a single full-width bar:
|
|
160
|
+
* │ ● device · host ◆ AUTOPILOT ON operator: admin │
|
|
161
|
+
*
|
|
162
|
+
* @param {{ device?: string, aiMode?: 'AUTOPILOT ON'|'MANUAL', operator?: string }} ctx
|
|
163
|
+
*/
|
|
164
|
+
export function topBar({ device = '', aiMode = 'MANUAL', operator = '' } = {}) {
|
|
165
|
+
const w = cols();
|
|
166
|
+
|
|
167
|
+
const lRaw = device ? `● ${device}` : '';
|
|
168
|
+
const cRaw = aiMode === 'AUTOPILOT ON' ? '◆ AUTOPILOT ON' : '◇ AI MANUAL';
|
|
169
|
+
const rRaw = operator ? `operator: ${operator}` : '';
|
|
170
|
+
|
|
171
|
+
const lC = lRaw ? chalk.hex('#8AB4F8').bold(lRaw) : '';
|
|
172
|
+
const cC = aiMode === 'AUTOPILOT ON'
|
|
173
|
+
? chalk.green.bold(cRaw)
|
|
174
|
+
: chalk.yellow(cRaw);
|
|
175
|
+
const rC = rRaw ? chalk.hex('#9AA0A6')(rRaw) : '';
|
|
176
|
+
|
|
177
|
+
// avail = w - 2 borders - 2 side padding spaces
|
|
178
|
+
const avail = w - 4;
|
|
179
|
+
const lv = [...lRaw].length;
|
|
180
|
+
const cv = [...cRaw].length;
|
|
181
|
+
const rv = [...rRaw].length;
|
|
182
|
+
|
|
183
|
+
const remaining = avail - lv - cv - rv;
|
|
184
|
+
const g1 = remaining > 0 ? Math.floor(remaining / 2) : 0;
|
|
185
|
+
const g2 = remaining > 0 ? remaining - g1 : 0;
|
|
186
|
+
|
|
187
|
+
return chalk.hex('#303134')('│') + ' ' +
|
|
188
|
+
lC + ' '.repeat(g1) + cC + ' '.repeat(g2) + rC + ' ' +
|
|
189
|
+
chalk.hex('#303134')('│');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ── Bottom command palette ────────────────────────────────────────────────────
|
|
193
|
+
/**
|
|
194
|
+
* Returns a single-line bottom palette string (no trailing newline).
|
|
195
|
+
* Caller is responsible for writing '\n'.
|
|
196
|
+
*/
|
|
197
|
+
export function bottomPalette() {
|
|
198
|
+
const accent = chalk.hex('#8AB4F8').bold;
|
|
199
|
+
const desc = chalk.hex('#80868B');
|
|
200
|
+
const sep = chalk.hex('#303134')(' · ');
|
|
201
|
+
const items = [
|
|
202
|
+
accent('F1') + desc(' Docs'),
|
|
203
|
+
accent('F2') + desc(' Logs'),
|
|
204
|
+
accent('F3') + desc(' Rollback'),
|
|
205
|
+
accent('F4') + desc(' Bulk Sweep'),
|
|
206
|
+
accent('Ctrl+C') + desc(' Exit'),
|
|
207
|
+
];
|
|
208
|
+
return ' ' + items.join(sep);
|
|
209
|
+
}
|