@vincemakes/kiso-tui 0.1.36 → 0.1.37
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/components.d.ts +26 -3
- package/dist/components.js +259 -46
- package/dist/compositor.d.ts +17 -3
- package/dist/compositor.js +104 -35
- package/dist/editor.d.ts +2 -6
- package/dist/editor.js +11 -53
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/render.d.ts +35 -4
- package/dist/render.js +115 -29
- package/dist/width.d.ts +15 -0
- package/dist/width.js +59 -0
- package/package.json +1 -1
package/dist/components.d.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* (untouched); render.ts supplies the original text (palette, escape,
|
|
16
16
|
* tint, fold wording).
|
|
17
17
|
*/
|
|
18
|
-
import { foldThinking, foldResult, renderToolSummary } from "./render.js";
|
|
18
|
+
import { foldThinking, foldResult, renderToolSummary, type ResumeMeta } from "./render.js";
|
|
19
19
|
/** The spinner glyphs, cycled by the compositor's on-demand tick. */
|
|
20
20
|
export declare const SPINNER: string[];
|
|
21
21
|
/** The frame context the compositor passes down — the pieces of time
|
|
@@ -24,6 +24,10 @@ export declare const SPINNER: string[];
|
|
|
24
24
|
export interface FrameCtx {
|
|
25
25
|
readonly spinnerI: number;
|
|
26
26
|
readonly now: number;
|
|
27
|
+
/** The terminal height (rows) — the banner cell's tier input (W1:
|
|
28
|
+
* the tier table reads H, so a resize RE-TIERS instead of
|
|
29
|
+
* re-folding frozen rows). */
|
|
30
|
+
readonly height: number;
|
|
27
31
|
}
|
|
28
32
|
/** ONE screen line a component emits (raw, SGR included). */
|
|
29
33
|
export type RenderLine = string;
|
|
@@ -43,7 +47,19 @@ export declare function visibleWidth(line: string): number;
|
|
|
43
47
|
export interface Component {
|
|
44
48
|
render(width: number, ctx: FrameCtx): string[];
|
|
45
49
|
}
|
|
46
|
-
/** The
|
|
50
|
+
/** The W11 spacing formula — "a row gets one blank line above it when
|
|
51
|
+
* the row is itself a block, or when the previous sibling was taller
|
|
52
|
+
* than one row". One-row siblings pack tight; anything multi-row
|
|
53
|
+
* breathes on both sides. The FIRST cell never gets the blank (it sits
|
|
54
|
+
* at the body's top — the banner would otherwise start one row down).
|
|
55
|
+
* `prev` is the previous sibling's OWN rows (raw — a cell's own blank
|
|
56
|
+
* must never count toward its height). The blank is a JOIN artifact:
|
|
57
|
+
* the cell's own render stays blank-free, so per-cell accounting
|
|
58
|
+
* (heights, the fold cache) never sees a fake row. */
|
|
59
|
+
export declare function bodySpacing(prev: readonly string[] | null, rows: readonly string[]): string[];
|
|
60
|
+
/** The container — vertical concatenation with the W11 formula. No
|
|
61
|
+
* component decides its own spacing: every blank in the body is the
|
|
62
|
+
* container's. */
|
|
47
63
|
export declare class Container implements Component {
|
|
48
64
|
private readonly children;
|
|
49
65
|
constructor(children: Component[]);
|
|
@@ -61,6 +77,7 @@ export type BodyCell = {
|
|
|
61
77
|
kind: "tool";
|
|
62
78
|
name: string;
|
|
63
79
|
input: string;
|
|
80
|
+
childRoles: string[];
|
|
64
81
|
state: "pending" | "approval" | "running" | "done";
|
|
65
82
|
isError: boolean;
|
|
66
83
|
resultText: string;
|
|
@@ -78,6 +95,12 @@ export type BodyCell = {
|
|
|
78
95
|
kind: "notice";
|
|
79
96
|
text: string;
|
|
80
97
|
done: true;
|
|
98
|
+
} | {
|
|
99
|
+
kind: "banner";
|
|
100
|
+
version: string;
|
|
101
|
+
extensionsText: string;
|
|
102
|
+
resume: ResumeMeta[];
|
|
103
|
+
done: true;
|
|
81
104
|
} | {
|
|
82
105
|
kind: "raw";
|
|
83
106
|
lines: string[];
|
|
@@ -105,7 +128,7 @@ export declare function cellComponent(cell: BodyCell): Component;
|
|
|
105
128
|
* the hint CUT FIRST when the width is short (the #16g rule); when
|
|
106
129
|
* the STATUS ITSELF cannot fit, it cuts with a "…" — the last resort,
|
|
107
130
|
* enforced by invariant ① (the old code let the status soft-wrap). */
|
|
108
|
-
export declare function statusLine(status: string, tail: string, question: boolean, W: number): string;
|
|
131
|
+
export declare function statusLine(status: string, tail: string, question: boolean, W: number, hint?: string): string;
|
|
109
132
|
/** The footer — the ONE dotted row (the old two-row chrome is gone;
|
|
110
133
|
* the wall cannot return by construction). */
|
|
111
134
|
export declare function footerLine(W: number): string;
|
package/dist/components.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* tint, fold wording).
|
|
17
17
|
*/
|
|
18
18
|
import { displayWidth } from "./editor.js";
|
|
19
|
-
import { escapeTerminal, foldThinking, foldResult, colorInlineCode, renderTerminalGap, renderToolSummary, palette, } from "./render.js";
|
|
19
|
+
import { bannerLines, escapeTerminal, foldThinking, foldResult, colorInlineCode, renderTerminalGap, renderToolSummary, palette, } from "./render.js";
|
|
20
20
|
/** The spinner glyphs, cycled by the compositor's on-demand tick. */
|
|
21
21
|
export const SPINNER = ["▖", "▘", "▝", "▗"];
|
|
22
22
|
/**
|
|
@@ -106,14 +106,39 @@ export function visibleWidth(line) {
|
|
|
106
106
|
}
|
|
107
107
|
return w;
|
|
108
108
|
}
|
|
109
|
-
/** The
|
|
109
|
+
/** The W11 spacing formula — "a row gets one blank line above it when
|
|
110
|
+
* the row is itself a block, or when the previous sibling was taller
|
|
111
|
+
* than one row". One-row siblings pack tight; anything multi-row
|
|
112
|
+
* breathes on both sides. The FIRST cell never gets the blank (it sits
|
|
113
|
+
* at the body's top — the banner would otherwise start one row down).
|
|
114
|
+
* `prev` is the previous sibling's OWN rows (raw — a cell's own blank
|
|
115
|
+
* must never count toward its height). The blank is a JOIN artifact:
|
|
116
|
+
* the cell's own render stays blank-free, so per-cell accounting
|
|
117
|
+
* (heights, the fold cache) never sees a fake row. */
|
|
118
|
+
export function bodySpacing(prev, rows) {
|
|
119
|
+
if (rows.length === 0 || prev === null || prev.length === 0)
|
|
120
|
+
return rows;
|
|
121
|
+
if (rows.length > 1 || prev.length > 1)
|
|
122
|
+
return ["", ...rows];
|
|
123
|
+
return rows;
|
|
124
|
+
}
|
|
125
|
+
/** The container — vertical concatenation with the W11 formula. No
|
|
126
|
+
* component decides its own spacing: every blank in the body is the
|
|
127
|
+
* container's. */
|
|
110
128
|
export class Container {
|
|
111
129
|
children;
|
|
112
130
|
constructor(children) {
|
|
113
131
|
this.children = children;
|
|
114
132
|
}
|
|
115
133
|
render(width, ctx) {
|
|
116
|
-
|
|
134
|
+
const out = [];
|
|
135
|
+
let prev = null;
|
|
136
|
+
for (const c of this.children) {
|
|
137
|
+
const rows = c.render(width, ctx);
|
|
138
|
+
out.push(...bodySpacing(prev, rows));
|
|
139
|
+
prev = rows;
|
|
140
|
+
}
|
|
141
|
+
return out;
|
|
117
142
|
}
|
|
118
143
|
}
|
|
119
144
|
const TOOL_SUMMARY_MAX = 60; // the tool line's parameter summary, chars
|
|
@@ -131,6 +156,8 @@ export function cellComponent(cell) {
|
|
|
131
156
|
return new AssistantMessage(cell);
|
|
132
157
|
case "notice":
|
|
133
158
|
return new ErrorLine(cell);
|
|
159
|
+
case "banner":
|
|
160
|
+
return new Banner(cell);
|
|
134
161
|
case "raw":
|
|
135
162
|
return new RawBlock(cell);
|
|
136
163
|
case "terminal":
|
|
@@ -141,10 +168,18 @@ export function cellComponent(cell) {
|
|
|
141
168
|
}
|
|
142
169
|
/**
|
|
143
170
|
* The user message — the left rail (bright-white BOLD ▍ per row, the
|
|
144
|
-
* v4.1 design)
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
171
|
+
* v4.1 design) + the W16 inset chip. The chip folds the text at W−6
|
|
172
|
+
* (the rail + the indent + the side pads), then pads EVERY row to the
|
|
173
|
+
* longest row's DISPLAY width + one space each side, indented two: the
|
|
174
|
+
* block is only as wide as what was said (never the full-width band —
|
|
175
|
+
* a short message like /think would paint a bar across the terminal).
|
|
176
|
+
* The padding is by cells (charWidth is the width authority), so a CJK
|
|
177
|
+
* row pads by width, never by chars, and the chip never overruns its
|
|
178
|
+
* fold. SGR 7 closed with SGR 27 — never SGR 0, the chip composes
|
|
179
|
+
* with a surrounding span — and NEVER dim: reverse video inverts the
|
|
180
|
+
* CURRENT colours, so dimmed text would invert into a dimmed block
|
|
181
|
+
* with no contrast. The ▍ rail stays: SGR is an emphasis on top, the
|
|
182
|
+
* rail is the structural fallback that survives a pipe.
|
|
148
183
|
*/
|
|
149
184
|
class UserMessage {
|
|
150
185
|
cell;
|
|
@@ -154,12 +189,15 @@ class UserMessage {
|
|
|
154
189
|
render(W, _ctx) {
|
|
155
190
|
const p = palette();
|
|
156
191
|
const rail = `${p.bold}▍${p.reset} `;
|
|
157
|
-
const
|
|
192
|
+
const chipW = Math.max(1, W - 6);
|
|
158
193
|
const rows = [];
|
|
159
194
|
for (const para of this.cell.text.split("\n")) {
|
|
160
|
-
const folded = foldLine(escapeTerminal(para),
|
|
161
|
-
|
|
162
|
-
|
|
195
|
+
const folded = foldLine(escapeTerminal(para), chipW);
|
|
196
|
+
const inner = Math.max(...folded.map((r) => displayWidth(r)));
|
|
197
|
+
for (const row of folded) {
|
|
198
|
+
const pad = inner - displayWidth(row);
|
|
199
|
+
rows.push(`${rail} ${p.rv} ${row}${" ".repeat(pad)} ${p.rvEnd}`);
|
|
200
|
+
}
|
|
163
201
|
}
|
|
164
202
|
return rows.length > 0 ? rows : [rail.trimEnd()];
|
|
165
203
|
}
|
|
@@ -168,7 +206,8 @@ class UserMessage {
|
|
|
168
206
|
* rides the fold's own row (the #17 fix's slice, componentized). The
|
|
169
207
|
* slice is DISPLAY-WIDTH-based (the char-based slice overflowed with
|
|
170
208
|
* CJK — 2 cells per char — and tripped invariant ① on a real
|
|
171
|
-
* Chinese session).
|
|
209
|
+
* Chinese session). W2: the leading ⋯ is the thinking gutter — the
|
|
210
|
+
* midline mark (the state), never the text ellipsis (the truncation). */
|
|
172
211
|
class ThinkingFold {
|
|
173
212
|
cell;
|
|
174
213
|
constructor(cell) {
|
|
@@ -182,11 +221,86 @@ class ThinkingFold {
|
|
|
182
221
|
// UNFOLDED and tripped invariant ① (the crash class still live on
|
|
183
222
|
// npm for short /think blocks after a resize).
|
|
184
223
|
if (trimmed.length <= 100)
|
|
185
|
-
return [`${palette().dim}
|
|
224
|
+
return [`${palette().dim}⋯${widthCut(trimmed, Math.max(1, W - 1))}${palette().reset}`];
|
|
186
225
|
const suffix = ` (${block.length} chars · /think)`;
|
|
187
226
|
const slice = Math.max(1, W - 1 - suffix.length);
|
|
188
|
-
return [`${palette().dim}
|
|
227
|
+
return [`${palette().dim}⋯${widthCut(trimmed, slice)}${suffix}${palette().reset}`];
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/** Fold a line's CONTENT at W−2 and prefix EVERY row with the gutter
|
|
231
|
+
* (W2: a wrapped tool row keeps its state mark — the left edge alone
|
|
232
|
+
* distinguishes the states at --plain; the UserMessage rail precedent,
|
|
233
|
+
* v5 #16f). The gutter carries its own SGR (e.g. the bold ✓). */
|
|
234
|
+
function gutterFold(gutter, line, W) {
|
|
235
|
+
const textW = Math.max(1, W - 2);
|
|
236
|
+
return foldLine(line, textW).map((r) => `${gutter}${r}`);
|
|
237
|
+
}
|
|
238
|
+
/** Lines without the phantom empty line after a trailing newline. */
|
|
239
|
+
function countLines(text) {
|
|
240
|
+
if (text === "")
|
|
241
|
+
return 0;
|
|
242
|
+
const parts = text.split("\n");
|
|
243
|
+
return parts[parts.length - 1] === "" ? parts.length - 1 : parts.length;
|
|
244
|
+
}
|
|
245
|
+
/** W4: the settled-row metadata — the human summary in parentheses. The
|
|
246
|
+
* separation NEVER relies on dim: a pipe drops the SGR, and the shapes
|
|
247
|
+
* below read at full strength with the palette off. read → the line
|
|
248
|
+
* count ("912 lines"; "200 of 3412 lines" when the tool cut it — the
|
|
249
|
+
* note names the remainder; ≥1000 k-formats, "2.4k lines"); write/edit
|
|
250
|
+
* → the ± diff stats (the approval diff's counts — an auto-allowed
|
|
251
|
+
* write never computed one, so the input's own counts fall back, then
|
|
252
|
+
* the result's line count); shell → the exit code (parsed from the
|
|
253
|
+
* failure text — the tool names it — 0 on success); a non-shell error
|
|
254
|
+
* → the error text's first line; anything else → the result's line
|
|
255
|
+
* count. */
|
|
256
|
+
function settledMeta(c) {
|
|
257
|
+
if (c.isError) {
|
|
258
|
+
// a shell EXECUTION failure names its code first ("exit 1: …") —
|
|
259
|
+
// that IS the metadata, and the body shows the full text. A
|
|
260
|
+
// shell without the code (a denial, a precondition) is not an
|
|
261
|
+
// exit failure: the first line stays the metadata, exactly like
|
|
262
|
+
// any other error.
|
|
263
|
+
if (c.name === "shell" && /^exit \d+/.test(c.resultText))
|
|
264
|
+
return `exit ${/^exit (\d+)/.exec(c.resultText)[1]}`;
|
|
265
|
+
return c.resultText.split("\n")[0].slice(0, 60);
|
|
266
|
+
}
|
|
267
|
+
if (c.name === "read_file") {
|
|
268
|
+
const noteAt = c.resultText.lastIndexOf("\n… ");
|
|
269
|
+
const shown = countLines(noteAt >= 0 ? c.resultText.slice(0, noteAt) : c.resultText);
|
|
270
|
+
const more = noteAt >= 0 ? /(\d+) more lines?/.exec(c.resultText.slice(noteAt)) : null;
|
|
271
|
+
const k = (n) => (n >= 1000 ? `${(n / 1000).toFixed(1).replace(/\.0$/, "")}k` : String(n));
|
|
272
|
+
if (more !== null) {
|
|
273
|
+
const total = shown + Number(more[1]);
|
|
274
|
+
return `${shown} of ${total} line${total === 1 ? "" : "s"}`;
|
|
275
|
+
}
|
|
276
|
+
return `${k(shown)} line${shown === 1 ? "" : "s"}`;
|
|
277
|
+
}
|
|
278
|
+
if (c.name === "write_file" || c.name === "edit_file") {
|
|
279
|
+
if (c.added + c.removed > 0)
|
|
280
|
+
return `+${c.added} -${c.removed}`;
|
|
281
|
+
// no approval diff (an auto-allowed write): the input summary may
|
|
282
|
+
// be sliced at TOOL_SUMMARY_MAX — best-effort, then the last resort
|
|
283
|
+
let parsed = null;
|
|
284
|
+
try {
|
|
285
|
+
parsed = JSON.parse(c.input);
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
parsed = null;
|
|
289
|
+
}
|
|
290
|
+
if (c.name === "write_file" && parsed !== null && typeof parsed.content === "string") {
|
|
291
|
+
return `+${countLines(parsed.content)}`;
|
|
292
|
+
}
|
|
293
|
+
if (c.name === "edit_file" && parsed !== null && typeof parsed.search === "string" && typeof parsed.replace === "string") {
|
|
294
|
+
const added = countLines(parsed.replace);
|
|
295
|
+
const removed = countLines(parsed.search);
|
|
296
|
+
if (added + removed > 0)
|
|
297
|
+
return `+${added} -${removed}`;
|
|
298
|
+
}
|
|
189
299
|
}
|
|
300
|
+
if (c.name === "shell")
|
|
301
|
+
return "exit 0";
|
|
302
|
+
const n = countLines(c.resultText);
|
|
303
|
+
return `${n} line${n === 1 ? "" : "s"}`;
|
|
190
304
|
}
|
|
191
305
|
/** The tool execution line + the bounded block — every state is its
|
|
192
306
|
* own render; the lines fold (the summary gives way first). W7 (the
|
|
@@ -195,7 +309,14 @@ class ThinkingFold {
|
|
|
195
309
|
* renderer-cut row (`└ +N … · ctrl+r`) sits INSIDE the cap (a
|
|
196
310
|
* truncated block is cap−1 output rows + the cut row); the TOOL-cut
|
|
197
311
|
* row (`└ capped by …` — the tool's OWN truncation note, W10) is a
|
|
198
|
-
* DIFFERENT fact, never counted in the output cap.
|
|
312
|
+
* DIFFERENT fact, never counted in the output cap. W3: the verb is
|
|
313
|
+
* stripped of its "_file" suffix and padded to 5 columns — the target
|
|
314
|
+
* paths line up (the pipe path strips the same suffix, render.ts —
|
|
315
|
+
* both paths print the same verb; a verb ≥ 5 columns is not padded).
|
|
316
|
+
* The block's cut note keeps the RAW name (it names the tool the
|
|
317
|
+
* model should call again). W4: the settled row's parentheses hold
|
|
318
|
+
* the human metadata (settledMeta) — the input summary lived in the
|
|
319
|
+
* running row; the OUTCOME is what the settled row says. */
|
|
199
320
|
class ToolExecution {
|
|
200
321
|
cell;
|
|
201
322
|
constructor(cell) {
|
|
@@ -204,29 +325,36 @@ class ToolExecution {
|
|
|
204
325
|
render(W, ctx) {
|
|
205
326
|
const p = palette();
|
|
206
327
|
const c = this.cell;
|
|
207
|
-
const
|
|
328
|
+
const verb = escapeTerminal(c.name.replace("_file", ""));
|
|
329
|
+
const verbCol = verb.length < 5 ? `${verb}${" ".repeat(5 - verb.length)}` : verb;
|
|
208
330
|
const summary = escapeTerminal(c.input);
|
|
209
331
|
if (c.state === "done") {
|
|
210
332
|
const elapsed = c.startedAt !== null && c.doneAt !== null ? ((c.doneAt - c.startedAt) / 1000).toFixed(1) : "?";
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
333
|
+
const meta = escapeTerminal(settledMeta(c));
|
|
334
|
+
const out = c.isError
|
|
335
|
+
? gutterFold(`${p.red}✗${p.reset} `, `${p.red}${verbCol} (${meta}, ${elapsed}s)${p.reset}`, W)
|
|
336
|
+
: gutterFold(`${p.bold}✓${p.reset} `, `${verbCol} (${meta}, ${elapsed}s)`, W);
|
|
215
337
|
out.push(...toolBlockBody(c, W));
|
|
216
338
|
return out;
|
|
217
339
|
}
|
|
218
340
|
if (c.state === "approval") {
|
|
219
|
-
|
|
341
|
+
// W2: the ⏸ is the GUTTER (the left edge), never the line's tail
|
|
342
|
+
const out = gutterFold(`${p.bold}⏸${p.reset} `, `${verbCol} ${summary}`, W);
|
|
220
343
|
out.push(...toolBlockBody(c, W));
|
|
221
344
|
return out;
|
|
222
345
|
}
|
|
223
346
|
if (c.state === "running") {
|
|
347
|
+
// W2: the spinner IS the gutter (the left edge); the elapsed
|
|
348
|
+
// rides the summary's tail
|
|
224
349
|
const elapsed = c.startedAt !== null ? Math.max(1, Math.round((ctx.now - c.startedAt) / 1000)) : 1;
|
|
225
|
-
const out =
|
|
350
|
+
const out = gutterFold(`${p.bold}${SPINNER[ctx.spinnerI % SPINNER.length]}${p.reset} `, `${verbCol} ${summary} ${elapsed}s`, W);
|
|
226
351
|
out.push(...toolBlockBody(c, W));
|
|
227
352
|
return out;
|
|
228
353
|
}
|
|
229
|
-
|
|
354
|
+
// W2: ◦ replaces → for QUEUED — · is the separator inside every
|
|
355
|
+
// metadata group; a queued marker that is also the separator
|
|
356
|
+
// glyph reads as noise
|
|
357
|
+
return gutterFold(`${p.dim}◦${p.reset} `, `${verbCol} ${summary}`, W);
|
|
230
358
|
}
|
|
231
359
|
}
|
|
232
360
|
// ---- the bounded-block flow contract (W7, W8, W10) ----
|
|
@@ -238,9 +366,10 @@ const CAP_DIFF = 12; // the approval diff: head + the named middle + tail
|
|
|
238
366
|
const CAP_ERROR = 3; // the error text head
|
|
239
367
|
/** The block body rows' prefixes (W2's gutter table): │ a bounded
|
|
240
368
|
* block's body, └ the block's last row — what was cut, where the rest
|
|
241
|
-
* is
|
|
242
|
-
|
|
243
|
-
const
|
|
369
|
+
* is — at the LEFT EDGE (the gutter column: the left edge alone
|
|
370
|
+
* distinguishes the states at --plain). Structural (constraint 1). */
|
|
371
|
+
const BODY_ROW = "│ ";
|
|
372
|
+
const CUT_ROW = "└ ";
|
|
244
373
|
const blockMemo = new WeakMap();
|
|
245
374
|
/** The block's body rows below the header (memoized, W9). */
|
|
246
375
|
function toolBlockBody(c, W) {
|
|
@@ -252,12 +381,16 @@ function toolBlockBody(c, W) {
|
|
|
252
381
|
const p = palette();
|
|
253
382
|
const rows = c.state === "done"
|
|
254
383
|
? c.isError
|
|
255
|
-
? errorBody(c
|
|
256
|
-
: c.name
|
|
257
|
-
?
|
|
258
|
-
:
|
|
384
|
+
? errorBody(c, W)
|
|
385
|
+
: c.name === "delegate"
|
|
386
|
+
? delegateSettled(c, W)
|
|
387
|
+
: c.name.startsWith("shell")
|
|
388
|
+
? shellTail(c.resultText, W)
|
|
389
|
+
: []
|
|
259
390
|
: c.state === "running"
|
|
260
|
-
?
|
|
391
|
+
? c.name === "delegate"
|
|
392
|
+
? delegateRunning(c, W)
|
|
393
|
+
: liveWindow(c.resultText, W)
|
|
261
394
|
: c.state === "approval"
|
|
262
395
|
? diffBody(c.diff, W)
|
|
263
396
|
: [];
|
|
@@ -297,9 +430,14 @@ function shellTail(text, W) {
|
|
|
297
430
|
/** The error text head: the FIRST rows, capped at 3 — the answer is at
|
|
298
431
|
* the start (opencode's collapseToolOutput direction). The header row
|
|
299
432
|
* already summarizes the first line, so the body starts at line 2. */
|
|
300
|
-
function errorBody(
|
|
433
|
+
function errorBody(c, W) {
|
|
301
434
|
const p = palette();
|
|
302
|
-
|
|
435
|
+
// W4: a shell EXECUTION failure's line 0 ("exit 1: …") no longer
|
|
436
|
+
// rides the header — the parsed code does — so the body keeps the
|
|
437
|
+
// FULL text. Any other error keeps the pre-W4 split: line 0 is the
|
|
438
|
+
// header's metadata, the body shows the rest.
|
|
439
|
+
const skipFirst = c.name === "shell" && /^exit \d+/.test(c.resultText) ? 0 : 1;
|
|
440
|
+
const rows = blockRows(c.resultText.split("\n").slice(skipFirst).join("\n"), W);
|
|
303
441
|
if (rows.length <= CAP_ERROR)
|
|
304
442
|
return rows;
|
|
305
443
|
const cut = foldLine(`${p.dim}${CUT_ROW}+${rows.length - (CAP_ERROR - 1)} more · ctrl+r${p.reset}`, W);
|
|
@@ -324,32 +462,88 @@ function liveWindow(text, W) {
|
|
|
324
462
|
const cut = foldLine(`${p.dim}${CUT_ROW}+${rows.length - (CAP_LIVE_WINDOW - 1)} earlier rows · ctrl+r${p.reset}`, W);
|
|
325
463
|
return [...rows.slice(rows.length - (CAP_LIVE_WINDOW - 1)), ...cut];
|
|
326
464
|
}
|
|
465
|
+
/** W12: the delegate's child sessions collapse to the tool row plus ONE
|
|
466
|
+
* line — the height NEVER changes (running → settled replaces the row
|
|
467
|
+
* in place). The running row derives from the INPUT: the parent has no
|
|
468
|
+
* live channel to a running child (ToolContext carries only
|
|
469
|
+
* signal/sessionId; execute returns ONE result), so the roles are the
|
|
470
|
+
* honest current data — the spec's "<child's current tool>" has no
|
|
471
|
+
* event source. The settled row parses the extension's summary marker
|
|
472
|
+
* (the blob's first line) — its absence falls back to no body (an old
|
|
473
|
+
* extension's output still renders). The one-line shape is shared with
|
|
474
|
+
* W18's status row (the work order: "implement them with one helper"). */
|
|
475
|
+
function delegateRunning(c, W) {
|
|
476
|
+
const p = palette();
|
|
477
|
+
const n = c.childRoles.length;
|
|
478
|
+
const text = n === 0 ? "children running…" : `${n === 1 ? "1 child" : `${n} children`} · ${c.childRoles.join(" · ")}`;
|
|
479
|
+
return [oneLineRow(p, text, W)];
|
|
480
|
+
}
|
|
481
|
+
function delegateSettled(c, W) {
|
|
482
|
+
const p = palette();
|
|
483
|
+
const m = /^summary: (.+)$/m.exec(c.resultText);
|
|
484
|
+
if (m === null)
|
|
485
|
+
return [];
|
|
486
|
+
return [oneLineRow(p, `${m[1]} · /last for the report`, W)];
|
|
487
|
+
}
|
|
488
|
+
/** ONE row at the left gutter, truncated to fit the width — never a
|
|
489
|
+
* fold (a fold would wrap into TWO rows and break the one-line height
|
|
490
|
+
* contract). */
|
|
491
|
+
function oneLineRow(p, text, W) {
|
|
492
|
+
const esc = escapeTerminal(text);
|
|
493
|
+
if (visibleWidth(`${p.dim}${CUT_ROW}${esc}${p.reset}`) <= W)
|
|
494
|
+
return `${p.dim}${CUT_ROW}${esc}${p.reset}`;
|
|
495
|
+
const w = Math.max(1, W - visibleWidth(`${p.dim}${CUT_ROW}${p.reset}`));
|
|
496
|
+
return `${p.dim}${CUT_ROW}${esc.slice(0, w - 1)}…${p.reset}`;
|
|
497
|
+
}
|
|
327
498
|
/** The approval mini-diff (W7): capped at 12 folded rows — the head +
|
|
328
499
|
* the named middle (the renderer cut — what was cut, how to expand) +
|
|
329
500
|
* the tail. The rows are folded at the current width BEFORE the cap —
|
|
330
501
|
* the R1 measured bug: truncateDiff capped at 40 ENTRIES while the
|
|
331
502
|
* fold turned them into 73 SCREEN rows at W≤80 (a 44-row terminal's
|
|
332
503
|
* content cap is H−4 = 40 — the approval force-committed a third of
|
|
333
|
-
* the screen into scrollback inside one frame).
|
|
504
|
+
* the screen into scrollback inside one frame).
|
|
505
|
+
* W17: the cap is a ROW budget at every width — the └ cut is ONE line
|
|
506
|
+
* (a folded cut pushed the total past 12 at narrow widths), and below
|
|
507
|
+
* a floor of 3 SOURCE lines visible the head/tail pair is noise (each
|
|
508
|
+
* fragment a sliver of a long line): drop to the head only — the head
|
|
509
|
+
* takes the whole budget — and the └ row carries the rest. */
|
|
334
510
|
function diffBody(diff, W) {
|
|
335
511
|
const p = palette();
|
|
336
512
|
if (diff === null)
|
|
337
513
|
return [];
|
|
338
514
|
const rows = [];
|
|
515
|
+
// W17: each line's fold START row (the running total) — the pair
|
|
516
|
+
// floor reads it for the head/tail SOURCE-line counts below.
|
|
517
|
+
const starts = [0];
|
|
339
518
|
for (const d of diff) {
|
|
340
519
|
const body = d.kind === "-"
|
|
341
520
|
? `${p.red}- ${escapeTerminal(d.text)}${p.reset}`
|
|
342
521
|
: d.kind === "+"
|
|
343
522
|
? `${p.green}+ ${escapeTerminal(d.text)}${p.reset}`
|
|
344
523
|
: `${p.dim} ${escapeTerminal(d.text)}${p.reset}`;
|
|
345
|
-
|
|
524
|
+
// W2: the diff body is a bounded block's body — the │ gutter
|
|
525
|
+
// (dim), never the old bold ▎ rail (the table lists no ▎); the
|
|
526
|
+
// +/- marks and their colors ride the content
|
|
527
|
+
rows.push(...gutterFold(`${p.dim}│${p.reset} `, body, W));
|
|
528
|
+
starts.push(rows.length);
|
|
346
529
|
}
|
|
347
530
|
if (rows.length <= CAP_DIFF)
|
|
348
531
|
return rows;
|
|
349
532
|
const head = Math.floor((CAP_DIFF - 1) / 2);
|
|
350
533
|
const tail = CAP_DIFF - 1 - head;
|
|
351
|
-
|
|
352
|
-
|
|
534
|
+
// W17: the └ cut is ONE row at every width — the count leads, the
|
|
535
|
+
// expand affordances are cuttable (the same one-line shape as W12's
|
|
536
|
+
// delegate row and W18's status row).
|
|
537
|
+
const cut = (n) => oneLineRow(p, `+${n} rows · ctrl+r to expand · /last for the full diff`, W);
|
|
538
|
+
// W17: the floor — the head window shows the lines whose fold starts
|
|
539
|
+
// before `head` rows; the tail window the lines whose fold ENDS after
|
|
540
|
+
// `rows.length - tail` (starts[i+1] is line i's end). When the pair
|
|
541
|
+
// shows fewer than 3 SOURCE lines together, it is noise at this width
|
|
542
|
+
// (each fragment a sliver of a long line): drop to the head only —
|
|
543
|
+
// the head takes the whole budget, the └ row carries the rest.
|
|
544
|
+
if (starts.filter((s) => s < head).length + starts.slice(1).filter((s) => s > rows.length - tail).length < 3)
|
|
545
|
+
return [...rows.slice(0, CAP_DIFF - 1), cut(rows.length - (CAP_DIFF - 1))];
|
|
546
|
+
return [...rows.slice(0, head), cut(rows.length - head - tail), ...rows.slice(rows.length - tail)];
|
|
353
547
|
}
|
|
354
548
|
/** The TOOL's OWN truncation note (W10) — a different fact from the
|
|
355
549
|
* renderer's cut: the tools truncate and append a continuation note
|
|
@@ -403,17 +597,32 @@ class RawBlock {
|
|
|
403
597
|
return this.cell.lines.flatMap((l) => foldLine(l, W));
|
|
404
598
|
}
|
|
405
599
|
}
|
|
406
|
-
/** The terminal label + the status line
|
|
600
|
+
/** The terminal label + the status line. W11: the rhythm gap blank is
|
|
601
|
+
* gone — the container's formula breathes below a multi-row cell (the
|
|
602
|
+
* terminal is always multi-row when labelled), never the component. */
|
|
407
603
|
class TerminalBlock {
|
|
408
604
|
cell;
|
|
409
605
|
constructor(cell) {
|
|
410
606
|
this.cell = cell;
|
|
411
607
|
}
|
|
412
608
|
render(W, _ctx) {
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
609
|
+
return [...foldLine(this.cell.label, W), ...foldLine(this.cell.line, W)];
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
/** The startup banner — a LIVE cell: every render re-derives the tier
|
|
613
|
+
* from the CURRENT width AND height (bannerLines), so a resize re-tiers
|
|
614
|
+
* the art instead of re-folding frozen rows (the W1 tier table: below
|
|
615
|
+
* 40 cols the logo never paints). W11: no trailing blank — the
|
|
616
|
+
* container's formula breathes below the (always multi-row) banner. */
|
|
617
|
+
class Banner {
|
|
618
|
+
cell;
|
|
619
|
+
constructor(cell) {
|
|
620
|
+
this.cell = cell;
|
|
621
|
+
}
|
|
622
|
+
render(W, ctx) {
|
|
623
|
+
const p = palette();
|
|
624
|
+
const rows = bannerLines(W, ctx.height, this.cell.version, this.cell.extensionsText, this.cell.resume, ctx.now);
|
|
625
|
+
return rows.map((r) => `${p.dim}${r}${p.reset}`);
|
|
417
626
|
}
|
|
418
627
|
}
|
|
419
628
|
/** The durable checklist — the ▞ header + one brick-glyph row per item. */
|
|
@@ -438,20 +647,24 @@ class Checklist {
|
|
|
438
647
|
* the hint CUT FIRST when the width is short (the #16g rule); when
|
|
439
648
|
* the STATUS ITSELF cannot fit, it cuts with a "…" — the last resort,
|
|
440
649
|
* enforced by invariant ① (the old code let the status soft-wrap). */
|
|
441
|
-
export function statusLine(status, tail, question, W) {
|
|
650
|
+
export function statusLine(status, tail, question, W, hint) {
|
|
442
651
|
const p = palette();
|
|
443
652
|
const text = `${status}${tail === "" ? "" : ` · ${tail}`}`;
|
|
444
653
|
if (question)
|
|
445
654
|
return `${p.dim}${widthCut(text, W)}${p.reset}`;
|
|
446
|
-
|
|
655
|
+
// W18: the hint is a parameter — the compacting row right-aligns its
|
|
656
|
+
// "esc to cancel" (the same one-line-bounded shape as W12's delegate
|
|
657
|
+
// row; the #16g rule still cuts the HINT first, then the status with
|
|
658
|
+
// a "…" — never a fold).
|
|
659
|
+
const hintText = hint ?? " / commands · ↑ history";
|
|
447
660
|
const statusW = visibleWidth(text);
|
|
448
661
|
if (statusW > W) {
|
|
449
662
|
return `${p.dim}${widthCut(text, W - 1)}…${p.reset}`;
|
|
450
663
|
}
|
|
451
|
-
const hintW = visibleWidth(
|
|
664
|
+
const hintW = visibleWidth(hintText);
|
|
452
665
|
if (statusW + hintW > W)
|
|
453
666
|
return `${p.dim}${text}${p.reset}`;
|
|
454
|
-
return `${p.dim}${text}${" ".repeat(Math.max(0, W - statusW - hintW))}${
|
|
667
|
+
return `${p.dim}${text}${" ".repeat(Math.max(0, W - statusW - hintW))}${hintText}${p.reset}`;
|
|
455
668
|
}
|
|
456
669
|
/** The display-width prefix of a plain (SGR-free) text. */
|
|
457
670
|
function widthCut(text, max) {
|
package/dist/compositor.d.ts
CHANGED
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
* line-mode bytes byte-for-byte (the e2e guards them).
|
|
42
42
|
*/
|
|
43
43
|
import { type MenuItem } from "./editor.js";
|
|
44
|
+
import { type ResumeMeta } from "./render.js";
|
|
44
45
|
/** The cursor marker — an APC private sequence the focus component
|
|
45
46
|
* embeds at the edit position; the compositor strips it and moves
|
|
46
47
|
* relatively (it never reaches the terminal). */
|
|
@@ -86,6 +87,14 @@ export declare class Body {
|
|
|
86
87
|
text: string;
|
|
87
88
|
status: "pending" | "active" | "done";
|
|
88
89
|
}[]): void;
|
|
90
|
+
/** The startup banner (W1): a LIVE cell — the tier re-derives per
|
|
91
|
+
* frame (bannerLines with the CURRENT W and H), so a resize re-tiers
|
|
92
|
+
* the art instead of re-folding frozen rows (a window below 40 cols
|
|
93
|
+
* never paints the logo). W5: the resume metas ride the cell — the
|
|
94
|
+
* list re-gates with the tier (BIG only) and re-times with the
|
|
95
|
+
* frame. The inactive path keeps the historical bytes (no resume —
|
|
96
|
+
* the pipe contract). */
|
|
97
|
+
banner(version: string, extensionsText: string, resume?: ResumeMeta[]): void;
|
|
89
98
|
raw(lines: string[]): void;
|
|
90
99
|
/** The last COMPLETE thinking block, for /think. */
|
|
91
100
|
lastThinking(): string | null;
|
|
@@ -112,7 +121,10 @@ export declare class Body {
|
|
|
112
121
|
* top: external writes (the CLI's console.error CRLF) can shift the
|
|
113
122
|
* committed content down, and a formula-top ED0 would clear it. */
|
|
114
123
|
onResize(): void;
|
|
115
|
-
|
|
124
|
+
/** W18: the status row's right-aligned hint is part of the status
|
|
125
|
+
* state — the compacting row passes "esc to cancel" (the affordance
|
|
126
|
+
* must survive repaints). */
|
|
127
|
+
setStatus(text: string, hint?: string | null): void;
|
|
116
128
|
setTail(tail: string): void;
|
|
117
129
|
showQuestion(question: string): void;
|
|
118
130
|
clearQuestion(): void;
|
|
@@ -138,7 +150,9 @@ export declare class Body {
|
|
|
138
150
|
/** Teardown — flush a pending frame, stop the timers. */
|
|
139
151
|
close(): void;
|
|
140
152
|
/** The live region's scalar — the unit tests assert the cap directly
|
|
141
|
-
* (the e2e gate pins the screen consequence).
|
|
153
|
+
* (the e2e gate pins the screen consequence). W11: the formula's
|
|
154
|
+
* blanks are join artifacts — the count includes them (they are real
|
|
155
|
+
* screen rows), threaded against the previous sibling's OWN rows. */
|
|
142
156
|
liveCount(): number;
|
|
143
157
|
render(): void;
|
|
144
158
|
}
|
|
@@ -152,7 +166,7 @@ export declare class Dock {
|
|
|
152
166
|
enter(): void;
|
|
153
167
|
exit(): void;
|
|
154
168
|
onResize(): void;
|
|
155
|
-
setStatus(text: string): void;
|
|
169
|
+
setStatus(text: string, hint?: string | null): void;
|
|
156
170
|
setTail(tail: string): void;
|
|
157
171
|
showQuestion(question: string): void;
|
|
158
172
|
clearQuestion(): void;
|