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.
@@ -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, onSwitch }) {
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} paddingX=${1} backgroundColor="#0D1117">
31
- ${TABS.map((t, i) => {
32
- const on = t.id === active;
33
- return html`
34
- <${Box} key=${t.id}>
35
- ${i > 0 ? html`<${Text} color=${C.muted}> <//>` : null}
36
- ${on
37
- ? html`<${Text} color="white" bold>[${t.label}]<//>`
38
- : html`<${Text} color=${C.muted}>${t.label}<//>`}
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
- /** Header banner full logo on wide/tall terminals, compact title otherwise. */
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}>${''.repeat(Math.max(1, cols))}<//>`;
154
+ return html`<${Text} color=${C.separator}>${'-'.repeat(Math.max(1, cols))}<//>`;
117
155
  }
118
156
 
119
- /** The user's prompt block separator + "You" label + text. */
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
- <${Box} paddingX=${2} marginTop=${1}>
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
- /** A finished assistant answer separator + "⬡ iCopilot" label + markdown. */
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
- <${Box} paddingX=${2} marginTop=${1}>
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
- // A sub-agent progress line, e.g. "✓ [1/3] nas#1: FREE" or "[2/3] olt#1: down".
149
- 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
+ }
150
196
 
151
- // A leading emoji/pictograph on a tool status label (from statusLabel()).
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
- /** 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
+
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}>