ispbills-icli 8.5.2 → 8.5.4
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 +2 -1
- package/package.json +1 -1
- package/src/commands.js +38 -35
- package/src/tui/app.js +508 -202
- package/src/tui/components.js +115 -56
- package/src/tui/composer.js +239 -73
- package/src/tui/run.js +34 -7
package/src/tui/components.js
CHANGED
|
@@ -15,9 +15,45 @@ const LOGO = [
|
|
|
15
15
|
'╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═╝ ',
|
|
16
16
|
];
|
|
17
17
|
|
|
18
|
+
const HELP_ROWS = [
|
|
19
|
+
['Shift+Tab', 'cycle ask / plan / autopilot'],
|
|
20
|
+
['Ctrl+S', 'run while preserving input'],
|
|
21
|
+
['Ctrl+Enter / Ctrl+Q', 'queue follow-up while busy'],
|
|
22
|
+
['Ctrl+G', 'edit prompt in external editor'],
|
|
23
|
+
['Ctrl+F', 'search timeline'],
|
|
24
|
+
['Ctrl+O / Ctrl+E', 'expand recent / all timeline items'],
|
|
25
|
+
['Ctrl+T', 'toggle reasoning'],
|
|
26
|
+
['Ctrl+R', 'reverse-search input history'],
|
|
27
|
+
['Ctrl+V', 'paste attachment help'],
|
|
28
|
+
['Ctrl+Z', 'suspend to background'],
|
|
29
|
+
['!', 'shell command / shell mode'],
|
|
30
|
+
['?', 'quick help overlay'],
|
|
31
|
+
['@mention', 'insert ISP entities'],
|
|
32
|
+
['#123', 'reference customer / ticket id'],
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
function formatTs(ts) {
|
|
36
|
+
if (!ts) return '';
|
|
37
|
+
try {
|
|
38
|
+
return new Date(ts).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
39
|
+
} catch {
|
|
40
|
+
return '';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function SpeakerLine({ label, color, cols = 80, ts, rightLabel = '' }) {
|
|
45
|
+
const stamp = ts ? `[${formatTs(ts)}]` : '';
|
|
46
|
+
return html`
|
|
47
|
+
<${Box} paddingX=${2} marginTop=${1} width=${cols}>
|
|
48
|
+
<${Text} color=${color} bold>${label}<//>
|
|
49
|
+
<${Box} flexGrow=${1} />
|
|
50
|
+
${rightLabel ? html`<${Text} color=${C.muted}>${rightLabel}<//>` : null}
|
|
51
|
+
${rightLabel && stamp ? html`<${Text} color=${C.muted}> <//>` : null}
|
|
52
|
+
${stamp ? html`<${Text} color=${C.muted}>${stamp}<//>` : null}
|
|
53
|
+
<//>`;
|
|
54
|
+
}
|
|
55
|
+
|
|
18
56
|
// ── Tab Bar ─────────────────────────────────────────────────────────────────
|
|
19
|
-
// Matches the June 2026 Copilot CLI GA redesign: deep-blue track, active tab
|
|
20
|
-
// wrapped in [brackets], inactive tabs dimmed. Adapted for ISP NOC context.
|
|
21
57
|
const TABS = [
|
|
22
58
|
{ id: 'session', label: 'Session' },
|
|
23
59
|
{ id: 'devices', label: 'Devices' },
|
|
@@ -25,25 +61,28 @@ const TABS = [
|
|
|
25
61
|
{ id: 'alarms', label: 'Alarms' },
|
|
26
62
|
];
|
|
27
63
|
|
|
28
|
-
export function TabBar({ active = 'session', cols = 80,
|
|
64
|
+
export function TabBar({ active = 'session', cols = 80, mode = 'ask' }) {
|
|
65
|
+
const modeBadge = mode !== 'ask'
|
|
66
|
+
? ` ${mode === 'autopilot' ? '⚡ AUTO' : mode === 'shell' ? '⌘ SHELL' : '📋 PLAN'}`
|
|
67
|
+
: '';
|
|
29
68
|
return html`
|
|
30
|
-
<${Box} width=${cols}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
69
|
+
<${Box} width=${cols} backgroundColor="blue">
|
|
70
|
+
<${Box} paddingX=${1} flexGrow=${1}>
|
|
71
|
+
${TABS.map((t, i) => {
|
|
72
|
+
const on = t.id === active;
|
|
73
|
+
return html`
|
|
74
|
+
<${Box} key=${t.id}>
|
|
75
|
+
${i > 0 ? html`<${Text} color="cyan" backgroundColor="blue"> <//>` : null}
|
|
76
|
+
${on
|
|
77
|
+
? html`<${Text} color="white" bold backgroundColor="blue">[${t.label}]<//>`
|
|
78
|
+
: html`<${Text} color="cyan" backgroundColor="blue">${t.label}<//>`}
|
|
79
|
+
<//>`;
|
|
80
|
+
})}
|
|
81
|
+
<//>
|
|
82
|
+
${modeBadge ? html`<${Box} paddingRight=${1}><${Text} color="yellow" bold backgroundColor="blue">${modeBadge}<//><//>` : null}
|
|
41
83
|
<//>`;
|
|
42
84
|
}
|
|
43
85
|
|
|
44
|
-
// ── Followup Chips ────────────────────────────────────────────────────────────
|
|
45
|
-
// Up to 4 suggested followup actions shown above the input after a response.
|
|
46
|
-
// Ctrl+N/P cycles; ↵ on empty input submits active; Esc clears.
|
|
47
86
|
export function FollowupChips({ chips = [], active = 0 }) {
|
|
48
87
|
if (!chips.length) return null;
|
|
49
88
|
return html`
|
|
@@ -61,12 +100,11 @@ export function FollowupChips({ chips = [], active = 0 }) {
|
|
|
61
100
|
<//>`;
|
|
62
101
|
}
|
|
63
102
|
|
|
64
|
-
|
|
65
|
-
export function Banner({ cfg = {}, model, width = 80, compact = false }) {
|
|
103
|
+
export function Banner({ cfg = {}, model, width = 80, compact = false, streamerMode = false }) {
|
|
66
104
|
const m = shortModel(model || cfg._model || 'IspBills AI');
|
|
67
105
|
const wide = !compact && width >= 60;
|
|
68
106
|
const meta = [
|
|
69
|
-
['model', m], ['user', cfg.user?.name], ['role', cfg.user?.role],
|
|
107
|
+
['model', streamerMode ? 'hidden' : m], ['user', cfg.user?.name], ['role', cfg.user?.role],
|
|
70
108
|
].filter(([, v]) => v);
|
|
71
109
|
|
|
72
110
|
if (compact) {
|
|
@@ -101,62 +139,64 @@ export function Banner({ cfg = {}, model, width = 80, compact = false }) {
|
|
|
101
139
|
<//>`;
|
|
102
140
|
}
|
|
103
141
|
|
|
104
|
-
/** Split text into spans, colouring @mentions. */
|
|
105
142
|
function withMentions(text, base = C.white) {
|
|
106
|
-
const parts = String(text).split(/(@[\w:.\-/]+)/g);
|
|
143
|
+
const parts = String(text).split(/(@[\w:.\-/]+|#\d+)/g);
|
|
107
144
|
return parts.map((p, i) =>
|
|
108
145
|
/^@[\w:.\-/]+$/.test(p)
|
|
109
146
|
? html`<${Text} key=${'mn' + i} color=${C.accent} bold>${p}<//>`
|
|
147
|
+
: /^#\d+$/.test(p)
|
|
148
|
+
? html`<${Text} key=${'mn' + i} color=${C.slash} bold>${p}<//>`
|
|
110
149
|
: html`<${Text} key=${'mn' + i} color=${base}>${p}<//>`,
|
|
111
150
|
);
|
|
112
151
|
}
|
|
113
152
|
|
|
114
|
-
// Full-width separator line, drawn in muted separator colour.
|
|
115
153
|
function Sep({ cols = 80 }) {
|
|
116
|
-
return html`<${Text} color=${C.separator}>${'
|
|
154
|
+
return html`<${Text} color=${C.separator}>${'-'.repeat(Math.max(1, cols))}<//>`;
|
|
117
155
|
}
|
|
118
156
|
|
|
119
|
-
|
|
120
|
-
export function UserMessage({ text, cols = 80 }) {
|
|
157
|
+
export function UserMessage({ text, cols = 80, ts }) {
|
|
121
158
|
return html`
|
|
122
159
|
<${Box} flexDirection="column" marginTop=${1}>
|
|
123
160
|
<${Sep} cols=${cols} />
|
|
124
|
-
<${
|
|
125
|
-
<${Text} color=${C.brand} bold>You<//>
|
|
126
|
-
<//>
|
|
161
|
+
<${SpeakerLine} label="You" color=${C.brand} cols=${cols} ts=${ts} />
|
|
127
162
|
<${Box} paddingX=${2}>
|
|
128
163
|
<${Text}>${withMentions(text)}<//>
|
|
129
164
|
<//>
|
|
130
165
|
<//>`;
|
|
131
166
|
}
|
|
132
167
|
|
|
133
|
-
|
|
134
|
-
export function AssistantMessage({ text, cols = 80 }) {
|
|
135
|
-
const dot = glyphs.copilotDot;
|
|
168
|
+
export function AssistantMessage({ text, cols = 80, ts, label = `${glyphs.copilotDot} iCopilot` }) {
|
|
136
169
|
return html`
|
|
137
170
|
<${Box} flexDirection="column" marginTop=${1}>
|
|
138
171
|
<${Sep} cols=${cols} />
|
|
139
|
-
<${
|
|
140
|
-
<${Text} color=${C.accent} bold>${dot} iCopilot<//>
|
|
141
|
-
<//>
|
|
172
|
+
<${SpeakerLine} label=${label} color=${C.accent} cols=${cols} ts=${ts} />
|
|
142
173
|
<${Box} paddingX=${2}>
|
|
143
174
|
<${Markdown} content=${text} />
|
|
144
175
|
<//>
|
|
145
176
|
<//>`;
|
|
146
177
|
}
|
|
147
178
|
|
|
148
|
-
|
|
149
|
-
|
|
179
|
+
export function ShellOutput({ command, output, cols = 80, ts, code = 0 }) {
|
|
180
|
+
return html`
|
|
181
|
+
<${Box} flexDirection="column" marginTop=${1}>
|
|
182
|
+
<${Sep} cols=${cols} />
|
|
183
|
+
<${SpeakerLine}
|
|
184
|
+
label="⌘ Shell"
|
|
185
|
+
color=${code === 0 ? C.warning : C.error}
|
|
186
|
+
cols=${cols}
|
|
187
|
+
ts=${ts}
|
|
188
|
+
rightLabel=${code === 0 ? '' : `exit ${code}`}
|
|
189
|
+
/>
|
|
190
|
+
<${Box} paddingX=${2} flexDirection="column">
|
|
191
|
+
<${Text} color=${C.warning}>$ ${command}<//>
|
|
192
|
+
<${Text}>${output || '(no output)'}<//>
|
|
193
|
+
<//>
|
|
194
|
+
<//>`;
|
|
195
|
+
}
|
|
150
196
|
|
|
151
|
-
|
|
197
|
+
const SUBAGENT = /^\s*[✓✗x]?\s*\[(\d+)\/(\d+)\]\s*(.+?)\s*[::]\s*(.+?)\s*$/;
|
|
152
198
|
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;
|
|
153
199
|
|
|
154
|
-
/**
|
|
155
|
-
* Live server activity — tool calls and sub-agents, rendered Copilot-CLI style:
|
|
156
|
-
* each activity gets a status marker (spinner while it's the in-flight step,
|
|
157
|
-
* green check once done), the tool's purpose label is kept, and sub-agent
|
|
158
|
-
* progress is nested underneath its parent as a tree.
|
|
159
|
-
*/
|
|
160
200
|
export function StatusLines({ lines = [], busy = false }) {
|
|
161
201
|
if (!lines.length) return null;
|
|
162
202
|
const subLines = lines.filter((l) => SUBAGENT.test(l));
|
|
@@ -184,7 +224,6 @@ export function StatusLines({ lines = [], busy = false }) {
|
|
|
184
224
|
<//>`;
|
|
185
225
|
}
|
|
186
226
|
|
|
187
|
-
// Tool / activity line. The last line while busy is the in-flight step.
|
|
188
227
|
const running = busy && i === lastIdx;
|
|
189
228
|
const em = String(line).match(LEAD_EMOJI);
|
|
190
229
|
const body = em ? String(line).slice(em[0].length) : String(line).replace(/^[\s◈◆●○◎⇢→>*✓✗×!⚠·-]+/u, '');
|
|
@@ -205,10 +244,8 @@ export function StatusLines({ lines = [], busy = false }) {
|
|
|
205
244
|
<//>`;
|
|
206
245
|
}
|
|
207
246
|
|
|
208
|
-
/** Reasoning trace — collapsed to a single dim line; Ctrl+T to expand. */
|
|
209
247
|
export function Reasoning({ text, show = true }) {
|
|
210
248
|
if (!text) return null;
|
|
211
|
-
// Estimate token count (~4 chars per token as rough heuristic)
|
|
212
249
|
const tokenEst = Math.round(text.length / 4);
|
|
213
250
|
if (!show) {
|
|
214
251
|
return html`
|
|
@@ -223,7 +260,6 @@ export function Reasoning({ text, show = true }) {
|
|
|
223
260
|
<//>`;
|
|
224
261
|
}
|
|
225
262
|
|
|
226
|
-
/** A proposed remediation plan. */
|
|
227
263
|
export function Plan({ reply = {}, live = false }) {
|
|
228
264
|
const steps = reply.steps || [];
|
|
229
265
|
const dot3 = '…';
|
|
@@ -257,7 +293,6 @@ export function Plan({ reply = {}, live = false }) {
|
|
|
257
293
|
<//>`;
|
|
258
294
|
}
|
|
259
295
|
|
|
260
|
-
/** A device-connect card. */
|
|
261
296
|
export function Connect({ reply = {} }) {
|
|
262
297
|
const d = reply.device ?? reply ?? {};
|
|
263
298
|
return html`
|
|
@@ -271,12 +306,41 @@ export function Connect({ reply = {} }) {
|
|
|
271
306
|
<//>`;
|
|
272
307
|
}
|
|
273
308
|
|
|
274
|
-
/** A local notice (help hint, errors, etc.). */
|
|
275
309
|
export function Notice({ text, color = C.gray }) {
|
|
276
310
|
return html`<${Box} marginTop=${1}><${Text} color=${color}>${text}<//><//>`;
|
|
277
311
|
}
|
|
278
312
|
|
|
279
|
-
|
|
313
|
+
export function HelpOverlay({ cols = 80, ts }) {
|
|
314
|
+
return html`
|
|
315
|
+
<${Box} flexDirection="column" marginTop=${1} borderStyle=${borderStyle()} borderColor=${C.accent} paddingX=${1} width=${cols}>
|
|
316
|
+
<${SpeakerLine} label="? Help" color=${C.accent} cols=${Math.max(10, cols - 2)} ts=${ts} />
|
|
317
|
+
${HELP_ROWS.map(([key, desc]) => html`
|
|
318
|
+
<${Box} key=${key} paddingX=${1}>
|
|
319
|
+
<${Text} color=${C.success} bold>${key.padEnd(22)}<//>
|
|
320
|
+
<${Text} color=${C.muted}>${desc}<//>
|
|
321
|
+
<//>`)}
|
|
322
|
+
<//>`;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export function SearchOverlay({ query = '', matches = [], active = 0, cols = 80 }) {
|
|
326
|
+
const snippetWidth = Math.max(12, cols - 20);
|
|
327
|
+
return html`
|
|
328
|
+
<${Box} flexDirection="column" marginTop=${1} borderStyle=${borderStyle()} borderColor=${C.accent} paddingX=${1} width=${cols}>
|
|
329
|
+
<${Text} color=${C.accent} bold>Search timeline<//>
|
|
330
|
+
<${Text} color=${C.muted}>query: <//><${Text} color=${C.white}>${query || 'type to search'}<//>
|
|
331
|
+
<${Text} color=${C.muted}>matches: ${matches.length}${matches.length ? ` active ${active + 1}/${matches.length}` : ''}<//>
|
|
332
|
+
${matches.slice(0, 5).map((m, i) => {
|
|
333
|
+
const on = i === active;
|
|
334
|
+
return html`
|
|
335
|
+
<${Box} key=${m.id || i}>
|
|
336
|
+
<${Text} color=${on ? C.success : C.muted}>${on ? glyphs.prompt : ' '} <//>
|
|
337
|
+
<${Text} color=${on ? C.white : C.muted}>${String(m.preview || '').slice(0, snippetWidth)}<//>
|
|
338
|
+
<//>`;
|
|
339
|
+
})}
|
|
340
|
+
<${Text} color=${C.muted}>Enter expand Tab/↑/↓ cycle Esc close<//>
|
|
341
|
+
<//>`;
|
|
342
|
+
}
|
|
343
|
+
|
|
280
344
|
export function Thinking({ label = 'Working', startedAt }) {
|
|
281
345
|
const [, tick] = useState(0);
|
|
282
346
|
useEffect(() => {
|
|
@@ -292,11 +356,6 @@ export function Thinking({ label = 'Working', startedAt }) {
|
|
|
292
356
|
<//>`;
|
|
293
357
|
}
|
|
294
358
|
|
|
295
|
-
/**
|
|
296
|
-
* An interactive prompt — pick an option with ↑/↓ + Enter, or (when
|
|
297
|
-
* `allowText`) type a free-form answer. Mirrors GitHub Copilot CLI prompts.
|
|
298
|
-
* Presentational only: the parent owns `sel`/`textValue` and key handling.
|
|
299
|
-
*/
|
|
300
359
|
export function Choice({ title, note, options = [], sel = 0, allowText = false, text = '', focusText = false }) {
|
|
301
360
|
return html`
|
|
302
361
|
<${Box} flexDirection="column" marginTop=${1} borderStyle=${borderStyle()} borderColor=${C.accent} paddingX=${1}>
|