ispbills-icli 8.4.12 → 8.4.13
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 +4 -24
- package/package.json +1 -1
- package/src/banner.js +1 -3
- package/src/client.js +0 -6
- package/src/clipboard.js +1 -35
- package/src/commands.js +0 -3
- package/src/tui/app.js +27 -171
- package/src/tui/components.js +89 -587
- package/src/tui/composer.js +4 -39
- package/src/tui/markdown.js +3 -19
- package/src/tui/run.js +1 -3
- package/src/tui/theme.js +11 -29
- package/src/logger.js +0 -23
- package/src/resume.js +0 -30
- package/src/shell.js +0 -18
- package/src/version-check.js +0 -43
package/src/tui/components.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// Presentational components for the iCli Ink TUI.
|
|
2
|
-
import { html, useState, useEffect
|
|
2
|
+
import { html, useState, useEffect } from './dom.js';
|
|
3
3
|
import { Box, Text } from 'ink';
|
|
4
4
|
import Spinner from 'ink-spinner';
|
|
5
|
-
import { C,
|
|
5
|
+
import { C, glyphs, spinnerType, borderStyle, midDot, dash } from './theme.js';
|
|
6
6
|
import { Markdown } from './markdown.js';
|
|
7
7
|
import { shortModel } from '../ui.js';
|
|
8
8
|
|
|
@@ -15,26 +15,13 @@ const LOGO = [
|
|
|
15
15
|
'╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═╝ ',
|
|
16
16
|
];
|
|
17
17
|
|
|
18
|
-
// Width of the widest logo line (used for the separator beneath).
|
|
19
|
-
const LOGO_WIDTH = Math.max(...LOGO.map((l) => l.length));
|
|
20
|
-
|
|
21
|
-
/** Thin separator that spans the full logo width (or less on narrow terminals). */
|
|
22
|
-
function LogoRule({ width }) {
|
|
23
|
-
const w = Math.min(width, LOGO_WIDTH + 4);
|
|
24
|
-
return html`<${Text} dimColor>${(asciiSafe() ? '-' : '─').repeat(Math.max(8, w))}<//>`;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
18
|
/** Header banner — full logo on wide/tall terminals, compact title otherwise. */
|
|
28
|
-
export function Banner({ cfg = {}, model, width = 80, compact = false
|
|
19
|
+
export function Banner({ cfg = {}, model, width = 80, compact = false }) {
|
|
29
20
|
const m = shortModel(model || cfg._model || 'IspBills AI');
|
|
30
21
|
const wide = !compact && width >= 60;
|
|
31
22
|
const meta = [
|
|
32
|
-
['model', m],
|
|
33
|
-
|
|
34
|
-
['role', cfg.user?.role],
|
|
35
|
-
version ? ['v', version] : null,
|
|
36
|
-
cwd ? ['cwd', cwd] : null,
|
|
37
|
-
].filter(Boolean).filter(([, v]) => v);
|
|
23
|
+
['model', m], ['user', cfg.user?.name], ['role', cfg.user?.role],
|
|
24
|
+
].filter(([, v]) => v);
|
|
38
25
|
|
|
39
26
|
if (compact) {
|
|
40
27
|
return html`
|
|
@@ -43,42 +30,28 @@ export function Banner({ cfg = {}, model, width = 80, compact = false, version =
|
|
|
43
30
|
${meta.map(([label, val], i) => html`
|
|
44
31
|
<${Box} key=${'m' + i}>
|
|
45
32
|
<${Text} color=${C.gray}>${midDot()}<//>
|
|
46
|
-
<${Text} color=${label === 'model' ? C.cyan :
|
|
33
|
+
<${Text} color=${label === 'model' ? C.cyan : C.white}>${val}<//>
|
|
47
34
|
<//>`)}
|
|
48
35
|
<//>`;
|
|
49
36
|
}
|
|
50
37
|
|
|
51
38
|
return html`
|
|
52
|
-
<${Box} flexDirection="column" marginBottom=${
|
|
39
|
+
<${Box} flexDirection="column" marginBottom=${1} alignItems="center">
|
|
53
40
|
${wide
|
|
54
41
|
? html`<${Box} flexDirection="column" alignItems="center">
|
|
55
42
|
${LOGO.map((l, i) => html`<${Text} key=${'l' + i} color=${C.accent} bold>${l}<//>`)}
|
|
56
|
-
<${Text}
|
|
43
|
+
<${Text} color=${C.gray}>iCopilot ${dash()} network engineer in your terminal<//>
|
|
57
44
|
<//>`
|
|
58
45
|
: html`<${Text} color=${C.accent} bold>iCopilot ${html`<${Text} color=${C.gray}>${dash()} IspBills AI terminal<//>`}<//>`}
|
|
59
46
|
<${Box} marginTop=${1}>
|
|
60
47
|
${meta.map(([label, val], i) => html`
|
|
61
48
|
<${Box} key=${'m' + i}>
|
|
62
49
|
${i > 0 ? html`<${Text} color=${C.gray}>${midDot()}<//>` : null}
|
|
63
|
-
<${Text}
|
|
64
|
-
<${Text} color=${
|
|
65
|
-
label === 'model' ? C.cyan :
|
|
66
|
-
label === 'v' ? C.gray :
|
|
67
|
-
label === 'cwd' ? C.yellow :
|
|
68
|
-
C.white}>${val}<//>
|
|
50
|
+
<${Text} color=${C.gray}>${label} <//>
|
|
51
|
+
<${Text} color=${label === 'model' ? C.cyan : C.white}>${val}<//>
|
|
69
52
|
<//>`)}
|
|
70
53
|
<//>
|
|
71
|
-
${cfg.url ? html`<${
|
|
72
|
-
<${Box} marginTop=${1}>
|
|
73
|
-
<${Text} dimColor>Type a message${midDot()}<//>
|
|
74
|
-
<${Text} color=${C.accent}>/help<//>
|
|
75
|
-
<${Text} dimColor> for commands${midDot()}<//>
|
|
76
|
-
<${Text} color=${C.orange}>!<//>
|
|
77
|
-
<${Text} dimColor> to run shell<//>
|
|
78
|
-
<//>
|
|
79
|
-
<${Box} marginTop=${1}>
|
|
80
|
-
<${LogoRule} width=${width} />
|
|
81
|
-
<//>
|
|
54
|
+
${cfg.url ? html`<${Text} color=${C.gray}>${cfg.url}<//>` : null}
|
|
82
55
|
<//>`;
|
|
83
56
|
}
|
|
84
57
|
|
|
@@ -95,20 +68,17 @@ function withMentions(text, base = C.white) {
|
|
|
95
68
|
/** The user's prompt line. */
|
|
96
69
|
export function UserMessage({ text }) {
|
|
97
70
|
return html`
|
|
98
|
-
<${Box} marginTop=${1}
|
|
71
|
+
<${Box} marginTop=${1}>
|
|
99
72
|
<${Text} color=${C.accent} bold>${glyphs.prompt} <//>
|
|
100
|
-
<${
|
|
101
|
-
<${Text} bold>${withMentions(text)}<//>
|
|
102
|
-
<//>
|
|
73
|
+
<${Text}>${withMentions(text)}<//>
|
|
103
74
|
<//>`;
|
|
104
75
|
}
|
|
105
76
|
|
|
106
|
-
/** A finished assistant answer (markdown).
|
|
107
|
-
export function AssistantMessage({ text
|
|
77
|
+
/** A finished assistant answer (markdown). */
|
|
78
|
+
export function AssistantMessage({ text }) {
|
|
108
79
|
return html`
|
|
109
80
|
<${Box} flexDirection="column" marginTop=${1}>
|
|
110
81
|
<${Markdown} content=${text} />
|
|
111
|
-
${streaming ? html`<${Text} color="cyan">▌<//>` : null}
|
|
112
82
|
<//>`;
|
|
113
83
|
}
|
|
114
84
|
|
|
@@ -118,248 +88,101 @@ const SUBAGENT = /^\s*[✓✗x]?\s*\[(\d+)\/(\d+)\]\s*(.+?)\s*[::]\s*(.+?)\s*$
|
|
|
118
88
|
// A leading emoji/pictograph on a tool status label (from statusLabel()).
|
|
119
89
|
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;
|
|
120
90
|
|
|
121
|
-
// Max tool-groups shown before collapsing older ones (GitHub Copilot CLI style).
|
|
122
|
-
const MAX_VISIBLE_GROUPS = 5;
|
|
123
|
-
|
|
124
|
-
// Max sub-agent rows per group before collapsing earlier agents.
|
|
125
|
-
const MAX_SUBAGENT_ROWS = 8;
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Group consecutive SUBAGENT lines under their preceding tool line.
|
|
129
|
-
* Returns [{ tool: string|null, subagents: string[] }]
|
|
130
|
-
*/
|
|
131
|
-
function groupStatusLines(lines) {
|
|
132
|
-
const groups = [];
|
|
133
|
-
let cur = null;
|
|
134
|
-
for (const line of lines) {
|
|
135
|
-
if (SUBAGENT.test(String(line))) {
|
|
136
|
-
if (!cur) { cur = { tool: null, subagents: [] }; groups.push(cur); }
|
|
137
|
-
cur.subagents.push(line);
|
|
138
|
-
} else {
|
|
139
|
-
cur = { tool: String(line), subagents: [] };
|
|
140
|
-
groups.push(cur);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
return groups;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Parse a tool status string into { em, action, detail }.
|
|
148
|
-
* Strips leading emoji/glyphs and splits on the first ':'.
|
|
149
|
-
*/
|
|
150
|
-
function parseTool(line) {
|
|
151
|
-
const em = String(line).match(LEAD_EMOJI);
|
|
152
|
-
const body = em
|
|
153
|
-
? String(line).slice(em[0].length)
|
|
154
|
-
: String(line).replace(/^[\s◈◆●○◎⇢→>*✓✗×!⚠·-]+/u, '');
|
|
155
|
-
const ci = body.indexOf(':');
|
|
156
|
-
return {
|
|
157
|
-
em: em ? em[1] : null,
|
|
158
|
-
action: ci > -1 ? body.slice(0, ci).trim() : body.trim(),
|
|
159
|
-
detail: ci > -1 ? body.slice(ci + 1).trim() : '',
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
|
|
163
91
|
/**
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*/
|
|
169
|
-
function toolLine(line, running, gi, extraLabel) {
|
|
170
|
-
const { em, action, detail } = parseTool(line);
|
|
171
|
-
return html`
|
|
172
|
-
<${Box} key=${'tl' + gi}>
|
|
173
|
-
${running
|
|
174
|
-
? html`<${Text} color=${C.accent}><${Spinner} type=${spinnerType()} /><//>`
|
|
175
|
-
: html`<${Text} dimColor>${SG.filled}<//>` }
|
|
176
|
-
<${Text}> <//>
|
|
177
|
-
${em ? html`<${Text}>${em} <//>` : null}
|
|
178
|
-
<${Text} color=${running ? C.white : void 0} dimColor=${!running} bold=${running}>${action}<//>
|
|
179
|
-
${detail
|
|
180
|
-
? html`<${Text} dimColor>: <//>
|
|
181
|
-
<${Text} dimColor>${detail}<//>` : null}
|
|
182
|
-
${extraLabel ? html`<${Text} dimColor> ${extraLabel}<//>` : null}
|
|
183
|
-
<//>`;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Render a group that has sub-agent rows nested inside it.
|
|
188
|
-
*
|
|
189
|
-
* GitHub Copilot CLI fleet/sub-agent style:
|
|
190
|
-
* ● spawn_subagent: fleet check (8/8) ← parent tool, progress badge
|
|
191
|
-
* ├─ 1/8 olt#1 ✓ 5 ONUs online ← completed OK (dim)
|
|
192
|
-
* ├─ 2/8 nas#2 ✗ unreachable ← completed ERR (dim red)
|
|
193
|
-
* │ 3/8 router#1 ⠙ checking… ← in-flight (│ pipe)
|
|
194
|
-
* └─ 4/8 switch#1 ✓ all OK ← last done (└─)
|
|
195
|
-
*/
|
|
196
|
-
function subagentGroup(group, groupRunning, gi) {
|
|
197
|
-
const { tool, subagents } = group;
|
|
198
|
-
|
|
199
|
-
// Cap visible sub-agents — collapse older ones to "N earlier agents".
|
|
200
|
-
const visibleSubs = subagents.slice(-MAX_SUBAGENT_ROWS);
|
|
201
|
-
const hiddenSubs = subagents.length - visibleSubs.length;
|
|
202
|
-
|
|
203
|
-
// Determine total from last sub-agent line.
|
|
204
|
-
const lastSm = SUBAGENT.exec(visibleSubs[visibleSubs.length - 1] || '');
|
|
205
|
-
const total = lastSm ? Number(lastSm[2]) : subagents.length;
|
|
206
|
-
|
|
207
|
-
// Count completed agents for progress badge.
|
|
208
|
-
const doneCount = subagents.filter(l => {
|
|
209
|
-
const m = SUBAGENT.exec(l);
|
|
210
|
-
return m && !/checking|running|pending/i.test(m[4]);
|
|
211
|
-
}).length;
|
|
212
|
-
|
|
213
|
-
// Is the last visible sub-agent still running?
|
|
214
|
-
const lastVisIdx = visibleSubs.length - 1;
|
|
215
|
-
const lastSmCheck = SUBAGENT.exec(visibleSubs[lastVisIdx] || '');
|
|
216
|
-
const lastRunning = groupRunning && lastSmCheck
|
|
217
|
-
? Number(lastSmCheck[1]) < Number(lastSmCheck[2])
|
|
218
|
-
: groupRunning;
|
|
219
|
-
|
|
220
|
-
// Progress badge on parent: "(3/8)" while running, "(8/8 done)" when complete.
|
|
221
|
-
const badge = lastRunning
|
|
222
|
-
? `(${doneCount}/${total})`
|
|
223
|
-
: `(${total}/${total} ${glyphs.check})`;
|
|
224
|
-
|
|
225
|
-
// Column-pad host names within this group for alignment.
|
|
226
|
-
const hosts = visibleSubs.map(l => (SUBAGENT.exec(l)?.[3] || '').trim());
|
|
227
|
-
const padW = Math.min(Math.max(...hosts.map(h => h.length), 6), 20);
|
|
228
|
-
|
|
229
|
-
// Width for idx/total fraction display (right-align idx).
|
|
230
|
-
const totLen = String(total).length;
|
|
231
|
-
|
|
232
|
-
return html`
|
|
233
|
-
<${Box} key=${'sg' + gi} flexDirection="column">
|
|
234
|
-
${tool ? toolLine(tool, groupRunning && lastRunning, gi, badge) : null}
|
|
235
|
-
${hiddenSubs > 0
|
|
236
|
-
? html`<${Box} key="hid">
|
|
237
|
-
<${Text} dimColor> ${glyphs.vbar} <//>
|
|
238
|
-
<${Text} dimColor>${glyphs.bullet} ${hiddenSubs} earlier agent${hiddenSubs > 1 ? 's' : ''} ${dash()} collapsed<//>
|
|
239
|
-
<//>` : null}
|
|
240
|
-
${visibleSubs.map((line, si) => {
|
|
241
|
-
const sm = SUBAGENT.exec(line);
|
|
242
|
-
if (!sm) return null;
|
|
243
|
-
const [, idx, tot, host, result] = sm;
|
|
244
|
-
const good = !/fail|error|down|offline|unreachable|timeout|deny/i.test(result);
|
|
245
|
-
const isLast = si === lastVisIdx;
|
|
246
|
-
const isRunning = groupRunning && isLast && Number(idx) < Number(tot);
|
|
247
|
-
|
|
248
|
-
// Tree marker: │ (pipe) for in-flight, └─ for last done, ├─ for others.
|
|
249
|
-
const isFinalEntry = Number(idx) === Number(tot) || (isLast && !isRunning);
|
|
250
|
-
const branch = isRunning
|
|
251
|
-
? glyphs.vbar + ' ' // "│ " in-flight
|
|
252
|
-
: (isFinalEntry ? glyphs.branchEnd + ' ' : glyphs.branchMid + ' '); // "└─ " or "├─ "
|
|
253
|
-
|
|
254
|
-
// Right-align idx within the fraction, e.g. " 3/8" vs "12/12".
|
|
255
|
-
const frac = String(idx).padStart(totLen) + '/' + tot;
|
|
256
|
-
const hostPad = host.padEnd(padW);
|
|
257
|
-
|
|
258
|
-
return html`
|
|
259
|
-
<${Box} key=${'sa' + si}>
|
|
260
|
-
<${Text} color=${isRunning ? C.accent : void 0} dimColor=${!isRunning}> ${branch}<//>
|
|
261
|
-
<${Text} color=${isRunning ? C.cyan : void 0} dimColor=${!isRunning}>${frac} <//>
|
|
262
|
-
<${Text} color=${isRunning ? C.white : void 0} dimColor=${!isRunning}>${hostPad} <//>
|
|
263
|
-
${isRunning
|
|
264
|
-
? html`<${Text} color=${C.accent}><${Spinner} type=${spinnerType()} /><//>
|
|
265
|
-
<${Text} color=${C.accent}> checking…<//>`
|
|
266
|
-
: html`<${Text} color=${good ? C.green : C.red} dimColor> ${good ? glyphs.check : glyphs.cross}<//>
|
|
267
|
-
<${Text} color=${good ? C.green : C.red} dimColor> ${result}<//>` }
|
|
268
|
-
<//>`;
|
|
269
|
-
})}
|
|
270
|
-
<//>`;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
/**
|
|
274
|
-
* Live server activity — tool calls and sub-agents, rendered Copilot-CLI style.
|
|
275
|
-
*
|
|
276
|
-
* Tool groups are the primary unit: each tool line is rendered with a spinner
|
|
277
|
-
* (in-flight) or dim ● (done). Sub-agent blocks are nested tree-style under
|
|
278
|
-
* their parent tool with ├─/└─/│ markers, padded columns, and a progress
|
|
279
|
-
* badge on the parent line — matching GitHub Copilot CLI fleet/sub-agent UI.
|
|
280
|
-
*
|
|
281
|
-
* Old groups collapse to a summary count so the live area stays compact.
|
|
92
|
+
* Live server activity — tool calls and sub-agents, rendered Copilot-CLI style:
|
|
93
|
+
* each activity gets a status marker (spinner while it's the in-flight step,
|
|
94
|
+
* green check once done), the tool's purpose label is kept, and sub-agent
|
|
95
|
+
* progress is nested underneath its parent as a tree.
|
|
282
96
|
*/
|
|
283
97
|
export function StatusLines({ lines = [], busy = false }) {
|
|
284
98
|
if (!lines.length) return null;
|
|
285
|
-
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
// Collapse old groups — keep last MAX_VISIBLE_GROUPS.
|
|
289
|
-
const hiddenGroupCount = groups.length > MAX_VISIBLE_GROUPS
|
|
290
|
-
? groups.length - MAX_VISIBLE_GROUPS : 0;
|
|
291
|
-
const visibleGroups = groups.slice(hiddenGroupCount);
|
|
292
|
-
|
|
293
|
-
// Total lines in hidden groups (for the summary count).
|
|
294
|
-
const hiddenLineCount = groups
|
|
295
|
-
.slice(0, hiddenGroupCount)
|
|
296
|
-
.reduce((acc, g) => acc + 1 + g.subagents.length, 0);
|
|
297
|
-
|
|
298
|
-
const lastGi = visibleGroups.length - 1;
|
|
299
|
-
|
|
99
|
+
const subLines = lines.filter((l) => SUBAGENT.test(l));
|
|
100
|
+
const lastIdx = lines.length - 1;
|
|
300
101
|
return html`
|
|
301
|
-
<${Box} flexDirection="column" marginTop=${
|
|
302
|
-
${
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
102
|
+
<${Box} flexDirection="column" marginTop=${1}>
|
|
103
|
+
${lines.map((line, i) => {
|
|
104
|
+
const sm = String(line).match(SUBAGENT);
|
|
105
|
+
if (sm) {
|
|
106
|
+
const [, idx, total, host, result] = sm;
|
|
107
|
+
const good = !/fail|error|down|offline|unreachable|timeout|deny/i.test(result);
|
|
108
|
+
const isLast = subLines[subLines.length - 1] === line;
|
|
109
|
+
const running = busy && isLast && Number(idx) < Number(total);
|
|
110
|
+
const branch = Number(idx) === Number(total) ? glyphs.branchEnd : glyphs.branchMid;
|
|
111
|
+
return html`
|
|
112
|
+
<${Box} key=${'s' + i}>
|
|
113
|
+
<${Text} color=${C.gray}> <//>
|
|
114
|
+
<${Text} color=${C.magenta}>${branch} <//>
|
|
115
|
+
<${Text} color=${C.gray}>${idx}/${total} <//>
|
|
116
|
+
<${Text} color=${C.white} bold>${host}<//>
|
|
117
|
+
<${Text} color=${C.gray}> ${dash()} <//>
|
|
118
|
+
${running
|
|
119
|
+
? html`<${Text} color=${C.accent}><${Spinner} type=${spinnerType()} /> working<//>`
|
|
120
|
+
: html`<${Text} color=${good ? C.green : C.red} bold>${good ? glyphs.check : glyphs.cross} ${result}<//>`}
|
|
121
|
+
<//>`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Tool / activity line. The last line while busy is the in-flight step.
|
|
125
|
+
const running = busy && i === lastIdx;
|
|
126
|
+
const em = String(line).match(LEAD_EMOJI);
|
|
127
|
+
const body = em ? String(line).slice(em[0].length) : String(line).replace(/^[\s◈◆●○◎⇢→>*✓✗×!⚠·-]+/u, '');
|
|
128
|
+
const ci = body.indexOf(':');
|
|
129
|
+
const action = ci > -1 ? body.slice(0, ci) : body;
|
|
130
|
+
const detail = ci > -1 ? body.slice(ci + 1).trim() : '';
|
|
131
|
+
return html`
|
|
132
|
+
<${Box} key=${'s' + i}>
|
|
133
|
+
${running
|
|
134
|
+
? html`<${Text} color=${C.accent}><${Spinner} type=${spinnerType()} /><//>`
|
|
135
|
+
: html`<${Text} color=${C.green}>${glyphs.check}<//>`}
|
|
136
|
+
<${Text}> <//>
|
|
137
|
+
${em ? html`<${Text}>${em[1]} <//>` : null}
|
|
138
|
+
<${Text} color=${running ? C.white : C.gray} bold=${running}>${action}<//>
|
|
139
|
+
${detail ? html`<${Text} color=${C.gray}>: ${detail}<//>` : null}
|
|
140
|
+
<//>`;
|
|
311
141
|
})}
|
|
312
142
|
<//>`;
|
|
313
143
|
}
|
|
314
144
|
|
|
315
|
-
/** Reasoning
|
|
145
|
+
/** Reasoning trace (shown by default; toggle with Ctrl+T). */
|
|
316
146
|
export function Reasoning({ text }) {
|
|
317
147
|
if (!text) return null;
|
|
318
148
|
return html`
|
|
319
|
-
<${Box} flexDirection="column" marginTop=${1}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
<${Text} color=${C.
|
|
323
|
-
<${Text} dimColor italic>${text}<//>
|
|
149
|
+
<${Box} flexDirection="column" marginTop=${1} borderStyle=${borderStyle()} borderColor=${C.gray}
|
|
150
|
+
borderTop=${false} borderRight=${false} borderBottom=${false} paddingLeft=${1}>
|
|
151
|
+
<${Text} color=${C.magenta}>${glyphs.reasoning} ${html`<${Text} color=${C.gray} bold>Reasoning<//>`}<//>
|
|
152
|
+
<${Text} color=${C.gray} italic>${text}<//>
|
|
324
153
|
<//>`;
|
|
325
154
|
}
|
|
326
155
|
|
|
327
156
|
/** A proposed remediation plan. */
|
|
328
157
|
export function Plan({ reply = {}, live = false }) {
|
|
329
158
|
const steps = reply.steps || [];
|
|
330
|
-
const
|
|
159
|
+
const dot3 = '…';
|
|
331
160
|
return html`
|
|
332
161
|
<${Box} flexDirection="column" marginTop=${1}>
|
|
333
|
-
<${
|
|
334
|
-
|
|
335
|
-
${reply.summary ? html`<${Text} key="sum" dimColor> ${dash()} ${reply.summary}<//>` : null}
|
|
336
|
-
<//>
|
|
337
|
-
${riskCount > 0
|
|
338
|
-
? html`<${Text} key="risk" color=${C.red}>${glyphs.warn} ${riskCount} destructive step${riskCount > 1 ? 's' : ''} — review carefully<//>` : null}
|
|
162
|
+
<${Text} color=${C.accent} bold>${glyphs.diamond} Plan<//>
|
|
163
|
+
${reply.summary ? html`<${Text} color=${C.gray}>${reply.summary}<//>` : null}
|
|
339
164
|
${steps.map((s, i) => {
|
|
340
165
|
const last = i === steps.length - 1;
|
|
341
166
|
const risk = (s.risk === 'destructive' || s.risk === 'high')
|
|
342
|
-
? html`<${Text}
|
|
167
|
+
? html`<${Text} color=${C.red}> ${glyphs.warn} destructive<//>`
|
|
343
168
|
: (s.risk === 'caution' || s.risk === 'medium')
|
|
344
|
-
? html`<${Text}
|
|
169
|
+
? html`<${Text} color=${C.yellow}> ${glyphs.bolt} caution<//>` : null;
|
|
345
170
|
return html`
|
|
346
171
|
<${Box} flexDirection="column" key=${'p' + i}>
|
|
347
|
-
<${Box}
|
|
348
|
-
<${Text}
|
|
349
|
-
<${Text}
|
|
350
|
-
<${Text} color=${C.yellow}
|
|
172
|
+
<${Box}>
|
|
173
|
+
<${Text} color=${C.gray}>${last ? glyphs.branchEnd : glyphs.branchMid} <//>
|
|
174
|
+
<${Text} color=${C.gray}>${i + 1}. <//>
|
|
175
|
+
<${Text} color=${C.yellow}>${s.cmd || ''}<//>
|
|
351
176
|
${risk}
|
|
352
|
-
${s.blocked ? html`<${Text}
|
|
177
|
+
${s.blocked ? html`<${Text} color=${C.red}> ${glyphs.block} blocked<//>` : null}
|
|
353
178
|
<//>
|
|
354
|
-
${(s.why || s.purpose) ? html`<${Text}
|
|
179
|
+
${(s.why || s.purpose) ? html`<${Text} color=${C.gray}> ${s.why || s.purpose}<//>` : null}
|
|
355
180
|
<//>`;
|
|
356
181
|
})}
|
|
357
|
-
<${Box}
|
|
182
|
+
<${Box} marginTop=${1}>
|
|
358
183
|
${live
|
|
359
|
-
? html`<${Text}
|
|
360
|
-
: html`<${Text}
|
|
361
|
-
<${Text} key="cf2" color=${C.white}>apply fix<//>
|
|
362
|
-
<${Text} key="cf3" dimColor> to confirm, or choose below.<//>` }
|
|
184
|
+
? html`<${Text} color=${C.gray} italic>${glyphs.bullet} forming plan${dot3}<//>`
|
|
185
|
+
: html`<${Text} color=${C.gray}>Type ${html`<${Text} color=${C.white}>apply fix<//>`} to confirm.<//>`}
|
|
363
186
|
<//>
|
|
364
187
|
<//>`;
|
|
365
188
|
}
|
|
@@ -378,34 +201,24 @@ export function Connect({ reply = {} }) {
|
|
|
378
201
|
<//>`;
|
|
379
202
|
}
|
|
380
203
|
|
|
381
|
-
/** A local notice (help hint, errors, etc.).
|
|
204
|
+
/** A local notice (help hint, errors, etc.). */
|
|
382
205
|
export function Notice({ text, color = C.gray }) {
|
|
383
|
-
|
|
384
|
-
return html`
|
|
385
|
-
<${Box} marginTop=${1} flexDirection="column">
|
|
386
|
-
${lines.map((ln, i) => html`<${Text} key=${'n' + i} color=${color}>${ln}<//>` )}
|
|
387
|
-
<//>`;
|
|
206
|
+
return html`<${Box} marginTop=${1}><${Text} color=${color}>${text}<//><//>`;
|
|
388
207
|
}
|
|
389
208
|
|
|
390
|
-
/**
|
|
391
|
-
|
|
392
|
-
* label: 'Thinking' when waiting for AI, 'Working' when tools are active.
|
|
393
|
-
*/
|
|
394
|
-
export function Thinking({ label = 'Thinking', startedAt }) {
|
|
209
|
+
/** The active "working" line — animated spinner, elapsed time, cancel hint. */
|
|
210
|
+
export function Thinking({ label = 'Working', startedAt }) {
|
|
395
211
|
const [, tick] = useState(0);
|
|
396
212
|
useEffect(() => {
|
|
397
213
|
const id = setInterval(() => tick((n) => n + 1), 1000);
|
|
398
214
|
return () => clearInterval(id);
|
|
399
215
|
}, []);
|
|
400
216
|
const secs = startedAt ? Math.max(0, Math.round((Date.now() - startedAt) / 1000)) : 0;
|
|
401
|
-
const timeStr = secs >= 60
|
|
402
|
-
? `${Math.floor(secs / 60)}m ${secs % 60}s`
|
|
403
|
-
: secs > 0 ? `${secs}s` : '';
|
|
404
217
|
return html`
|
|
405
218
|
<${Box} marginTop=${1}>
|
|
406
219
|
<${Text} color=${C.accent}><${Spinner} type=${spinnerType()} /><//>
|
|
407
|
-
<${Text} color=${C.accent} bold> ${label}
|
|
408
|
-
<${Text}
|
|
220
|
+
<${Text} color=${C.accent} bold> ${label}<//>
|
|
221
|
+
<${Text} color=${C.gray}>… (${secs}s ${midDot()}esc to interrupt)<//>
|
|
409
222
|
<//>`;
|
|
410
223
|
}
|
|
411
224
|
|
|
@@ -418,7 +231,7 @@ export function Choice({ title, note, options = [], sel = 0, allowText = false,
|
|
|
418
231
|
return html`
|
|
419
232
|
<${Box} flexDirection="column" marginTop=${1} borderStyle=${borderStyle()} borderColor=${C.accent} paddingX=${1}>
|
|
420
233
|
${title ? html`<${Text} color=${C.accent} bold>${glyphs.diamond} ${title}<//>` : null}
|
|
421
|
-
${note ? html`<${Text}
|
|
234
|
+
${note ? html`<${Text} color=${C.gray}>${note}<//>` : null}
|
|
422
235
|
<${Box} flexDirection="column" marginTop=${title || note ? 1 : 0}>
|
|
423
236
|
${options.map((o, i) => {
|
|
424
237
|
const on = !focusText && i === sel;
|
|
@@ -428,330 +241,19 @@ export function Choice({ title, note, options = [], sel = 0, allowText = false,
|
|
|
428
241
|
<${Box} key=${'o' + i}>
|
|
429
242
|
<${Text} color=${on ? C.accent : C.gray}>${on ? glyphs.prompt + ' ' : ' '}<//>
|
|
430
243
|
<${Text} color=${tone} bold=${on}>${label}<//>
|
|
431
|
-
${o.hint ? html`<${Text}
|
|
244
|
+
${o.hint ? html`<${Text} color=${C.gray}> ${dash()} ${o.hint}<//>` : null}
|
|
432
245
|
<//>`;
|
|
433
246
|
})}
|
|
434
247
|
${allowText
|
|
435
248
|
? html`<${Box} key="freetext">
|
|
436
249
|
<${Text} color=${focusText ? C.accent : C.gray}>${focusText ? glyphs.prompt + ' ' : ' '}<//>
|
|
437
|
-
<${Text}
|
|
250
|
+
<${Text} color=${C.gray}>${text ? '' : 'Type a custom answer…'}<//>
|
|
438
251
|
<${Text} color=${C.white}>${text}<//>${focusText ? html`<${Text} inverse> <//>` : null}
|
|
439
252
|
<//>`
|
|
440
253
|
: null}
|
|
441
254
|
<//>
|
|
442
255
|
<${Box} marginTop=${1}>
|
|
443
|
-
<${Text}
|
|
444
|
-
<//>
|
|
445
|
-
<//>`;
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
/**
|
|
449
|
-
* Shell command prompt line — shown in history when the user runs a ! command.
|
|
450
|
-
* Renders as a yellow $ prompt with the command text, matching GitHub Copilot
|
|
451
|
-
* CLI's visual treatment of direct shell commands.
|
|
452
|
-
*/
|
|
453
|
-
export function ShellInput({ cmd }) {
|
|
454
|
-
return html`
|
|
455
|
-
<${Box} marginTop=${1} marginBottom=${0}>
|
|
456
|
-
<${Text} color=${C.orange} bold>$ <//>
|
|
457
|
-
<${Box} flexShrink=${1}>
|
|
458
|
-
<${Text} color=${C.white} bold>${cmd}<//>
|
|
459
|
-
<//>
|
|
460
|
-
<//>`;
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
/**
|
|
464
|
-
* Shell command output — shown below a ShellInput. Uses a left border to
|
|
465
|
-
* visually group the output with the command. Long outputs are capped at
|
|
466
|
-
* 80 lines to keep the terminal readable.
|
|
467
|
-
*/
|
|
468
|
-
export function ShellOut({ ok, output = '' }) {
|
|
469
|
-
const lines = output.split('\n');
|
|
470
|
-
const capped = lines.slice(0, 80);
|
|
471
|
-
const more = lines.length - capped.length;
|
|
472
|
-
return html`
|
|
473
|
-
<${Box} flexDirection="column" marginTop=${0}
|
|
474
|
-
borderStyle="single" borderColor=${ok ? C.gray : C.red}
|
|
475
|
-
borderTop=${false} borderRight=${false} borderBottom=${false}
|
|
476
|
-
paddingLeft=${1}>
|
|
477
|
-
${capped.map((l, i) => html`<${Text} key=${'so' + i} color=${ok ? C.white : C.red}>${l || ' '}<//>` )}
|
|
478
|
-
${more > 0
|
|
479
|
-
? html`<${Text} dimColor>${glyphs.bullet} ${more} more line${more > 1 ? 's' : ''} hidden<//>` : null}
|
|
480
|
-
<//>`;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
// ── Tab bar ──────────────────────────────────────────────────────────────────
|
|
484
|
-
// GitHub Copilot CLI-style tab bar pinned above the content area.
|
|
485
|
-
// Tabs are keyboard-accessible via Ctrl+1/2/3/4 (wired in app.js).
|
|
486
|
-
|
|
487
|
-
/**
|
|
488
|
-
* Top-of-screen tab bar: Session | Monitoring | Issues | Gists
|
|
489
|
-
* Active tab rendered with accent underline + bold; inactive tabs are dimmed.
|
|
490
|
-
* The separator rule beneath uses the brand accent colour.
|
|
491
|
-
*/
|
|
492
|
-
export function TabBar({ tabs = [], active = 0, onSelect, width = 80 }) {
|
|
493
|
-
const A = asciiSafe();
|
|
494
|
-
const sep = A ? '-' : '─';
|
|
495
|
-
return html`
|
|
496
|
-
<${Box} flexDirection="column" width=${width}>
|
|
497
|
-
<${Box} flexDirection="row" paddingX=${1} paddingTop=${0}>
|
|
498
|
-
${tabs.map((label, i) => {
|
|
499
|
-
const on = i === active;
|
|
500
|
-
return html`
|
|
501
|
-
<${Box} key=${'tab' + i} marginRight=${2}>
|
|
502
|
-
<${Text}
|
|
503
|
-
color=${on ? C.accent : C.gray}
|
|
504
|
-
bold=${on}
|
|
505
|
-
dimColor=${!on}
|
|
506
|
-
underline=${on}
|
|
507
|
-
>${on ? glyphs.prompt + ' ' : ' '}${label}<//><//>`;
|
|
508
|
-
})}
|
|
509
|
-
<${Box} flexGrow=${1}>
|
|
510
|
-
<${Text} dimColor> click or Ctrl+${tabs.map((_, i) => i + 1).join('/')}<//><//>
|
|
511
|
-
<//>
|
|
512
|
-
<${Text} color=${C.accent} dimColor>${sep.repeat(Math.max(8, width - 2))}<//>
|
|
513
|
-
<//>`;
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
// ── Monitoring tab ────────────────────────────────────────────────────────────
|
|
517
|
-
|
|
518
|
-
/**
|
|
519
|
-
* Auto-fetch /api/v2/monitoring/devices and /api/v2/monitoring/status-checks.
|
|
520
|
-
* Refreshes every 30 seconds.
|
|
521
|
-
*/
|
|
522
|
-
export function MonitoringView({ cfg }) {
|
|
523
|
-
const [data, setData] = useState(null); // { devices, checks }
|
|
524
|
-
const [loading, setLoading] = useState(true);
|
|
525
|
-
const [err, setErr] = useState(null);
|
|
526
|
-
const [lastAt, setLastAt] = useState(null);
|
|
527
|
-
const timerRef = useRef(null);
|
|
528
|
-
|
|
529
|
-
const fetch_ = useCallback(async () => {
|
|
530
|
-
if (!cfg?.url || !cfg?.token) {
|
|
531
|
-
setErr('No server configured. Run icli login first.');
|
|
532
|
-
setLoading(false);
|
|
533
|
-
return;
|
|
534
|
-
}
|
|
535
|
-
try {
|
|
536
|
-
const headers = { Authorization: `Bearer ${cfg.token}`, Accept: 'application/json' };
|
|
537
|
-
const [devRes, chkRes] = await Promise.all([
|
|
538
|
-
fetch(`${cfg.url}/api/v2/monitoring/devices`, { headers }).catch(() => null),
|
|
539
|
-
fetch(`${cfg.url}/api/v2/monitoring/status-checks`, { headers }).catch(() => null),
|
|
540
|
-
]);
|
|
541
|
-
const devices = devRes?.ok ? ((await devRes.json()).data || []) : [];
|
|
542
|
-
const checks = chkRes?.ok ? ((await chkRes.json()).data || []) : [];
|
|
543
|
-
setData({ devices, checks });
|
|
544
|
-
setErr(null);
|
|
545
|
-
} catch (e) {
|
|
546
|
-
setErr(e.message || 'Fetch failed');
|
|
547
|
-
} finally {
|
|
548
|
-
setLoading(false);
|
|
549
|
-
setLastAt(new Date().toLocaleTimeString());
|
|
550
|
-
}
|
|
551
|
-
}, [cfg]);
|
|
552
|
-
|
|
553
|
-
useEffect(() => {
|
|
554
|
-
fetch_();
|
|
555
|
-
timerRef.current = setInterval(fetch_, 30000);
|
|
556
|
-
return () => clearInterval(timerRef.current);
|
|
557
|
-
}, [fetch_]);
|
|
558
|
-
|
|
559
|
-
if (loading) return html`
|
|
560
|
-
<${Box} marginTop=${2} paddingX=${2}>
|
|
561
|
-
<${Text} color=${C.accent}><${Spinner} type=${spinnerType()} /><//>
|
|
562
|
-
<${Text} color=${C.gray}> Fetching device status…<//>
|
|
563
|
-
<//>`;
|
|
564
|
-
|
|
565
|
-
if (err) return html`
|
|
566
|
-
<${Box} marginTop=${2} paddingX=${2} flexDirection="column">
|
|
567
|
-
<${Text} color=${C.red}>${glyphs.cross} Monitoring error: ${err}<//>
|
|
568
|
-
<${Text} dimColor>Make sure the server is reachable and you are logged in.<//>
|
|
569
|
-
<//>`;
|
|
570
|
-
|
|
571
|
-
const { devices = [], checks = [] } = data || {};
|
|
572
|
-
const devUp = devices.filter((d) => String(d.status).toLowerCase() === 'up' || d.status === 1).length;
|
|
573
|
-
const devDown = devices.length - devUp;
|
|
574
|
-
const chkOk = checks.filter((c) => c.latest_result?.status === 'up').length;
|
|
575
|
-
const chkFail = checks.filter((c) => c.latest_result?.status !== 'up' && c.latest_result).length;
|
|
576
|
-
|
|
577
|
-
return html`
|
|
578
|
-
<${Box} flexDirection="column" paddingX=${2} marginTop=${1}>
|
|
579
|
-
<${Box} marginBottom=${1}>
|
|
580
|
-
<${Text} color=${C.accent} bold>Network Monitoring<//>
|
|
581
|
-
${lastAt ? html`<${Text} dimColor> · refreshes every 30s · last: ${lastAt}<//>` : null}
|
|
582
|
-
<//>
|
|
583
|
-
|
|
584
|
-
${/* ── Device summary ── */ null}
|
|
585
|
-
<${Box} marginBottom=${1} flexDirection="row" gap=${3}>
|
|
586
|
-
<${Box} flexDirection="column">
|
|
587
|
-
<${Text} dimColor>Managed Devices<//>
|
|
588
|
-
<${Box}>
|
|
589
|
-
<${Text} color=${C.green} bold>${devUp}<//>
|
|
590
|
-
<${Text} color=${C.gray}> up <//>
|
|
591
|
-
<${Text} color=${devDown > 0 ? C.red : C.gray} bold=${devDown > 0}>${devDown}<//>
|
|
592
|
-
<${Text} color=${C.gray}> down<//>
|
|
593
|
-
<//>
|
|
594
|
-
<//>
|
|
595
|
-
<${Box} flexDirection="column">
|
|
596
|
-
<${Text} dimColor>Status Checks<//>
|
|
597
|
-
<${Box}>
|
|
598
|
-
<${Text} color=${C.green} bold>${chkOk}<//>
|
|
599
|
-
<${Text} color=${C.gray}> ok <//>
|
|
600
|
-
<${Text} color=${chkFail > 0 ? C.red : C.gray} bold=${chkFail > 0}>${chkFail}<//>
|
|
601
|
-
<${Text} color=${C.gray}> fail<//>
|
|
602
|
-
<//>
|
|
603
|
-
<//>
|
|
604
|
-
<//>
|
|
605
|
-
|
|
606
|
-
${/* ── Down devices ── */ null}
|
|
607
|
-
${devDown > 0
|
|
608
|
-
? html`<${Box} flexDirection="column" marginBottom=${1}>
|
|
609
|
-
<${Text} color=${C.red} bold>${glyphs.warn} Down devices<//>
|
|
610
|
-
${devices.filter((d) => String(d.status).toLowerCase() !== 'up' && d.status !== 1).slice(0, 10).map((d, i) =>
|
|
611
|
-
html`<${Box} key=${'dd' + i}>
|
|
612
|
-
<${Text} color=${C.red}> ${glyphs.cross} <//>
|
|
613
|
-
<${Text} color=${C.white}>${d.name || d.ip_address}<//>
|
|
614
|
-
<${Text} dimColor> ${d.device_type || ''} ${d.ip_address}<//>
|
|
615
|
-
<//>` )}
|
|
616
|
-
<//>` : null}
|
|
617
|
-
|
|
618
|
-
${/* ── Failed checks ── */ null}
|
|
619
|
-
${chkFail > 0
|
|
620
|
-
? html`<${Box} flexDirection="column">
|
|
621
|
-
<${Text} color=${C.yellow} bold>${glyphs.warn} Failed checks<//>
|
|
622
|
-
${checks.filter((c) => c.latest_result?.status !== 'up' && c.latest_result).slice(0, 10).map((c, i) =>
|
|
623
|
-
html`<${Box} key=${'cf' + i}>
|
|
624
|
-
<${Text} color=${C.yellow}> ${glyphs.warn} <//>
|
|
625
|
-
<${Text} color=${C.white}>${c.name}<//>
|
|
626
|
-
<${Text} dimColor> ${c.target || ''} ${c.latest_result?.status || 'unknown'}<//>
|
|
627
|
-
<//>` )}
|
|
628
|
-
<//>` : null}
|
|
629
|
-
|
|
630
|
-
${devDown === 0 && chkFail === 0
|
|
631
|
-
? html`<${Box}><${Text} color=${C.green}>${glyphs.check} All devices and checks are up<//> <//>`
|
|
632
|
-
: null}
|
|
633
|
-
|
|
634
|
-
<${Box} marginTop=${1}>
|
|
635
|
-
<${Text} dimColor>Switch to Session tab (Ctrl+1) · type /check in Session for details<//>
|
|
636
|
-
<//>
|
|
637
|
-
<//>`;
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
// ── Issues tab ────────────────────────────────────────────────────────────────
|
|
641
|
-
|
|
642
|
-
/**
|
|
643
|
-
* Shows AI-flagged issues collected during the current session.
|
|
644
|
-
* Issues are populated by app.js whenever the AI notices an error/alarm.
|
|
645
|
-
*/
|
|
646
|
-
export function IssuesView({ issues = [] }) {
|
|
647
|
-
if (!issues.length) return html`
|
|
648
|
-
<${Box} flexDirection="column" paddingX=${2} marginTop=${2}>
|
|
649
|
-
<${Text} color=${C.accent} bold>Issues<//>
|
|
650
|
-
<${Box} marginTop=${1}>
|
|
651
|
-
<${Text} dimColor>${glyphs.bullet} No issues flagged in this session.<//>
|
|
652
|
-
<//>
|
|
653
|
-
<${Box} marginTop=${1}>
|
|
654
|
-
<${Text} dimColor>Run /alarms or /check in the Session tab to detect problems.<//>
|
|
655
|
-
<//>
|
|
656
|
-
<//>`;
|
|
657
|
-
|
|
658
|
-
return html`
|
|
659
|
-
<${Box} flexDirection="column" paddingX=${2} marginTop=${1}>
|
|
660
|
-
<${Text} color=${C.accent} bold>Issues <${Text} dimColor>(${issues.length} flagged this session)<//><//>
|
|
661
|
-
<${Box} flexDirection="column" marginTop=${1}>
|
|
662
|
-
${issues.slice().reverse().slice(0, 20).map((iss, i) => html`
|
|
663
|
-
<${Box} key=${'iss' + i} flexDirection="column" marginBottom=${1}>
|
|
664
|
-
<${Box}>
|
|
665
|
-
<${Text} color=${iss.severity === 'high' ? C.red : iss.severity === 'medium' ? C.yellow : C.gray}
|
|
666
|
-
>${iss.severity === 'high' ? glyphs.cross : glyphs.warn} <//>
|
|
667
|
-
<${Text} color=${C.white} bold>${iss.title}<//>
|
|
668
|
-
<${Text} dimColor> ${iss.ts || ''}<//>
|
|
669
|
-
<//>
|
|
670
|
-
${iss.body ? html`<${Text} dimColor> ${iss.body.slice(0, 120)}<//>` : null}
|
|
671
|
-
<//>` )}
|
|
672
|
-
<//>
|
|
673
|
-
<//>`;
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
// ── Gists tab ─────────────────────────────────────────────────────────────────
|
|
677
|
-
|
|
678
|
-
/**
|
|
679
|
-
* Lists GitHub gists created with /gist during this session or stored locally.
|
|
680
|
-
* If no GitHub token is set, shows instructions.
|
|
681
|
-
*/
|
|
682
|
-
export function GistsView({ cfg }) {
|
|
683
|
-
const [gists, setGists] = useState([]);
|
|
684
|
-
const [loading, setLoading] = useState(true);
|
|
685
|
-
const [err, setErr] = useState(null);
|
|
686
|
-
|
|
687
|
-
useEffect(() => {
|
|
688
|
-
const token = cfg?.github_token;
|
|
689
|
-
if (!token) {
|
|
690
|
-
setLoading(false);
|
|
691
|
-
return;
|
|
692
|
-
}
|
|
693
|
-
fetch('https://api.github.com/gists?per_page=20', {
|
|
694
|
-
headers: { Authorization: `token ${token}`, Accept: 'application/vnd.github.v3+json' },
|
|
695
|
-
})
|
|
696
|
-
.then((r) => (r.ok ? r.json() : Promise.reject(new Error(`HTTP ${r.status}`))))
|
|
697
|
-
.then((data) => {
|
|
698
|
-
const icliGists = Array.isArray(data)
|
|
699
|
-
? data.filter((g) => (g.description || '').startsWith('[icli]') || (g.description || '').includes('iCopilot'))
|
|
700
|
-
: [];
|
|
701
|
-
setGists(icliGists.length ? icliGists : data.slice(0, 10));
|
|
702
|
-
})
|
|
703
|
-
.catch((e) => setErr(e.message))
|
|
704
|
-
.finally(() => setLoading(false));
|
|
705
|
-
}, [cfg?.github_token]);
|
|
706
|
-
|
|
707
|
-
if (!cfg?.github_token) return html`
|
|
708
|
-
<${Box} flexDirection="column" paddingX=${2} marginTop=${2}>
|
|
709
|
-
<${Text} color=${C.accent} bold>Gists<//>
|
|
710
|
-
<${Box} marginTop=${1} flexDirection="column">
|
|
711
|
-
<${Text} dimColor>No GitHub token configured.<//>
|
|
712
|
-
<${Text} dimColor>Run: ${''}<//>
|
|
713
|
-
<${Text} color=${C.cyan}> /gist token YOUR_TOKEN<//>
|
|
714
|
-
<${Text} dimColor>Then use ${''}<//>
|
|
715
|
-
<${Text} color=${C.cyan}> /gist<//>
|
|
716
|
-
<${Text} dimColor> in Session to save AI answers as secret gists.<//>
|
|
717
|
-
<//>
|
|
718
|
-
<//>`;
|
|
719
|
-
|
|
720
|
-
if (loading) return html`
|
|
721
|
-
<${Box} paddingX=${2} marginTop=${2}>
|
|
722
|
-
<${Text} color=${C.accent}><${Spinner} type=${spinnerType()} /><//>
|
|
723
|
-
<${Text} color=${C.gray}> Loading gists…<//>
|
|
724
|
-
<//>`;
|
|
725
|
-
|
|
726
|
-
if (err) return html`
|
|
727
|
-
<${Box} paddingX=${2} marginTop=${2} flexDirection="column">
|
|
728
|
-
<${Text} color=${C.red}>${glyphs.cross} GitHub error: ${err}<//>
|
|
729
|
-
<//>`;
|
|
730
|
-
|
|
731
|
-
if (!gists.length) return html`
|
|
732
|
-
<${Box} paddingX=${2} marginTop=${2} flexDirection="column">
|
|
733
|
-
<${Text} color=${C.accent} bold>Gists<//>
|
|
734
|
-
<${Box} marginTop=${1}><${Text} dimColor>No gists yet. Use /gist in Session to save an answer.<//> <//><//>
|
|
735
|
-
<//>`;
|
|
736
|
-
|
|
737
|
-
return html`
|
|
738
|
-
<${Box} flexDirection="column" paddingX=${2} marginTop=${1}>
|
|
739
|
-
<${Text} color=${C.accent} bold>Gists <${Text} dimColor>(${gists.length} recent)<//><//>
|
|
740
|
-
<${Box} flexDirection="column" marginTop=${1}>
|
|
741
|
-
${gists.slice(0, 15).map((g, i) => {
|
|
742
|
-
const fname = Object.keys(g.files || {})[0] || 'unknown';
|
|
743
|
-
const desc = (g.description || fname).slice(0, 60);
|
|
744
|
-
const date = g.updated_at ? g.updated_at.slice(0, 10) : '';
|
|
745
|
-
return html`
|
|
746
|
-
<${Box} key=${'g' + i} marginBottom=${0}>
|
|
747
|
-
<${Text} color=${C.cyan}>${glyphs.bullet} <//>
|
|
748
|
-
<${Text} color=${C.white}>${desc}<//>
|
|
749
|
-
<${Text} dimColor> ${date}<//>
|
|
750
|
-
<//>
|
|
751
|
-
<${Box} key=${'gu' + i}>
|
|
752
|
-
<${Text} dimColor> ${g.html_url || ''}<//>
|
|
753
|
-
<//>`;
|
|
754
|
-
})}
|
|
256
|
+
<${Text} color=${C.gray}>${glyphs.up}${glyphs.down} choose ${midDot()}${allowText ? 'Tab type ' + midDot() : ''}Enter confirm ${midDot()}Esc dismiss<//>
|
|
755
257
|
<//>
|
|
756
258
|
<//>`;
|
|
757
259
|
}
|