ispbills-icli 8.5.3 → 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.
@@ -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' },
@@ -26,11 +62,8 @@ const TABS = [
26
62
  ];
27
63
 
28
64
  export function TabBar({ active = 'session', cols = 80, mode = 'ask' }) {
29
- // Deep-blue background track per spec (mirrors \x1b[44m ANSI blue).
30
- // Active tab wrapped in [brackets] bold/white; inactive dimmed.
31
- // Right side shows the current mode badge.
32
65
  const modeBadge = mode !== 'ask'
33
- ? ` ${mode === 'autopilot' ? '⚡ AUTO' : '📋 PLAN'}`
66
+ ? ` ${mode === 'autopilot' ? '⚡ AUTO' : mode === 'shell' ? '⌘ SHELL' : '📋 PLAN'}`
34
67
  : '';
35
68
  return html`
36
69
  <${Box} width=${cols} backgroundColor="blue">
@@ -50,9 +83,6 @@ export function TabBar({ active = 'session', cols = 80, mode = 'ask' }) {
50
83
  <//>`;
51
84
  }
52
85
 
53
- // ── Followup Chips ────────────────────────────────────────────────────────────
54
- // Up to 4 suggested followup actions shown above the input after a response.
55
- // Ctrl+N/P cycles; ↵ on empty input submits active; Esc clears.
56
86
  export function FollowupChips({ chips = [], active = 0 }) {
57
87
  if (!chips.length) return null;
58
88
  return html`
@@ -70,12 +100,11 @@ export function FollowupChips({ chips = [], active = 0 }) {
70
100
  <//>`;
71
101
  }
72
102
 
73
- /** Header banner full logo on wide/tall terminals, compact title otherwise. */
74
- export function Banner({ cfg = {}, model, width = 80, compact = false }) {
103
+ export function Banner({ cfg = {}, model, width = 80, compact = false, streamerMode = false }) {
75
104
  const m = shortModel(model || cfg._model || 'IspBills AI');
76
105
  const wide = !compact && width >= 60;
77
106
  const meta = [
78
- ['model', m], ['user', cfg.user?.name], ['role', cfg.user?.role],
107
+ ['model', streamerMode ? 'hidden' : m], ['user', cfg.user?.name], ['role', cfg.user?.role],
79
108
  ].filter(([, v]) => v);
80
109
 
81
110
  if (compact) {
@@ -110,61 +139,64 @@ export function Banner({ cfg = {}, model, width = 80, compact = false }) {
110
139
  <//>`;
111
140
  }
112
141
 
113
- /** Split text into spans, colouring @mentions. */
114
142
  function withMentions(text, base = C.white) {
115
- const parts = String(text).split(/(@[\w:.\-/]+)/g);
143
+ const parts = String(text).split(/(@[\w:.\-/]+|#\d+)/g);
116
144
  return parts.map((p, i) =>
117
145
  /^@[\w:.\-/]+$/.test(p)
118
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}<//>`
119
149
  : html`<${Text} key=${'mn' + i} color=${base}>${p}<//>`,
120
150
  );
121
151
  }
122
152
 
123
- // Full-width separator line using - (ASCII dash) per spec §6: "Full-width `-` characters in #30363D".
124
153
  function Sep({ cols = 80 }) {
125
- return html`<${Text} color=${C.separator}>${'-'.repeat(Math.max(1, cols))}<//>`;}
154
+ return html`<${Text} color=${C.separator}>${'-'.repeat(Math.max(1, cols))}<//>`;
155
+ }
126
156
 
127
- /** The user's prompt block separator + "You" label + text. */
128
- export function UserMessage({ text, cols = 80 }) {
157
+ export function UserMessage({ text, cols = 80, ts }) {
129
158
  return html`
130
159
  <${Box} flexDirection="column" marginTop=${1}>
131
160
  <${Sep} cols=${cols} />
132
- <${Box} paddingX=${2} marginTop=${1}>
133
- <${Text} color=${C.brand} bold>You<//>
134
- <//>
161
+ <${SpeakerLine} label="You" color=${C.brand} cols=${cols} ts=${ts} />
135
162
  <${Box} paddingX=${2}>
136
163
  <${Text}>${withMentions(text)}<//>
137
164
  <//>
138
165
  <//>`;
139
166
  }
140
167
 
141
- /** A finished assistant answer separator + "⬡ iCopilot" label + markdown. */
142
- export function AssistantMessage({ text, cols = 80 }) {
143
- const dot = glyphs.copilotDot;
168
+ export function AssistantMessage({ text, cols = 80, ts, label = `${glyphs.copilotDot} iCopilot` }) {
144
169
  return html`
145
170
  <${Box} flexDirection="column" marginTop=${1}>
146
171
  <${Sep} cols=${cols} />
147
- <${Box} paddingX=${2} marginTop=${1}>
148
- <${Text} color=${C.accent} bold>${dot} iCopilot<//>
149
- <//>
172
+ <${SpeakerLine} label=${label} color=${C.accent} cols=${cols} ts=${ts} />
150
173
  <${Box} paddingX=${2}>
151
174
  <${Markdown} content=${text} />
152
175
  <//>
153
176
  <//>`;
154
177
  }
155
178
 
156
- // A sub-agent progress line, e.g. "✓ [1/3] nas#1: FREE" or "[2/3] olt#1: down".
157
- const SUBAGENT = /^\s*[✓✗x]?\s*\[(\d+)\/(\d+)\]\s*(.+?)\s*[::]\s*(.+?)\s*$/;
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
+ }
158
196
 
159
- // A leading emoji/pictograph on a tool status label (from statusLabel()).
197
+ const SUBAGENT = /^\s*[✓✗x]?\s*\[(\d+)\/(\d+)\]\s*(.+?)\s*[::]\s*(.+?)\s*$/;
160
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;
161
199
 
162
- /**
163
- * Live server activity — tool calls and sub-agents, rendered Copilot-CLI style:
164
- * each activity gets a status marker (spinner while it's the in-flight step,
165
- * green check once done), the tool's purpose label is kept, and sub-agent
166
- * progress is nested underneath its parent as a tree.
167
- */
168
200
  export function StatusLines({ lines = [], busy = false }) {
169
201
  if (!lines.length) return null;
170
202
  const subLines = lines.filter((l) => SUBAGENT.test(l));
@@ -192,7 +224,6 @@ export function StatusLines({ lines = [], busy = false }) {
192
224
  <//>`;
193
225
  }
194
226
 
195
- // Tool / activity line. The last line while busy is the in-flight step.
196
227
  const running = busy && i === lastIdx;
197
228
  const em = String(line).match(LEAD_EMOJI);
198
229
  const body = em ? String(line).slice(em[0].length) : String(line).replace(/^[\s◈◆●○◎⇢→>*✓✗×!⚠·-]+/u, '');
@@ -213,10 +244,8 @@ export function StatusLines({ lines = [], busy = false }) {
213
244
  <//>`;
214
245
  }
215
246
 
216
- /** Reasoning trace — collapsed to a single dim line; Ctrl+T to expand. */
217
247
  export function Reasoning({ text, show = true }) {
218
248
  if (!text) return null;
219
- // Estimate token count (~4 chars per token as rough heuristic)
220
249
  const tokenEst = Math.round(text.length / 4);
221
250
  if (!show) {
222
251
  return html`
@@ -231,7 +260,6 @@ export function Reasoning({ text, show = true }) {
231
260
  <//>`;
232
261
  }
233
262
 
234
- /** A proposed remediation plan. */
235
263
  export function Plan({ reply = {}, live = false }) {
236
264
  const steps = reply.steps || [];
237
265
  const dot3 = '…';
@@ -265,7 +293,6 @@ export function Plan({ reply = {}, live = false }) {
265
293
  <//>`;
266
294
  }
267
295
 
268
- /** A device-connect card. */
269
296
  export function Connect({ reply = {} }) {
270
297
  const d = reply.device ?? reply ?? {};
271
298
  return html`
@@ -279,12 +306,41 @@ export function Connect({ reply = {} }) {
279
306
  <//>`;
280
307
  }
281
308
 
282
- /** A local notice (help hint, errors, etc.). */
283
309
  export function Notice({ text, color = C.gray }) {
284
310
  return html`<${Box} marginTop=${1}><${Text} color=${color}>${text}<//><//>`;
285
311
  }
286
312
 
287
- /** The active "working" line — animated spinner (warning colour), elapsed time, cancel hint. */
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
+
288
344
  export function Thinking({ label = 'Working', startedAt }) {
289
345
  const [, tick] = useState(0);
290
346
  useEffect(() => {
@@ -300,11 +356,6 @@ export function Thinking({ label = 'Working', startedAt }) {
300
356
  <//>`;
301
357
  }
302
358
 
303
- /**
304
- * An interactive prompt — pick an option with ↑/↓ + Enter, or (when
305
- * `allowText`) type a free-form answer. Mirrors GitHub Copilot CLI prompts.
306
- * Presentational only: the parent owns `sel`/`textValue` and key handling.
307
- */
308
359
  export function Choice({ title, note, options = [], sel = 0, allowText = false, text = '', focusText = false }) {
309
360
  return html`
310
361
  <${Box} flexDirection="column" marginTop=${1} borderStyle=${borderStyle()} borderColor=${C.accent} paddingX=${1}>