@zenera/cli 1.1.19 → 1.1.21
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 +6 -0
- package/dist/banner.d.ts +20 -0
- package/dist/banner.js +10 -0
- package/dist/commands/cache.js +1 -0
- package/dist/commands/check.js +34 -2
- package/dist/commands/index.js +1 -1
- package/dist/commands/init.js +1 -1
- package/dist/commands/meta.js +1 -1
- package/dist/commands/models.js +1 -0
- package/dist/commands/open.js +1 -1
- package/dist/lib.d.ts +1 -1
- package/dist/lib.js +1 -1
- package/dist/main.js +8 -1
- package/dist/meta.js +19 -7
- package/dist/scaffold.d.ts +37 -0
- package/dist/scaffold.js +67 -3
- package/dist/term.d.ts +2 -0
- package/dist/term.js +25 -0
- package/dist/tui/app.js +237 -65
- package/dist/tui/theme.d.ts +10 -0
- package/dist/tui/theme.js +4 -0
- package/dist/tui/wrap.d.ts +100 -18
- package/dist/tui/wrap.js +500 -27
- package/dist/validate.js +115 -9
- package/package.json +2 -2
- package/templates/editor/.github/copilot-instructions.md +33 -8
- package/templates/editor/.github/skills/zen-cli/references/projects.md +19 -8
- package/templates/editor/.github/skills/zen-memory/SKILL.md +77 -33
- package/templates/editor/.github/skills/zen-memory/scripts/check-instructions.sh +2 -1
- package/templates/editor/.vscode/agents.schema.json +611 -0
- package/templates/editor/.vscode/extensions.json +3 -0
- package/templates/editor/.vscode/settings.json +8 -0
- package/templates/project/agents/fork-instructions.md +151 -0
- package/templates/project/agents/memory-instructions.md +120 -8
package/dist/tui/app.js
CHANGED
|
@@ -10,7 +10,7 @@ import { format } from "../narrate.js";
|
|
|
10
10
|
import { display } from "../session.js";
|
|
11
11
|
import { CliError } from "../term.js";
|
|
12
12
|
import { resolveTheme, THEMES } from "./theme.js";
|
|
13
|
-
import { ACTIVITY_ROWS, answerWidth, BOX_CHROME, BRANCH_ROWS, branchRows, budgetOf, CHROME_ROWS, clip, describeCall, gistOf,
|
|
13
|
+
import { ACTIVITY_ROWS, answerWidth, blocksOf, BOX_CHROME, boxWidth, BRANCH_ROWS, branchRows, budgetOf, CHROME_ROWS, clip, describeCall, gistOf, gridOf, summarise, THINKING_ROWS, TIME_COL, unmarked, windowOf, } from "./wrap.js";
|
|
14
14
|
const BANNER = { key: 'banner' };
|
|
15
15
|
const isBanner = (item) => item.key === 'banner';
|
|
16
16
|
const MARK = {
|
|
@@ -78,6 +78,49 @@ function Call({ line }) {
|
|
|
78
78
|
const theme = useTheme();
|
|
79
79
|
return (_jsx(Box, { marginTop: line.gap ? 1 : 0, children: _jsxs(Text, { wrap: "truncate-end", color: theme.chrome.color, children: [_jsx(Text, { children: ` ${(line.time ?? '').padStart(TIME_COL)} ` }), _jsx(Text, { children: line.lead }), _jsx(Text, { children: line.text ? ` ${line.text}` : '' }), _jsx(Text, { children: line.detail ? ` ${line.detail}` : '' })] }) }));
|
|
80
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* A run of inline markup, drawn as nested children of an unstyled parent.
|
|
83
|
+
*
|
|
84
|
+
* Ink styles a whole `<Text>` or none of it, so the weights have to be
|
|
85
|
+
* separate elements. They are still one text node to the layout, which is what
|
|
86
|
+
* lets the parent wrap across a `**bold**` without it becoming its own line.
|
|
87
|
+
*/
|
|
88
|
+
function Spans({ spans }) {
|
|
89
|
+
const theme = useTheme();
|
|
90
|
+
return (_jsx(_Fragment, { children: spans.map((s, i) => (_jsx(Text, { bold: s.bold === true, italic: s.italic === true, underline: s.href !== undefined && s.href !== '', ...(s.code ? { color: theme.code.color } : {}), children: s.text }, i))) }));
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* One block of an answer.
|
|
94
|
+
*
|
|
95
|
+
* A fence is cut rather than wrapped, because its indentation is its meaning.
|
|
96
|
+
* A table is laid out by `gridOf` and only ever styled a whole row at a time —
|
|
97
|
+
* a span is measured by its text, so nothing here can move a column.
|
|
98
|
+
*/
|
|
99
|
+
function BlockView({ block, gap, width, }) {
|
|
100
|
+
const theme = useTheme();
|
|
101
|
+
const spans = block.spans ?? [];
|
|
102
|
+
switch (block.kind) {
|
|
103
|
+
case 'code':
|
|
104
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: gap, children: [_jsx(Text, { color: theme.chrome.color, children: `\u250c\u2500${block.title ? ` ${block.title}` : ''}` }), (block.lines ?? []).map((l, j) => (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: theme.chrome.color, children: '\u2502 ' }), _jsx(Text, { wrap: "truncate-end", children: l || ' ' })] }, j))), _jsx(Text, { color: theme.chrome.color, children: '\u2514\u2500' })] }));
|
|
105
|
+
case 'table':
|
|
106
|
+
return (_jsx(Box, { flexDirection: "column", marginTop: gap, children: gridOf(block).map((row, j) => (_jsx(Text, { wrap: "truncate-end", bold: block.align !== undefined && j === 0, ...(block.align !== undefined && j === 1
|
|
107
|
+
? { color: theme.chrome.color }
|
|
108
|
+
: {}), children: _jsx(Spans, { spans: row }) }, j))) }));
|
|
109
|
+
case 'heading':
|
|
110
|
+
// One weight, not a ladder of them: a terminal has bold and it has
|
|
111
|
+
// nothing else, so every level of heading is the same bold and the
|
|
112
|
+
// nesting is carried by the words.
|
|
113
|
+
return (_jsx(Box, { marginTop: gap, children: _jsx(Text, { bold: true, children: _jsx(Spans, { spans: spans }) }) }));
|
|
114
|
+
case 'item':
|
|
115
|
+
return (_jsxs(Box, { flexDirection: "row", marginTop: gap, paddingLeft: (block.level ?? 0) * 2, children: [_jsx(Text, { color: theme.chrome.color, children: `${block.marker ?? '\u2022'} ` }), _jsx(Box, { flexGrow: 1, children: _jsx(Text, { children: _jsx(Spans, { spans: spans }) }) })] }));
|
|
116
|
+
case 'quote':
|
|
117
|
+
return (_jsxs(Box, { flexDirection: "row", marginTop: gap, children: [_jsx(Text, { color: theme.chrome.color, children: '\u2502 ' }), _jsx(Box, { flexGrow: 1, children: _jsx(Text, { italic: true, children: _jsx(Spans, { spans: spans }) }) })] }));
|
|
118
|
+
case 'rule':
|
|
119
|
+
return (_jsx(Box, { marginTop: gap, children: _jsx(Text, { color: theme.chrome.color, children: '\u2500'.repeat(Math.max(3, width)) }) }));
|
|
120
|
+
default:
|
|
121
|
+
return (_jsx(Box, { marginTop: gap, children: _jsx(Text, { children: _jsx(Spans, { spans: spans }) }) }));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
81
124
|
/**
|
|
82
125
|
* Prose the agent wrote, at the width a paragraph is worth reading at.
|
|
83
126
|
*
|
|
@@ -88,9 +131,13 @@ function Call({ line }) {
|
|
|
88
131
|
function Answer({ text, boxed }) {
|
|
89
132
|
const theme = useTheme();
|
|
90
133
|
const { stdout } = useStdout();
|
|
91
|
-
const width =
|
|
92
|
-
const
|
|
93
|
-
const
|
|
134
|
+
const width = boxWidth(text, stdout?.columns ?? 80);
|
|
135
|
+
const blocks = blocksOf(text);
|
|
136
|
+
const inner = width - (boxed ? 4 : GUTTER);
|
|
137
|
+
const body = blocks.map((block, i) => (
|
|
138
|
+
// Items of one list are one thing; a blank row between them is two
|
|
139
|
+
// lists, and a list that reads as a list is most of why it was written.
|
|
140
|
+
_jsx(BlockView, { block: block, width: inner, gap: i > 0 && !(block.kind === 'item' && blocks[i - 1]?.kind === 'item') ? 1 : 0 }, i)));
|
|
94
141
|
// Bounded, like every answer in `examples/sdk/`: prose that runs the width of a
|
|
95
142
|
// wide terminal is a worse read than prose that stops.
|
|
96
143
|
return boxed ? (_jsx(Box, { flexDirection: "column", width: width, borderStyle: "round", borderColor: theme.chrome.color, paddingX: 1, marginY: 1, children: body })) : (_jsx(Box, { flexDirection: "column", width: width, paddingLeft: GUTTER, marginTop: 1, children: body }));
|
|
@@ -116,6 +163,8 @@ function App({ engine, options, theme }) {
|
|
|
116
163
|
// activity region below is redrawn by the frame timer regardless.
|
|
117
164
|
const running = useRef(new Map());
|
|
118
165
|
const branches = useRef(new Map());
|
|
166
|
+
/** The context mode of the fork now open; `branch_started` does not carry it. */
|
|
167
|
+
const context = useRef('inherit');
|
|
119
168
|
const lane = useLanes(theme);
|
|
120
169
|
// What the turn now running has cost so far, summed per model call rather
|
|
121
170
|
// than read off the state: it is wanted between calls, not after them. The
|
|
@@ -227,7 +276,8 @@ function App({ engine, options, theme }) {
|
|
|
227
276
|
// below draw. The answer keeps a blank row above it, so what it is separated
|
|
228
277
|
// from is whatever the last call left on screen.
|
|
229
278
|
const textRows = Math.max(1, budget.live - 1);
|
|
230
|
-
const
|
|
279
|
+
const liveWidth = live ? boxWidth(live, columns) - 4 : 0;
|
|
280
|
+
const liveRows = live ? windowOf(unmarked(live, liveWidth), liveWidth, textRows).length + 1 : 0;
|
|
231
281
|
const wanted = activityHeight(fitted.boxes, fitted.trunk) +
|
|
232
282
|
(fitted.hidden ? 1 : 0) +
|
|
233
283
|
(streaming ? budget.thinking : 0) +
|
|
@@ -273,6 +323,22 @@ function App({ engine, options, theme }) {
|
|
|
273
323
|
push('text', text);
|
|
274
324
|
}
|
|
275
325
|
}, [push]);
|
|
326
|
+
/**
|
|
327
|
+
* The same, for a branch: its prose moves from *arriving* into its trail.
|
|
328
|
+
*
|
|
329
|
+
* The trail is where the box's rows come from, so prose committed here is
|
|
330
|
+
* still on screen under the call it introduced — which is the whole reason
|
|
331
|
+
* a branch says anything before calling something.
|
|
332
|
+
*/
|
|
333
|
+
const keep = useCallback((b) => {
|
|
334
|
+
const text = b.said.trim();
|
|
335
|
+
b.said = '';
|
|
336
|
+
if (!text) {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
b.trail.push({ kind: 'said', id: `${seq.current++}`, text });
|
|
340
|
+
b.trail.splice(0, b.trail.length - BRANCH_ROWS);
|
|
341
|
+
}, []);
|
|
276
342
|
// Deltas arrive far faster than a terminal can usefully redraw, so text is
|
|
277
343
|
// accumulated in one string and React coalesces the repaints. The finished
|
|
278
344
|
// answer replaces it in one piece when the turn lands.
|
|
@@ -303,7 +369,17 @@ function App({ engine, options, theme }) {
|
|
|
303
369
|
}
|
|
304
370
|
return;
|
|
305
371
|
}
|
|
306
|
-
if (event.type === 'text_delta'
|
|
372
|
+
if (event.type === 'text_delta') {
|
|
373
|
+
if (owner) {
|
|
374
|
+
// Same rule the trunk follows: prose supersedes the
|
|
375
|
+
// reasoning that decided on it.
|
|
376
|
+
owner.thinking = '';
|
|
377
|
+
owner.said += event.delta;
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
if (event.branch) {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
307
383
|
// Whichever of prose or a call the step arrives at first
|
|
308
384
|
// retires the gist; the content streaming under a heading
|
|
309
385
|
// does not, which is why the heading holds still.
|
|
@@ -324,6 +400,8 @@ function App({ engine, options, theme }) {
|
|
|
324
400
|
if (b) {
|
|
325
401
|
b.steps++;
|
|
326
402
|
b.thinking = '';
|
|
403
|
+
keep(b);
|
|
404
|
+
b.musing = true;
|
|
327
405
|
}
|
|
328
406
|
}
|
|
329
407
|
else {
|
|
@@ -339,6 +417,12 @@ function App({ engine, options, theme }) {
|
|
|
339
417
|
// Branches included: they are what this turn is spending on.
|
|
340
418
|
spent.current = addUsage(spent.current, event.node.usage);
|
|
341
419
|
if (from) {
|
|
420
|
+
// Its gist stays up until the call it decided on goes
|
|
421
|
+
// out; only the shimmer stops, as on the trunk.
|
|
422
|
+
const b = branches.current.get(from);
|
|
423
|
+
if (b) {
|
|
424
|
+
b.musing = false;
|
|
425
|
+
}
|
|
342
426
|
break;
|
|
343
427
|
}
|
|
344
428
|
setModel(event.node.model);
|
|
@@ -346,12 +430,17 @@ function App({ engine, options, theme }) {
|
|
|
346
430
|
// the shimmer stops, because nothing is arriving any more.
|
|
347
431
|
setMusing(false);
|
|
348
432
|
break;
|
|
349
|
-
case 'before_tool_call':
|
|
433
|
+
case 'before_tool_call': {
|
|
350
434
|
// Everything the model said on the way to this call is now
|
|
351
435
|
// history, and history belongs above the call, not below it.
|
|
352
436
|
// The gist goes with it: the call is what it decided on, so
|
|
353
437
|
// the row saying so supersedes the row explaining it.
|
|
354
|
-
|
|
438
|
+
const b = from ? branches.current.get(from) : undefined;
|
|
439
|
+
if (b) {
|
|
440
|
+
keep(b);
|
|
441
|
+
b.thinking = '';
|
|
442
|
+
}
|
|
443
|
+
else if (!from) {
|
|
355
444
|
settle();
|
|
356
445
|
mind.current = '';
|
|
357
446
|
setThinking('');
|
|
@@ -364,6 +453,7 @@ function App({ engine, options, theme }) {
|
|
|
364
453
|
branch: from,
|
|
365
454
|
});
|
|
366
455
|
break;
|
|
456
|
+
}
|
|
367
457
|
case 'after_tool_call': {
|
|
368
458
|
const { node } = event;
|
|
369
459
|
const call = running.current.get(node.callId);
|
|
@@ -376,14 +466,15 @@ function App({ engine, options, theme }) {
|
|
|
376
466
|
// scrollback is a fan-out shuffled, and the box is
|
|
377
467
|
// the only place the shape of the fork survives.
|
|
378
468
|
// The join summarises it; the report has all of it.
|
|
379
|
-
b.
|
|
380
|
-
|
|
469
|
+
b.trail.push({
|
|
470
|
+
kind: 'call',
|
|
471
|
+
id: node.callId,
|
|
381
472
|
name: node.name,
|
|
382
473
|
args: call?.args ?? '',
|
|
383
474
|
ms: node.durationMs,
|
|
384
475
|
failed: node.isError,
|
|
385
476
|
});
|
|
386
|
-
b.
|
|
477
|
+
b.trail.splice(0, b.trail.length - BRANCH_ROWS);
|
|
387
478
|
break;
|
|
388
479
|
}
|
|
389
480
|
// What it was asked and what it answered, which is the
|
|
@@ -408,23 +499,31 @@ function App({ engine, options, theme }) {
|
|
|
408
499
|
break;
|
|
409
500
|
}
|
|
410
501
|
case 'before_fork':
|
|
502
|
+
// Naming the branches here said nothing a box below does
|
|
503
|
+
// not say better, under the work it is doing. The one
|
|
504
|
+
// thing a box cannot say for itself is what it started
|
|
505
|
+
// from, so the fork leaves it for the boxes to carry.
|
|
506
|
+
context.current = event.node.contextMode;
|
|
411
507
|
// A fork is what the trunk concluded, so whatever it was
|
|
412
508
|
// saying is finished as far as the screen is concerned.
|
|
413
509
|
if (from) {
|
|
414
510
|
break;
|
|
415
511
|
}
|
|
416
512
|
settle();
|
|
417
|
-
push('note', `⑂ ${event.node.branches.length}
|
|
513
|
+
push('note', `⑂ ${event.node.branches.length} ${event.node.branches.length === 1 ? 'branch' : 'branches'}`);
|
|
418
514
|
break;
|
|
419
515
|
case 'branch_started':
|
|
420
516
|
branches.current.set(event.child.name, {
|
|
421
517
|
name: event.child.name,
|
|
422
518
|
agent: event.childState.agentName,
|
|
519
|
+
context: context.current,
|
|
423
520
|
startedAt: Date.now(),
|
|
424
521
|
steps: 0,
|
|
425
522
|
tools: 0,
|
|
426
523
|
thinking: '',
|
|
427
|
-
|
|
524
|
+
said: '',
|
|
525
|
+
musing: false,
|
|
526
|
+
trail: [],
|
|
428
527
|
});
|
|
429
528
|
break;
|
|
430
529
|
case 'branch_finished': {
|
|
@@ -437,7 +536,7 @@ function App({ engine, options, theme }) {
|
|
|
437
536
|
default:
|
|
438
537
|
break;
|
|
439
538
|
}
|
|
440
|
-
}, [columns, push, settle]);
|
|
539
|
+
}, [columns, keep, push, settle]);
|
|
441
540
|
const submit = useCallback((value) => {
|
|
442
541
|
const text = value.trim();
|
|
443
542
|
if (busy) {
|
|
@@ -552,7 +651,7 @@ function App({ engine, options, theme }) {
|
|
|
552
651
|
recall(1);
|
|
553
652
|
}
|
|
554
653
|
});
|
|
555
|
-
return (_jsx(ThemeContext.Provider, { value: theme, children: _jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: [BANNER, ...lines], children: (item) => isBanner(item) ? (_jsx(Header, { engine: engine, readOnly: options.readOnly, started: options.started }, item.key)) : (_jsx(Row, { line: item }, item.key)) }), _jsxs(Box, { flexDirection: "column", justifyContent: "flex-end", height: busy ? grown.current : undefined, overflow: "hidden", children: [_jsx(Branches, { boxes: fitted.boxes, spin: spin, columns: columns }), streaming && budget.thinking ? (_jsx(Reasoning, { text: thinking, columns: columns, frame: frame, live: musing })) : null, _jsx(Activity, { rows: fitted.trunk, hidden: fitted.hidden }), live ? _jsx(Streaming, { text: live, columns: columns, rows: textRows }) : null] }), _jsx(Footer, { agent: agent, busy: busy, spin: spin, frame: frame, running: mine, forked: branches.current.size, step: step, model: model, stats: stats, inflight: inflight, reasoning: musing && streaming }), busy ? null : (_jsxs(Box, { children: [_jsx(Text, { color: theme.accent, children: "\u203A " }), _jsx(TextInput, { defaultValue: draft, placeholder: "Ask something\u2026 (\u2191 for history, /help for commands)", onChange: remember, onSubmit: submit }, generation)] }))] }) }));
|
|
654
|
+
return (_jsx(ThemeContext.Provider, { value: theme, children: _jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: [BANNER, ...lines], children: (item) => isBanner(item) ? (_jsx(Header, { engine: engine, readOnly: options.readOnly, started: options.started }, item.key)) : (_jsx(Row, { line: item }, item.key)) }), _jsxs(Box, { flexDirection: "column", justifyContent: "flex-end", height: busy ? grown.current : undefined, overflow: "hidden", children: [_jsx(Branches, { boxes: fitted.boxes, spin: spin, frame: frame, columns: columns }), streaming && budget.thinking ? (_jsx(Reasoning, { text: thinking, columns: columns, frame: frame, live: musing })) : null, _jsx(Activity, { rows: fitted.trunk, hidden: fitted.hidden, frame: frame }), live ? _jsx(Streaming, { text: live, columns: columns, rows: textRows }) : null] }), _jsx(Footer, { agent: agent, busy: busy, spin: spin, frame: frame, running: mine, forked: branches.current.size, step: step, model: model, stats: stats, inflight: inflight, reasoning: musing && streaming }), busy ? null : (_jsxs(Box, { children: [_jsx(Text, { color: theme.accent, children: "\u203A " }), _jsx(TextInput, { defaultValue: draft, placeholder: "Ask something\u2026 (\u2191 for history, /help for commands)", onChange: remember, onSubmit: submit }, generation)] }))] }) }));
|
|
556
655
|
}
|
|
557
656
|
function Header({ engine, readOnly, started, }) {
|
|
558
657
|
const theme = useTheme();
|
|
@@ -585,6 +684,8 @@ function Header({ engine, readOnly, started, }) {
|
|
|
585
684
|
// ---------------------------------------------------------------------------
|
|
586
685
|
/** The gutter every streaming block is indented behind. */
|
|
587
686
|
const GUTTER = 2;
|
|
687
|
+
/** The most rows of a branch's own prose a box will show. */
|
|
688
|
+
const SAY_ROWS = 2;
|
|
588
689
|
/**
|
|
589
690
|
* A fan-out, demultiplexed into one box per branch. Interleaving eight
|
|
590
691
|
* branches' calls into a single list is not a picture of parallel work, it is
|
|
@@ -603,13 +704,63 @@ function branchBoxesOf(tools, branches, now, width, lane, spin, share) {
|
|
|
603
704
|
const boxes = [];
|
|
604
705
|
const room = (w) => Math.max(12, w - TIME_COL - 8);
|
|
605
706
|
for (const b of branches.values()) {
|
|
606
|
-
// What it
|
|
607
|
-
//
|
|
608
|
-
|
|
609
|
-
|
|
707
|
+
// What it has done and said, in the order it happened. A branch's prose
|
|
708
|
+
// never reaches the scrollback — several of them landing in one
|
|
709
|
+
// transcript is a fan-out shuffled — so the box is the only place it is
|
|
710
|
+
// ever read, and it stays there once the call it introduced goes out.
|
|
711
|
+
const trail = [];
|
|
712
|
+
for (const note of b.trail) {
|
|
713
|
+
if (note.kind === 'said') {
|
|
714
|
+
windowOf(note.text, width - 2, SAY_ROWS).forEach((line, i) => trail.push({
|
|
715
|
+
key: `s:${note.id}:${i}`,
|
|
716
|
+
mark: '',
|
|
717
|
+
time: '',
|
|
718
|
+
lead: '',
|
|
719
|
+
text: line,
|
|
720
|
+
said: true,
|
|
721
|
+
}));
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
const view = describeCall(note.name, note.args);
|
|
725
|
+
trail.push({
|
|
726
|
+
key: `d:${note.id}`,
|
|
727
|
+
mark: note.failed ? '✗' : '✓',
|
|
728
|
+
time: note.failed ? 'failed' : (durationOf(note.ms) ?? ''),
|
|
729
|
+
lead: view.verb,
|
|
730
|
+
text: clip(view.subject, room(width) - view.verb.length),
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
// Prose arriving now, and failing that what it is reasoning about —
|
|
734
|
+
// one supersedes the other exactly as on the trunk. Either way it sits
|
|
735
|
+
// between what the branch did and what it is doing, because it is what
|
|
736
|
+
// got it from one to the other.
|
|
737
|
+
const say = b.said.trim() ? windowOf(b.said, width - 2, SAY_ROWS) : [];
|
|
738
|
+
const gist = !say.length && b.thinking.trim() ? gistOf(b.thinking, width - TIME_COL - 6) : '';
|
|
739
|
+
const narration = say.length
|
|
740
|
+
? say.map((line, i) => ({
|
|
741
|
+
key: `s:${b.name}:${i}`,
|
|
742
|
+
mark: '',
|
|
743
|
+
time: '',
|
|
744
|
+
lead: '',
|
|
745
|
+
text: line,
|
|
746
|
+
said: true,
|
|
747
|
+
}))
|
|
748
|
+
: gist
|
|
749
|
+
? [
|
|
750
|
+
{
|
|
751
|
+
key: `g:${b.name}`,
|
|
752
|
+
mark: '',
|
|
753
|
+
time: '',
|
|
754
|
+
lead: '',
|
|
755
|
+
text: gist,
|
|
756
|
+
thinking: true,
|
|
757
|
+
shimmer: b.musing,
|
|
758
|
+
},
|
|
759
|
+
]
|
|
760
|
+
: [];
|
|
610
761
|
const live = [];
|
|
611
762
|
for (const t of tools.values()) {
|
|
612
|
-
if (t.branch === b.name && live.length <
|
|
763
|
+
if (t.branch === b.name && live.length < share) {
|
|
613
764
|
const view = describeCall(t.name, t.args);
|
|
614
765
|
live.push({
|
|
615
766
|
key: `t:${t.callId}`,
|
|
@@ -620,53 +771,28 @@ function branchBoxesOf(tools, branches, now, width, lane, spin, share) {
|
|
|
620
771
|
});
|
|
621
772
|
}
|
|
622
773
|
}
|
|
623
|
-
//
|
|
624
|
-
//
|
|
625
|
-
const
|
|
626
|
-
const rows = [
|
|
627
|
-
...past.map((d) => {
|
|
628
|
-
const view = describeCall(d.name, d.args);
|
|
629
|
-
return {
|
|
630
|
-
key: `d:${d.callId}`,
|
|
631
|
-
mark: d.failed ? '✗' : '✓',
|
|
632
|
-
time: d.failed ? 'failed' : (durationOf(d.ms) ?? ''),
|
|
633
|
-
lead: view.verb,
|
|
634
|
-
text: clip(view.subject, room(width) - view.verb.length),
|
|
635
|
-
};
|
|
636
|
-
}),
|
|
637
|
-
// Between the calls it made and the one it is making: the reasoning
|
|
638
|
-
// is what got it from one to the other.
|
|
639
|
-
...(gist
|
|
640
|
-
? [
|
|
641
|
-
{
|
|
642
|
-
key: `g:${b.name}`,
|
|
643
|
-
mark: '✻',
|
|
644
|
-
time: '',
|
|
645
|
-
lead: '',
|
|
646
|
-
text: gist,
|
|
647
|
-
thinking: true,
|
|
648
|
-
},
|
|
649
|
-
]
|
|
650
|
-
: []),
|
|
651
|
-
...live,
|
|
652
|
-
];
|
|
774
|
+
// The tail, so the newest is always the row nearest the closing rule
|
|
775
|
+
// and what is in flight is never the thing that got cut.
|
|
776
|
+
const rows = [...trail, ...narration, ...live];
|
|
653
777
|
if (!rows.length) {
|
|
654
778
|
rows.push({
|
|
655
779
|
key: `w:${b.name}`,
|
|
656
|
-
mark: '
|
|
780
|
+
mark: '',
|
|
657
781
|
time: '',
|
|
658
782
|
lead: '',
|
|
659
783
|
text: 'thinking…',
|
|
660
784
|
thinking: true,
|
|
785
|
+
shimmer: b.musing,
|
|
661
786
|
});
|
|
662
787
|
}
|
|
663
788
|
boxes.push({
|
|
664
789
|
name: b.name,
|
|
665
790
|
color: lane(b.name),
|
|
666
791
|
title: `${b.name}${b.agent ? ` · ${b.agent}` : ''}`,
|
|
792
|
+
context: `context ${b.context}`,
|
|
667
793
|
stats: `${b.steps} ${b.steps === 1 ? 'step' : 'steps'}` +
|
|
668
|
-
(b.tools ? ` ${b.tools} ${b.tools === 1 ? 'tool' : 'tools'}` : '')
|
|
669
|
-
|
|
794
|
+
(b.tools ? ` ${b.tools} ${b.tools === 1 ? 'tool' : 'tools'}` : ''),
|
|
795
|
+
elapsed: secs(now - b.startedAt),
|
|
670
796
|
rows: rows.slice(-share),
|
|
671
797
|
});
|
|
672
798
|
}
|
|
@@ -708,23 +834,58 @@ function fitActivity(boxes, trunk, allowance) {
|
|
|
708
834
|
kept.push(b);
|
|
709
835
|
used += h;
|
|
710
836
|
}
|
|
711
|
-
|
|
712
|
-
|
|
837
|
+
let hidden = boxes.length - kept.length;
|
|
838
|
+
// A branch that is neither drawn nor counted has simply vanished from the
|
|
839
|
+
// fork. If the boxes filled the region exactly there is no row left to say
|
|
840
|
+
// so, so the last of them gives one back.
|
|
841
|
+
while (hidden && used + 1 > allowance && kept.length) {
|
|
842
|
+
const last = kept.pop();
|
|
843
|
+
used -= BOX_CHROME + last.rows.length;
|
|
844
|
+
hidden++;
|
|
845
|
+
}
|
|
713
846
|
return {
|
|
714
847
|
boxes: kept,
|
|
715
848
|
trunk: trunk.slice(0, Math.max(0, allowance - used - (hidden ? 1 : 0))),
|
|
716
849
|
hidden,
|
|
717
850
|
};
|
|
718
851
|
}
|
|
719
|
-
function Branches({ boxes, spin, columns, }) {
|
|
852
|
+
function Branches({ boxes, spin, frame, columns, }) {
|
|
720
853
|
const theme = useTheme();
|
|
854
|
+
// What a row has between the two sides. Ink truncates rather than wraps, so
|
|
855
|
+
// anything wider than this would eat the closing side rather than the box
|
|
856
|
+
// costing itself a row it was not given.
|
|
857
|
+
const inner = Math.max(1, columns - 4);
|
|
721
858
|
return (_jsx(Box, { flexDirection: "column", children: boxes.map((b) => {
|
|
722
|
-
|
|
723
|
-
//
|
|
724
|
-
|
|
725
|
-
|
|
859
|
+
const title = clip(b.title, Math.max(1, columns - 10));
|
|
860
|
+
// The name is the branch's; the context is the fork's doing,
|
|
861
|
+
// so it is set after the spinner in the chrome's own grey
|
|
862
|
+
// rather than lifted into the lane with the name.
|
|
863
|
+
const said = `${title} ${spin} ${b.context}`;
|
|
864
|
+
// Who it is heads the box; what it has done and how long it has
|
|
865
|
+
// been doing it close it. A count belongs with the clock, and a
|
|
866
|
+
// name reads better without two numbers after it.
|
|
867
|
+
const foot = clip(`${b.stats} · ${b.elapsed}`, Math.max(1, columns - 8));
|
|
868
|
+
// One hue for the whole frame. A rule in one colour meeting a
|
|
869
|
+
// side in another is read as two things that failed to join.
|
|
870
|
+
const edge = b.color ?? theme.chrome.color;
|
|
871
|
+
return (_jsxs(Box, { flexDirection: "column", height: BOX_CHROME + b.rows.length, children: [_jsxs(Text, { wrap: "truncate-end", color: edge, children: [_jsx(Text, { children: '╭─ ' }), _jsx(Text, { bold: true, children: title }), _jsx(Text, { children: ` ${spin}` }), _jsx(Text, { color: theme.chrome.color, children: ` ${b.context} ` }), _jsxs(Text, { children: ['─'.repeat(Math.max(0, columns - said.length - 5)), '╮'] })] }), b.rows.map((r) => (_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { color: edge, children: '│ ' }), _jsx(Work, { row: r, frame: frame }), _jsx(Text, { color: edge, children: `${' '.repeat(Math.max(0, inner - widthOf(r)))} │` })] }, r.key))), _jsxs(Text, { wrap: "truncate-end", color: edge, children: [_jsx(Text, { children: '╰─ ' }), _jsx(Text, { color: theme.chrome.color, children: foot }), _jsxs(Text, { children: [` ${'─'.repeat(Math.max(0, columns - foot.length - 5))}`, '╯'] })] })] }, b.name));
|
|
726
872
|
}) }));
|
|
727
873
|
}
|
|
874
|
+
/**
|
|
875
|
+
* The columns `Work` draws `row` in. It lives here so the two stay in step:
|
|
876
|
+
* a box closes on the right, and a side rule can only be put where the content
|
|
877
|
+
* is known to have stopped.
|
|
878
|
+
*/
|
|
879
|
+
function widthOf(row) {
|
|
880
|
+
if (row.thinking || row.said) {
|
|
881
|
+
return row.text.length;
|
|
882
|
+
}
|
|
883
|
+
return (2 +
|
|
884
|
+
Math.max(TIME_COL, row.time.length) +
|
|
885
|
+
2 +
|
|
886
|
+
row.lead.length +
|
|
887
|
+
(row.text ? row.text.length + 1 : 0));
|
|
888
|
+
}
|
|
728
889
|
/**
|
|
729
890
|
* A row of work in the same shape the transcript will keep it in.
|
|
730
891
|
*
|
|
@@ -732,19 +893,25 @@ function Branches({ boxes, spin, columns, }) {
|
|
|
732
893
|
* sits where nothing will be once the call returns and the time, verb and
|
|
733
894
|
* subject never move.
|
|
734
895
|
*/
|
|
735
|
-
function Work({ row }) {
|
|
896
|
+
function Work({ row, frame }) {
|
|
736
897
|
const theme = useTheme();
|
|
898
|
+
// Reasoning and prose are drawn in a branch's box exactly as they are drawn
|
|
899
|
+
// on the trunk: no mark, at the trunk's own gutter, the sweep saying it is
|
|
900
|
+
// still arriving. A branch is an agent, not a subsystem.
|
|
737
901
|
if (row.thinking) {
|
|
738
|
-
return _jsx(Text, { color: theme.thinking.color, children:
|
|
902
|
+
return row.shimmer ? (_jsx(Shimmer, { text: row.text, frame: frame, color: theme.thinking.color })) : (_jsx(Text, { color: theme.thinking.color, children: row.text }));
|
|
903
|
+
}
|
|
904
|
+
if (row.said) {
|
|
905
|
+
return _jsx(Text, { children: row.text });
|
|
739
906
|
}
|
|
740
907
|
return (_jsxs(Text, { color: theme.chrome.color, children: [_jsx(Text, { color: row.color, children: `${row.mark} ` }), _jsx(Text, { children: `${row.time.padStart(TIME_COL)} ` }), _jsx(Text, { color: row.color, children: row.lead }), _jsx(Text, { children: row.text ? ` ${row.text}` : '' })] }));
|
|
741
908
|
}
|
|
742
|
-
function Activity({ rows, hidden, }) {
|
|
909
|
+
function Activity({ rows, hidden, frame, }) {
|
|
743
910
|
const theme = useTheme();
|
|
744
911
|
if (!rows.length && !hidden) {
|
|
745
912
|
return null;
|
|
746
913
|
}
|
|
747
|
-
return (_jsxs(Box, { flexDirection: "column", height: rows.length + (hidden ? 1 : 0), overflow: "hidden", children: [rows.map((r) => (_jsx(Text, { wrap: "truncate-end", children: _jsx(Work, { row: r }) }, r.key))), hidden ? (_jsx(Text, { color: theme.chrome.color, children: ` + ${hidden} more ${hidden === 1 ? 'branch' : 'branches'}` })) : null] }));
|
|
914
|
+
return (_jsxs(Box, { flexDirection: "column", height: rows.length + (hidden ? 1 : 0), overflow: "hidden", children: [rows.map((r) => (_jsx(Text, { wrap: "truncate-end", children: _jsx(Work, { row: r, frame: frame }) }, r.key))), hidden ? (_jsx(Text, { color: theme.chrome.color, children: ` + ${hidden} more ${hidden === 1 ? 'branch' : 'branches'}` })) : null] }));
|
|
748
915
|
}
|
|
749
916
|
/**
|
|
750
917
|
* A stable colour per branch, handed out in order of first sight. Held in a ref
|
|
@@ -788,7 +955,12 @@ function Reasoning({ text, columns, frame, live, }) {
|
|
|
788
955
|
function Streaming({ text, columns, rows }) {
|
|
789
956
|
// The same width the finished answer will take, so landing it reflows
|
|
790
957
|
// nothing: what is on screen is what stays there.
|
|
791
|
-
|
|
958
|
+
//
|
|
959
|
+
// Markers are resolved but nothing is styled. This region repaints, and
|
|
960
|
+
// everything that repaints is measured by `.length` — so the stream gets
|
|
961
|
+
// the words the answer will have and the weights it will not, which is the
|
|
962
|
+
// half of the change that can be made without lying about the height.
|
|
963
|
+
const shown = windowOf(unmarked(text, boxWidth(text, columns) - 4), boxWidth(text, columns) - 4, rows);
|
|
792
964
|
return (_jsx(Box, { flexDirection: "column", paddingLeft: GUTTER, marginTop: 1, height: shown.length, overflow: "hidden", children: shown.map((row, i) => (_jsx(Text, { wrap: "truncate-end", children: row }, i))) }));
|
|
793
965
|
}
|
|
794
966
|
/** The label column the two number rows line up behind. */
|
package/dist/tui/theme.d.ts
CHANGED
|
@@ -33,6 +33,16 @@ export interface Theme {
|
|
|
33
33
|
* model's thinking from the work it went on to do.
|
|
34
34
|
*/
|
|
35
35
|
readonly thinking: LineStyle;
|
|
36
|
+
/**
|
|
37
|
+
* Inline code inside prose — `zen check`, `--theme`, a path.
|
|
38
|
+
*
|
|
39
|
+
* The one role added for markdown, and it is added because the alternative
|
|
40
|
+
* is worse: bold is already what `**strong**` means, and grey would say a
|
|
41
|
+
* command name matters less than the sentence around it when it is usually
|
|
42
|
+
* the only part worth copying. A hue says *this is literal* without
|
|
43
|
+
* ranking it.
|
|
44
|
+
*/
|
|
45
|
+
readonly code: LineStyle;
|
|
36
46
|
/**
|
|
37
47
|
* The three things a turn spends time on. One word in the footer says which,
|
|
38
48
|
* and it is read at a glance from across a desk — so the word changing is
|
package/dist/tui/theme.js
CHANGED
|
@@ -56,6 +56,9 @@ const DARK = {
|
|
|
56
56
|
// Bright: the gist is one line among many and plain `magenta` on black is
|
|
57
57
|
// the darkest of the six.
|
|
58
58
|
thinking: { color: 'magentaBright' },
|
|
59
|
+
// Green is the one hue with no other job here: it is not the accent, not
|
|
60
|
+
// the warning, and not the thinking.
|
|
61
|
+
code: { color: 'green' },
|
|
59
62
|
phase: { reasoning: 'magenta', waiting: 'yellow', working: 'cyan' },
|
|
60
63
|
lanes: ['cyan', 'green', 'yellow', 'blue', 'red', 'magenta'],
|
|
61
64
|
};
|
|
@@ -75,6 +78,7 @@ const LIGHT = {
|
|
|
75
78
|
warn: 'magenta',
|
|
76
79
|
chrome: LIGHT_CHROME,
|
|
77
80
|
thinking: { color: 'magenta' },
|
|
81
|
+
code: { color: 'green' },
|
|
78
82
|
// No yellow: `waiting` takes blue and `working` green, which are the two
|
|
79
83
|
// that survive paper.
|
|
80
84
|
phase: { reasoning: 'magenta', waiting: 'blue', working: 'green' },
|