ispbills-icli 8.7.6 → 8.7.8
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 +1 -1
- package/src/banner.js +3 -3
- package/src/commands.js +3 -3
- package/src/tui/app.js +20 -10
- package/src/tui/components.js +128 -131
- package/src/tui/composer.js +1 -1
- package/src/tui/run.js +3 -3
- package/src/tui/theme.js +2 -0
package/package.json
CHANGED
package/src/banner.js
CHANGED
|
@@ -13,7 +13,7 @@ const LOGO = [
|
|
|
13
13
|
const LOGO_WIDTH = Math.max(...LOGO.map((l) => l.length));
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* Full
|
|
16
|
+
* Full iPolot banner. On narrow terminals falls back to a compact header.
|
|
17
17
|
* A full-width rule frames the header so it uses the whole window.
|
|
18
18
|
*
|
|
19
19
|
* @param {object} cfg Loaded config ({ url, user, _model }).
|
|
@@ -26,9 +26,9 @@ export function printBanner(cfg = {}, opts = {}) {
|
|
|
26
26
|
console.log();
|
|
27
27
|
if (width >= LOGO_WIDTH) {
|
|
28
28
|
for (const line of LOGO) console.log(ACCENT + BOLD + line + RESET);
|
|
29
|
-
console.log(` ${DIM}
|
|
29
|
+
console.log(` ${DIM}iPolot — network engineer in your terminal${RESET}`);
|
|
30
30
|
} else {
|
|
31
|
-
console.log(` ${BOLD}
|
|
31
|
+
console.log(` ${BOLD}iPolot${RESET} ${DIM}— IspBills AI terminal${RESET}`);
|
|
32
32
|
}
|
|
33
33
|
console.log();
|
|
34
34
|
|
package/src/commands.js
CHANGED
|
@@ -88,7 +88,7 @@ export const SLASH_COMMANDS = [
|
|
|
88
88
|
{ cmd: '/list-dirs', hint: '', desc: 'Show all allowed directories', group: 'Permissions', local: true },
|
|
89
89
|
{ cmd: '/sandbox', hint: '[enable|disable]', desc: 'Enable/disable local sandboxing', group: 'Permissions', local: true },
|
|
90
90
|
{ cmd: '/experimental', hint: '[on|off|show]', desc: 'Toggle experimental features', group: 'Permissions', local: true },
|
|
91
|
-
{ cmd: '/init', hint: '', desc: 'Initialize
|
|
91
|
+
{ cmd: '/init', hint: '', desc: 'Initialize .github/copilot-instructions.md for this repo', group: 'Permissions', local: true },
|
|
92
92
|
|
|
93
93
|
// ── Agents & models ──
|
|
94
94
|
{ cmd: '/agent', hint: '', desc: 'Browse and select agents', group: 'Agents', local: true },
|
|
@@ -121,9 +121,9 @@ export const SLASH_COMMANDS = [
|
|
|
121
121
|
{ cmd: '/bug', hint: '', desc: 'Report a bug (alias for /feedback)', group: 'Help', local: true },
|
|
122
122
|
{ cmd: '/update', hint: '', desc: 'Update iCli to the latest version', group: 'Help', local: true },
|
|
123
123
|
{ cmd: '/downgrade', hint: 'VERSION', desc: 'Roll back to a specific CLI version', group: 'Help', local: true },
|
|
124
|
-
{ cmd: '/app', hint: '', desc: '
|
|
124
|
+
{ cmd: '/app', hint: '', desc: 'Show instructions to open the iPolot web app', group: 'Help', local: true },
|
|
125
125
|
{ cmd: '/user', hint: '', desc: 'Manage GitHub user list', group: 'Help', local: true },
|
|
126
|
-
{ cmd: '/login', hint: '', desc: 'Log in to
|
|
126
|
+
{ cmd: '/login', hint: '', desc: 'Log in to iPolot', group: 'Help', local: true },
|
|
127
127
|
{ cmd: '/whoami', hint: '', desc: 'Show current user info', group: 'Help', local: true },
|
|
128
128
|
{ cmd: '/clikit', hint: '[component]', desc: 'Preview CLI UI components', group: 'Help', local: true },
|
|
129
129
|
{ cmd: '/extensions', hint: '[manage|mode]', desc: 'Manage CLI extensions', group: 'Help', local: true },
|
package/src/tui/app.js
CHANGED
|
@@ -69,6 +69,8 @@ function approxTokens(messages) {
|
|
|
69
69
|
|
|
70
70
|
const TOKEN_LIMIT = 100000;
|
|
71
71
|
const SESSION_SORTS = ['relevance', 'created', 'name', 'last used'];
|
|
72
|
+
const TOOL_APPROVAL_ONCE = 'yes, proceed with this tool call';
|
|
73
|
+
const TOOL_APPROVAL_SESSION = '__session_allow__';
|
|
72
74
|
|
|
73
75
|
function sessionAutoName(messages = []) {
|
|
74
76
|
const firstUser = messages.find((m) => m?.role === 'user' && String(m.content || '').trim());
|
|
@@ -899,9 +901,9 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, sh
|
|
|
899
901
|
const toolArgs = reply.toolCall?.args || reply.toolCall?.command || '';
|
|
900
902
|
const toolDesc = toolArgs ? `${toolName}: ${toolArgs}` : (reply.note || toolName);
|
|
901
903
|
choiceActionRef.current = (value) => {
|
|
902
|
-
if (value ===
|
|
904
|
+
if (value === TOOL_APPROVAL_SESSION) {
|
|
903
905
|
cfg.allowedTools = [...(cfg.allowedTools || []), toolName];
|
|
904
|
-
runTurn(
|
|
906
|
+
runTurn(TOOL_APPROVAL_ONCE);
|
|
905
907
|
return;
|
|
906
908
|
}
|
|
907
909
|
if (value != null) runTurn(String(value));
|
|
@@ -912,9 +914,9 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, sh
|
|
|
912
914
|
sel: 0, text: '', focusText: false, allowText: true,
|
|
913
915
|
isToolApproval: true,
|
|
914
916
|
options: [
|
|
915
|
-
{ label: '1. Yes', value:
|
|
916
|
-
{ label:
|
|
917
|
-
{ label: '3. No, and tell
|
|
917
|
+
{ label: '1. Yes', value: TOOL_APPROVAL_ONCE },
|
|
918
|
+
{ label: `2. Yes, and approve ${toolName} for the rest of the session`, value: TOOL_APPROVAL_SESSION },
|
|
919
|
+
{ label: '3. No, and tell iPolot what to do differently (Esc)', value: null },
|
|
918
920
|
],
|
|
919
921
|
});
|
|
920
922
|
} else if (type === 'prompt' || type === 'choice') {
|
|
@@ -1449,7 +1451,7 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, sh
|
|
|
1449
1451
|
if (q === '/init') {
|
|
1450
1452
|
const fp = resolve(process.cwd(), '.github/copilot-instructions.md');
|
|
1451
1453
|
if (existsSync(fp)) {
|
|
1452
|
-
pushNotice(
|
|
1454
|
+
pushNotice(`.github/copilot-instructions.md already exists at ${shortenPath(fp)}. Edit it directly.`);
|
|
1453
1455
|
} else {
|
|
1454
1456
|
runTurn('Create a .github/copilot-instructions.md file for this repository with sensible defaults for an ISP NOC tool.');
|
|
1455
1457
|
}
|
|
@@ -1683,7 +1685,7 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, sh
|
|
|
1683
1685
|
return;
|
|
1684
1686
|
}
|
|
1685
1687
|
if (q === '/app') {
|
|
1686
|
-
pushNotice('
|
|
1688
|
+
pushNotice('iPolot web app — open your IspBills instance in the browser with /o or by typing the URL.', C.muted);
|
|
1687
1689
|
return;
|
|
1688
1690
|
}
|
|
1689
1691
|
if (q === '/user' || q.startsWith('/user ')) {
|
|
@@ -1774,7 +1776,7 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, sh
|
|
|
1774
1776
|
if (value == null) {
|
|
1775
1777
|
if (currentChoice?.isToolApproval || /⚙ Tool request|tool request|approval|shell:/i.test(`${currentChoice?.title || ''} ${currentChoice?.note || ''}`)) {
|
|
1776
1778
|
setChoice({
|
|
1777
|
-
title: 'Tell
|
|
1779
|
+
title: 'Tell iPolot what to do differently:',
|
|
1778
1780
|
note: '',
|
|
1779
1781
|
sel: 0,
|
|
1780
1782
|
text: '',
|
|
@@ -1836,6 +1838,13 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, sh
|
|
|
1836
1838
|
if (!choice) return;
|
|
1837
1839
|
if (sessionPicker || diffState) return;
|
|
1838
1840
|
if (key.escape || (key.ctrl && input === 'c')) { setChoice(null); return; }
|
|
1841
|
+
if (choice.isToolApproval && !choice.focusText) {
|
|
1842
|
+
const yesValue = choice.options.find((o) => o?.value === TOOL_APPROVAL_ONCE)?.value;
|
|
1843
|
+
const alwaysValue = choice.options.find((o) => o?.value === TOOL_APPROVAL_SESSION)?.value;
|
|
1844
|
+
if (input === 'y') { pickChoice(yesValue); return; }
|
|
1845
|
+
if (input === 'a') { pickChoice(alwaysValue); return; }
|
|
1846
|
+
if (input === 'n') { pickChoice(null); return; }
|
|
1847
|
+
}
|
|
1839
1848
|
if (key.tab && choice.allowText) { setChoice((c) => ({ ...c, focusText: !c.focusText })); return; }
|
|
1840
1849
|
if (key.upArrow) { setChoice((c) => ({ ...c, focusText: false, sel: Math.max(0, c.sel - 1) })); return; }
|
|
1841
1850
|
if (key.downArrow) {
|
|
@@ -2156,7 +2165,7 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, sh
|
|
|
2156
2165
|
|
|
2157
2166
|
const liveArea = live
|
|
2158
2167
|
? html`<${Box} flexDirection="column">
|
|
2159
|
-
<${StatusLines} lines=${live.status} busy=${busy} />
|
|
2168
|
+
<${StatusLines} lines=${live.status} busy=${busy} cols=${cols} />
|
|
2160
2169
|
${live.reasoning ? html`<${Reasoning} text=${live.reasoning} show=${showReasoning} />` : null}
|
|
2161
2170
|
${live.text ? html`<${AssistantMessage} text=${live.text} cols=${cols} ts=${startedAt || Date.now()} streaming=${true} />` : null}
|
|
2162
2171
|
${live.reply?.steps ? html`<${Plan} reply=${live.reply} live=${true} />` : null}
|
|
@@ -2185,7 +2194,7 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, sh
|
|
|
2185
2194
|
/>
|
|
2186
2195
|
${diffState.commentInput
|
|
2187
2196
|
? html`<${Choice}
|
|
2188
|
-
title=${`Tell
|
|
2197
|
+
title=${`Tell iPolot what stands out about ${diffState.commentInput.file}:${diffState.commentInput.lineNumber || '?'}`}
|
|
2189
2198
|
note=${diffState.commentInput.content}
|
|
2190
2199
|
sel=${0}
|
|
2191
2200
|
allowText=${true}
|
|
@@ -2258,6 +2267,7 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, sh
|
|
|
2258
2267
|
clearToken=${composerClearToken}
|
|
2259
2268
|
/>`}
|
|
2260
2269
|
|
|
2270
|
+
<${Text} color=${C.separator}>${'─'.repeat(Math.max(1, cols))}<//>
|
|
2261
2271
|
<${Box} paddingX=${1} width=${cols}>
|
|
2262
2272
|
<${Box} flexGrow=${1}>
|
|
2263
2273
|
<${Text} color=${footerStatusColor} bold=${busy} wrap="truncate-end">${footerStatusLeft}<//>
|
package/src/tui/components.js
CHANGED
|
@@ -124,7 +124,7 @@ export function TabBar({ active = 'session', cols = 80 }) {
|
|
|
124
124
|
const label = ` ${i + 1} ${tab.label} `.padEnd(TAB_W);
|
|
125
125
|
return html`
|
|
126
126
|
<${Box} key=${tab.id} width=${TAB_W}>
|
|
127
|
-
<${Text} bold=${on} color=${on ?
|
|
127
|
+
<${Text} bold=${on} color=${on ? C.brand : C.muted} dimColor=${!on} inverse=${on}>
|
|
128
128
|
${label}
|
|
129
129
|
<//>
|
|
130
130
|
<//>`;
|
|
@@ -168,7 +168,7 @@ export function Banner({ cfg = {}, model, width = 80, compact = false, streamerM
|
|
|
168
168
|
if (compact) {
|
|
169
169
|
return html`
|
|
170
170
|
<${Box} marginBottom=${1}>
|
|
171
|
-
<${Text} color=${C.accent} bold>${glyphs.ring}
|
|
171
|
+
<${Text} color=${C.accent} bold>${glyphs.ring} iPolot <//>
|
|
172
172
|
<${Text} color=${C.muted}>${midDot()}<//>
|
|
173
173
|
<${Text} color=${C.muted}>${m}<//>
|
|
174
174
|
${cfg.user?.name ? html`<${Text} color=${C.muted}>${midDot()}<//>` : null}
|
|
@@ -183,7 +183,7 @@ export function Banner({ cfg = {}, model, width = 80, compact = false, streamerM
|
|
|
183
183
|
<//>
|
|
184
184
|
|
|
185
185
|
<${Box} marginTop=${1} justifyContent="center">
|
|
186
|
-
<${Text} color=${C.accent} bold>${glyphs.ring}
|
|
186
|
+
<${Text} color=${C.accent} bold>${glyphs.ring} iPolot<//>
|
|
187
187
|
<${Text} color=${C.muted}> ${dash()} IspBills network engineer in your terminal<//>
|
|
188
188
|
<//>
|
|
189
189
|
|
|
@@ -229,8 +229,9 @@ export function UserMessage({ text, cols = 80, ts }) {
|
|
|
229
229
|
const stamp = ts ? new Date(ts).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }) : '';
|
|
230
230
|
return html`
|
|
231
231
|
<${Box} flexDirection="column" marginBottom=${1}>
|
|
232
|
+
<${Sep} cols=${cols} />
|
|
232
233
|
<${Box} flexDirection="row" justifyContent="space-between" width=${cols} paddingX=${1}>
|
|
233
|
-
<${Text} bold color
|
|
234
|
+
<${Text} bold color=${C.brand}>You<//>
|
|
234
235
|
<${Text} color=${C.muted}>${stamp}<//>
|
|
235
236
|
<//>
|
|
236
237
|
<${Box} paddingX=${1} marginLeft=${2} flexWrap="wrap">
|
|
@@ -264,12 +265,14 @@ function StreamingText({ text = '' }) {
|
|
|
264
265
|
|
|
265
266
|
export function AssistantMessage({ text, cols = 80, ts, label, streaming = false }) {
|
|
266
267
|
const stamp = ts ? new Date(ts).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }) : '';
|
|
267
|
-
const speakerLabel = streaming ? 'iCopilot ●' : 'iCopilot';
|
|
268
268
|
const bodyWidth = Math.min(cols, 100);
|
|
269
269
|
return html`
|
|
270
270
|
<${Box} flexDirection="column" marginBottom=${1}>
|
|
271
|
+
<${Sep} cols=${cols} />
|
|
271
272
|
<${Box} flexDirection="row" justifyContent="space-between" width=${cols} paddingX=${1}>
|
|
272
|
-
<${
|
|
273
|
+
<${Box}>
|
|
274
|
+
<${Text} color=${C.accent} bold>${glyphs.copilotDot} <//><${Text} bold>iPolot${streaming ? ' ●' : ''}<//>
|
|
275
|
+
<//>
|
|
273
276
|
<${Text} color=${C.muted}>${stamp}<//>
|
|
274
277
|
<//>
|
|
275
278
|
<${Box} paddingX=${1} marginLeft=${2} flexDirection="column" width=${bodyWidth}>
|
|
@@ -279,18 +282,22 @@ export function AssistantMessage({ text, cols = 80, ts, label, streaming = false
|
|
|
279
282
|
}
|
|
280
283
|
|
|
281
284
|
export function ShellOutput({ command, output, cols = 80, ts, code = 0 }) {
|
|
285
|
+
const ok = code === 0;
|
|
286
|
+
const tone = ok ? C.success : C.error;
|
|
282
287
|
return html`
|
|
283
288
|
<${Box} flexDirection="column" marginTop=${1}>
|
|
289
|
+
<${Sep} cols=${cols} />
|
|
284
290
|
<${SpeakerLine}
|
|
285
291
|
label=${"$ Shell"}
|
|
286
|
-
color=${
|
|
292
|
+
color=${tone}
|
|
287
293
|
cols=${cols}
|
|
288
294
|
ts=${ts}
|
|
289
|
-
rightLabel=${
|
|
295
|
+
rightLabel=${ok ? '' : `exit ${code}`}
|
|
290
296
|
/>
|
|
291
297
|
<${Box} paddingX=${1} flexDirection="column">
|
|
292
298
|
<${Text} color=${C.warning}>$ ${command}<//>
|
|
293
|
-
<${Text}>${
|
|
299
|
+
<${Text} color=${tone}>${ok ? glyphs.check : `${glyphs.cross} exit ${code}` }<//>
|
|
300
|
+
<${Text} color=${ok ? C.white : tone}>${output || '(no output)'}<//>
|
|
294
301
|
<//>
|
|
295
302
|
<//>`;
|
|
296
303
|
}
|
|
@@ -299,8 +306,9 @@ const SUBAGENT = /^\s*[✓✗x]?\s*\[(\d+)\/(\d+)\]\s*(.+?)\s*[::]\s*(.+?)\s*$
|
|
|
299
306
|
const LEAD_EMOJI = /^\s*([\u{1F000}-\u{1FAFF}\u{2600}-\u{27BF}\u{2B00}-\u{2BFF}\u{2190}-\u{21FF}\u{FE0F}\u{2049}\u{203C}]+)\s*/u;
|
|
300
307
|
|
|
301
308
|
/**
|
|
302
|
-
* Classify a status line
|
|
303
|
-
*
|
|
309
|
+
* Classify a status line into op type, a dim type-tag, and display parts.
|
|
310
|
+
* Tag is metadata only (file ext, device protocol, service) — never colored.
|
|
311
|
+
* Op type drives color: read=green, edit=red, step=blue.
|
|
304
312
|
*/
|
|
305
313
|
function parseToolLine(lineStr) {
|
|
306
314
|
const s = String(lineStr).trim();
|
|
@@ -313,177 +321,164 @@ function parseToolLine(lineStr) {
|
|
|
313
321
|
const ci = body.indexOf(':');
|
|
314
322
|
const action = (ci > -1 ? body.slice(0, ci) : body).trim();
|
|
315
323
|
const subject = (ci > -1 ? body.slice(ci + 1) : '').trim();
|
|
316
|
-
const
|
|
324
|
+
const aL = action.toLowerCase();
|
|
325
|
+
const sL = subject.toLowerCase();
|
|
317
326
|
|
|
318
|
-
// ── Op type
|
|
327
|
+
// ── Op type + descriptive verb (from action keywords) ────────────────────
|
|
319
328
|
let opType = 'other';
|
|
320
329
|
let verb = '';
|
|
321
|
-
if
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// ── Type tag from subject
|
|
330
|
+
if (/running on\b/i.test(aL)) { opType = 'edit'; verb = 'Exec'; }
|
|
331
|
+
else if (/\b(save|saving|generat|rollback|write|creat|update|apply)\b/i.test(aL)) { opType = 'edit'; verb = aL.includes('save') || aL.includes('saving') ? 'Save' : aL.includes('generat') ? 'Generate' : aL.includes('rollback') ? 'Rollback' : 'Edit'; }
|
|
332
|
+
else if (/\b(search|searching)\b/i.test(aL)) { opType = 'read'; verb = 'Search'; }
|
|
333
|
+
else if (/\b(lookup|look.?up|looking)\b/i.test(aL)) { opType = 'read'; verb = 'Lookup'; }
|
|
334
|
+
else if (/\b(check|checking|analys|correlat|simulat|predict|retrieve)\b/i.test(aL)) { opType = 'read'; verb = 'Check'; }
|
|
335
|
+
else if (/\b(fetch|reading|read)\b/i.test(aL)) { opType = 'read'; verb = 'Read'; }
|
|
336
|
+
else if (/\b(assign|spawn|sub.?agent|delegat)\b/i.test(aL)) { opType = 'step'; verb = 'Spawn'; }
|
|
337
|
+
|
|
338
|
+
// ── Type tag — ONLY from explicit context; never inherit device from subject ──
|
|
339
|
+
// Tag identifies the protocol/format involved, not the device mentioned.
|
|
330
340
|
let typeTag = '';
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
else if (/\.
|
|
335
|
-
else if (/\.
|
|
336
|
-
else if (/\.
|
|
337
|
-
else if (/\.(
|
|
338
|
-
else if (/\.(
|
|
339
|
-
else if (
|
|
340
|
-
|
|
341
|
-
else if (/
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
else if (/
|
|
348
|
-
else if (/
|
|
349
|
-
else if (/
|
|
350
|
-
|
|
351
|
-
|
|
341
|
+
// File extensions in subject or action (most specific)
|
|
342
|
+
const allText = aL + ' ' + sL;
|
|
343
|
+
if (/\.(js|jsx|mjs|cjs)\b/.test(allText)) typeTag = 'JS';
|
|
344
|
+
else if (/\.(ts|tsx)\b/.test(allText)) typeTag = 'TS';
|
|
345
|
+
else if (/\.php\b/.test(allText)) typeTag = 'PHP';
|
|
346
|
+
else if (/\.py\b/.test(allText)) typeTag = 'PY';
|
|
347
|
+
else if (/\.(rb)\b/.test(allText)) typeTag = 'RB';
|
|
348
|
+
else if (/\.(go)\b/.test(allText)) typeTag = 'GO';
|
|
349
|
+
else if (/\.(sh|bash)\b/.test(allText)) typeTag = 'SH';
|
|
350
|
+
// Device protocol — ONLY when action is a device exec ("running on")
|
|
351
|
+
else if (/running on/i.test(aL)) {
|
|
352
|
+
if (/nas#|mikrotik|\/export|\/ip |\/ppp|\/interface|\/system|\/queue|\/routing/.test(allText)) typeTag = 'ROS';
|
|
353
|
+
else if (/olt#|gpon|epon|optical/.test(allText)) typeTag = 'OLT';
|
|
354
|
+
else typeTag = 'DEV';
|
|
355
|
+
}
|
|
356
|
+
// Action-based tags
|
|
357
|
+
else if (/github/i.test(aL)) typeTag = 'GIT';
|
|
358
|
+
else if (/web|url|page|http/i.test(aL)) typeTag = 'WEB';
|
|
359
|
+
else if (/customer|subscriber/i.test(aL)) typeTag = 'CRM';
|
|
360
|
+
else if (/firmware|version/i.test(aL)) typeTag = 'FW';
|
|
361
|
+
else if (/sub.?agent|specialist|assign/i.test(aL)) typeTag = 'AI';
|
|
362
|
+
else if (/shell|command/i.test(aL)) typeTag = 'SH';
|
|
363
|
+
else if (/checkpoint|backup/i.test(aL)) typeTag = 'CFG';
|
|
364
|
+
else if (/config/i.test(aL)) typeTag = 'CFG';
|
|
365
|
+
else if (/docs|documentation/i.test(aL)) typeTag = 'DOC';
|
|
366
|
+
else if (/signal|optical|dbm/i.test(aL)) typeTag = 'OPT';
|
|
367
|
+
else if (/device|router|nas|olt/i.test(aL)) typeTag = 'DEV';
|
|
368
|
+
|
|
369
|
+
// Display text: verb + detail
|
|
370
|
+
// When action has no colon split, subject is empty — show trimmed action as detail
|
|
371
|
+
const display = subject || action;
|
|
372
|
+
|
|
373
|
+
return { opType, typeTag, verb, action, subject, display, done, ok, full: s };
|
|
352
374
|
}
|
|
353
375
|
|
|
354
376
|
export function StatusLines({ lines = [], busy = false, navigable = false }) {
|
|
355
377
|
const [sel, setSel] = useState(-1);
|
|
356
378
|
const [expanded, setExpanded] = useState(new Set());
|
|
357
379
|
|
|
358
|
-
|
|
359
|
-
useEffect(() => {
|
|
360
|
-
setSel(-1);
|
|
361
|
-
setExpanded(new Set());
|
|
362
|
-
}, [lines.length]);
|
|
380
|
+
useEffect(() => { setSel(-1); setExpanded(new Set()); }, [lines.length]);
|
|
363
381
|
|
|
364
382
|
useInput((input, key) => {
|
|
365
383
|
if (!navigable) return;
|
|
366
|
-
if (key.upArrow
|
|
367
|
-
|
|
368
|
-
return;
|
|
369
|
-
}
|
|
370
|
-
if (key.downArrow || input === 'j') {
|
|
371
|
-
setSel((i) => Math.min(lines.length - 1, (i < 0 ? -1 : i) + 1));
|
|
372
|
-
return;
|
|
373
|
-
}
|
|
384
|
+
if (key.upArrow || input === 'k') { setSel((i) => Math.max(0, (i < 0 ? lines.length - 1 : i) - 1)); return; }
|
|
385
|
+
if (key.downArrow || input === 'j') { setSel((i) => Math.min(lines.length - 1, (i < 0 ? -1 : i) + 1)); return; }
|
|
374
386
|
if ((key.return || input === ' ') && sel >= 0) {
|
|
375
|
-
setExpanded((prev) => {
|
|
376
|
-
const next = new Set(prev);
|
|
377
|
-
next.has(sel) ? next.delete(sel) : next.add(sel);
|
|
378
|
-
return next;
|
|
379
|
-
});
|
|
387
|
+
setExpanded((prev) => { const n = new Set(prev); n.has(sel) ? n.delete(sel) : n.add(sel); return n; });
|
|
380
388
|
return;
|
|
381
389
|
}
|
|
382
|
-
// Esc clears selection
|
|
383
390
|
if (key.escape) { setSel(-1); return; }
|
|
384
391
|
});
|
|
385
392
|
|
|
386
393
|
if (!lines.length) return null;
|
|
387
394
|
const lastIdx = lines.length - 1;
|
|
388
395
|
|
|
396
|
+
|
|
389
397
|
return html`
|
|
390
398
|
<${Box} flexDirection="column" marginTop=${1}>
|
|
391
399
|
${lines.map((line, i) => {
|
|
392
|
-
const lineStr
|
|
400
|
+
const lineStr = String(line);
|
|
393
401
|
const isRunning = busy && i === lastIdx;
|
|
394
|
-
const isSel
|
|
395
|
-
const isExp
|
|
402
|
+
const isSel = navigable && sel === i;
|
|
403
|
+
const isExp = expanded.has(i);
|
|
396
404
|
|
|
397
|
-
// ── Sub-agent tree line
|
|
405
|
+
// ── Sub-agent tree line ─────────────────────────────────────────
|
|
398
406
|
const sm = lineStr.match(SUBAGENT);
|
|
399
407
|
if (sm) {
|
|
400
408
|
const [, idx, total, host, result] = sm;
|
|
401
409
|
const isCompleted = /^\s*[✓✗x]/.test(lineStr);
|
|
402
|
-
const running
|
|
410
|
+
const running = busy && !isCompleted;
|
|
403
411
|
const good = !/fail|error|down|offline|unreachable|timeout|deny/i.test(result);
|
|
404
412
|
const branch = Number(idx) === Number(total) ? glyphs.branchEnd : glyphs.branchMid;
|
|
405
|
-
const hostTrunc = host.length > 22 ? host.slice(0, 20) + '…' : host;
|
|
406
|
-
const resultTrunc = result.length > 30 ? result.slice(0, 28) + '…' : result;
|
|
407
413
|
return html`
|
|
408
|
-
<${Box} key=${
|
|
409
|
-
<${
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
: html`<${Text} color=${good ? C.success : C.error} bold>${good ? glyphs.check : glyphs.cross} ${resultTrunc}<//>`}
|
|
418
|
-
<//>
|
|
414
|
+
<${Box} key=${`sa${i}`}>
|
|
415
|
+
<${Text} color=${C.muted}> <//>
|
|
416
|
+
<${Text} color=${C.accent}>${branch} <//>
|
|
417
|
+
<${Text} color=${C.muted}>${idx}/${total} <//>
|
|
418
|
+
<${Text} color=${C.white} bold>${host.slice(0, 20)}<//>
|
|
419
|
+
<${Text} color=${C.muted}> ${dash()} <//>
|
|
420
|
+
${running
|
|
421
|
+
? html`<${Text} color=${C.warning}><${Spinner} type=${spinnerType()} /><//> <${Text} color=${C.muted}>running<//>`
|
|
422
|
+
: html`<${Text} color=${good ? C.success : C.error} bold>${good ? glyphs.check : glyphs.cross} ${result.slice(0, 32)}<///>`}
|
|
419
423
|
<//>`;
|
|
420
424
|
}
|
|
421
425
|
|
|
422
|
-
// ── Regular tool status line
|
|
423
|
-
const { opType, typeTag, verb,
|
|
426
|
+
// ── Regular tool status line ────────────────────────────────────
|
|
427
|
+
const { opType, typeTag, verb, display, done, ok, full } = parseToolLine(lineStr);
|
|
424
428
|
|
|
425
429
|
const opColor = opType === 'read' ? C.success
|
|
426
|
-
: opType === 'edit'
|
|
427
|
-
: opType === 'step'
|
|
430
|
+
: opType === 'edit' ? C.error
|
|
431
|
+
: opType === 'step' ? C.brand
|
|
428
432
|
: C.muted;
|
|
429
433
|
|
|
430
|
-
//
|
|
431
|
-
const
|
|
434
|
+
// Type tag: always dim/muted — it's just metadata, never colored
|
|
435
|
+
const tag = typeTag ? typeTag.slice(0, 3).padEnd(3) : ' ';
|
|
432
436
|
|
|
433
|
-
//
|
|
434
|
-
const
|
|
437
|
+
// Verb label: padded to 8 chars for alignment
|
|
438
|
+
const verbLabel = (verb || 'Run').slice(0, 8).padEnd(8);
|
|
435
439
|
|
|
436
|
-
//
|
|
437
|
-
const
|
|
438
|
-
const actionTrunc = action.length > 28 ? action.slice(0, 26) + '…' : action;
|
|
440
|
+
// Detail text: as much as fits
|
|
441
|
+
const detail = display.length > 52 ? display.slice(0, 50) + '…' : display;
|
|
439
442
|
|
|
440
|
-
// Status
|
|
443
|
+
// Status icon
|
|
441
444
|
const statusIcon = isRunning
|
|
442
445
|
? html`<${Text} color=${C.warning}><${Spinner} type=${spinnerType()} /><//>`
|
|
443
|
-
: done && ok ? html`<${Text} color=${C.success}> ${glyphs.check}<//>`
|
|
444
|
-
: done && !ok ? html`<${Text} color=${C.error}> ${glyphs.cross}<//>`
|
|
446
|
+
: (done && ok) ? html`<${Text} color=${C.success}> ${glyphs.check}<//>`
|
|
447
|
+
: (done && !ok) ? html`<${Text} color=${C.error}> ${glyphs.cross}<//>`
|
|
445
448
|
: null;
|
|
446
449
|
|
|
447
|
-
// Expand toggle hint for navigable lines
|
|
448
|
-
const expHint = navigable && isSel
|
|
449
|
-
? html`<${Text} color=${C.muted} dimColor> [${isExp ? '−' : '+'}]<//>` : null;
|
|
450
|
-
|
|
451
450
|
return html`
|
|
452
|
-
<${Box} key=${
|
|
453
|
-
|
|
454
|
-
<${Box} backgroundColor=${isSel ? '#1a1a2e' : undefined}>
|
|
451
|
+
<${Box} key=${`sl${i}`} flexDirection="column">
|
|
452
|
+
<${Box}>
|
|
455
453
|
<${Text} color=${C.muted}> <//>
|
|
456
|
-
|
|
457
|
-
<${Text} color=${C.muted}
|
|
458
|
-
<${Text} color=${
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
454
|
+
<!-- Type tag: dim neutral — NOT colored by op type -->
|
|
455
|
+
<${Text} color=${C.muted} dimColor>${tag}<//>
|
|
456
|
+
<${Text} color=${C.muted}> <//>
|
|
457
|
+
<!-- Colored dot — the ONLY colored part -->
|
|
458
|
+
<${Text} color=${opColor}>● <//>
|
|
459
|
+
<!-- Verb: bold for reads, normal otherwise -->
|
|
460
|
+
<${Text} color=${C.white} bold=${opType === 'read'}>${verbLabel}<//>
|
|
461
|
+
<!-- Detail: plain white -->
|
|
462
|
+
<${Text} color=${C.white} dimColor> ${detail}<//>
|
|
463
463
|
${statusIcon}
|
|
464
|
-
${
|
|
464
|
+
${isSel ? html`<${Text} color=${C.muted} dimColor> [${isExp ? '−' : '+'}]<//>` : null}
|
|
465
465
|
<//>
|
|
466
|
-
|
|
467
|
-
<!-- Expanded detail -->
|
|
468
466
|
${isExp ? html`
|
|
469
|
-
<${Box} flexDirection="column"
|
|
470
|
-
<${Text} color=${C.separator}>${'┌' + '─'.repeat(
|
|
471
|
-
<${Box}
|
|
472
|
-
<${Text} color=${C.
|
|
473
|
-
<${Text} color=${C.muted} italic>${lineStr.slice(0, 120)}<//>
|
|
467
|
+
<${Box} flexDirection="column" paddingLeft=${8}>
|
|
468
|
+
<${Text} color=${C.separator}>${'┌' + '─'.repeat(48)}<//>
|
|
469
|
+
<${Box}><${Text} color=${C.separator}>│ <//>
|
|
470
|
+
<${Text} color=${C.muted}>${full.slice(0, 100)}<//>
|
|
474
471
|
<//>
|
|
475
|
-
${
|
|
476
|
-
<${
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
<//>` : null}
|
|
480
|
-
<${Text} color=${C.separator}>${'└' + '─'.repeat(50)}<//>
|
|
472
|
+
${full.length > 100 ? html`<${Box}><${Text} color=${C.separator}>│ <//>
|
|
473
|
+
<${Text} color=${C.muted}>${full.slice(100, 200)}<//>
|
|
474
|
+
<//>` : null}
|
|
475
|
+
<${Text} color=${C.separator}>${'└' + '─'.repeat(48)}<//>
|
|
481
476
|
<//>` : null}
|
|
482
477
|
<//>`;
|
|
483
478
|
})}
|
|
484
479
|
${navigable && lines.length > 1
|
|
485
480
|
? html`<${Box} paddingLeft=${2} marginTop=${0}>
|
|
486
|
-
<${Text} color=${C.muted} dimColor>j/k
|
|
481
|
+
<${Text} color=${C.muted} dimColor>j/k select Enter expand Esc dismiss<//>
|
|
487
482
|
<//>`
|
|
488
483
|
: null}
|
|
489
484
|
<//>`;
|
|
@@ -667,15 +662,17 @@ export function Thinking({ label = 'Working', startedAt }) {
|
|
|
667
662
|
}
|
|
668
663
|
|
|
669
664
|
export function Choice({ title, note, options = [], sel = 0, allowText = false, text = '', focusText = false, isToolApproval = false, cols = 80 }) {
|
|
670
|
-
// §13: tool approval uses
|
|
665
|
+
// §13: tool approval uses separator-line style matching TUI.md spec exactly
|
|
671
666
|
if (isToolApproval) {
|
|
672
667
|
return html`
|
|
673
668
|
<${Box} flexDirection="column" marginTop=${1}>
|
|
674
|
-
<${
|
|
675
|
-
|
|
669
|
+
<${Sep} cols=${cols} />
|
|
670
|
+
<${Box} paddingX=${1} marginBottom=${1}>
|
|
671
|
+
<${Text} color=${C.accent} bold>${glyphs.settings} Tool request<//>
|
|
676
672
|
<//>
|
|
677
|
-
|
|
678
|
-
|
|
673
|
+
<${Sep} cols=${cols} />
|
|
674
|
+
${note ? html`<${Box} paddingX=${1} marginTop=${1} marginBottom=${1}><${Text} color=${C.white}>${note}<//><//>` : null}
|
|
675
|
+
<${Box} flexDirection="column" marginTop=${1} paddingX=${1}>
|
|
679
676
|
${options.map((o, i) => {
|
|
680
677
|
const on = !focusText && i === sel;
|
|
681
678
|
const label = o.label ?? o.value ?? String(o);
|
|
@@ -689,13 +686,13 @@ export function Choice({ title, note, options = [], sel = 0, allowText = false,
|
|
|
689
686
|
${allowText
|
|
690
687
|
? html`<${Box} key="freetext" marginTop=${1}>
|
|
691
688
|
<${Text} color=${focusText ? C.accent : C.muted}>${focusText ? glyphs.prompt + ' ' : ' '}<//>
|
|
692
|
-
<${Text} color=${C.muted}>Tell
|
|
689
|
+
<${Text} color=${C.muted}>Tell iPolot what to do differently: <//>
|
|
693
690
|
<${Text} color=${C.white}>${text}<//>${focusText ? html`<${Text} inverse> <//>` : null}
|
|
694
691
|
<//>`
|
|
695
692
|
: null}
|
|
696
693
|
<//>
|
|
697
|
-
<${Box} marginTop=${1} paddingX=${
|
|
698
|
-
<${Text} color=${C.muted}
|
|
694
|
+
<${Box} marginTop=${1} paddingX=${1}>
|
|
695
|
+
<${Text} color=${C.muted}>y / a / n ${midDot()} ${glyphs.up}${glyphs.down} move ${midDot()} Enter confirm ${midDot()} Esc cancel<//>
|
|
699
696
|
<//>
|
|
700
697
|
<//>`;
|
|
701
698
|
}
|
package/src/tui/composer.js
CHANGED
|
@@ -512,7 +512,7 @@ export function Composer({
|
|
|
512
512
|
<//>`
|
|
513
513
|
: null}
|
|
514
514
|
|
|
515
|
-
<${Text} color=${C.
|
|
515
|
+
<${Text} color=${C.accent}>${'_'.repeat(Math.max(1, width))}<//>
|
|
516
516
|
|
|
517
517
|
<${Box} width=${width} paddingX=${1} justifyContent="space-between">
|
|
518
518
|
<${Box} flexGrow=${1}>
|
package/src/tui/run.js
CHANGED
|
@@ -93,7 +93,7 @@ function exitAltScreen() {
|
|
|
93
93
|
altScreenActive = false;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
//
|
|
96
|
+
// iPolot wordmark in block letters
|
|
97
97
|
const LOGO = [
|
|
98
98
|
' ██╗ ██████╗ ██████╗ ██████╗ ██╗██╗ ██████╗ ████████╗',
|
|
99
99
|
' ██║██╔════╝██╔═══██╗██╔══██╗██║██║ ██╔═══██╗╚══██╔══╝',
|
|
@@ -145,8 +145,8 @@ async function animateSplash(cfg = {}) {
|
|
|
145
145
|
const R = '\x1b[0m';
|
|
146
146
|
|
|
147
147
|
const hr = `${SEP}${'─'.repeat(cols)}${R}`;
|
|
148
|
-
const subtitle = `${ACC}◈
|
|
149
|
-
const subtitleLen =
|
|
148
|
+
const subtitle = `${ACC}◈ iPolot${R} ${DIM}IspBills network engineer in your terminal${R}`;
|
|
149
|
+
const subtitleLen = subtitle.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
150
150
|
const subPad = ' '.repeat(Math.max(0, Math.floor((cols - subtitleLen) / 2)));
|
|
151
151
|
|
|
152
152
|
process.stdout.write(`\n${hr}\n`);
|
package/src/tui/theme.js
CHANGED
|
@@ -46,6 +46,7 @@ const ASCII_EXTRA = {
|
|
|
46
46
|
chip: '|', // chip separator
|
|
47
47
|
expand: '+', // expand indicator
|
|
48
48
|
collapse: '-', // collapse indicator
|
|
49
|
+
settings: '#', // tool-approval header (ASCII safe)
|
|
49
50
|
};
|
|
50
51
|
const UNICODE_EXTRA = {
|
|
51
52
|
copilotDot: '◈', // spec §6: ◈ Copilot speaker label (◈ in accent/purple)
|
|
@@ -56,6 +57,7 @@ const UNICODE_EXTRA = {
|
|
|
56
57
|
chip: '·',
|
|
57
58
|
expand: '▸',
|
|
58
59
|
collapse: '▾',
|
|
60
|
+
settings: '⚙', // spec §13: tool request header glyph
|
|
59
61
|
};
|
|
60
62
|
|
|
61
63
|
// Merged glyph proxy: own extras first, then ui.js base glyphs.
|