aegiscode 6.0.0 → 6.1.0
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/README.md +69 -45
- package/package.json +2 -2
- package/scripts/demo.mjs +1 -1
- package/src/app.js +100 -24
- package/src/art.js +219 -29
- package/src/commands.js +150 -50
- package/src/fuzzy.js +116 -0
- package/src/markdown.js +303 -0
- package/src/overlays.js +286 -0
- package/src/render.js +235 -120
- package/src/screen.js +196 -21
- package/src/theme.js +102 -66
package/src/render.js
CHANGED
|
@@ -1,67 +1,150 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* The CLI's renderers
|
|
5
|
-
*
|
|
4
|
+
* The CLI's renderers — the start-up banner, transcript turns, the per-turn
|
|
5
|
+
* accounting line, the status bar, the live working line and the transcript's
|
|
6
|
+
* markdown body. The presentation layer is a fidelity port of
|
|
7
|
+
* `aegiscodex-dev`: gold `━`/`─` rules, the two-tone welcome mark (gold mascot,
|
|
8
|
+
* blue/lavender/dim whale), the coral-bold welcome title, the `❯` prompt, the
|
|
9
|
+
* `⎿` hook rows, the `●` answer/stream cursor and the `✻` done glyph.
|
|
6
10
|
*
|
|
7
|
-
* Every function returns
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
+
* Every function returns lines (strings carrying SGR escapes) rather than
|
|
12
|
+
* writing to stdout, so the whole surface is assertable from a plain Node test
|
|
13
|
+
* with no TTY and no child process — the same split the desktop uses for its
|
|
14
|
+
* pure renderer modules. The markdown body is delegated to `./markdown.js`
|
|
15
|
+
* (returning span lines); the require is guarded so this file always loads even
|
|
16
|
+
* if that module is absent, falling back to the built-in `mdLines`.
|
|
11
17
|
*/
|
|
12
18
|
|
|
13
|
-
const {
|
|
14
|
-
|
|
15
|
-
|
|
19
|
+
const {
|
|
20
|
+
RGB,
|
|
21
|
+
BG,
|
|
22
|
+
RESET,
|
|
23
|
+
BOLD,
|
|
24
|
+
DIM,
|
|
25
|
+
ITALIC,
|
|
26
|
+
UNDER,
|
|
27
|
+
RESET_FG,
|
|
28
|
+
RESET_BG,
|
|
29
|
+
GLYPH,
|
|
30
|
+
VERBS,
|
|
31
|
+
DONE_VERBS,
|
|
32
|
+
themeOf,
|
|
33
|
+
} = require('./theme.js');
|
|
34
|
+
const { WELCOME_TITLE, WELCOME_BACK, TAGLINE, welcomeArtParts } = require('./art.js');
|
|
35
|
+
const { w, pad, padStart, wrapBlock, clip, span } = require('./screen.js');
|
|
16
36
|
const { fmtTokens, fmtEur, fmtElapsed } = require('./format.js');
|
|
17
37
|
|
|
18
|
-
|
|
38
|
+
// Guarded: a concurrent workstream owns ./markdown.js.
|
|
39
|
+
let markdown = null;
|
|
40
|
+
try {
|
|
41
|
+
// eslint-disable-next-line global-require
|
|
42
|
+
markdown = require('./markdown.js');
|
|
43
|
+
} catch {
|
|
44
|
+
markdown = null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Error mark — the reference's failed-check glyph (theme.js has no GLYPH.err). */
|
|
48
|
+
const ERR = '✗';
|
|
49
|
+
/** Warning mark — the reference's ⚠ (theme.js has no GLYPH.warn). */
|
|
50
|
+
const WARN = '⚠';
|
|
51
|
+
|
|
52
|
+
const fg = (t, c) => `${c}`;
|
|
19
53
|
const bg = (c) => BG(...c);
|
|
20
54
|
|
|
55
|
+
/** Flatten a span line back to an ANSI string (what the transcript emits). */
|
|
56
|
+
const spansToString = (line) => line.map((sp) => (sp.s || '') + sp.t).join('') + RESET;
|
|
57
|
+
|
|
58
|
+
/** Centre plain `text` inside `width`, painted with `style` (never overflows). */
|
|
59
|
+
function centerStyled(text, width, style) {
|
|
60
|
+
const tt = clip(text, width);
|
|
61
|
+
const p = Math.max(0, Math.floor((width - w(tt)) / 2));
|
|
62
|
+
return ' '.repeat(p) + style + tt + RESET;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The right (moon + whale) half of the mark: ▓ blue, ▒ lavender, ░ dim,
|
|
66
|
+
* eye white, stars gold. */
|
|
67
|
+
function tintWhale(str, t) {
|
|
68
|
+
const map = { '▓': t.blue, '▒': t.lavender, '░': t.dim, '█': t.white, '✦': t.gold, '·': t.dim };
|
|
69
|
+
const spans = [];
|
|
70
|
+
let cur = null;
|
|
71
|
+
let style = '';
|
|
72
|
+
for (const ch of str) {
|
|
73
|
+
const s = map[ch] != null ? map[ch] : '';
|
|
74
|
+
if (cur !== null && s === style) {
|
|
75
|
+
cur += ch;
|
|
76
|
+
} else {
|
|
77
|
+
if (cur !== null) spans.push(span(style, cur));
|
|
78
|
+
cur = ch;
|
|
79
|
+
style = s;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (cur !== null) spans.push(span(style, cur));
|
|
83
|
+
return spans;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** One centred welcome-art row: gold mascot on the left, tinted whale right. */
|
|
87
|
+
function artRow(ctx, row, parts, leftPad) {
|
|
88
|
+
const t = themeOf(ctx);
|
|
89
|
+
const left = row[0] || '';
|
|
90
|
+
const right = row[1] || '';
|
|
91
|
+
const spans = [span('', ' '.repeat(leftPad))];
|
|
92
|
+
if (left && right) {
|
|
93
|
+
spans.push(span(t.gold, left));
|
|
94
|
+
spans.push(span('', ' '.repeat(parts.gutter)));
|
|
95
|
+
spans.push(...tintWhale(right, t));
|
|
96
|
+
} else if (left) {
|
|
97
|
+
spans.push(span(t.gold, left));
|
|
98
|
+
if (w(left) < parts.width) spans.push(span('', ' '.repeat(parts.width - w(left))));
|
|
99
|
+
} else if (right) {
|
|
100
|
+
spans.push(...tintWhale(right, t));
|
|
101
|
+
if (w(right) < parts.width) spans.push(span('', ' '.repeat(parts.width - w(right))));
|
|
102
|
+
} else {
|
|
103
|
+
spans.push(span('', ' '.repeat(parts.width)));
|
|
104
|
+
}
|
|
105
|
+
return spansToString(spans);
|
|
106
|
+
}
|
|
107
|
+
|
|
21
108
|
/**
|
|
22
|
-
* The start-up banner
|
|
23
|
-
*
|
|
24
|
-
*
|
|
109
|
+
* The start-up banner, matching aegiscodex-dev's welcome legs: a gold `━`+`─`
|
|
110
|
+
* rule across the full terminal, the centred welcome mark (mascot half gold,
|
|
111
|
+
* moon/whale half blue/lavender/dim), the coral-bold welcome title and a
|
|
112
|
+
* version line, then the compact identity block.
|
|
25
113
|
*/
|
|
26
114
|
function renderBanner(ctx, info = {}) {
|
|
27
115
|
const t = themeOf(ctx);
|
|
28
|
-
const width = Number(info.width) || 80;
|
|
116
|
+
const width = Math.max(20, Number(info.width) || 80);
|
|
29
117
|
const lines = [];
|
|
30
118
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
// Paint the core cell in the secondary colour so the mark reads as a
|
|
43
|
-
// signal rather than a solid block.
|
|
44
|
-
let painted = '';
|
|
45
|
-
[...row].forEach((ch, c) => {
|
|
46
|
-
painted += coreCols.includes(c) ? fg(t, t.beam) + ch + fg(t, t.plasma) : ch;
|
|
47
|
-
});
|
|
48
|
-
lines.push(fg(t, t.plasma) + painted + RESET);
|
|
49
|
-
}
|
|
50
|
-
lines.push('');
|
|
51
|
-
lines.push(' '.repeat(Math.max(0, Math.floor((width - w(WORDMARK)) / 2))) + fg(t, t.plasma) + BOLD + WORDMARK + RESET);
|
|
52
|
-
lines.push(' '.repeat(Math.max(0, Math.floor((width - w(TAGLINE)) / 2))) + fg(t, t.muted) + TAGLINE + RESET);
|
|
53
|
-
lines.push('');
|
|
119
|
+
// Full-width gold header rule.
|
|
120
|
+
lines.push(`${t.gold}━${'─'.repeat(Math.max(0, width - 2))}━${RESET}`);
|
|
121
|
+
lines.push('');
|
|
122
|
+
|
|
123
|
+
const parts = welcomeArtParts(width);
|
|
124
|
+
if (width >= parts.width + 2) {
|
|
125
|
+
const leftPad = Math.max(0, Math.floor((width - parts.width) / 2));
|
|
126
|
+
for (const row of parts.rows) lines.push(artRow(ctx, row, parts, leftPad));
|
|
127
|
+
} else {
|
|
128
|
+
// Too narrow for the mark: a compact gold wordmark keeps every row in width.
|
|
129
|
+
lines.push(centerStyled('AEGIS CODE', width, t.gold + BOLD));
|
|
54
130
|
}
|
|
131
|
+
lines.push('');
|
|
132
|
+
|
|
133
|
+
const title = info.firstRun === false ? WELCOME_BACK : WELCOME_TITLE;
|
|
134
|
+
lines.push(centerStyled(title, width, t.coral + BOLD));
|
|
135
|
+
const verLine = [info.version ? `v${info.version}` : null, TAGLINE].filter(Boolean).join(` ${GLYPH.bullet} `);
|
|
136
|
+
lines.push(centerStyled(verLine, width, t.gray));
|
|
137
|
+
lines.push('');
|
|
55
138
|
|
|
56
139
|
lines.push(...renderIdentityBox(ctx, info, width));
|
|
57
140
|
return lines;
|
|
58
141
|
}
|
|
59
142
|
|
|
60
|
-
/** The
|
|
61
|
-
|
|
143
|
+
/** The identity panel: what you are talking to, and as what. Routed through
|
|
144
|
+
* renderHeading so it shares the CLI's ruled-section look (no rounded frame). */
|
|
145
|
+
function renderIdentityBox(ctx, info = {}, width = 80) {
|
|
62
146
|
const t = themeOf(ctx);
|
|
63
|
-
const
|
|
64
|
-
const inner = boxW - 2;
|
|
147
|
+
const lines = [renderHeading(ctx, 'aegiscode', width)];
|
|
65
148
|
|
|
66
149
|
const rows = [
|
|
67
150
|
['version', info.version ? `v${info.version}` : null],
|
|
@@ -71,30 +154,18 @@ function renderIdentityBox(ctx, info, width) {
|
|
|
71
154
|
['render', info.stream === false ? 'buffered' : 'streaming'],
|
|
72
155
|
].filter(([, v]) => v);
|
|
73
156
|
|
|
74
|
-
const title = ' aegiscode ';
|
|
75
|
-
const headFill = Math.max(0, inner - w(title) - 1);
|
|
76
|
-
const out = [
|
|
77
|
-
fg(t, t.plasma) + GLYPH.box.tl + GLYPH.box.h + BOLD + title + RESET + fg(t, t.plasma) + GLYPH.box.h.repeat(headFill) + GLYPH.box.tr + RESET,
|
|
78
|
-
];
|
|
79
157
|
for (const [k, v] of rows) {
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
// + 1 border = boxW, so the value gets `inner - 11` cells.
|
|
83
|
-
const value = clip(String(v), Math.max(0, inner - 11));
|
|
84
|
-
out.push(
|
|
85
|
-
fg(t, t.plasma) + GLYPH.box.v + RESET + ' ' + label + ' ' + fg(t, t.text) + value + RESET +
|
|
86
|
-
' '.repeat(Math.max(0, inner - 11 - w(value))) + ' ' + fg(t, t.plasma) + GLYPH.box.v + RESET
|
|
87
|
-
);
|
|
158
|
+
const value = clip(String(v), Math.max(0, width - 12));
|
|
159
|
+
lines.push(` ${t.gray}${k.padEnd(8)}${RESET}${t.white}${value}${RESET}`);
|
|
88
160
|
}
|
|
89
|
-
out.push(fg(t, t.plasma) + GLYPH.box.bl + GLYPH.box.h.repeat(inner) + GLYPH.box.br + RESET);
|
|
90
|
-
// Clip the hint as plain text *before* styling — clipping an already-styled
|
|
91
|
-
// line would cut through an escape sequence and bleed colour.
|
|
92
161
|
const hint = clip(` type /help for commands ${GLYPH.bullet} /quit to exit`, width);
|
|
93
|
-
|
|
94
|
-
|
|
162
|
+
lines.push('');
|
|
163
|
+
lines.push(`${t.dim}${hint}${RESET}`);
|
|
164
|
+
return lines;
|
|
95
165
|
}
|
|
96
166
|
|
|
97
|
-
/** Markdown-lite body
|
|
167
|
+
/** Markdown-lite body (fallback when ./markdown.js is unavailable): fenced
|
|
168
|
+
* code, bullets, headings, inline code/bold. */
|
|
98
169
|
function mdLines(text, width, ctx) {
|
|
99
170
|
const t = themeOf(ctx);
|
|
100
171
|
const out = [];
|
|
@@ -108,7 +179,7 @@ function mdLines(text, width, ctx) {
|
|
|
108
179
|
continue;
|
|
109
180
|
}
|
|
110
181
|
if (inFence) {
|
|
111
|
-
for (const l of wrapBlock(raw, bodyW)) out.push(
|
|
182
|
+
for (const l of wrapBlock(raw, bodyW)) out.push(`${t.gray} ${l}${RESET}`);
|
|
112
183
|
continue;
|
|
113
184
|
}
|
|
114
185
|
if (raw.trim() === '') {
|
|
@@ -119,13 +190,13 @@ function mdLines(text, width, ctx) {
|
|
|
119
190
|
if (bullet) {
|
|
120
191
|
const indent = ' '.repeat(Math.min(6, bullet[1].length));
|
|
121
192
|
for (const l of wrapBlock(inline(bullet[2], ctx), bodyW - 2)) {
|
|
122
|
-
out.push(`${indent}${
|
|
193
|
+
out.push(`${indent}${t.gray}${GLYPH.bullet}${RESET} ${l}`);
|
|
123
194
|
}
|
|
124
195
|
continue;
|
|
125
196
|
}
|
|
126
197
|
const heading = /^(#{1,6})\s+(.*)$/.exec(raw);
|
|
127
198
|
if (heading) {
|
|
128
|
-
out.push(
|
|
199
|
+
out.push(`${t.gold}${BOLD}${inline(heading[2], ctx)}${RESET}`);
|
|
129
200
|
continue;
|
|
130
201
|
}
|
|
131
202
|
for (const l of wrapBlock(inline(raw, ctx), bodyW, '')) out.push(l);
|
|
@@ -133,111 +204,146 @@ function mdLines(text, width, ctx) {
|
|
|
133
204
|
return out;
|
|
134
205
|
}
|
|
135
206
|
|
|
136
|
-
/** Inline span treatment: `code` in
|
|
207
|
+
/** Inline span treatment: `code` in green, **bold** in bold. */
|
|
137
208
|
function inline(text, ctx) {
|
|
138
209
|
const t = themeOf(ctx);
|
|
139
|
-
return String(text)
|
|
140
|
-
.replace(/`([^`]+)`/g, (_, code) =>
|
|
141
|
-
.replace(/\*\*([^*]+)\*\*/g, (_, bold) => BOLD
|
|
210
|
+
return String(text == null ? '' : text)
|
|
211
|
+
.replace(/`([^`]+)`/g, (_, code) => `${t.green}${code}${RESET}`)
|
|
212
|
+
.replace(/\*\*([^*]+)\*\*/g, (_, bold) => `${BOLD}${bold}${RESET}`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Assistant body: ./markdown.js span lines flattened to ANSI strings, else the
|
|
216
|
+
* plain mdLines fallback. */
|
|
217
|
+
function assistantLines(ctx, text, width) {
|
|
218
|
+
if (markdown && typeof markdown.renderMarkdown === 'function') {
|
|
219
|
+
return markdown.renderMarkdown(String(text == null ? '' : text), width, ctx).map(spansToString);
|
|
220
|
+
}
|
|
221
|
+
return mdLines(text, width, ctx);
|
|
142
222
|
}
|
|
143
223
|
|
|
144
224
|
/**
|
|
145
|
-
* One transcript turn
|
|
146
|
-
*
|
|
225
|
+
* One transcript turn: a `❯`-prefixed user line, an assistant answer whose
|
|
226
|
+
* first line carries the `●` marker (and a `●` cursor while streaming), a tool
|
|
227
|
+
* line with its `⎿ $ …` hook row, and each turn's accounting line beneath.
|
|
147
228
|
*/
|
|
148
229
|
function renderTurn(ctx, turn, width = 80) {
|
|
149
230
|
const t = themeOf(ctx);
|
|
150
231
|
const role = turn.role || 'assistant';
|
|
151
|
-
const
|
|
232
|
+
const text = String(turn.text == null ? '' : turn.text);
|
|
152
233
|
const lines = [];
|
|
153
234
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
235
|
+
if (role === 'user') {
|
|
236
|
+
const body = wrapBlock(text, Math.max(8, width - 2));
|
|
237
|
+
body.forEach((l, i) => {
|
|
238
|
+
if (i === 0) lines.push(`${t.gray}${GLYPH.cursor}${RESET} ${t.white}${l}${RESET}`);
|
|
239
|
+
else lines.push(` ${t.white}${l}${RESET}`);
|
|
240
|
+
});
|
|
241
|
+
} else if (role === 'assistant' || role === 'system') {
|
|
242
|
+
const body = assistantLines(ctx, text, Math.max(8, width - 2));
|
|
243
|
+
if (!body.length) body.push('');
|
|
244
|
+
body[0] = `${t.white}${GLYPH.block}${RESET} ` + body[0];
|
|
245
|
+
if (turn.streaming) body[body.length - 1] += `${t.white}${GLYPH.block}${RESET}`;
|
|
246
|
+
lines.push(...body);
|
|
247
|
+
} else if (role === 'tool') {
|
|
248
|
+
lines.push(`${t.white}${GLYPH.block}${RESET} ${t.gray}${turn.label || 'tool'}${RESET}`);
|
|
249
|
+
const args =
|
|
250
|
+
turn.args == null
|
|
251
|
+
? ''
|
|
252
|
+
: typeof turn.args === 'string'
|
|
253
|
+
? turn.args
|
|
254
|
+
: JSON.stringify(turn.args);
|
|
255
|
+
if (args) lines.push(` ${t.gray}${GLYPH.hook} $ ${clip(args, Math.max(0, width - 6))}${RESET}`);
|
|
256
|
+
} else if (role === 'error') {
|
|
257
|
+
lines.push(`${t.red}${ERR} ${text}${RESET}`);
|
|
258
|
+
} else {
|
|
259
|
+
for (const l of wrapBlock(text, Math.max(8, width - 2))) lines.push(`${t.white}${l}${RESET}`);
|
|
260
|
+
}
|
|
166
261
|
|
|
167
|
-
if (turn.meta)
|
|
262
|
+
if (turn.meta) {
|
|
263
|
+
const m = renderMeta(ctx, turn.meta, width);
|
|
264
|
+
if (m) lines.push(m);
|
|
265
|
+
}
|
|
168
266
|
return lines;
|
|
169
267
|
}
|
|
170
268
|
|
|
171
269
|
/**
|
|
172
|
-
* The per-turn accounting line — tokens
|
|
173
|
-
*
|
|
270
|
+
* The per-turn accounting line — tokens beside what they cost, in the `⎿` hook
|
|
271
|
+
* row style, e.g. `⎿ 1,562 tok · 1,250/312 · €0.0007 · 4.2s`.
|
|
174
272
|
*/
|
|
175
|
-
function renderMeta(ctx, meta, width = 80) {
|
|
273
|
+
function renderMeta(ctx, meta = {}, width = 80) {
|
|
176
274
|
const t = themeOf(ctx);
|
|
177
275
|
const bits = [];
|
|
178
|
-
if (meta.model) bits.push(
|
|
179
|
-
|
|
180
|
-
if (tokens) bits.push(fg(t, t.text) + tokens + RESET);
|
|
276
|
+
if (meta.model) bits.push(`${t.blue}${meta.model}${RESET}`);
|
|
277
|
+
if (meta.tokens != null) bits.push(`${t.white}${fmtTokens(meta.tokens)} tok${RESET}`);
|
|
181
278
|
if (meta.usage) {
|
|
182
279
|
const { input, output } = meta.usage;
|
|
183
280
|
if (Number.isFinite(input) || Number.isFinite(output)) {
|
|
184
|
-
bits.push(
|
|
281
|
+
bits.push(`${t.gray}${fmtTokens(input || 0)}/${fmtTokens(output || 0)}${RESET}`);
|
|
185
282
|
}
|
|
186
283
|
}
|
|
187
|
-
if (meta.eur != null) bits.push(
|
|
188
|
-
if (meta.ms != null) bits.push(
|
|
189
|
-
if (meta.calls > 1) bits.push(
|
|
284
|
+
if (meta.eur != null) bits.push(`${meta.eur > 0 ? t.coral : t.green}${fmtEur(meta.eur)}${RESET}`);
|
|
285
|
+
if (meta.ms != null) bits.push(`${t.gray}${fmtElapsed(meta.ms)}${RESET}`);
|
|
286
|
+
if (meta.calls > 1) bits.push(`${t.gray}${meta.calls} calls${RESET}`);
|
|
190
287
|
if (!bits.length) return '';
|
|
191
|
-
|
|
288
|
+
const sep = `${t.dim} ${GLYPH.bullet} ${RESET}`;
|
|
289
|
+
return `${t.dim}${GLYPH.hook} ${RESET}${bits.join(sep)}`;
|
|
192
290
|
}
|
|
193
291
|
|
|
194
292
|
/**
|
|
195
|
-
* The bottom bar:
|
|
196
|
-
*
|
|
197
|
-
*
|
|
293
|
+
* The bottom bar: segments packed left, the hint right-aligned, always exactly
|
|
294
|
+
* `width` cells (clipped, never wrapped, so a narrow terminal degrades instead
|
|
295
|
+
* of corrupting the transcript). No background — the CLI stays pipeable.
|
|
198
296
|
*/
|
|
199
297
|
function renderStatus(ctx, state = {}, width = 80) {
|
|
200
298
|
const t = themeOf(ctx);
|
|
201
|
-
const left = [];
|
|
202
|
-
left.push(`${GLYPH.sigil} aegis`);
|
|
299
|
+
const left = ['aegis'];
|
|
203
300
|
if (state.model) left.push(state.model);
|
|
204
301
|
if (state.tokens != null) left.push(`${fmtTokens(state.tokens)} tok`);
|
|
205
302
|
if (state.spend != null) left.push(fmtEur(state.spend));
|
|
206
303
|
if (state.mode) left.push(state.mode);
|
|
207
304
|
|
|
208
305
|
const right = state.hint || 'ctrl+c quit';
|
|
209
|
-
const body = ` ${left.join(
|
|
306
|
+
const body = ` ${left.join(` ${GLYPH.bullet} `)} `;
|
|
210
307
|
const gap = width - w(body) - w(right) - 1;
|
|
211
|
-
// Always exactly `width` cells: the bar is a background, so a short line
|
|
212
|
-
// would leave the terminal's own background showing through the strip.
|
|
213
308
|
const content = gap > 0 ? body + ' '.repeat(gap) + right + ' ' : clip(body, width);
|
|
214
|
-
return
|
|
309
|
+
return `${t.gray}${pad(content, width)}${RESET}`;
|
|
215
310
|
}
|
|
216
311
|
|
|
217
|
-
/**
|
|
312
|
+
/**
|
|
313
|
+
* The working line (drawn in the live region, then replaced by the turn):
|
|
314
|
+
* the `✻`-family spinner cycling with a VERBS entry. With `state.done`, the
|
|
315
|
+
* completion line `✻ <DoneVerb> for Ns` in gray (Churned after a chat turn,
|
|
316
|
+
* Worked after a turn that used tools).
|
|
317
|
+
*/
|
|
218
318
|
function renderWorking(ctx, state = {}) {
|
|
219
319
|
const t = themeOf(ctx);
|
|
220
|
-
|
|
221
|
-
|
|
320
|
+
if (state.done) {
|
|
321
|
+
const verb = state.verb || DONE_VERBS[state.tools ? 1 : 0] || DONE_VERBS[0];
|
|
322
|
+
const secs =
|
|
323
|
+
state.secs != null ? state.secs : Math.max(1, Math.round((state.elapsedMs || 0) / 1000));
|
|
324
|
+
return `${t.gray}${GLYPH.bloom} ${verb} for ${secs}s${RESET}`;
|
|
325
|
+
}
|
|
326
|
+
const frame = GLYPH.spin[Math.abs(Math.floor(state.tick || 0)) % GLYPH.spin.length];
|
|
327
|
+
const verb = state.verb || VERBS[0];
|
|
222
328
|
const tail = [];
|
|
223
329
|
if (state.elapsedMs != null) tail.push(fmtElapsed(state.elapsedMs));
|
|
224
|
-
if (state.streamed) tail.push(
|
|
330
|
+
if (state.streamed != null) tail.push(`↓${fmtTokens(state.streamed)} tokens`);
|
|
331
|
+
const tailStr = tail.length ? ` ${t.gray}(${tail.join(` ${GLYPH.bullet} `)})${RESET}` : '';
|
|
225
332
|
return (
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
fg(t, t.dim) + ' esc to interrupt' + RESET
|
|
333
|
+
`${t.coral}${frame}${RESET} ${t.white}${verb}${RESET}${t.white}…${RESET}${tailStr}` +
|
|
334
|
+
`${t.dim} esc to interrupt${RESET}`
|
|
229
335
|
);
|
|
230
336
|
}
|
|
231
337
|
|
|
232
|
-
/** A ruled section header used by command output (`/
|
|
338
|
+
/** A ruled section header used by command output (`/help`, `/cost`). */
|
|
233
339
|
function renderHeading(ctx, text, width = 80) {
|
|
234
340
|
const t = themeOf(ctx);
|
|
235
341
|
const label = ` ${text} `;
|
|
236
|
-
const fill = Math.max(0, width - w(label)
|
|
342
|
+
const fill = Math.max(0, width - w(label));
|
|
237
343
|
const left = Math.floor(fill / 2);
|
|
238
344
|
return (
|
|
239
|
-
|
|
240
|
-
|
|
345
|
+
`${t.gray}${'─'.repeat(left)}${RESET}${t.gold}${BOLD}${label}${RESET}` +
|
|
346
|
+
`${t.gray}${'─'.repeat(Math.max(0, fill - left))}${RESET}`
|
|
241
347
|
);
|
|
242
348
|
}
|
|
243
349
|
|
|
@@ -245,18 +351,25 @@ function renderHeading(ctx, text, width = 80) {
|
|
|
245
351
|
function renderToolResult(ctx, name, text, width = 80) {
|
|
246
352
|
const t = themeOf(ctx);
|
|
247
353
|
const lines = [renderHeading(ctx, name, width)];
|
|
248
|
-
for (const l of wrapBlock(String(text == null ? '' : text), width - 2)) {
|
|
249
|
-
lines.push(
|
|
354
|
+
for (const l of wrapBlock(String(text == null ? '' : text), Math.max(8, width - 2))) {
|
|
355
|
+
lines.push(l ? ` ${t.white}${l}${RESET}` : '');
|
|
250
356
|
}
|
|
251
357
|
return lines;
|
|
252
358
|
}
|
|
253
359
|
|
|
254
|
-
/**
|
|
360
|
+
/** A transient notice. Kinds map gray / coral / red / green with the matching
|
|
361
|
+
* glyph: info `·`, warn `⚠`, error `✗`, ok `✔`. */
|
|
362
|
+
const NOTICE = {
|
|
363
|
+
info: ['gray', GLYPH.bullet],
|
|
364
|
+
warn: ['coral', WARN],
|
|
365
|
+
error: ['red', ERR],
|
|
366
|
+
ok: ['green', GLYPH.check],
|
|
367
|
+
};
|
|
368
|
+
|
|
255
369
|
function renderNotice(ctx, kind, text) {
|
|
256
370
|
const t = themeOf(ctx);
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
return fg(t, colour) + `${mark} ${text}` + RESET;
|
|
371
|
+
const [token, mark] = NOTICE[kind] || NOTICE.info;
|
|
372
|
+
return `${t[token]}${mark} ${text}${RESET}`;
|
|
260
373
|
}
|
|
261
374
|
|
|
262
375
|
module.exports = {
|
|
@@ -271,11 +384,13 @@ module.exports = {
|
|
|
271
384
|
renderNotice,
|
|
272
385
|
mdLines,
|
|
273
386
|
inline,
|
|
387
|
+
// Style helpers kept for legacy consumers.
|
|
274
388
|
fg,
|
|
275
389
|
bg,
|
|
276
390
|
UNDER,
|
|
277
391
|
ITALIC,
|
|
278
392
|
RESET_FG,
|
|
393
|
+
RESET_BG,
|
|
279
394
|
pad,
|
|
280
395
|
padStart,
|
|
281
396
|
};
|