@vincemakes/kiso-code 0.1.29 → 0.1.31
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/dist/chat.d.ts +5 -5
- package/dist/chat.js +22 -22
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/dispatch.d.ts +1 -1
- package/dist/dispatch.js +4 -4
- package/dist/faux-glue.d.ts +5 -5
- package/dist/faux-glue.js +6 -6
- package/dist/index.d.ts +4 -4
- package/dist/index.js +20 -20
- package/dist/mode.d.ts +1 -1
- package/dist/mode.js +1 -1
- package/dist/resume.d.ts +2 -2
- package/dist/resume.js +5 -5
- package/dist/state.d.ts +7 -7
- package/dist/state.js +7 -7
- package/dist/trust-ui.d.ts +2 -2
- package/dist/trust-ui.js +4 -4
- package/package.json +8 -8
- package/dist/body.d.ts +0 -121
- package/dist/body.js +0 -491
- package/dist/dock.d.ts +0 -75
- package/dist/dock.js +0 -176
- package/dist/editor.d.ts +0 -75
- package/dist/editor.js +0 -444
- package/dist/render.d.ts +0 -135
- package/dist/render.js +0 -325
package/dist/body.js
DELETED
|
@@ -1,491 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* v2d — the body renderer: ONE writer for the stdout scroll region.
|
|
3
|
-
*
|
|
4
|
-
* The v2b/v2c body writes streamed directly (each event wrote its bytes),
|
|
5
|
-
* so the tool lines, thinking folds, and text deltas could interleave in
|
|
6
|
-
* the same frame — and a tool's life was scattered across several writes
|
|
7
|
-
* (the "leak"). v2d: every event handler ONLY mutates cell state; the
|
|
8
|
-
* Body's render loop is the only thing that writes the region.
|
|
9
|
-
*
|
|
10
|
-
* Frozen semantics: a completed cell prints its final form into the
|
|
11
|
-
* scroll region ONCE and is never touched again. The ACTIVE TAIL — the
|
|
12
|
-
* unfinished cells — renders at the region's bottom (between the frozen
|
|
13
|
-
* area and the dock) and redraws in place, CSI 2026 wrapped. State
|
|
14
|
-
* changes coalesce to ≥16ms frames; a 200ms heartbeat drives the running
|
|
15
|
-
* spinners and elapsed timers. An over-height tail (rare) overflows to
|
|
16
|
-
* freeze by completion order.
|
|
17
|
-
*
|
|
18
|
-
* Pipes / NO_COLOR: the Body runs in PASSTHROUGH — every mutation writes
|
|
19
|
-
* the v2b/v2c line-mode bytes immediately, byte-for-byte (the existing
|
|
20
|
-
* e2e guards it). The cell renderer never activates.
|
|
21
|
-
*
|
|
22
|
-
* The cell model is ours (UserCell / ThinkingCell / ToolCell / TextCell /
|
|
23
|
-
* NoticeCell / raw block) — deliberately NOT pi's Component interface
|
|
24
|
-
* shape (ADR-0040).
|
|
25
|
-
*/
|
|
26
|
-
import { truncateDiff } from "./diff.js";
|
|
27
|
-
import { displayWidth } from "./editor.js";
|
|
28
|
-
import { escapeTerminal, foldResult, foldThinking, palette, renderTerminalGap, renderToolSummary, } from "./render.js";
|
|
29
|
-
/** The spinner glyphs, cycled by the heartbeat (v3 §05 — the working family). */
|
|
30
|
-
const SPINNER = ["▖", "▘", "▝", "▗"];
|
|
31
|
-
const TOOL_SUMMARY_MAX = 60; // the tool line's parameter summary, chars
|
|
32
|
-
const FRAME_MS = 16; // state changes coalesce to ≥16ms frames
|
|
33
|
-
const HEARTBEAT_MS = 200; // spinner / elapsed cadence
|
|
34
|
-
export class Body {
|
|
35
|
-
#active;
|
|
36
|
-
#opts;
|
|
37
|
-
#cells = [];
|
|
38
|
-
#nextFrozen = 0; // index of the first not-yet-printed cell
|
|
39
|
-
#frozenRows = 0; // the frozen area's rows filled WITHOUT scrolling (then the real LFs take over)
|
|
40
|
-
#oldTailTop = 0; // the tail's previous first row — for the clear pass
|
|
41
|
-
#frameTimer = null;
|
|
42
|
-
#heartbeat = null;
|
|
43
|
-
#dirty = false;
|
|
44
|
-
#spinnerI = 0;
|
|
45
|
-
#lastThinking = null;
|
|
46
|
-
#lastTool = null;
|
|
47
|
-
#pendingCalls = new Map();
|
|
48
|
-
#pipeBuf = ""; // the passthrough's thinking buffer — the cell model needs no buffer
|
|
49
|
-
#toolCells = new Map(); // callId → cell index (parallel tools)
|
|
50
|
-
#write;
|
|
51
|
-
constructor(opts) {
|
|
52
|
-
this.#opts = opts;
|
|
53
|
-
this.#write = opts.write ?? ((s) => process.stdout.write(s));
|
|
54
|
-
this.#active = opts.active();
|
|
55
|
-
if (this.#isActive()) {
|
|
56
|
-
this.#heartbeat = setInterval(() => {
|
|
57
|
-
// #14/#15: the idle heartbeat PAINTS NOTHING unless an
|
|
58
|
-
// ANIMATION advances — only a RUNNING tool's glyph/elapsed
|
|
59
|
-
// changes between beats. The #14 fix skipped an all-frozen
|
|
60
|
-
// body; #15 widened the skip to ANY no-change body: a cell
|
|
61
|
-
// that stays unfinished without animating (an unclosed text
|
|
62
|
-
// or thinking block) would otherwise re-paint the tail AND
|
|
63
|
-
// the dock every 200ms with zero change — the short-session
|
|
64
|
-
// leak (measured: 51KB / 46 beats after the recap, LF=0).
|
|
65
|
-
if (!this.#cells.some((c) => c.kind === "tool" && c.state === "running"))
|
|
66
|
-
return;
|
|
67
|
-
this.#spinnerI = (this.#spinnerI + 1) % SPINNER.length;
|
|
68
|
-
this.#dirty = true; // the running cells' glyph/elapsed advance
|
|
69
|
-
this.#scheduleFrame();
|
|
70
|
-
}, HEARTBEAT_MS);
|
|
71
|
-
this.#heartbeat.unref();
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
/** Live re-check — a TTY whose size lands after construction flips in. */
|
|
75
|
-
#isActive() {
|
|
76
|
-
this.#active = this.#opts.active();
|
|
77
|
-
return this.#active;
|
|
78
|
-
}
|
|
79
|
-
/** Teardown — flush a pending frame, stop the timers. */
|
|
80
|
-
close() {
|
|
81
|
-
if (this.#frameTimer !== null) {
|
|
82
|
-
clearTimeout(this.#frameTimer);
|
|
83
|
-
this.#frameTimer = null;
|
|
84
|
-
}
|
|
85
|
-
if (this.#heartbeat !== null) {
|
|
86
|
-
clearInterval(this.#heartbeat);
|
|
87
|
-
this.#heartbeat = null;
|
|
88
|
-
}
|
|
89
|
-
if (this.#dirty)
|
|
90
|
-
this.render();
|
|
91
|
-
}
|
|
92
|
-
/** The last COMPLETE thinking block, for /think. */
|
|
93
|
-
lastThinking() {
|
|
94
|
-
return this.#lastThinking;
|
|
95
|
-
}
|
|
96
|
-
/** The last completed tool call, for /last. */
|
|
97
|
-
lastTool() {
|
|
98
|
-
return this.#lastTool;
|
|
99
|
-
}
|
|
100
|
-
// ---- mutations (the ONLY way the CLI touches the body) ----
|
|
101
|
-
userLine(text) {
|
|
102
|
-
if (!this.#isActive()) {
|
|
103
|
-
this.#closeOpenThinking();
|
|
104
|
-
this.#closeOpenText();
|
|
105
|
-
const p = palette();
|
|
106
|
-
this.#write(`${p.blue}you> ${escapeTerminal(text)}${p.reset}\n`);
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
this.#closeOpenThinking();
|
|
110
|
-
this.#closeOpenText();
|
|
111
|
-
this.#cells.push({ kind: "user", text, done: true });
|
|
112
|
-
this.#mark();
|
|
113
|
-
}
|
|
114
|
-
thinkingAppend(text) {
|
|
115
|
-
if (!this.#isActive()) {
|
|
116
|
-
this.#pipeBuf += text; // buffered; the fold prints at the block's end
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
const last = this.#cells[this.#cells.length - 1];
|
|
120
|
-
if (last !== undefined && last.kind === "thinking" && !last.done) {
|
|
121
|
-
last.text += text;
|
|
122
|
-
}
|
|
123
|
-
else {
|
|
124
|
-
this.#cells.push({ kind: "thinking", text, done: false });
|
|
125
|
-
}
|
|
126
|
-
this.#mark();
|
|
127
|
-
}
|
|
128
|
-
thinkingEnd() {
|
|
129
|
-
const last = this.#cells[this.#cells.length - 1];
|
|
130
|
-
if (last !== undefined && last.kind === "thinking" && !last.done) {
|
|
131
|
-
last.done = true;
|
|
132
|
-
this.#lastThinking = last.text;
|
|
133
|
-
if (!this.#isActive())
|
|
134
|
-
process.stdout.write(foldThinking(last.text));
|
|
135
|
-
this.#mark();
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
toolStart(name, callId, input) {
|
|
139
|
-
const summary = JSON.stringify(input).slice(0, TOOL_SUMMARY_MAX);
|
|
140
|
-
// Registered BEFORE the passthrough branch — /last and the pipe
|
|
141
|
-
// summary need the call on BOTH paths.
|
|
142
|
-
this.#pendingCalls.set(callId, { name, input, result: { content: "", isError: false } });
|
|
143
|
-
if (!this.#isActive()) {
|
|
144
|
-
this.#closeOpenThinking();
|
|
145
|
-
this.#closeOpenText();
|
|
146
|
-
process.stdout.write(`→ ${escapeTerminal(name)}(${escapeTerminal(JSON.stringify(input).slice(0, 200))})\n`);
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
this.#toolCells.set(callId, this.#cells.length);
|
|
150
|
-
this.#cells.push({ kind: "tool", name, input: summary, state: "pending", isError: false, resultText: "", diff: null, added: 0, removed: 0, startedAt: null, doneAt: null, done: false });
|
|
151
|
-
this.#mark();
|
|
152
|
-
}
|
|
153
|
-
toolApproval(callId, diff) {
|
|
154
|
-
if (!this.#isActive())
|
|
155
|
-
return;
|
|
156
|
-
const cell = this.#toolCell(callId);
|
|
157
|
-
if (cell !== null && cell.kind === "tool" && !cell.done) {
|
|
158
|
-
cell.state = "approval";
|
|
159
|
-
// v2e: the mini-diff renders BELOW the tool line at the approval
|
|
160
|
-
// moment — the human sees the change before deciding. Auto-allowed
|
|
161
|
-
// tools pass null (nobody is looking — no diff, no cost).
|
|
162
|
-
cell.diff = diff === null ? null : truncateDiff(diff.lines);
|
|
163
|
-
cell.added = diff?.added ?? 0;
|
|
164
|
-
cell.removed = diff?.removed ?? 0;
|
|
165
|
-
}
|
|
166
|
-
this.#mark();
|
|
167
|
-
}
|
|
168
|
-
toolRunning(callId) {
|
|
169
|
-
if (!this.#isActive()) {
|
|
170
|
-
const p = palette();
|
|
171
|
-
process.stdout.write(`${p.dim} running…${p.reset}\n`);
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
const cell = this.#toolCell(callId);
|
|
175
|
-
if (cell !== null && cell.kind === "tool" && !cell.done) {
|
|
176
|
-
cell.state = "running";
|
|
177
|
-
cell.startedAt = Date.now();
|
|
178
|
-
}
|
|
179
|
-
this.#mark();
|
|
180
|
-
}
|
|
181
|
-
toolSucceeded(callId) {
|
|
182
|
-
if (!this.#isActive()) {
|
|
183
|
-
process.stdout.write(` ok\n`);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
toolFailed(callId, error) {
|
|
187
|
-
if (!this.#isActive()) {
|
|
188
|
-
const p = palette();
|
|
189
|
-
process.stdout.write(`${p.red} failed: ${escapeTerminal(error.slice(0, 160))}${p.reset}\n`);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
toolResult(callId, result) {
|
|
193
|
-
const call = this.#pendingCalls.get(callId);
|
|
194
|
-
if (call !== undefined) {
|
|
195
|
-
call.result = result;
|
|
196
|
-
this.#lastTool = { name: call.name, input: call.input, result };
|
|
197
|
-
this.#pendingCalls.delete(callId);
|
|
198
|
-
}
|
|
199
|
-
if (!this.#isActive()) {
|
|
200
|
-
// The v2b/v2c pipe bytes: the summary line + the [result] line.
|
|
201
|
-
const p = palette();
|
|
202
|
-
process.stdout.write(`${renderToolSummary(call?.name ?? "?", call?.input ?? {}, result)}\n` +
|
|
203
|
-
`${p.dim}${result.isError ? p.red : p.dim} [result${result.isError ? " ✗" : ""}] ${foldResult(result.content)}${p.reset}\n`);
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
|
-
const cell = this.#toolCell(callId);
|
|
207
|
-
this.#toolCells.delete(callId);
|
|
208
|
-
if (cell !== null && cell.kind === "tool" && !cell.done) {
|
|
209
|
-
cell.state = "done";
|
|
210
|
-
cell.isError = result.isError;
|
|
211
|
-
cell.resultText = result.content;
|
|
212
|
-
cell.doneAt = Date.now();
|
|
213
|
-
cell.done = true;
|
|
214
|
-
}
|
|
215
|
-
this.#mark();
|
|
216
|
-
}
|
|
217
|
-
textAppend(text) {
|
|
218
|
-
if (!this.#isActive()) {
|
|
219
|
-
this.#closeOpenThinking();
|
|
220
|
-
this.#closeOpenText();
|
|
221
|
-
process.stdout.write(escapeTerminal(text));
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
const last = this.#cells[this.#cells.length - 1];
|
|
225
|
-
if (last !== undefined && last.kind === "text" && !last.done) {
|
|
226
|
-
last.text += text;
|
|
227
|
-
}
|
|
228
|
-
else {
|
|
229
|
-
this.#closeOpenThinking();
|
|
230
|
-
this.#closeOpenText();
|
|
231
|
-
this.#cells.push({ kind: "text", text, done: false });
|
|
232
|
-
}
|
|
233
|
-
this.#mark();
|
|
234
|
-
}
|
|
235
|
-
textEnd() {
|
|
236
|
-
if (!this.#isActive()) {
|
|
237
|
-
process.stdout.write("\n");
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
240
|
-
const last = this.#cells[this.#cells.length - 1];
|
|
241
|
-
if (last !== undefined && last.kind === "text" && !last.done)
|
|
242
|
-
last.done = true;
|
|
243
|
-
this.#mark();
|
|
244
|
-
}
|
|
245
|
-
/** The terminal's status line + the rhythm gap (one blank). */
|
|
246
|
-
terminal(label, statusLine) {
|
|
247
|
-
if (!this.#isActive()) {
|
|
248
|
-
this.#closeOpenThinking();
|
|
249
|
-
this.#closeOpenText();
|
|
250
|
-
// the v2c bytes: the terminal label (\ndone\n) + the status gap.
|
|
251
|
-
process.stdout.write(label + renderTerminalGap(statusLine));
|
|
252
|
-
return;
|
|
253
|
-
}
|
|
254
|
-
this.#closeOpenThinking();
|
|
255
|
-
this.#closeOpenText();
|
|
256
|
-
this.#cells.push({ kind: "terminal", label: label.trim(), line: statusLine, done: true });
|
|
257
|
-
this.#mark();
|
|
258
|
-
}
|
|
259
|
-
notice(text) {
|
|
260
|
-
if (!this.#isActive()) {
|
|
261
|
-
this.#closeOpenThinking();
|
|
262
|
-
this.#closeOpenText();
|
|
263
|
-
process.stdout.write(`${text}\n`);
|
|
264
|
-
return;
|
|
265
|
-
}
|
|
266
|
-
this.#closeOpenThinking();
|
|
267
|
-
this.#closeOpenText();
|
|
268
|
-
this.#cells.push({ kind: "notice", text, done: true });
|
|
269
|
-
this.#mark();
|
|
270
|
-
}
|
|
271
|
-
/** A pre-rendered block (the banner, the session line, slash-command
|
|
272
|
-
* outputs) — frozen immediately. */
|
|
273
|
-
raw(lines) {
|
|
274
|
-
if (!this.#isActive()) {
|
|
275
|
-
this.#closeOpenThinking();
|
|
276
|
-
this.#closeOpenText();
|
|
277
|
-
for (const line of lines)
|
|
278
|
-
process.stdout.write(`${line}\n`);
|
|
279
|
-
return;
|
|
280
|
-
}
|
|
281
|
-
this.#closeOpenThinking();
|
|
282
|
-
this.#closeOpenText();
|
|
283
|
-
this.#cells.push({ kind: "raw", lines, done: true });
|
|
284
|
-
this.#mark();
|
|
285
|
-
}
|
|
286
|
-
// ---- rendering ----
|
|
287
|
-
#mark() {
|
|
288
|
-
if (!this.#isActive())
|
|
289
|
-
return;
|
|
290
|
-
this.#dirty = true;
|
|
291
|
-
this.#scheduleFrame();
|
|
292
|
-
}
|
|
293
|
-
#scheduleFrame() {
|
|
294
|
-
if (this.#frameTimer !== null)
|
|
295
|
-
return;
|
|
296
|
-
this.#frameTimer = setTimeout(() => {
|
|
297
|
-
this.#frameTimer = null;
|
|
298
|
-
if (this.#dirty) {
|
|
299
|
-
this.#dirty = false;
|
|
300
|
-
this.render();
|
|
301
|
-
}
|
|
302
|
-
}, FRAME_MS);
|
|
303
|
-
this.#frameTimer.unref();
|
|
304
|
-
}
|
|
305
|
-
/** The one writer. Frozen cells print once; the tail redraws in place,
|
|
306
|
-
* CSI 2026 wrapped; the cursor lands at the input edit column. */
|
|
307
|
-
render() {
|
|
308
|
-
if (!this.#isActive())
|
|
309
|
-
return;
|
|
310
|
-
const H = this.#opts.height();
|
|
311
|
-
const W = this.#opts.width();
|
|
312
|
-
if (H < 4)
|
|
313
|
-
return;
|
|
314
|
-
const out = [];
|
|
315
|
-
// #13 (P1), v2d-B: NO DECSTBM — overflow scrolls with a REAL LF at
|
|
316
|
-
// the screen's last row, so the frozen lines enter the terminal's
|
|
317
|
-
// NATIVE scrollback deterministically (region-scrolled lines are
|
|
318
|
-
// terminal-dependent; some terminals drop them — the measured v2d-A
|
|
319
|
-
// defect). The body fills from the top without scrolling; once full,
|
|
320
|
-
// every new frozen line scrolls the whole screen (\x1b[H;1H\n — the
|
|
321
|
-
// top line leaves into the scrollback) and lands at the body's
|
|
322
|
-
// bottom row, just above the active tail. The dock is redrawn after.
|
|
323
|
-
// The tail (the remaining ACTIVE cells) and its geometry — computed
|
|
324
|
-
// FIRST from the final nextFrozen, so the frozen cells are NOT in it
|
|
325
|
-
// (a stale tail would re-draw them — the double-render).
|
|
326
|
-
let nextFrozen = this.#nextFrozen;
|
|
327
|
-
while (nextFrozen < this.#cells.length && this.#cells[nextFrozen].done)
|
|
328
|
-
nextFrozen += 1;
|
|
329
|
-
const tail = this.#cells.slice(nextFrozen);
|
|
330
|
-
const tailHeight = tail.reduce((n, c) => n + this.#cellHeight(c, W), 0);
|
|
331
|
-
const tailTop = Math.max(1, H - 3 - tailHeight); // v3 §03: 4 dock rows below
|
|
332
|
-
const writeRow = Math.max(1, tailTop - 1); // the frozen area's bottom row
|
|
333
|
-
let scrolled = 0;
|
|
334
|
-
for (let i = this.#nextFrozen; i < nextFrozen; i += 1) {
|
|
335
|
-
for (const line of this.#cellLines(this.#cells[i], W)) {
|
|
336
|
-
if (this.#frozenRows < writeRow) {
|
|
337
|
-
this.#frozenRows += 1;
|
|
338
|
-
out.push(`\x1b[${this.#frozenRows};1H\x1b[0K${line}`);
|
|
339
|
-
}
|
|
340
|
-
else {
|
|
341
|
-
out.push(`\x1b[${H};1H\n`); // the REAL LF — the whole screen scrolls
|
|
342
|
-
out.push(`\x1b[${writeRow};1H\x1b[0K${line}`);
|
|
343
|
-
scrolled += 1;
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
this.#nextFrozen += 1;
|
|
347
|
-
}
|
|
348
|
-
// 2. the active tail — clear its old area (shifted up by the freeze
|
|
349
|
-
// scrolls) and the current area, draw the cells at the body's bottom.
|
|
350
|
-
out.push("\x1b[?2026h");
|
|
351
|
-
const clearFrom = Math.min(this.#oldTailTop === 0 ? tailTop : this.#oldTailTop - scrolled, tailTop);
|
|
352
|
-
for (let row = clearFrom; row <= H - 4; row += 1) {
|
|
353
|
-
out.push(`\x1b[${row};1H\x1b[0K`);
|
|
354
|
-
}
|
|
355
|
-
let row = tailTop;
|
|
356
|
-
for (const cell of tail) {
|
|
357
|
-
for (const line of this.#cellLines(cell, W)) {
|
|
358
|
-
out.push(`\x1b[${row};1H${line}`);
|
|
359
|
-
row += 1;
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
// 3. the cursor home — the input line's edit column.
|
|
363
|
-
out.push(`\x1b[${H};${this.#opts.editCol()}H`);
|
|
364
|
-
out.push("\x1b[?2026l");
|
|
365
|
-
this.#write(out.join(""));
|
|
366
|
-
// 4. the dock rows — the freeze scrolls shifted them; redraw (the
|
|
367
|
-
// dock's own redraw re-pins the cursor at the edit position).
|
|
368
|
-
this.#opts.onDock?.();
|
|
369
|
-
}
|
|
370
|
-
// ---- cell → lines ----
|
|
371
|
-
#cellLines(cell, W) {
|
|
372
|
-
const p = palette();
|
|
373
|
-
switch (cell.kind) {
|
|
374
|
-
case "user":
|
|
375
|
-
// v3 §02: the user message is a SGR BACKGROUND block, no
|
|
376
|
-
// prefix — every line carries the block's background
|
|
377
|
-
// (multi-line whole; resize-safe). Pipes stay plain.
|
|
378
|
-
return cell.text.split("\n").map((l) => `${p.bg}${escapeTerminal(l)}${p.reset}`);
|
|
379
|
-
case "thinking": {
|
|
380
|
-
const block = cell.text;
|
|
381
|
-
const trimmed = escapeTerminal(block.trim());
|
|
382
|
-
if (trimmed.length <= 100)
|
|
383
|
-
return [`${p.dim}…${trimmed}${p.reset}`];
|
|
384
|
-
return [`${p.dim}…${trimmed.slice(0, 100)} (${block.length} chars · /think)${p.reset}`];
|
|
385
|
-
}
|
|
386
|
-
case "tool": {
|
|
387
|
-
const name = escapeTerminal(cell.name);
|
|
388
|
-
const summary = escapeTerminal(cell.input);
|
|
389
|
-
if (cell.state === "done") {
|
|
390
|
-
const elapsed = cell.startedAt !== null && cell.doneAt !== null ? ((cell.doneAt - cell.startedAt) / 1000).toFixed(1) : "?";
|
|
391
|
-
if (cell.isError) {
|
|
392
|
-
const err = escapeTerminal(cell.resultText.split("\n")[0].slice(0, 60));
|
|
393
|
-
return [`${p.red}✗ ${name} (${err}, ${elapsed}s)${p.reset}`];
|
|
394
|
-
}
|
|
395
|
-
const delta = cell.added + cell.removed > 0 ? `, +${cell.added} -${cell.removed}` : "";
|
|
396
|
-
return [`${p.blue}✓ ${name}${p.reset} (${summary}${delta}, ${elapsed}s)`];
|
|
397
|
-
}
|
|
398
|
-
if (cell.state === "approval") {
|
|
399
|
-
const lines = [`→ ${name} ${summary} ${p.blue}⏸${p.reset}`];
|
|
400
|
-
// v2e: the mini-diff — ▎ blue edge (the brick motif), - red /
|
|
401
|
-
// + green / context dim; NO_COLOR keeps the ± prefixes plain.
|
|
402
|
-
if (cell.diff !== null) {
|
|
403
|
-
for (const d of cell.diff) {
|
|
404
|
-
const body = d.kind === "-"
|
|
405
|
-
? `${p.red}- ${escapeTerminal(d.text)}${p.reset}`
|
|
406
|
-
: d.kind === "+"
|
|
407
|
-
? `${p.green}+ ${escapeTerminal(d.text)}${p.reset}`
|
|
408
|
-
: `${p.dim} ${escapeTerminal(d.text)}${p.reset}`;
|
|
409
|
-
lines.push(`${p.blue}▎${p.reset}${body}`);
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
return lines;
|
|
413
|
-
}
|
|
414
|
-
if (cell.state === "running") {
|
|
415
|
-
const elapsed = cell.startedAt !== null ? Math.max(1, Math.round((Date.now() - cell.startedAt) / 1000)) : 1;
|
|
416
|
-
return [`→ ${name} ${summary} ${p.blue}${SPINNER[this.#spinnerI % SPINNER.length]}${p.reset} ${elapsed}s`];
|
|
417
|
-
}
|
|
418
|
-
return [`→ ${name} ${summary}`];
|
|
419
|
-
}
|
|
420
|
-
case "text": {
|
|
421
|
-
const text = escapeTerminal(cell.text);
|
|
422
|
-
const wrapped = this.#wrap(text, W);
|
|
423
|
-
return wrapped.length > 0 ? wrapped : [""];
|
|
424
|
-
}
|
|
425
|
-
case "notice":
|
|
426
|
-
return [escapeTerminal(cell.text)];
|
|
427
|
-
case "raw":
|
|
428
|
-
return cell.lines.map((l) => escapeTerminal(l));
|
|
429
|
-
case "terminal":
|
|
430
|
-
// the honest label (done / aborted / error) + the status + the
|
|
431
|
-
// rhythm gap blank
|
|
432
|
-
return [cell.label, cell.line, ""];
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
#cellHeight(cell, W) {
|
|
436
|
-
const lines = this.#cellLines(cell, W);
|
|
437
|
-
return Math.max(1, lines.length);
|
|
438
|
-
}
|
|
439
|
-
/** Wrap a body text by display width (the terminal's own wrapping,
|
|
440
|
-
* approximated — documented; the region clamp keeps the dock safe). */
|
|
441
|
-
#wrap(text, W) {
|
|
442
|
-
if (W < 4)
|
|
443
|
-
return [text];
|
|
444
|
-
const out = [];
|
|
445
|
-
let current = "";
|
|
446
|
-
let width = 0;
|
|
447
|
-
for (const ch of text) {
|
|
448
|
-
const cw = displayWidth(ch);
|
|
449
|
-
if (ch === "\n" || width + cw > W) {
|
|
450
|
-
out.push(current);
|
|
451
|
-
current = "";
|
|
452
|
-
width = 0;
|
|
453
|
-
if (ch === "\n")
|
|
454
|
-
continue;
|
|
455
|
-
}
|
|
456
|
-
current += ch;
|
|
457
|
-
width += cw;
|
|
458
|
-
}
|
|
459
|
-
out.push(current);
|
|
460
|
-
return out;
|
|
461
|
-
}
|
|
462
|
-
#toolCell(callId) {
|
|
463
|
-
const i = this.#toolCells.get(callId);
|
|
464
|
-
return i === undefined ? null : (this.#cells[i] ?? null);
|
|
465
|
-
}
|
|
466
|
-
/** Close an open TEXT cell when a new cell starts — the runtime emits
|
|
467
|
-
* no text_end (it is an adapter-level event), so the stream's next
|
|
468
|
-
* cell is the close signal; without it the freeze blocks behind the
|
|
469
|
-
* open text and everything after it re-renders in the tail forever
|
|
470
|
-
* (the #13 flood reproduced the overwrite). */
|
|
471
|
-
#closeOpenText() {
|
|
472
|
-
const last = this.#cells[this.#cells.length - 1];
|
|
473
|
-
if (last !== undefined && last.kind === "text" && !last.done)
|
|
474
|
-
last.done = true;
|
|
475
|
-
}
|
|
476
|
-
/** Close an open thinking cell when a new cell starts (the block's
|
|
477
|
-
* fold freezes at the transition). */
|
|
478
|
-
#closeOpenThinking() {
|
|
479
|
-
if (!this.#isActive() && this.#pipeBuf !== "") {
|
|
480
|
-
this.#lastThinking = this.#pipeBuf;
|
|
481
|
-
process.stdout.write(foldThinking(this.#pipeBuf));
|
|
482
|
-
this.#pipeBuf = "";
|
|
483
|
-
return;
|
|
484
|
-
}
|
|
485
|
-
const last = this.#cells[this.#cells.length - 1];
|
|
486
|
-
if (last !== undefined && last.kind === "thinking" && !last.done) {
|
|
487
|
-
last.done = true;
|
|
488
|
-
this.#lastThinking = last.text;
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
}
|
package/dist/dock.d.ts
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* v2b/v2c — the bottom-anchored UI (TTY + color only). Borrows pi-tui's
|
|
3
|
-
* IDEA — a DECSTBM scroll region with a reserved bottom — without its
|
|
4
|
-
* implementation: zero dependencies, line-level ANSI, no differential
|
|
5
|
-
* renderer.
|
|
6
|
-
*
|
|
7
|
-
* Layout (H = terminal height): rows 1..H-4 = the scroll region (the body
|
|
8
|
-
* streams and scrolls here, never touching the bottom), row H-3 = the
|
|
9
|
-
* upper dim dotted separator (╌), row H-2 = the input line (the blue
|
|
10
|
-
* brick ▌you> + the v2c editor's row — readline is gone from the TTY
|
|
11
|
-
* path), row H-1 = the lower dotted separator, row H = the live status
|
|
12
|
-
* bar (v3 §03: idle "▸ <mode> · /mode to switch · …", running
|
|
13
|
-
* "▖ working Ns · …"; a takeover question replaces it). Bottom redraws
|
|
14
|
-
* are wrapped in CSI 2026 (synchronized output) to avoid flicker — the
|
|
15
|
-
* pi trick. The visual identity is the kiso brick motif — ▌ half-block,
|
|
16
|
-
* dotted separators — deliberately NOT the CC rounded frame nor the pi
|
|
17
|
-
* editor (ADR-0039 Amendment 2).
|
|
18
|
-
*
|
|
19
|
-
* Pipes / NO_COLOR: the dock never activates; the v2a line mode stays
|
|
20
|
-
* byte-for-byte (the existing e2e assertions guard it).
|
|
21
|
-
*/
|
|
22
|
-
export declare class Dock {
|
|
23
|
-
#private;
|
|
24
|
-
/** v2b: docked only on a color TTY — pipes and NO_COLOR stay v2a. */
|
|
25
|
-
get active(): boolean;
|
|
26
|
-
/** Bind the CURRENT input line's state — called when a readline takes
|
|
27
|
-
* over (the chat REPL, the trust question's short-lived rl, resume). */
|
|
28
|
-
bindInput(state: () => {
|
|
29
|
-
line: string;
|
|
30
|
-
cursor: number;
|
|
31
|
-
}, prompt: string): void;
|
|
32
|
-
/** Enter docked mode: draw the chrome. #13 (P1): the DECSTBM scroll
|
|
33
|
-
* region is GONE — v2d-B (ADR-0040): the body uses plain LF scrolling
|
|
34
|
-
* so frozen lines enter the native scrollback deterministically
|
|
35
|
-
* (region-scrolled lines are terminal-dependent — some terminals drop
|
|
36
|
-
* them). The dock rows are redrawn by the body after every scroll. A
|
|
37
|
-
* TTY without a real window size (rows < 4) stays in the v2a line
|
|
38
|
-
* mode — the bottom three rows need room to exist. */
|
|
39
|
-
enter(): void;
|
|
40
|
-
/** Teardown — CSI r resets the scroll region, the cursor lands at the
|
|
41
|
-
* input line, the bottom rows are cleared: no broken terminal. Called
|
|
42
|
-
* from main's finally on EVERY exit path (kill -9 excepted — README:
|
|
43
|
-
* `reset` saves it). */
|
|
44
|
-
exit(): void;
|
|
45
|
-
/** SIGWINCH: recompute the size, redraw the chrome. */
|
|
46
|
-
onResize(): void;
|
|
47
|
-
/** v3 §04: bind the editor's slash-command menu state — the menu rows
|
|
48
|
-
* render ABOVE the chrome (over the body's bottom rows; the menu
|
|
49
|
-
* opens while the buffer is a "/" prefix, when no tail is live). */
|
|
50
|
-
bindMenu(state: () => {
|
|
51
|
-
items: readonly import("./editor.js").MenuItem[];
|
|
52
|
-
selected: number;
|
|
53
|
-
} | null): void;
|
|
54
|
-
/** The input line's edit column — prompt width + cursor + 1. The
|
|
55
|
-
* dock's redraw and the body's cursor return both end here, so the
|
|
56
|
-
* ACTUAL cursor always equals what the editor tracks. The width is
|
|
57
|
-
* DISPLAY width (the editor's cursor column is already width-based —
|
|
58
|
-
* the CJK drift root cause, editor.ts). v2d: public — the Body's
|
|
59
|
-
* render loop ends at this column. */
|
|
60
|
-
editCol(): number;
|
|
61
|
-
/** The status bar's base text (usage, ctx, session, …). */
|
|
62
|
-
setStatus(text: string): void;
|
|
63
|
-
/** The live tail — the spinner glyph or "running <tool> Ns". */
|
|
64
|
-
setTail(tail: string): void;
|
|
65
|
-
/** Show a takeover question at the status position (answered at the
|
|
66
|
-
* input line by the caller's readline); clearQuestion() restores. */
|
|
67
|
-
showQuestion(question: string): void;
|
|
68
|
-
clearQuestion(): void;
|
|
69
|
-
/** The bottom four rows, wrapped in CSI 2026 (synchronized output —
|
|
70
|
-
* the pi trick against flicker). The cursor ends at the input line's
|
|
71
|
-
* edit position. v3 §03: the upper ╌ row, the input row, the lower
|
|
72
|
-
* ╌ row, the status row — the status is dim (blue accents inside
|
|
73
|
-
* come from the CLI's composition). */
|
|
74
|
-
redraw(): void;
|
|
75
|
-
}
|