@vincemakes/kiso-tui-cells 0.16.3 → 0.16.4
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/approval-panel.d.ts +4 -3
- package/dist/approval-panel.js +134 -38
- package/dist/components.d.ts +12 -10
- package/dist/components.js +111 -43
- package/dist/md.js +4 -2
- package/dist/render.d.ts +0 -10
- package/dist/render.js +99 -18
- package/dist/strings.js +4 -4
- package/package.json +1 -1
package/dist/approval-panel.d.ts
CHANGED
|
@@ -262,7 +262,7 @@ export interface PanelView {
|
|
|
262
262
|
/** The fix hint per speaker (the v8 design §3.5 table). */
|
|
263
263
|
readonly hint?: string;
|
|
264
264
|
/** The options-phase status-left text — the CLI knows the context
|
|
265
|
-
* ("
|
|
265
|
+
* ("⏸ run paused", the trust gate's line). */
|
|
266
266
|
readonly statusText: string;
|
|
267
267
|
/** The ALWAYS-verbose args — the full command/content/diff. */
|
|
268
268
|
readonly args: PanelArgs;
|
|
@@ -385,12 +385,13 @@ export declare function panelBlockLayout(view: PanelView, phase: PanelPhase, cur
|
|
|
385
385
|
*/
|
|
386
386
|
export declare function panelLead(view: PanelView, phase: PanelPhase, cursor: number): string;
|
|
387
387
|
/** The lead's plain text — the editor's reflow width (the line must
|
|
388
|
-
* fit the lead + the
|
|
388
|
+
* fit the lead + the drawn cursor's own cell — R2 retired the box and
|
|
389
|
+
* its walls with it). */
|
|
389
390
|
export declare function panelLeadPlain(view: PanelView, phase: PanelPhase, cursor: number): string;
|
|
390
391
|
export declare function panelLeadWidth(view: PanelView, phase: PanelPhase, cursor: number): number;
|
|
391
392
|
/** The status row's left text while the panel is up — the phase, not
|
|
392
393
|
* the CLI's painting status (the compositor derives it from the panel
|
|
393
|
-
* state; the "
|
|
394
|
+
* state; the "⏸ run paused" etc. ride the options phase). */
|
|
394
395
|
export declare function panelStatus(view: PanelView, phase: PanelPhase, cursor: number): string;
|
|
395
396
|
/**
|
|
396
397
|
* The status row's right-aligned hint — the v4 frame's line, verbatim.
|
package/dist/approval-panel.js
CHANGED
|
@@ -205,21 +205,52 @@ function panelRuleText(view) {
|
|
|
205
205
|
* trade to make: each option owns a row, a narrow window cuts LABELS,
|
|
206
206
|
* and every choice stays reachable at every width the product survives.
|
|
207
207
|
*
|
|
208
|
-
* The unselected row
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
208
|
+
* The unselected row is a two-space indent; the selected row is the
|
|
209
|
+
* shared selectionBar, which spends its own two cells of frame. Both
|
|
210
|
+
* build their span against W−2, so the digit column does not shift as
|
|
211
|
+
* the bar walks — a column that moves per row reads as damage, which is
|
|
212
|
+
* the R2 picker's finding, inherited.
|
|
213
|
+
*
|
|
214
|
+
* R2 — two changes. The unselected row carried the block's │ gutter: a
|
|
215
|
+
* gutter SCOPES a verbatim block (the args keep theirs), and an option
|
|
216
|
+
* list is not verbatim, so it draws a boundary the block already has a
|
|
217
|
+
* rule for. And the cursor now carries `→` as well as the bar (design
|
|
218
|
+
* §7.5) — the bar is the loud signal, the arrow is the one that
|
|
219
|
+
* survives a strip, which is law 1.3's test applied to a selection.
|
|
213
220
|
*/
|
|
214
|
-
function panelOptionRow(option, n, selected, W) {
|
|
221
|
+
function panelOptionRow(option, n, selected, W, note, stop = 0) {
|
|
215
222
|
const p = palette();
|
|
216
223
|
const room = Math.max(1, W - 2);
|
|
217
|
-
const plain =
|
|
218
|
-
const
|
|
224
|
+
const plain = optionLead(option, n, selected);
|
|
225
|
+
const tail = note === undefined || note === ""
|
|
226
|
+
? ""
|
|
227
|
+
: stop > 0
|
|
228
|
+
? `${" ".repeat(Math.max(1, stop - visibleWidth(plain)))}${p.dim}${widthCut(escapeTerminal(note), Math.max(0, room - stop))}${p.reset}`
|
|
229
|
+
: `${p.dim} — ${escapeTerminal(note)}${p.reset}`;
|
|
230
|
+
const text = cutLine(`${selected ? p.bold : ""}${escapeTerminal(plain)}${p.reset}${tail}`, room);
|
|
219
231
|
if (!selected)
|
|
220
|
-
return
|
|
232
|
+
return ` ${text}`;
|
|
221
233
|
return selectionBar(text, visibleWidth(text), W);
|
|
222
234
|
}
|
|
235
|
+
/** The row's left span, PLAIN — written once so the column arithmetic
|
|
236
|
+
* and the row cannot disagree about how wide it is. */
|
|
237
|
+
function optionLead(option, n, selected) {
|
|
238
|
+
return `${selected ? "→" : " "} ${n} ${option.label}`;
|
|
239
|
+
}
|
|
240
|
+
/** R2 — the safer list's `why` column. Same rule as the ask panel's
|
|
241
|
+
* descriptions: computed over the WHOLE list so the column belongs to
|
|
242
|
+
* the list, and 0 (the em-dash fallback) when there is no room for it. */
|
|
243
|
+
function saferStop(options, W) {
|
|
244
|
+
// an empty list has no column to compute — `Math.max()` of nothing is
|
|
245
|
+
// -Infinity, which would sail through both guards below and return a
|
|
246
|
+
// negative stop
|
|
247
|
+
if (options.length === 0)
|
|
248
|
+
return 0;
|
|
249
|
+
const room = Math.max(1, W - 2);
|
|
250
|
+
const widest = Math.max(...options.map((o, i) => visibleWidth(optionLead({ kind: "allow", label: o.command }, i + 1, false))));
|
|
251
|
+
const stop = widest + 2;
|
|
252
|
+
return stop > Math.floor(room / 2) || room - stop < 18 ? 0 : stop;
|
|
253
|
+
}
|
|
223
254
|
/** The block's rows — EXACTLY the preview's frame shape, the gutter at
|
|
224
255
|
* the left edge (the preview's two-space mock indent is its own
|
|
225
256
|
* styling; the real rows sit at column 1, like every tool cell).
|
|
@@ -229,15 +260,27 @@ export function panelBlockRows(view, phase, cursor, W, maxRows, note, safer) {
|
|
|
229
260
|
}
|
|
230
261
|
export function panelBlockLayout(view, phase, cursor, W, maxRows, note, safer) {
|
|
231
262
|
const p = palette();
|
|
232
|
-
|
|
263
|
+
// R2: the block's own PROSE rows (the risk line, the safer-options
|
|
264
|
+
// note, the affordance) take the two-space indent every other row in
|
|
265
|
+
// the block takes. The │ gutter stays where it means something — on
|
|
266
|
+
// the args, which are verbatim, and which is the whole distinction:
|
|
267
|
+
// a gutter SCOPES a quotation, it is not a left edge for a panel.
|
|
268
|
+
const gutter = " ";
|
|
233
269
|
const rows = [];
|
|
234
|
-
|
|
235
|
-
|
|
270
|
+
// R2 — the block opens and closes with the SAME dashed rule the
|
|
271
|
+
// composer uses. It used to open with the │ gutter, divide with a
|
|
272
|
+
// ─ run and close with a └ rule: three edge vocabularies inside one
|
|
273
|
+
// block, and none of them the composer's. A rule SEPARATES, a gutter
|
|
274
|
+
// SCOPES — the args keep their gutter because they are a verbatim
|
|
275
|
+
// block; everything that was drawing a boundary is one rule now.
|
|
276
|
+
rows.push(`${p.dim}${"\u254c".repeat(Math.max(0, W))}${p.reset}`);
|
|
277
|
+
rows.push(` ${cutLine(panelRuleText(view), Math.max(1, W - 2))}`);
|
|
278
|
+
rows.push(` ${cutLine(`${p.bold}${escapeTerminal(view.title)}${p.reset}`, Math.max(1, W - 2))}`);
|
|
236
279
|
// TUI2-R1.5 ⑤ (VD-11): the divider is a LABEL, not a design note. "the
|
|
237
280
|
// full args — never truncated" is a sentence about the implementation,
|
|
238
281
|
// addressed to whoever was building the panel; the human reading it
|
|
239
282
|
// during an approval wants to know what the block below is.
|
|
240
|
-
rows.push(
|
|
283
|
+
rows.push("");
|
|
241
284
|
// the args — the bounded block's body: fold, then cap. The └ cut is
|
|
242
285
|
// ONE row (the W20 discipline): when the args exceed the budget, one
|
|
243
286
|
// notice row carries the count and where the rest is (the event log).
|
|
@@ -252,7 +295,12 @@ export function panelBlockLayout(view, phase, cursor, W, maxRows, note, safer) {
|
|
|
252
295
|
// they can also read in the event log is worth less than the row that
|
|
253
296
|
// carries the choice. The args keep a floor of one row so the block
|
|
254
297
|
// never claims to show what it is asking about and then shows nothing.
|
|
255
|
-
const chrome =
|
|
298
|
+
const chrome =
|
|
299
|
+
// R2: SIX rows of frame, not five — the block opens with a rule now
|
|
300
|
+
// as well as closing with one, and the divider row became a blank.
|
|
301
|
+
// The count is the same shape it always was: every row the block
|
|
302
|
+
// spends on itself before the args and the list share what is left.
|
|
303
|
+
6 +
|
|
256
304
|
(phase === "options" && note !== undefined ? 1 : 0) +
|
|
257
305
|
(view.riskHint !== undefined && view.riskHint !== "" ? 1 : 0) +
|
|
258
306
|
(phase === "asking" ? 1 : 0) +
|
|
@@ -297,9 +345,13 @@ export function panelBlockLayout(view, phase, cursor, W, maxRows, note, safer) {
|
|
|
297
345
|
// out of would be a trap.
|
|
298
346
|
if (phase === "safer" && safer !== undefined) {
|
|
299
347
|
offset = rows.length;
|
|
348
|
+
// R2: the `why` takes a COLUMN rather than running on after an em
|
|
349
|
+
// dash — the commands are what is being chosen between, and they
|
|
350
|
+
// only scan when they all start and end at the same columns.
|
|
351
|
+
const stop = saferStop(safer.options, W);
|
|
300
352
|
for (let i = 0; i < safer.options.length; i += 1) {
|
|
301
353
|
const o = safer.options[i];
|
|
302
|
-
rows.push(panelOptionRow({ kind: "allow", label:
|
|
354
|
+
rows.push(panelOptionRow({ kind: "allow", label: o.command }, i + 1, i === safer.cursor, W, o.why, stop));
|
|
303
355
|
}
|
|
304
356
|
rows.push(panelOptionRow({ kind: "deny", label: SAFER_BACK }, safer.options.length + 1, safer.cursor === safer.options.length, W));
|
|
305
357
|
}
|
|
@@ -336,7 +388,7 @@ export function panelBlockLayout(view, phase, cursor, W, maxRows, note, safer) {
|
|
|
336
388
|
// else in the product, so a CAPPED panel emitted two elbow rows in a
|
|
337
389
|
// row meaning entirely different things. The rule reads as an edge,
|
|
338
390
|
// and the cut notice above it reads as a notice.
|
|
339
|
-
rows.push(`${p.dim}
|
|
391
|
+
rows.push(`${p.dim}${"\u254c".repeat(Math.max(0, W))}${p.reset}`);
|
|
340
392
|
return { rows, ...layout };
|
|
341
393
|
}
|
|
342
394
|
/**
|
|
@@ -354,13 +406,24 @@ export function panelLead(view, phase, cursor) {
|
|
|
354
406
|
const p = palette();
|
|
355
407
|
if (phase === "amend")
|
|
356
408
|
return `${p.dim}amend› ${p.reset}`;
|
|
357
|
-
|
|
409
|
+
// R2: an EMPTY lead emits no bytes at all — `dim + reset` around
|
|
410
|
+
// nothing is eight bytes on the composer row of every frame a panel
|
|
411
|
+
// is up, and the row it wraps has no content to style.
|
|
412
|
+
return PANEL_IDLE_LEAD === "" ? "" : `${p.dim}${PANEL_IDLE_LEAD}${p.reset}`;
|
|
358
413
|
}
|
|
359
|
-
/** The composer's lead while a selection list owns the keys
|
|
360
|
-
*
|
|
361
|
-
|
|
414
|
+
/** The composer's lead while a selection list owns the keys.
|
|
415
|
+
*
|
|
416
|
+
* R2: EMPTY. It was a quiet chevron, on the argument that it is "not a
|
|
417
|
+
* prompt for input that is not being asked for" — but the composer
|
|
418
|
+
* dropped its own chevron this round (the cursor sits at column one),
|
|
419
|
+
* so the panel would have been the one surface reintroducing the glyph
|
|
420
|
+
* the rest of the product just removed. The NAMED leads stay: `amend›`
|
|
421
|
+
* and the pick panel's `1-4>` say where the keystrokes go, which is
|
|
422
|
+
* information rather than decoration. */
|
|
423
|
+
const PANEL_IDLE_LEAD = "";
|
|
362
424
|
/** The lead's plain text — the editor's reflow width (the line must
|
|
363
|
-
* fit the lead + the
|
|
425
|
+
* fit the lead + the drawn cursor's own cell — R2 retired the box and
|
|
426
|
+
* its walls with it). */
|
|
364
427
|
export function panelLeadPlain(view, phase, cursor) {
|
|
365
428
|
return phase === "amend" ? "amend› " : PANEL_IDLE_LEAD;
|
|
366
429
|
}
|
|
@@ -369,20 +432,25 @@ export function panelLeadWidth(view, phase, cursor) {
|
|
|
369
432
|
}
|
|
370
433
|
/** The status row's left text while the panel is up — the phase, not
|
|
371
434
|
* the CLI's painting status (the compositor derives it from the panel
|
|
372
|
-
* state; the "
|
|
435
|
+
* state; the "⏸ run paused" etc. ride the options phase). */
|
|
436
|
+
// R2 (design §4, the ⏸ ruling): a panel that is WAITING ON A HUMAN says
|
|
437
|
+
// so with the one mark that means it. `▸` is the checklist's "the
|
|
438
|
+
// current one" — a mark meaning two things is worse than two marks
|
|
439
|
+
// (law 4.2), and the thing this row has to convey is not "here" but
|
|
440
|
+
// "nothing moves until you answer".
|
|
373
441
|
export function panelStatus(view, phase, cursor) {
|
|
374
442
|
// TUI2-R3v2 ③: the frames' own words — what the panel is doing, and
|
|
375
443
|
// (in the safer list) what it did.
|
|
376
444
|
if (phase === "asking")
|
|
377
|
-
return "\
|
|
445
|
+
return "\u23f8 asked the model for safer options";
|
|
378
446
|
if (phase === "safer")
|
|
379
|
-
return "\
|
|
447
|
+
return "\u23f8 asked the model for safer options";
|
|
380
448
|
// TUI2-R3v2 ①: the typed phase says where the words GO. "the words ride
|
|
381
449
|
// the verdict" described the plumbing to whoever wrote it; the human
|
|
382
450
|
// typing needs to know the model will read this and answer with a new
|
|
383
451
|
// call — which is what the v4 frame says, in those words.
|
|
384
452
|
if (phase === "amend")
|
|
385
|
-
return "
|
|
453
|
+
return "⏸ your note goes to the model — it will propose a new call";
|
|
386
454
|
return view.statusText;
|
|
387
455
|
}
|
|
388
456
|
/**
|
|
@@ -425,32 +493,60 @@ export function panelAffordance(view, phase, cursor, safer) {
|
|
|
425
493
|
export function pickBlockRows(view, state, W, maxRows) {
|
|
426
494
|
const p = palette();
|
|
427
495
|
const spec = view.pick;
|
|
428
|
-
const
|
|
429
|
-
const rows = [];
|
|
496
|
+
const rows = [`${p.dim}${"\u254c".repeat(Math.max(0, W))}${p.reset}`]; // R2: the same rule the composer and the other panels use
|
|
430
497
|
const room = Math.max(1, W - 2);
|
|
431
|
-
rows.push(
|
|
498
|
+
rows.push(` ${cutLine(`${p.bold}${escapeTerminal(spec.header.split(" \u2014 ")[0] ?? spec.header)}${p.reset}${p.dim}${escapeTerminal(spec.header.slice((spec.header.split(" \u2014 ")[0] ?? "").length))}${p.reset}`, room)}`);
|
|
432
499
|
if (spec.options.length === 0) {
|
|
433
500
|
// the honest empty state \u2014 the caller's own copy, verbatim
|
|
434
|
-
rows.push(
|
|
501
|
+
rows.push(` ${cutLine(`${p.dim} ${escapeTerminal(spec.emptyNote ?? "no options")}${p.reset}`, room)}`);
|
|
435
502
|
}
|
|
436
503
|
else {
|
|
437
|
-
// the budget: the header, the t row, the
|
|
438
|
-
|
|
504
|
+
// the budget: the OPENING rule, the header, the t row, the
|
|
505
|
+
// affordance and the CLOSING rule — five, not four. R2 added the
|
|
506
|
+
// opening rule to this block and bumped the ask panel (5→6) and the
|
|
507
|
+
// approval panel (5→6) to pay for it, and missed this one: the
|
|
508
|
+
// block ran two rows over its budget, and two rows of committed
|
|
509
|
+
// content were scrolled irreversibly into the scrollback every time
|
|
510
|
+
// `/model` opened on a tight screen. The `+N more` row is a sixth
|
|
511
|
+
// when it appears, so it is paid for too.
|
|
512
|
+
const chrome = 5 + (spec.options.length > Math.min(Math.max(1, maxRows - 5), PICK_MAX) ? 1 : 0);
|
|
513
|
+
const budget = Math.max(1, maxRows - chrome);
|
|
439
514
|
const shown = spec.options.slice(0, Math.min(budget, PICK_MAX));
|
|
515
|
+
// R2: the note takes a COLUMN, not three spaces after a label of
|
|
516
|
+
// whatever length this row happened to have, and the cursor row
|
|
517
|
+
// wears the bar and the arrow like every other list in the
|
|
518
|
+
// product. This panel was the last one still saying "selected"
|
|
519
|
+
// with bold alone.
|
|
520
|
+
const lead = (o, i, cursor) => `${cursor ? "\u2192" : " "} ${i + 1} ${escapeTerminal(o.label)}`;
|
|
521
|
+
const widest = Math.max(...shown.map((o, i) => visibleWidth(lead(o, i, false))));
|
|
522
|
+
const stop = shown.some((o) => o.note !== undefined) && widest + 2 <= Math.floor(room / 2) && room - widest - 2 >= 18 ? widest + 2 : 0;
|
|
440
523
|
for (let i = 0; i < shown.length; i += 1) {
|
|
441
524
|
const o = shown[i];
|
|
442
525
|
const mark = i === state.cursor && state.phase === "options";
|
|
443
|
-
const
|
|
444
|
-
const
|
|
445
|
-
|
|
526
|
+
const plain = lead(o, i, mark);
|
|
527
|
+
const head = `${mark ? p.bold : ""}${plain}${mark ? p.reset : ""}`;
|
|
528
|
+
const note = o.note === undefined
|
|
529
|
+
? ""
|
|
530
|
+
: stop > 0
|
|
531
|
+
? `${" ".repeat(Math.max(1, stop - visibleWidth(plain)))}${p.dim}${widthCut(escapeTerminal(o.note), Math.max(0, room - stop))}${p.reset}`
|
|
532
|
+
: `${p.dim} ${escapeTerminal(o.note)}${p.reset}`;
|
|
533
|
+
const text = cutLine(`${head}${note}`, room);
|
|
534
|
+
// ONE space, like the approval and ask panels: the bar spends a
|
|
535
|
+
// leading cell of its own, so a two-space unselected prefix
|
|
536
|
+
// moves the digit column by one as the cursor walks — the exact
|
|
537
|
+
// "a column that moves per row reads as damage" this file
|
|
538
|
+
// quotes twice as its standard.
|
|
539
|
+
rows.push(mark ? selectionBar(text, visibleWidth(text), W) : ` ${text}`);
|
|
446
540
|
}
|
|
447
541
|
if (spec.options.length > shown.length) {
|
|
448
|
-
rows.push(
|
|
542
|
+
rows.push(` ${cutLine(`${p.dim} \u2514 +${spec.options.length - shown.length} more \u2014 /model <name> takes any of them${p.reset}`, room)}`);
|
|
449
543
|
}
|
|
450
544
|
}
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
rows.push(
|
|
545
|
+
const typing = state.phase === "custom";
|
|
546
|
+
const tText = cutLine(`${typing ? p.bold : ""}${typing ? "\u2192" : " "} t ${p.reset}${p.dim}${escapeTerminal(spec.typeHint)}${p.reset}`, room);
|
|
547
|
+
rows.push(typing ? selectionBar(tText, visibleWidth(tText), W) : ` ${tText}`);
|
|
548
|
+
rows.push(` ${p.dim}${cutLine(pickAffordance(state), room)}${p.reset}`);
|
|
549
|
+
rows.push(`${p.dim}${"\u254c".repeat(Math.max(0, W))}${p.reset}`);
|
|
454
550
|
return rows;
|
|
455
551
|
}
|
|
456
552
|
/** The digits are the keys, so the list the panel offers is bounded by
|
package/dist/components.d.ts
CHANGED
|
@@ -378,20 +378,22 @@ export declare function selectionBar(styled: string, visible: number, W: number)
|
|
|
378
378
|
/**
|
|
379
379
|
* R2 — the composer's rails, and the ONE edge vocabulary.
|
|
380
380
|
*
|
|
381
|
-
* W6 turned two
|
|
382
|
-
* box already says input lives here". That is reversed here, and
|
|
383
|
-
* reason is not taste: a rule is a DELIMITER and a box is a
|
|
384
|
-
* and the screen was carrying six edge vocabularies at once
|
|
385
|
-
* the panel's
|
|
386
|
-
*
|
|
387
|
-
* the ones that
|
|
381
|
+
* W6 turned two \u254c dotted rows into a rounded box, reasoning that
|
|
382
|
+
* "the box already says input lives here". That is reversed here, and
|
|
383
|
+
* the reason is not taste: a rule is a DELIMITER and a box is a
|
|
384
|
+
* CONTAINER, and the screen was carrying six edge vocabularies at once
|
|
385
|
+
* (this box, the panel's \u2502 gutter and \u2514\u2500\u2500 tail, the
|
|
386
|
+
* diff gutter, the quote's \u258f, the table's rails, the markdown
|
|
387
|
+
* rule). One dashed rule replaces the ones that SEPARATE; the \u2502
|
|
388
|
+
* gutter survives where it SCOPES.
|
|
388
389
|
*
|
|
389
390
|
* Row-neutral by construction: CHROME_ROWS is still 4, so every gate
|
|
390
|
-
* keyed on H
|
|
391
|
-
* the walls were taking.
|
|
391
|
+
* keyed on H \u2212 4 is untouched, and the input row gains the two
|
|
392
|
+
* columns the walls were taking.
|
|
392
393
|
*/
|
|
393
394
|
export declare function boxTop(W: number): string;
|
|
394
|
-
/**
|
|
395
|
+
/** R2 — the same rule below. Named for its POSITION, not its shape, so
|
|
396
|
+
* the compositor's two call sites did not have to move. */
|
|
395
397
|
export declare function boxBottom(W: number): string;
|
|
396
398
|
/** The terminal label + rhythm gap (the pipe path's v2c bytes — the
|
|
397
399
|
* exact render the passthrough needs). */
|
package/dist/components.js
CHANGED
|
@@ -171,12 +171,17 @@ export function cellComponent(cell) {
|
|
|
171
171
|
* the ▍ rail and the indent are retired — the rail's stated pipe
|
|
172
172
|
* fallback was theoretical redundancy: the CLI's pipe path is the
|
|
173
173
|
* line-mode "you>" form and never renders UserMessage). The chip folds
|
|
174
|
-
* the text at W−2 (the side pads)
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
174
|
+
* the text at W−2 (the side pads) and pads every row to the FULL width.
|
|
175
|
+
*
|
|
176
|
+
* R2 — law 1.6's recorded reversal. This used to size the block to its
|
|
177
|
+
* longest row, on the argument that "a short message like /think would
|
|
178
|
+
* paint a bar across the terminal". That optimises the degenerate case
|
|
179
|
+
* at the cost of every real message, which is a paragraph and reads as
|
|
180
|
+
* a block only when the block has an edge. The `/think` case is the
|
|
181
|
+
* accepted price, and it is recorded as such in design.md §1.6.
|
|
182
|
+
*
|
|
183
|
+
* The padding is by cells (charWidth is the width authority), so a CJK
|
|
184
|
+
* row pads by width, never by chars, and the chip never overruns.
|
|
180
185
|
* SGR 7 closed with SGR 27 — never SGR 0, the chip composes with a
|
|
181
186
|
* surrounding span — and NEVER dim: reverse video inverts the CURRENT
|
|
182
187
|
* colours, so dimmed text would invert into a dimmed block with no
|
|
@@ -226,11 +231,18 @@ class UserMessage {
|
|
|
226
231
|
content.push(row);
|
|
227
232
|
}
|
|
228
233
|
}
|
|
234
|
+
// R2 (law 1.6's recorded reversal): the band is FULL WIDTH. It was
|
|
235
|
+
// sized to its longest row, on the argument that a one-word turn
|
|
236
|
+
// like `/think` would otherwise paint a bar across the terminal —
|
|
237
|
+
// which optimises the degenerate case at the cost of every real
|
|
238
|
+
// message. The human's words are the one surface that gets the
|
|
239
|
+
// whole row.
|
|
240
|
+
//
|
|
229
241
|
// displayWidth stays the padding authority (never `length`): a CJK
|
|
230
242
|
// row is two cells per character and pads by cells.
|
|
231
|
-
const inner =
|
|
243
|
+
const inner = chipW;
|
|
232
244
|
for (const row of content) {
|
|
233
|
-
rows.push(`${p.rv} ${row}${" ".repeat(inner - displayWidth(row))} ${p.rvEnd}`);
|
|
245
|
+
rows.push(`${p.rv} ${row}${" ".repeat(Math.max(0, inner - displayWidth(row)))} ${p.rvEnd}`);
|
|
234
246
|
}
|
|
235
247
|
if (!truncated)
|
|
236
248
|
return rows;
|
|
@@ -275,19 +287,57 @@ class ThinkingFold {
|
|
|
275
287
|
this.cell = cell;
|
|
276
288
|
}
|
|
277
289
|
render(W, _ctx) {
|
|
290
|
+
const p = palette();
|
|
278
291
|
const block = this.cell.text;
|
|
279
292
|
const trimmed = escapeTerminal(block.trim());
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
//
|
|
283
|
-
//
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
293
|
+
// R2 (owner, 2026-08-27) — three changes, each independent.
|
|
294
|
+
//
|
|
295
|
+
// ITALIC marks the row as not-the-answer without spending a colour,
|
|
296
|
+
// on the same argument that admitted italic to the alphabet.
|
|
297
|
+
//
|
|
298
|
+
// The cut lands on a WORD. It used to cut on a byte, so the fold
|
|
299
|
+
// read as `…the user's h` and the reader's eye had to reassemble a
|
|
300
|
+
// word it already knew.
|
|
301
|
+
//
|
|
302
|
+
// The affordance moves to the RIGHT EDGE, so the left edge of every
|
|
303
|
+
// row on screen is content. The char count goes with the move: it
|
|
304
|
+
// told the reader nothing they could act on, and the row it was
|
|
305
|
+
// crowding is the one thing this cell says.
|
|
306
|
+
//
|
|
307
|
+
// The ≤100 short-circuit stays width-aware: a short block at a
|
|
308
|
+
// narrow width returned the line UNFOLDED and tripped invariant ①.
|
|
309
|
+
// DC-15: the affordance is DROPPED, not squeezed. `room` floored at
|
|
310
|
+
// 1 while `pad` floored at 1 independently, so a narrow terminal
|
|
311
|
+
// produced 2 + cut + pad + 6 = 11 cells no matter what W was — and
|
|
312
|
+
// invariant ① does not truncate, it THROWS. Measured 11 cells at
|
|
313
|
+
// every W ≤ 10. Below the width where the tail and one word of
|
|
314
|
+
// content can both stand, the row is the CONTENT: a cell that
|
|
315
|
+
// cannot hold the key's name has nothing to say about the key.
|
|
316
|
+
const tail = "/think";
|
|
317
|
+
const room = W - 2 - tail.length - 1;
|
|
318
|
+
// the belt: cutLine is SGR-aware and single-row, so the invariant
|
|
319
|
+
// holds by CONSTRUCTION at every width rather than by arithmetic
|
|
320
|
+
// that has to be re-proved every time a span moves.
|
|
321
|
+
if (room < 2)
|
|
322
|
+
return [cutLine(`${p.dim}⋯ ${p.italic}${wordCut(trimmed, Math.max(1, W - 2))}${p.italicEnd}${p.reset}`, W)];
|
|
323
|
+
const cut = wordCut(trimmed, room);
|
|
324
|
+
const body = `⋯ ${p.italic}${cut}${p.italicEnd}`;
|
|
325
|
+
const pad = Math.max(1, W - 2 - visibleWidth(cut) - tail.length);
|
|
326
|
+
return [cutLine(`${p.dim}${body}${" ".repeat(pad)}${tail}${p.reset}`, W)];
|
|
289
327
|
}
|
|
290
328
|
}
|
|
329
|
+
/** R2 — cut at a word boundary, with the honest ellipsis. widthCut cuts
|
|
330
|
+
* at a cell, which is right for verbatim output and wrong for prose:
|
|
331
|
+
* the reader has to reassemble "h" into "home". Falls back to the cell
|
|
332
|
+
* cut when the first word alone overruns, because a row that cannot
|
|
333
|
+
* hold one word has no boundary to find. */
|
|
334
|
+
function wordCut(text, room) {
|
|
335
|
+
if (visibleWidth(text) <= room)
|
|
336
|
+
return text;
|
|
337
|
+
const hard = widthCut(text, Math.max(1, room - 1));
|
|
338
|
+
const at = hard.lastIndexOf(" ");
|
|
339
|
+
return `${at > room / 3 ? hard.slice(0, at) : hard}\u2026`;
|
|
340
|
+
}
|
|
291
341
|
/** Fold a line's CONTENT at W−2 and prefix EVERY row with the gutter
|
|
292
342
|
* (W2: a wrapped tool row keeps its state mark — the left edge alone
|
|
293
343
|
* distinguishes the states at --plain; the UserMessage rail precedent,
|
|
@@ -508,7 +558,7 @@ class ToolExecution {
|
|
|
508
558
|
// list of calls, not a body of output.
|
|
509
559
|
const r = c.rolled;
|
|
510
560
|
const counts = exploreCounts(parts);
|
|
511
|
-
const head =
|
|
561
|
+
const head = ` explored ${p.bold}${counts}${p.reset}`; // R2: no tick
|
|
512
562
|
const tail = ` (${r.elapsed}s)`;
|
|
513
563
|
const room = W - visibleWidth(head) - tail.length;
|
|
514
564
|
const affordance = " · ctrl+r lists them";
|
|
@@ -523,7 +573,7 @@ class ToolExecution {
|
|
|
523
573
|
// W15 expand history — the head's commit captures it).
|
|
524
574
|
const r = c.rolled;
|
|
525
575
|
const noun = ROLLUP_NOUN[c.name] ?? "calls";
|
|
526
|
-
const out = gutterCut(
|
|
576
|
+
const out = gutterCut(" ", `${verbCol} ${r.count} ${noun} (${kUnit(r.lines)} lines, ${r.elapsed}s)`, W); // R2: no tick
|
|
527
577
|
const shown = r.targets.slice(0, 3);
|
|
528
578
|
if (shown.length > 0)
|
|
529
579
|
out.push(` ${p.dim}${CUT_ROW}${escapeTerminal(shown.join(" · "))}${p.reset}`);
|
|
@@ -541,7 +591,7 @@ class ToolExecution {
|
|
|
541
591
|
// names the decider; a human denial (no decidedBy) has no tail.
|
|
542
592
|
if (c.reason !== null) {
|
|
543
593
|
const by = attribution(c);
|
|
544
|
-
const out = gutterCut(
|
|
594
|
+
const out = gutterCut(" ", `${p.red}${escapeTerminal(`${c.name} ${toolTargetOf(c)}`)} (${escapeTerminal(c.reason)}${by})${p.reset}`, W);
|
|
545
595
|
out.push(...toolBlockBody(c, W));
|
|
546
596
|
return out;
|
|
547
597
|
}
|
|
@@ -578,7 +628,14 @@ class ToolExecution {
|
|
|
578
628
|
// the semantics. TUI2-R1.5 pin 4: and the parts give way in a
|
|
579
629
|
// PINNED ORDER, rather than whichever happened to be last.
|
|
580
630
|
const text = settledHeadText(verbCol, escapeTerminal(toolTargetOf(c)), meta, approvedBy, elapsed, W - 2 - (hidden === null ? 0 : SUFFIX_MIN));
|
|
581
|
-
|
|
631
|
+
// R2 (owner, 2026-08-27): no tick, no cross. A symbol earns its
|
|
632
|
+
// cell by carrying a fact the words do not, and a row that
|
|
633
|
+
// already says `exit 0` does not need one more thing saying it
|
|
634
|
+
// went fine. The gutter is two spaces; the OUTCOME lives in the
|
|
635
|
+
// metadata, in words, which is also the only form that survives
|
|
636
|
+
// a pipe with the colour stripped. A failure keeps its colour
|
|
637
|
+
// AND its words — see settledMeta.
|
|
638
|
+
const out = c.isError ? [` ${p.red}${text}${p.reset}`] : [` ${text}`];
|
|
582
639
|
out[0] = appendSuffix(out[0], expandSuffix(hidden, W - visibleWidth(out[0])));
|
|
583
640
|
out.push(...toolBlockBody(c, W));
|
|
584
641
|
return out;
|
|
@@ -912,26 +969,26 @@ export function turnFold(t, W) {
|
|
|
912
969
|
const meta = parts.join(" · ");
|
|
913
970
|
const words = escapeTerminal(t.words);
|
|
914
971
|
if (words === "") {
|
|
915
|
-
const row = `${p.bold}
|
|
916
|
-
return visibleWidth(row) <= W ? [row] : [`${p.bold}
|
|
972
|
+
const row = `${p.bold}✦${p.reset} ${meta}`;
|
|
973
|
+
return visibleWidth(row) <= W ? [row] : [`${p.bold}✦${p.reset} ${widthCut(meta, Math.max(1, W - 3))}…`]; // a wordless turn folds to the W14 shape
|
|
917
974
|
}
|
|
918
975
|
// A9 (ruling R2, mock A): the user chip rides the fold — the human's
|
|
919
976
|
// words LEAD the one line, the same SGR-7 bracket as the live user
|
|
920
977
|
// row (#16f, side pads included). The words take the fold's width
|
|
921
|
-
// budget: W − the gutter ("
|
|
978
|
+
// budget: W − the gutter ("✦ " = 2) − the join (" · " = 3) − the
|
|
922
979
|
// chip's side pads (2) − the cut-tail reserve (1, the "…") − the
|
|
923
980
|
// metadata's own width — the metadata survives, the words width-cut
|
|
924
981
|
// at the end with the honest "…" (the "…" alone is the honest floor:
|
|
925
982
|
// the words were there, cut).
|
|
926
|
-
const budget = Math.max(0, W - visibleWidth(
|
|
983
|
+
const budget = Math.max(0, W - visibleWidth(`✦ ${meta}`) - 6);
|
|
927
984
|
const cut = visibleWidth(words) > budget ? `${widthCut(words, budget)}…` : words;
|
|
928
|
-
const row = `${p.bold}
|
|
985
|
+
const row = `${p.bold}✦${p.reset} ${p.rv} ${cut} ${p.rvEnd} · ${meta}`;
|
|
929
986
|
if (visibleWidth(row) <= W)
|
|
930
987
|
return [row];
|
|
931
988
|
// the last resort: the METADATA gives way — the words hold their
|
|
932
989
|
// budget, the meta cuts with the honest "…"; invariant ① never trips
|
|
933
990
|
// at ANY width (a degenerate W's fold is a cut, never a crash).
|
|
934
|
-
return [`${p.bold}
|
|
991
|
+
return [`${p.bold}✦${p.reset} ${p.rv} ${cut} ${p.rvEnd} · ${widthCut(meta, Math.max(1, W - 8 - visibleWidth(cut)))}…`];
|
|
935
992
|
}
|
|
936
993
|
// ---- the bounded-block flow contract (W7, W8, W10) ----
|
|
937
994
|
/** The caps — screen rows counted AFTER the fold, at the current width
|
|
@@ -1288,8 +1345,11 @@ class Banner {
|
|
|
1288
1345
|
}
|
|
1289
1346
|
render(W, ctx) {
|
|
1290
1347
|
const p = palette();
|
|
1291
|
-
|
|
1292
|
-
|
|
1348
|
+
// R2: NO blanket dim. bannerLines styles itself — the labels are
|
|
1349
|
+
// dim, the values are ink — and wrapping the whole thing in dim
|
|
1350
|
+
// made the answers as faint as the questions.
|
|
1351
|
+
void p;
|
|
1352
|
+
return bannerLines(W, ctx.height, this.cell.version, this.cell.extensionsText, this.cell.resume, ctx.now, this.cell.meta);
|
|
1293
1353
|
}
|
|
1294
1354
|
}
|
|
1295
1355
|
/** W20 — the task block's fixed-window height: the whole live block
|
|
@@ -1373,7 +1433,7 @@ class Checklist {
|
|
|
1373
1433
|
const fixed = done
|
|
1374
1434
|
? `task done · ${plural(items.length, "item")} · ${formatDuration(durationSeconds)}`
|
|
1375
1435
|
: `task · ${plural(items.length, "item")} · ${active.length} active · ${doneCount} done`;
|
|
1376
|
-
const header = `${p.bold}
|
|
1436
|
+
const header = `${p.bold}✦${p.reset} ${escapeTerminal(fixed + tail)}`;
|
|
1377
1437
|
// the FULL-list forms: SETTLED — the durable record (the fold is
|
|
1378
1438
|
// fine — committed content wraps naturally) — and the LIVE ctrl+r
|
|
1379
1439
|
// toggle (the header CUTS — the block stays one window high; the
|
|
@@ -1475,30 +1535,38 @@ export function widthCut(text, max) {
|
|
|
1475
1535
|
*/
|
|
1476
1536
|
export function selectionBar(styled, visible, W) {
|
|
1477
1537
|
const p = palette();
|
|
1478
|
-
|
|
1538
|
+
// R2 (design §2.1 — nothing dim ever sits on the wash): the bar IS a
|
|
1539
|
+
// wash. A dim span inside it renders grey-on-grey — 3.91:1 on the
|
|
1540
|
+
// light ground, under the 4.5 floor — and the dim spans are exactly
|
|
1541
|
+
// the descriptions and the metadata, i.e. the half of the row the
|
|
1542
|
+
// selection was supposed to help you read. Dim is dropped INSIDE the
|
|
1543
|
+
// bar and nowhere else; the same row unselected keeps it.
|
|
1544
|
+
const inner = (p.dim === "" ? styled : styled.replaceAll(p.dim, "")).replaceAll(p.reset, `${p.reset}${p.rv}`);
|
|
1479
1545
|
return `${p.rv} ${inner}${" ".repeat(Math.max(0, W - visible - 2))} ${p.rvEnd}`;
|
|
1480
1546
|
}
|
|
1481
1547
|
/**
|
|
1482
1548
|
* R2 — the composer's rails, and the ONE edge vocabulary.
|
|
1483
1549
|
*
|
|
1484
|
-
* W6 turned two
|
|
1485
|
-
* box already says input lives here". That is reversed here, and
|
|
1486
|
-
* reason is not taste: a rule is a DELIMITER and a box is a
|
|
1487
|
-
* and the screen was carrying six edge vocabularies at once
|
|
1488
|
-
* the panel's
|
|
1489
|
-
*
|
|
1490
|
-
* the ones that
|
|
1550
|
+
* W6 turned two \u254c dotted rows into a rounded box, reasoning that
|
|
1551
|
+
* "the box already says input lives here". That is reversed here, and
|
|
1552
|
+
* the reason is not taste: a rule is a DELIMITER and a box is a
|
|
1553
|
+
* CONTAINER, and the screen was carrying six edge vocabularies at once
|
|
1554
|
+
* (this box, the panel's \u2502 gutter and \u2514\u2500\u2500 tail, the
|
|
1555
|
+
* diff gutter, the quote's \u258f, the table's rails, the markdown
|
|
1556
|
+
* rule). One dashed rule replaces the ones that SEPARATE; the \u2502
|
|
1557
|
+
* gutter survives where it SCOPES.
|
|
1491
1558
|
*
|
|
1492
1559
|
* Row-neutral by construction: CHROME_ROWS is still 4, so every gate
|
|
1493
|
-
* keyed on H
|
|
1494
|
-
* the walls were taking.
|
|
1560
|
+
* keyed on H \u2212 4 is untouched, and the input row gains the two
|
|
1561
|
+
* columns the walls were taking.
|
|
1495
1562
|
*/
|
|
1496
1563
|
export function boxTop(W) {
|
|
1497
|
-
return `\x1b[2m
|
|
1564
|
+
return `\x1b[2m${"\u254c".repeat(Math.max(0, W))}\x1b[0m`;
|
|
1498
1565
|
}
|
|
1499
|
-
/**
|
|
1566
|
+
/** R2 — the same rule below. Named for its POSITION, not its shape, so
|
|
1567
|
+
* the compositor's two call sites did not have to move. */
|
|
1500
1568
|
export function boxBottom(W) {
|
|
1501
|
-
return `\x1b[2m
|
|
1569
|
+
return `\x1b[2m${"\u254c".repeat(Math.max(0, W))}\x1b[0m`;
|
|
1502
1570
|
}
|
|
1503
1571
|
/** The terminal label + rhythm gap (the pipe path's v2c bytes — the
|
|
1504
1572
|
* exact render the passthrough needs). */
|
package/dist/md.js
CHANGED
|
@@ -282,8 +282,10 @@ function blockBody(b, W) {
|
|
|
282
282
|
// DC-3: a fenced BODY carries no colour token. It used to take
|
|
283
283
|
// `code` — 1.54:1 on a white terminal, applied to whole blocks,
|
|
284
284
|
// which made the code the model just wrote the least readable
|
|
285
|
-
// thing on screen. The
|
|
286
|
-
// verbatim"
|
|
285
|
+
// thing on screen. The block's own ``` RAILS already say "this
|
|
286
|
+
// is verbatim" (E2 replaced the `│` gutter this comment used to
|
|
287
|
+
// name with them); saying it twice cost legibility and bought
|
|
288
|
+
// nothing.
|
|
287
289
|
return foldLineWidth(src.slice(indent.length), W - visibleWidth(gutter), indent).map((r) => `${gutter}${r}`);
|
|
288
290
|
}
|
|
289
291
|
case "quote": {
|
package/dist/render.d.ts
CHANGED
|
@@ -141,16 +141,6 @@ export declare function kUnit(value: number | null): string;
|
|
|
141
141
|
* this verbatim; the render tests pin the sequence.
|
|
142
142
|
*/
|
|
143
143
|
export declare function renderTerminalGap(statusLine: string | null): string;
|
|
144
|
-
/**
|
|
145
|
-
* v3 §01 (V6-2) — the banner, block-split. The logo is THREE BRICK rows
|
|
146
|
-
* (the logo.svg pixel form — K I S O), then a BLANK, then the info rows:
|
|
147
|
-
* "kiso vX — tagline" + extensions. The tagline rides the version line
|
|
148
|
-
* (the old logo MIDDLE row was the tagline — a text row masquerading as
|
|
149
|
-
* the logo's centre). Every row truncates at the terminal width with a
|
|
150
|
-
* " (+N)" marker (N = the hidden display width); a window narrower than
|
|
151
|
-
* 40 columns skips the logo + the blank entirely — only the info rows.
|
|
152
|
-
* Pure.
|
|
153
|
-
*/
|
|
154
144
|
export declare const TAGLINE = "the coding agent that survives kill -9";
|
|
155
145
|
/** R2 — what the opening knows about the session. Optional because the
|
|
156
146
|
* off-TTY caller prints a banner before a model is bound. */
|
package/dist/render.js
CHANGED
|
@@ -7,7 +7,28 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { charWidth, displayWidth } from "./width.js";
|
|
9
9
|
const BASE = { bold: "\x1b[1m", dim: "\x1b[2m", red: "\x1b[31m", green: "\x1b[32m", warn: "\x1b[33m", italic: "\x1b[3m", italicEnd: "\x1b[23m", underline: "\x1b[4m", underlineEnd: "\x1b[24m", rv: "\x1b[7m", rvEnd: "\x1b[27m", reset: "\x1b[0m" };
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* DC-9 (design §2.3) — the failure colour is theme-resolved.
|
|
12
|
+
*
|
|
13
|
+
* ANSI 31 is 5.89:1 on a white ground and 2.83:1 on a dark one: the one
|
|
14
|
+
* token in the alphabet whose whole job is "this went wrong" was the
|
|
15
|
+
* least readable thing on the screen exactly where a dark-terminal user
|
|
16
|
+
* reads it. A failure is CONTENT (law 1.2 admits colour there), so it
|
|
17
|
+
* cannot degrade to an attribute the way `dim` does — it needs a value
|
|
18
|
+
* per ground, and the ground is what §3's ladder is for.
|
|
19
|
+
*
|
|
20
|
+
* 256-cube indices, never truecolor (§2). Measured against the grounds
|
|
21
|
+
* §2 measures against — white, and #1E1E1E:
|
|
22
|
+
*
|
|
23
|
+
* light 124 `#af0000` 7.44:1
|
|
24
|
+
* dark 173 `#d7875f` 5.97:1
|
|
25
|
+
*
|
|
26
|
+
* With NO ground established the token stays ANSI 31 — the TERMINAL's
|
|
27
|
+
* own red, which its theme picked for its own background. That is rung
|
|
28
|
+
* 4's principle exactly: when the ground is unknown, use the thing that
|
|
29
|
+
* is correct on any ground rather than guessing one.
|
|
30
|
+
*/
|
|
31
|
+
const withWash = (wash, washEnd, red = BASE.red) => ({ ...BASE, red, wash, washEnd, code: wash });
|
|
11
32
|
/**
|
|
12
33
|
* DC-3 — one table per ground.
|
|
13
34
|
*
|
|
@@ -18,8 +39,8 @@ const withWash = (wash, washEnd) => ({ ...BASE, wash, washEnd, code: wash });
|
|
|
18
39
|
* varies.
|
|
19
40
|
*/
|
|
20
41
|
export const COLOR_NEUTRAL = withWash("\x1b[7m", "\x1b[27m");
|
|
21
|
-
export const COLOR_LIGHT = withWash("\x1b[48;5;255m", "\x1b[49m");
|
|
22
|
-
export const COLOR_DARK = withWash("\x1b[48;5;236m", "\x1b[49m");
|
|
42
|
+
export const COLOR_LIGHT = withWash("\x1b[48;5;255m", "\x1b[49m", "\x1b[38;5;124m");
|
|
43
|
+
export const COLOR_DARK = withWash("\x1b[48;5;236m", "\x1b[49m", "\x1b[38;5;173m");
|
|
23
44
|
/** The historical name — the palette for a colour TTY whose ground has
|
|
24
45
|
* not been established. Unchanged in every byte except `code`, which
|
|
25
46
|
* was the defect. */
|
|
@@ -190,6 +211,20 @@ export function renderTerminalGap(statusLine) {
|
|
|
190
211
|
* 40 columns skips the logo + the blank entirely — only the info rows.
|
|
191
212
|
* Pure.
|
|
192
213
|
*/
|
|
214
|
+
/** DC-18: the display-width prefix of PLAIN text. `widthCut` lives in
|
|
215
|
+
* components.ts, which imports this module — the dependency runs one
|
|
216
|
+
* way, so the four lines live here rather than inverting it. */
|
|
217
|
+
function plainCut(text, max) {
|
|
218
|
+
let w = 0;
|
|
219
|
+
let i = 0;
|
|
220
|
+
for (; i < text.length; i += 1) {
|
|
221
|
+
const cw = charWidth(text.codePointAt(i));
|
|
222
|
+
if (w + cw > max)
|
|
223
|
+
break;
|
|
224
|
+
w += cw;
|
|
225
|
+
}
|
|
226
|
+
return text.slice(0, i);
|
|
227
|
+
}
|
|
193
228
|
export const TAGLINE = "the coding agent that survives kill -9";
|
|
194
229
|
/**
|
|
195
230
|
* R2 — the wordmark is retired (2026-08-27, the nineteen-screen review).
|
|
@@ -210,7 +245,11 @@ export const TAGLINE = "the coding agent that survives kill -9";
|
|
|
210
245
|
/** R2 — the keys a first screen teaches. One dim row, and deliberately
|
|
211
246
|
* NOT derived from KEY_BINDINGS: the sheet is the complete list and
|
|
212
247
|
* this is the opening's five, chosen rather than generated. */
|
|
213
|
-
|
|
248
|
+
// R2: the keys row names bindings the product ACTUALLY has. The first
|
|
249
|
+
// draft advertised `! bash` — there is no bang passthrough in kiso and
|
|
250
|
+
// KEY_BINDINGS never had one, so the opening screen was teaching a key
|
|
251
|
+
// that does nothing. A first screen that lies is worse than a short one.
|
|
252
|
+
const BANNER_KEYS = "esc interrupt · ctrl+c exit · / commands · @ files · ? keys";
|
|
214
253
|
/** R2 — the labels. Uppercase mono, dim, letter-spaced by the column
|
|
215
254
|
* rather than by SGR: they mark sections and are never content. */
|
|
216
255
|
const BANNER_LABELS = ["MODEL", "WORKSPACE", "EXTENSIONS"];
|
|
@@ -225,6 +264,13 @@ export function truncateRow(row, width) {
|
|
|
225
264
|
const total = displayWidth(row);
|
|
226
265
|
if (total <= width)
|
|
227
266
|
return row;
|
|
267
|
+
// DC-18: a width too narrow to HOLD the marker gets a hard cut. The
|
|
268
|
+
// fixpoint below floors `cut` at 0 and then appends a 6-cell marker
|
|
269
|
+
// regardless, so every width ≤ 6 returned a row WIDER than the
|
|
270
|
+
// terminal — and invariant ① throws rather than truncating. A marker
|
|
271
|
+
// wider than the row it marks is not a marker.
|
|
272
|
+
if (width < 7)
|
|
273
|
+
return plainCut(row, Math.max(0, width));
|
|
228
274
|
// iterate the marker to a fixpoint: the marker's width changes the
|
|
229
275
|
// cut, the cut changes the hidden count the marker reports
|
|
230
276
|
let marker = " (+0)";
|
|
@@ -256,7 +302,24 @@ export function truncateRow(row, width) {
|
|
|
256
302
|
* resume list (BIG only, W5). Every row truncates at the terminal width
|
|
257
303
|
* with a " (+N)" marker. Pure. */
|
|
258
304
|
export function bannerLines(W, H, version, extensionsText, resume = [], now = Date.now(), meta) {
|
|
259
|
-
const
|
|
305
|
+
const p = palette();
|
|
306
|
+
// R2: the banner styles itself per span. It used to be wrapped in one
|
|
307
|
+
// blanket dim by its component, which made the answers as faint as the
|
|
308
|
+
// labels asking the questions — the labels are the quiet half, the
|
|
309
|
+
// values are what a human came to read.
|
|
310
|
+
//
|
|
311
|
+
// Every width decision below is taken on PLAIN text and the styling is
|
|
312
|
+
// applied after, because truncateRow measures with displayWidth, which
|
|
313
|
+
// counts SGR bytes as columns. Style then measure is a bug waiting.
|
|
314
|
+
// DC-18: the name row is CUT like every other row here. It was the one
|
|
315
|
+
// row in this function pushed unguarded, so at W ≤ 10 `kiso 0.16.4`
|
|
316
|
+
// measured 11 cells and invariant ① threw AT STARTUP — the function
|
|
317
|
+
// whose own comment preaches "invariant ① holds at every width".
|
|
318
|
+
// The cut is taken on the plain text, per the note above.
|
|
319
|
+
const namePlain = plainCut(`kiso ${version}`, Math.max(1, W));
|
|
320
|
+
const nameCut = namePlain.slice(0, 4); // "kiso", or its surviving prefix
|
|
321
|
+
const verCut = namePlain.slice(5); // the version, if the width left room for it
|
|
322
|
+
const rows = [`${p.bold}${nameCut}${p.reset}${verCut === "" ? "" : `${p.dim} ${verCut}${p.reset}`}`];
|
|
260
323
|
const facts = [];
|
|
261
324
|
if (meta !== undefined) {
|
|
262
325
|
facts.push([BANNER_LABELS[0], `${meta.model}${meta.mode === "" ? "" : ` · ${meta.mode}`}`], [BANNER_LABELS[1], meta.cwd]);
|
|
@@ -268,32 +331,50 @@ export function bannerLines(W, H, version, extensionsText, resume = [], now = Da
|
|
|
268
331
|
// The value column HANGS rather than truncating. The label costs
|
|
269
332
|
// columns the value used to have, and an extension list cut at the
|
|
270
333
|
// width would hide which extensions loaded — on the one screen whose
|
|
271
|
-
// job is to say what is loaded.
|
|
272
|
-
|
|
273
|
-
|
|
334
|
+
// job is to say what is loaded.
|
|
335
|
+
const indent = 2 + LABEL_STOP;
|
|
336
|
+
// a terminal too narrow to hold the label column at all: the room is
|
|
337
|
+
// what is left, floored at one column, and the assembled row is
|
|
338
|
+
// truncated as a unit so invariant ① holds at every width.
|
|
339
|
+
const room = Math.max(1, W - indent);
|
|
274
340
|
for (const [label, value] of facts) {
|
|
275
|
-
const lead = ` ${label}${" ".repeat(LABEL_STOP - label.length)}`;
|
|
276
|
-
const hang = " ".repeat(
|
|
341
|
+
const lead = ` ${p.dim}${label}${p.reset}${" ".repeat(LABEL_STOP - label.length)}`;
|
|
342
|
+
const hang = " ".repeat(indent);
|
|
343
|
+
const lines = [];
|
|
277
344
|
let line = "";
|
|
278
|
-
const out = [];
|
|
279
345
|
for (const word of value.split(" ")) {
|
|
280
346
|
if (line === "")
|
|
281
347
|
line = word;
|
|
282
348
|
else if (displayWidth(`${line} ${word}`) <= room)
|
|
283
349
|
line += ` ${word}`;
|
|
284
350
|
else {
|
|
285
|
-
|
|
351
|
+
lines.push(line);
|
|
286
352
|
line = word;
|
|
287
353
|
}
|
|
288
354
|
}
|
|
289
355
|
if (line !== "")
|
|
290
|
-
|
|
291
|
-
for (const [i, l] of
|
|
292
|
-
|
|
356
|
+
lines.push(line);
|
|
357
|
+
for (const [i, l] of lines.entries()) {
|
|
358
|
+
const styled = `${i === 0 ? lead : hang}${truncateRow(l, room)}`;
|
|
359
|
+
rows.push(displayWidth(styled) - (i === 0 ? p.dim.length + p.reset.length : 0) <= W ? styled : truncateRow(`${hang}${l}`, W));
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (meta !== undefined && W >= 40) {
|
|
364
|
+
// R2/DC-2's device: the keys row is a list of independent clauses,
|
|
365
|
+
// so a narrow terminal drops whole clauses from the end rather than
|
|
366
|
+
// cutting one in half. `ctrl+r ex (+8)` teaches nothing.
|
|
367
|
+
const clauses = BANNER_KEYS.split(" \u00b7 ");
|
|
368
|
+
let keys = clauses[0];
|
|
369
|
+
for (let n = clauses.length; n > 1; n -= 1) {
|
|
370
|
+
const row = clauses.slice(0, n).join(" \u00b7 ");
|
|
371
|
+
if (displayWidth(row) <= W - 2) {
|
|
372
|
+
keys = row;
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
293
375
|
}
|
|
376
|
+
rows.push("", ` ${p.dim}${truncateRow(keys, W - 2)}${p.reset}`);
|
|
294
377
|
}
|
|
295
|
-
if (meta !== undefined && W >= 40)
|
|
296
|
-
rows.push("", truncateRow(` ${BANNER_KEYS}`, W));
|
|
297
378
|
if (W >= 40 && H >= 20 && resume.length > 0) {
|
|
298
379
|
rows.push("", ...renderResumeList(resume, W, now));
|
|
299
380
|
}
|
|
@@ -333,7 +414,7 @@ function titleCut(text, max) {
|
|
|
333
414
|
export function renderResumeList(metas, W, now) {
|
|
334
415
|
if (metas.length === 0)
|
|
335
416
|
return [];
|
|
336
|
-
const rows = ["
|
|
417
|
+
const rows = [" ✦ resume"]; // R2: the ONE fold/segment mark (§4.2)
|
|
337
418
|
const whens = metas.map((m) => relativeTime(m.updatedAt, now));
|
|
338
419
|
const metaTexts = metas.map((m) => `${m.events} events · ${m.runs} runs`);
|
|
339
420
|
const metaW = Math.max(...metaTexts.map((t) => t.length));
|
package/dist/strings.js
CHANGED
|
@@ -51,7 +51,7 @@ export function projectTrustView(root, files) {
|
|
|
51
51
|
name: "project trust",
|
|
52
52
|
title: root,
|
|
53
53
|
speaker: "kiso",
|
|
54
|
-
statusText: "
|
|
54
|
+
statusText: "⏸ project trust",
|
|
55
55
|
args: { kind: "text", lines: projectTrustRows(files) },
|
|
56
56
|
ruleOverride: "trust this project's .kiso?",
|
|
57
57
|
fallbackQuestion: `trust this project's .kiso? (y/n) `,
|
|
@@ -96,7 +96,7 @@ export function verifyOfferView() {
|
|
|
96
96
|
name: "verification",
|
|
97
97
|
title: "finish the checklist?",
|
|
98
98
|
speaker: "kiso",
|
|
99
|
-
statusText: "\
|
|
99
|
+
statusText: "\u23f8 run paused",
|
|
100
100
|
args: { kind: "text", lines: [] },
|
|
101
101
|
ruleOverride: "every item is marked done \u2014 run a check?",
|
|
102
102
|
simpleOptions: ["run a verification pass", "not now"],
|
|
@@ -109,7 +109,7 @@ export function uncertainView(name, executionId) {
|
|
|
109
109
|
name: "uncertain execution",
|
|
110
110
|
title: `${name} (${executionId})`,
|
|
111
111
|
speaker: "kiso",
|
|
112
|
-
statusText: "
|
|
112
|
+
statusText: "⏸ uncertain execution",
|
|
113
113
|
args: { kind: "text", lines: [executionId] },
|
|
114
114
|
ruleOverride: "an interrupted execution may have applied — rerun it?",
|
|
115
115
|
simpleOptions: ["rerun it", "abandon it"],
|
|
@@ -136,7 +136,7 @@ export function unansweredAskView(executionId) {
|
|
|
136
136
|
name: "unanswered question",
|
|
137
137
|
title: `ask_user (${executionId})`,
|
|
138
138
|
speaker: "kiso",
|
|
139
|
-
statusText: "
|
|
139
|
+
statusText: "⏸ unanswered question",
|
|
140
140
|
args: { kind: "text", lines: [executionId] },
|
|
141
141
|
ruleOverride: "an unanswered question was interrupted — ask it again?",
|
|
142
142
|
simpleOptions: ["ask it again", "drop it"],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-tui-cells",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.4",
|
|
4
4
|
"description": "kiso tui-cells — the components cell renderer (components, diff, width, the render slice). Zero runtime dependencies: input is data, output is bytes.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|