@vincemakes/kiso-tui-cells 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/approval-panel.d.ts +75 -0
- package/dist/approval-panel.js +93 -1
- package/dist/components.d.ts +45 -2
- package/dist/components.js +94 -29
- package/dist/index.d.ts +2 -1
- package/dist/index.js +4 -1
- package/dist/md.d.ts +137 -0
- package/dist/md.js +720 -0
- package/dist/render.d.ts +18 -0
- package/dist/render.js +2 -2
- package/dist/strings.d.ts +4 -0
- package/dist/strings.js +34 -0
- package/dist/width.d.ts +30 -6
- package/dist/width.js +145 -26
- package/package.json +1 -1
package/dist/approval-panel.d.ts
CHANGED
|
@@ -75,6 +75,38 @@ export interface AskRuntime {
|
|
|
75
75
|
readonly custom: readonly (string | null)[];
|
|
76
76
|
readonly phase: "options" | "custom";
|
|
77
77
|
}
|
|
78
|
+
/** One thing that can be picked: what it is, and what qualifies it. */
|
|
79
|
+
export interface PickOption {
|
|
80
|
+
readonly label: string;
|
|
81
|
+
/** the dim qualifier ("profile: ds \u00b7 current") \u2014 what tells two
|
|
82
|
+
* similar rows apart */
|
|
83
|
+
readonly note?: string;
|
|
84
|
+
}
|
|
85
|
+
/** The whole pick: the header sentence, the options, the free-text
|
|
86
|
+
* escape hatch, and the honest empty state. */
|
|
87
|
+
export interface PickSpec {
|
|
88
|
+
readonly header: string;
|
|
89
|
+
readonly options: readonly PickOption[];
|
|
90
|
+
/** the `t` row \u2014 typing it directly is always available, because a
|
|
91
|
+
* list of profiles is never the list of models that exist */
|
|
92
|
+
readonly typeHint: string;
|
|
93
|
+
/** shown INSTEAD of the options when there are none. The copy is the
|
|
94
|
+
* caller's and is reproduced verbatim. */
|
|
95
|
+
readonly emptyNote?: string;
|
|
96
|
+
}
|
|
97
|
+
/** The pick panel's runtime state \u2014 the editor owns it, the compositor
|
|
98
|
+
* reads it (the AskRuntime precedent, two fields instead of five). */
|
|
99
|
+
export interface PickRuntime {
|
|
100
|
+
readonly cursor: number;
|
|
101
|
+
readonly phase: "options" | "custom";
|
|
102
|
+
}
|
|
103
|
+
/** What was picked: a listed option by INDEX (never a label the caller
|
|
104
|
+
* would have to re-match against its own list), or typed text. */
|
|
105
|
+
export type PickResult = {
|
|
106
|
+
readonly index: number;
|
|
107
|
+
} | {
|
|
108
|
+
readonly custom: string;
|
|
109
|
+
};
|
|
78
110
|
/** The ALWAYS-verbose args (the panel's body): the untruncated diff
|
|
79
111
|
* (edit/write), or the full text (shell = the command line, other =
|
|
80
112
|
* the pretty-printed JSON). The CLI composes them UNTRUNCATED — the
|
|
@@ -116,6 +148,9 @@ export interface PanelView {
|
|
|
116
148
|
* panel renders the ask block and the editor routes the ask keys;
|
|
117
149
|
* absent = the approval/simple panel, unchanged. */
|
|
118
150
|
readonly ask?: AskSpec;
|
|
151
|
+
/** TUI2-R2 \u2463: the options, when this view is a PICK. Same contract
|
|
152
|
+
* as `ask`, one payload over. */
|
|
153
|
+
readonly pick?: PickSpec;
|
|
119
154
|
}
|
|
120
155
|
export type PanelVerdict = {
|
|
121
156
|
readonly action: "allow";
|
|
@@ -135,6 +170,13 @@ export type PanelVerdict = {
|
|
|
135
170
|
| {
|
|
136
171
|
readonly action: "answers";
|
|
137
172
|
readonly result: AskResult;
|
|
173
|
+
}
|
|
174
|
+
/** TUI2-R2 \u2463: the pick's verdict \u2014 the chosen index or the typed
|
|
175
|
+
* text. Only pick views ever produce it, so the approval path's
|
|
176
|
+
* switch is untouched. */
|
|
177
|
+
| {
|
|
178
|
+
readonly action: "picked";
|
|
179
|
+
readonly result: PickResult;
|
|
138
180
|
};
|
|
139
181
|
/** The bound panel state the compositor reads — the editor owns the
|
|
140
182
|
* phase/selection state machine and the key routing; the compositor
|
|
@@ -145,6 +187,8 @@ export interface PanelState {
|
|
|
145
187
|
readonly sel: PanelSel;
|
|
146
188
|
/** KC3.5: the ask's walk — present exactly when `view.ask` is. */
|
|
147
189
|
readonly ask?: AskRuntime;
|
|
190
|
+
/** TUI2-R2 \u2463: the pick's walk — present exactly when `view.pick` is. */
|
|
191
|
+
readonly pick?: PickRuntime;
|
|
148
192
|
}
|
|
149
193
|
/** The block's rows — EXACTLY the preview's frame shape, the gutter at
|
|
150
194
|
* the left edge (the preview's two-space mock indent is its own
|
|
@@ -168,3 +212,34 @@ export declare function panelStatus(view: PanelView, phase: PanelPhase, sel: Pan
|
|
|
168
212
|
* phase's keys. The approval flavor gains the tab-amend path; the
|
|
169
213
|
* simple flavor (the trust/uncertain gates) never does. */
|
|
170
214
|
export declare function panelAffordance(view: PanelView, phase: PanelPhase, sel: PanelSel): string;
|
|
215
|
+
/**
|
|
216
|
+
* The pick block's rows — the prototype's C frame.
|
|
217
|
+
*
|
|
218
|
+
* The header says what is in effect right now, because the first
|
|
219
|
+
* question anyone opening this panel has is "what am I on?". The
|
|
220
|
+
* options are numbered from 1 and the number IS the key. The `t` row is
|
|
221
|
+
* last and always present: a profile list is a convenience, never the
|
|
222
|
+
* set of models that exist, and a picker that can only offer what
|
|
223
|
+
* someone remembered to configure is a smaller product than the one it
|
|
224
|
+
* replaced.
|
|
225
|
+
*
|
|
226
|
+
* Single-row discipline: every row CUTS, never folds — the block's
|
|
227
|
+
* height is its row count (the W20 rule the #checked throw demands).
|
|
228
|
+
*/
|
|
229
|
+
export declare function pickBlockRows(view: PanelView, state: PickRuntime, W: number, maxRows: number): string[];
|
|
230
|
+
/** The digits are the keys, so the list the panel offers is bounded by
|
|
231
|
+
* the digits there are. Beyond it, `/model <name>` still takes any
|
|
232
|
+
* profile — the panel says so rather than paginating. */
|
|
233
|
+
export declare const PICK_MAX = 9;
|
|
234
|
+
/** The input row's lead: the digit range while picking, the named
|
|
235
|
+
* prompt while typing one out. */
|
|
236
|
+
export declare function pickLeadPlain(view: PanelView, state: PickRuntime): string;
|
|
237
|
+
export declare function pickLead(view: PanelView, state: PickRuntime): string;
|
|
238
|
+
/** The status row's left text \u2014 the CALLER's, because only the caller
|
|
239
|
+
* knows whether a run is paused behind this panel. */
|
|
240
|
+
export declare function pickStatus(view: PanelView): string;
|
|
241
|
+
export declare function pickAffordance(state: PickRuntime): string;
|
|
242
|
+
/** Compose a pick view. The flavor/name/title/args fields exist for the
|
|
243
|
+
* approval path and are given inert values here \u2014 the pick block
|
|
244
|
+
* reads none of them. */
|
|
245
|
+
export declare function modelPickView(spec: PickSpec, statusText: string): PanelView;
|
package/dist/approval-panel.js
CHANGED
|
@@ -26,6 +26,9 @@
|
|
|
26
26
|
*/
|
|
27
27
|
import { displayWidth } from "./width.js";
|
|
28
28
|
import { cutLine, diffBody, gutterFold, visibleWidth, widthCut } from "./components.js";
|
|
29
|
+
// TUI2-R2pre ④: strings.js takes only a TYPE from this module, so the
|
|
30
|
+
// import is erased at compile time and no runtime cycle exists.
|
|
31
|
+
import { displayVerb } from "./strings.js";
|
|
29
32
|
import { escapeTerminal, palette } from "./render.js";
|
|
30
33
|
/** The rule line's text — the why-asked line (the R3 chain): the tool
|
|
31
34
|
* name, the first non-abstain speaker, the fix hint (the §3.5 table,
|
|
@@ -37,7 +40,11 @@ function panelRuleText(view) {
|
|
|
37
40
|
if (view.ruleOverride !== undefined)
|
|
38
41
|
return escapeTerminal(view.ruleOverride);
|
|
39
42
|
const hint = view.hint;
|
|
40
|
-
|
|
43
|
+
// TUI2-R2pre ④: the rule line is the panel's header — it says the ACT
|
|
44
|
+
// ("edit needs approval"). view.name keeps the RAW tool name, which is
|
|
45
|
+
// what the option-2 rule prefill and the fallbackQuestion (the
|
|
46
|
+
// dock-less/pipe path — byte-identical by ruling) still read.
|
|
47
|
+
const base = `${p.bold}${escapeTerminal(displayVerb(view.name))}${p.reset} ${p.dim}needs approval — asked by${p.reset} ${p.bold}${escapeTerminal(view.speaker)}${p.reset}`;
|
|
41
48
|
return hint ? `${base}${p.dim} ·${p.reset} ${p.code}${escapeTerminal(hint)}${p.reset}` : base;
|
|
42
49
|
}
|
|
43
50
|
/** The numbered options row — "1 Yes 2 Yes, don't ask again for
|
|
@@ -170,3 +177,88 @@ export function panelAffordance(view, phase, sel) {
|
|
|
170
177
|
return sel === 0 ? "tab amend · esc cancel" : "enter sends · esc backs out";
|
|
171
178
|
return sel === 0 ? "esc cancel" : "enter sends";
|
|
172
179
|
}
|
|
180
|
+
// ── TUI2-R2 ④: the pick block, its lead, its status, its affordance ──
|
|
181
|
+
/**
|
|
182
|
+
* The pick block's rows — the prototype's C frame.
|
|
183
|
+
*
|
|
184
|
+
* The header says what is in effect right now, because the first
|
|
185
|
+
* question anyone opening this panel has is "what am I on?". The
|
|
186
|
+
* options are numbered from 1 and the number IS the key. The `t` row is
|
|
187
|
+
* last and always present: a profile list is a convenience, never the
|
|
188
|
+
* set of models that exist, and a picker that can only offer what
|
|
189
|
+
* someone remembered to configure is a smaller product than the one it
|
|
190
|
+
* replaced.
|
|
191
|
+
*
|
|
192
|
+
* Single-row discipline: every row CUTS, never folds — the block's
|
|
193
|
+
* height is its row count (the W20 rule the #checked throw demands).
|
|
194
|
+
*/
|
|
195
|
+
export function pickBlockRows(view, state, W, maxRows) {
|
|
196
|
+
const p = palette();
|
|
197
|
+
const spec = view.pick;
|
|
198
|
+
const gutter = `${p.dim}\u2502${p.reset} `;
|
|
199
|
+
const rows = [];
|
|
200
|
+
const room = Math.max(1, W - 2);
|
|
201
|
+
rows.push(`${gutter}${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)}`);
|
|
202
|
+
if (spec.options.length === 0) {
|
|
203
|
+
// the honest empty state \u2014 the caller's own copy, verbatim
|
|
204
|
+
rows.push(`${gutter}${cutLine(`${p.dim} ${escapeTerminal(spec.emptyNote ?? "no options")}${p.reset}`, room)}`);
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
// the budget: the header, the t row, the affordance and the rule
|
|
208
|
+
const budget = Math.max(1, maxRows - 4);
|
|
209
|
+
const shown = spec.options.slice(0, Math.min(budget, PICK_MAX));
|
|
210
|
+
for (let i = 0; i < shown.length; i += 1) {
|
|
211
|
+
const o = shown[i];
|
|
212
|
+
const mark = i === state.cursor && state.phase === "options";
|
|
213
|
+
const head = `${mark ? p.bold : ""} ${i + 1} ${escapeTerminal(o.label)}${mark ? p.reset : ""}`;
|
|
214
|
+
const note = o.note === undefined ? "" : `${p.dim} ${escapeTerminal(o.note)}${p.reset}`;
|
|
215
|
+
rows.push(`${gutter}${cutLine(`${head}${note}`, room)}`);
|
|
216
|
+
}
|
|
217
|
+
if (spec.options.length > shown.length) {
|
|
218
|
+
rows.push(`${gutter}${cutLine(`${p.dim} \u2514 +${spec.options.length - shown.length} more \u2014 /model <name> takes any of them${p.reset}`, room)}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
rows.push(`${gutter}${cutLine(`${state.phase === "custom" ? p.bold : ""} t ${p.reset}${p.dim}${escapeTerminal(spec.typeHint)}${p.reset}`, room)}`);
|
|
222
|
+
rows.push(`${gutter}${p.dim}${cutLine(pickAffordance(state), room)}${p.reset}`);
|
|
223
|
+
rows.push(`${p.dim}\u2514${"\u2500".repeat(Math.max(0, W - 1))}${p.reset}`);
|
|
224
|
+
return rows;
|
|
225
|
+
}
|
|
226
|
+
/** The digits are the keys, so the list the panel offers is bounded by
|
|
227
|
+
* the digits there are. Beyond it, `/model <name>` still takes any
|
|
228
|
+
* profile — the panel says so rather than paginating. */
|
|
229
|
+
export const PICK_MAX = 9;
|
|
230
|
+
/** The input row's lead: the digit range while picking, the named
|
|
231
|
+
* prompt while typing one out. */
|
|
232
|
+
export function pickLeadPlain(view, state) {
|
|
233
|
+
if (state.phase === "custom")
|
|
234
|
+
return "provider/model: ";
|
|
235
|
+
const n = Math.min(view.pick.options.length, PICK_MAX);
|
|
236
|
+
return n === 0 ? "t> " : `1-${n}> `;
|
|
237
|
+
}
|
|
238
|
+
export function pickLead(view, state) {
|
|
239
|
+
const p = palette();
|
|
240
|
+
return `${p.bold}${pickLeadPlain(view, state)}${p.reset}`;
|
|
241
|
+
}
|
|
242
|
+
/** The status row's left text \u2014 the CALLER's, because only the caller
|
|
243
|
+
* knows whether a run is paused behind this panel. */
|
|
244
|
+
export function pickStatus(view) {
|
|
245
|
+
return view.statusText;
|
|
246
|
+
}
|
|
247
|
+
export function pickAffordance(state) {
|
|
248
|
+
return state.phase === "custom" ? "enter commits \u00b7 esc backs out" : "digits pick \u00b7 \u23ce confirms \u00b7 esc";
|
|
249
|
+
}
|
|
250
|
+
/** Compose a pick view. The flavor/name/title/args fields exist for the
|
|
251
|
+
* approval path and are given inert values here \u2014 the pick block
|
|
252
|
+
* reads none of them. */
|
|
253
|
+
export function modelPickView(spec, statusText) {
|
|
254
|
+
return {
|
|
255
|
+
flavor: "simple",
|
|
256
|
+
name: "model",
|
|
257
|
+
title: "model",
|
|
258
|
+
speaker: "you",
|
|
259
|
+
statusText,
|
|
260
|
+
args: { kind: "text", lines: [] },
|
|
261
|
+
fallbackQuestion: "switch model? (name) ",
|
|
262
|
+
pick: spec,
|
|
263
|
+
};
|
|
264
|
+
}
|
package/dist/components.d.ts
CHANGED
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
* tint, fold wording).
|
|
19
19
|
*/
|
|
20
20
|
import { foldThinking, foldResult, renderToolSummary, type ResumeMeta } from "./render.js";
|
|
21
|
+
import { type MdBlock } from "./md.js";
|
|
22
|
+
export { MdStream, renderBlock, renderMarkdown, type MdBlock, type MdKind } from "./md.js";
|
|
21
23
|
/** The spinner glyphs, cycled by the compositor's on-demand tick. */
|
|
22
24
|
export declare const SPINNER: string[];
|
|
23
25
|
/** The frame context the compositor passes down — the pieces of time
|
|
@@ -43,8 +45,12 @@ export type RenderLine = string;
|
|
|
43
45
|
*/
|
|
44
46
|
export declare function foldLine(line: string, W: number): string[];
|
|
45
47
|
/** The visible width of a rendered line (SGR stripped — the invariant
|
|
46
|
-
* the compositor enforces on every emitted line).
|
|
47
|
-
|
|
48
|
+
* the compositor enforces on every emitted line). TUI2-MD ⑤: the body
|
|
49
|
+
* moved to width.ts (the width authority's own home) so the markdown
|
|
50
|
+
* renderer can measure without importing this module back — the
|
|
51
|
+
* re-export is verbatim, so every existing importer and the barrel see
|
|
52
|
+
* exactly what they saw. */
|
|
53
|
+
export { visibleWidth } from "./width.js";
|
|
48
54
|
/** A component: render the display lines for one piece of state. */
|
|
49
55
|
export interface Component {
|
|
50
56
|
render(width: number, ctx: FrameCtx): string[];
|
|
@@ -145,6 +151,18 @@ export type BodyCell = {
|
|
|
145
151
|
kind: "text";
|
|
146
152
|
text: string;
|
|
147
153
|
done: boolean;
|
|
154
|
+
}
|
|
155
|
+
/** TUI2-MD ⑤ — ONE markdown block of assistant body text. The cell is
|
|
156
|
+
* the commit unit the compositor already had, so block-freeze needs
|
|
157
|
+
* no new commit machinery: a CLOSED block is a DONE cell and the
|
|
158
|
+
* natural loop freezes it; the OPEN tail block is the one cell left
|
|
159
|
+
* live. `block` carries the block's SOURCE (never rendered rows), so
|
|
160
|
+
* a resize re-renders it at the new width exactly as every other
|
|
161
|
+
* cell does. */
|
|
162
|
+
| {
|
|
163
|
+
kind: "md";
|
|
164
|
+
block: MdBlock;
|
|
165
|
+
done: boolean;
|
|
148
166
|
} | {
|
|
149
167
|
kind: "notice";
|
|
150
168
|
text: string;
|
|
@@ -215,12 +233,37 @@ export declare function gutterFold(gutter: string, line: string, W: number): str
|
|
|
215
233
|
* ellipsis ride the row (the invariant ① cap holds). */
|
|
216
234
|
export declare function gutterCut(gutter: string, line: string, W: number): string[];
|
|
217
235
|
export declare function expandSuffix(lines: number | null, room: number): string;
|
|
236
|
+
/**
|
|
237
|
+
* TUI2-R2 ⑤ (D, candidate 1) — the FOCUS tint.
|
|
238
|
+
*
|
|
239
|
+
* The cell the next ctrl+r will act on brightens its own `ctrl+r` token
|
|
240
|
+
* to the code tint; the rest of the suffix — the separator, the count —
|
|
241
|
+
* stays dim, because what is being marked is the KEY's target, not the
|
|
242
|
+
* row. Zero new rows, zero new columns: the affordance the cell already
|
|
243
|
+
* prints is the marker.
|
|
244
|
+
*
|
|
245
|
+
* Applied to a row rather than composed into it on purpose. The token is
|
|
246
|
+
* emitted from several places (the settled suffix, the renderer's own
|
|
247
|
+
* `└ +N … · ctrl+r` cut rows) and threading a flag through all of them
|
|
248
|
+
* would put the invariant "exactly one bright token" in as many hands as
|
|
249
|
+
* there are emitters. Here it has exactly one.
|
|
250
|
+
*
|
|
251
|
+
* NO_COLOR: p.code is empty, so the row's bytes are untouched.
|
|
252
|
+
*/
|
|
253
|
+
export declare function focusToken(row: string, W: number): string;
|
|
218
254
|
/** W13 — the rollup opt-in table: which tools collapse, and the count
|
|
219
255
|
* NOUN (read_file calls → "5 files", list_dir → "5 dirs", search_text
|
|
220
256
|
* → "5 matches"). Only these tools opt in — a shell burst is never
|
|
221
257
|
* rolled up (its rows carry meaning). The folded-turn line (W14) reuses
|
|
222
258
|
* the plurals for its other-tool terms ("2 dirs", "1 match"). */
|
|
223
259
|
export declare const ROLLUP_NOUN: Readonly<Record<string, string>>;
|
|
260
|
+
/** TUI2-R1 (B) — the verb column of the expanded list names the ACT.
|
|
261
|
+
* TUI2-R2pre ④: this used to be a private three-tool table saying the
|
|
262
|
+
* same thing as the card head's `_file` strip, in a different way and
|
|
263
|
+
* for a different set of tools. Both are `displayVerb` now — the whole
|
|
264
|
+
* point of the ruling is that there is ONE answer to "what does the
|
|
265
|
+
* screen call this". The cut note, which used to be the deliberate
|
|
266
|
+
* exception here, moved with it (see toolCutNote). */
|
|
224
267
|
/** Whether a tool joins an exploration run. Exactly the read-only set —
|
|
225
268
|
* writes, edits, shells and extension tools never group (a burst of
|
|
226
269
|
* side effects is a list of things that HAPPENED, and every row of it
|
package/dist/components.js
CHANGED
|
@@ -17,8 +17,18 @@
|
|
|
17
17
|
* (untouched); render.ts supplies the original text (palette, escape,
|
|
18
18
|
* tint, fold wording).
|
|
19
19
|
*/
|
|
20
|
-
import { displayWidth } from "./width.js";
|
|
20
|
+
import { displayWidth, visibleWidth } from "./width.js";
|
|
21
|
+
// TUI2-R2pre ④: the ONE display-verb table (strings.ts, beside
|
|
22
|
+
// KEY_BINDINGS). strings.js imports only render/width here, so this edge
|
|
23
|
+
// adds no cycle.
|
|
24
|
+
import { displayVerb } from "./strings.js";
|
|
21
25
|
import { bannerLines, escapeTerminal, foldThinking, foldResult, colorInlineCode, renderTerminalGap, renderToolSummary, toolTarget, kUnit, palette, } from "./render.js";
|
|
26
|
+
// TUI2-MD: the markdown renderer's surface reaches the tui through this
|
|
27
|
+
// module (the tui's components shim re-exports it) — one import edge,
|
|
28
|
+
// and it points one way: md.ts measures with the width authority, never
|
|
29
|
+
// back through here.
|
|
30
|
+
import { renderBlock } from "./md.js";
|
|
31
|
+
export { MdStream, renderBlock, renderMarkdown } from "./md.js";
|
|
22
32
|
/** The spinner glyphs, cycled by the compositor's on-demand tick. */
|
|
23
33
|
export const SPINNER = ["▖", "▘", "▝", "▗"];
|
|
24
34
|
/**
|
|
@@ -90,24 +100,12 @@ export function foldLine(line, W) {
|
|
|
90
100
|
return out;
|
|
91
101
|
}
|
|
92
102
|
/** The visible width of a rendered line (SGR stripped — the invariant
|
|
93
|
-
* the compositor enforces on every emitted line).
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (m !== null) {
|
|
100
|
-
i += m[0].length;
|
|
101
|
-
continue;
|
|
102
|
-
}
|
|
103
|
-
i += 1;
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
|
-
w += displayWidth(line[i]);
|
|
107
|
-
i += 1;
|
|
108
|
-
}
|
|
109
|
-
return w;
|
|
110
|
-
}
|
|
103
|
+
* the compositor enforces on every emitted line). TUI2-MD ⑤: the body
|
|
104
|
+
* moved to width.ts (the width authority's own home) so the markdown
|
|
105
|
+
* renderer can measure without importing this module back — the
|
|
106
|
+
* re-export is verbatim, so every existing importer and the barrel see
|
|
107
|
+
* exactly what they saw. */
|
|
108
|
+
export { visibleWidth } from "./width.js";
|
|
111
109
|
/** The W11 spacing formula — "a row gets one blank line above it when
|
|
112
110
|
* the row is itself a block, or when the previous sibling was taller
|
|
113
111
|
* than one row". One-row siblings pack tight; anything multi-row
|
|
@@ -156,6 +154,8 @@ export function cellComponent(cell) {
|
|
|
156
154
|
return new ToolExecution(cell);
|
|
157
155
|
case "text":
|
|
158
156
|
return new AssistantMessage(cell);
|
|
157
|
+
case "md":
|
|
158
|
+
return new MarkdownBlock(cell);
|
|
159
159
|
case "notice":
|
|
160
160
|
return new ErrorLine(cell);
|
|
161
161
|
case "banner":
|
|
@@ -454,7 +454,7 @@ class ToolExecution {
|
|
|
454
454
|
render(W, ctx) {
|
|
455
455
|
const p = palette();
|
|
456
456
|
const c = this.cell;
|
|
457
|
-
const verb = escapeTerminal(c.name
|
|
457
|
+
const verb = escapeTerminal(displayVerb(c.name));
|
|
458
458
|
const verbCol = verb.length < 5 ? `${verb}${" ".repeat(5 - verb.length)}` : verb;
|
|
459
459
|
const parts = c.rolled?.parts;
|
|
460
460
|
if (c.rolled !== null && parts !== undefined) {
|
|
@@ -711,6 +711,46 @@ function appendSuffix(row, suffix) {
|
|
|
711
711
|
const p = palette();
|
|
712
712
|
return `${row}${p.dim}${suffix}${p.reset}`;
|
|
713
713
|
}
|
|
714
|
+
/**
|
|
715
|
+
* TUI2-R2 ⑤ (D, candidate 1) — the FOCUS tint.
|
|
716
|
+
*
|
|
717
|
+
* The cell the next ctrl+r will act on brightens its own `ctrl+r` token
|
|
718
|
+
* to the code tint; the rest of the suffix — the separator, the count —
|
|
719
|
+
* stays dim, because what is being marked is the KEY's target, not the
|
|
720
|
+
* row. Zero new rows, zero new columns: the affordance the cell already
|
|
721
|
+
* prints is the marker.
|
|
722
|
+
*
|
|
723
|
+
* Applied to a row rather than composed into it on purpose. The token is
|
|
724
|
+
* emitted from several places (the settled suffix, the renderer's own
|
|
725
|
+
* `└ +N … · ctrl+r` cut rows) and threading a flag through all of them
|
|
726
|
+
* would put the invariant "exactly one bright token" in as many hands as
|
|
727
|
+
* there are emitters. Here it has exactly one.
|
|
728
|
+
*
|
|
729
|
+
* NO_COLOR: p.code is empty, so the row's bytes are untouched.
|
|
730
|
+
*/
|
|
731
|
+
export function focusToken(row, W) {
|
|
732
|
+
const p = palette();
|
|
733
|
+
const at = row.lastIndexOf(CTRL_R);
|
|
734
|
+
if (at !== -1) {
|
|
735
|
+
// the row already names the key — brighten the token in place, and
|
|
736
|
+
// leave every other span exactly as it was
|
|
737
|
+
if (p.code === "")
|
|
738
|
+
return row;
|
|
739
|
+
return `${row.slice(0, at)}${p.code}${CTRL_R}${p.reset}${p.dim}${row.slice(at + CTRL_R.length)}`;
|
|
740
|
+
}
|
|
741
|
+
// A LIVE row does not carry the affordance today, and the live cell is
|
|
742
|
+
// the one ctrl+r takes FIRST (expandNext scans the live tail before
|
|
743
|
+
// the committed ring) — so the row the key is aimed at was the one row
|
|
744
|
+
// that never said the key existed. The affordance IS the marker here:
|
|
745
|
+
// it appears on the focused row and nowhere else, which is why no
|
|
746
|
+
// unfocused row's bytes move (every existing live-row assertion
|
|
747
|
+
// renders a cell with no focus and is untouched).
|
|
748
|
+
const room = W - visibleWidth(row);
|
|
749
|
+
if (room < SUFFIX_MIN)
|
|
750
|
+
return row; // never at the cost of invariant ①
|
|
751
|
+
return `${row}${p.dim} · ${p.reset}${p.code}${CTRL_R}${p.reset}`;
|
|
752
|
+
}
|
|
753
|
+
const CTRL_R = "ctrl+r";
|
|
714
754
|
/** TUI2-R1 (A) — the expanded block's last row: the way back. The
|
|
715
755
|
* rollup's expanded list carries a second clause (its members' full
|
|
716
756
|
* outputs live in /last, which the group row cannot show). */
|
|
@@ -736,10 +776,13 @@ const EXPLORE_NOUN = {
|
|
|
736
776
|
list_dir: ["dir", "dirs"],
|
|
737
777
|
search_text: ["search", "searches"],
|
|
738
778
|
};
|
|
739
|
-
/** TUI2-R1 (B) — the verb column of the expanded list.
|
|
740
|
-
*
|
|
741
|
-
*
|
|
742
|
-
|
|
779
|
+
/** TUI2-R1 (B) — the verb column of the expanded list names the ACT.
|
|
780
|
+
* TUI2-R2pre ④: this used to be a private three-tool table saying the
|
|
781
|
+
* same thing as the card head's `_file` strip, in a different way and
|
|
782
|
+
* for a different set of tools. Both are `displayVerb` now — the whole
|
|
783
|
+
* point of the ruling is that there is ONE answer to "what does the
|
|
784
|
+
* screen call this". The cut note, which used to be the deliberate
|
|
785
|
+
* exception here, moved with it (see toolCutNote). */
|
|
743
786
|
/** Whether a tool joins an exploration run. Exactly the read-only set —
|
|
744
787
|
* writes, edits, shells and extension tools never group (a burst of
|
|
745
788
|
* side effects is a list of things that HAPPENED, and every row of it
|
|
@@ -770,7 +813,7 @@ export function exploreRows(parts, W) {
|
|
|
770
813
|
counts.set(s, (counts.get(s) ?? 0) + 1);
|
|
771
814
|
const shown = [...counts.entries()].slice(0, 3).map(([s, n]) => (n > 1 ? `${s} ×${n}` : s));
|
|
772
815
|
const more = counts.size > 3 ? ` (+${counts.size - 3})` : "";
|
|
773
|
-
const verb =
|
|
816
|
+
const verb = displayVerb(part.name);
|
|
774
817
|
rows.push(cutLine(`${p.dim}${BODY_ROW}${escapeTerminal(`${verb.padEnd(6)} ${shown.join(" · ")}${more}`)}${p.reset}`, W));
|
|
775
818
|
}
|
|
776
819
|
// TUI2-R1.5 ① (VD-15): the footer used to promise "/last shows the full
|
|
@@ -812,7 +855,7 @@ export function turnFold(t, W) {
|
|
|
812
855
|
parts.push(countTerm(n, noun.endsWith("es") ? noun.slice(0, -2) : noun.slice(0, -1), noun));
|
|
813
856
|
}
|
|
814
857
|
else {
|
|
815
|
-
const verb = name
|
|
858
|
+
const verb = displayVerb(name);
|
|
816
859
|
parts.push(countTerm(n, verb, `${verb}s`));
|
|
817
860
|
}
|
|
818
861
|
}
|
|
@@ -1110,14 +1153,22 @@ export function diffBody(diff, W, expanded = false) {
|
|
|
1110
1153
|
* offset=N", the output cap, list_dir's entry cap). The note reaches
|
|
1111
1154
|
* the MODEL and never the human — this row surfaces it. Detected in
|
|
1112
1155
|
* the result's TAIL (the note is appended at the end); returns null
|
|
1113
|
-
* when the tool did not truncate.
|
|
1156
|
+
* when the tool did not truncate.
|
|
1157
|
+
*
|
|
1158
|
+
* TUI2-R2pre ④: the verb here is the DISPLAY one now. This row used to
|
|
1159
|
+
* be the sanctioned raw-name exception, on the reasoning that it names
|
|
1160
|
+
* the tool the model should call again — but the row is addressed to
|
|
1161
|
+
* the HUMAN (the model already has the note in its own transcript, which
|
|
1162
|
+
* is where it read it), and the ruling names this advisory family
|
|
1163
|
+
* explicitly. The `offset=N` it carries is the actionable half and is
|
|
1164
|
+
* untouched. */
|
|
1114
1165
|
function toolCutNote(name, resultText) {
|
|
1115
1166
|
const tail = resultText.slice(-300);
|
|
1116
1167
|
const m = /offset=(\d+)/.exec(tail);
|
|
1117
1168
|
if (m !== null)
|
|
1118
|
-
return `capped by ${escapeTerminal(name)} · offset=${m[1]} for the rest`;
|
|
1169
|
+
return `capped by ${escapeTerminal(displayVerb(name))} · offset=${m[1]} for the rest`;
|
|
1119
1170
|
if (/…\[truncated\]/.test(tail) || /… \+?\d+ more (?:lines|entries)/.test(tail))
|
|
1120
|
-
return `capped by ${escapeTerminal(name)} · /last for the rest`;
|
|
1171
|
+
return `capped by ${escapeTerminal(displayVerb(name))} · /last for the rest`;
|
|
1121
1172
|
return null;
|
|
1122
1173
|
}
|
|
1123
1174
|
/** The assistant body text — wrapped at W, the inline-code tint per
|
|
@@ -1135,6 +1186,20 @@ class AssistantMessage {
|
|
|
1135
1186
|
return wrapped.length > 0 ? wrapped.map((l) => colorInlineCode(l)) : [""];
|
|
1136
1187
|
}
|
|
1137
1188
|
}
|
|
1189
|
+
/** TUI2-MD ⑤ — one markdown block. Pure in (block, W): the same source
|
|
1190
|
+
* and the same width give the same bytes, which is the freeze property
|
|
1191
|
+
* the commit path relies on. The block carries its own leading blank
|
|
1192
|
+
* (the style table's rhythm), so the compositor's W11 join formula
|
|
1193
|
+
* steps aside between two of these. */
|
|
1194
|
+
class MarkdownBlock {
|
|
1195
|
+
cell;
|
|
1196
|
+
constructor(cell) {
|
|
1197
|
+
this.cell = cell;
|
|
1198
|
+
}
|
|
1199
|
+
render(W, _ctx) {
|
|
1200
|
+
return renderBlock(this.cell.block, W);
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1138
1203
|
/** The ⚠ / notice lines — the error surface. */
|
|
1139
1204
|
class ErrorLine {
|
|
1140
1205
|
cell;
|
package/dist/index.d.ts
CHANGED
|
@@ -6,11 +6,12 @@
|
|
|
6
6
|
* package); the cli never imports it directly. Experimental — no
|
|
7
7
|
* API-stability promise yet.
|
|
8
8
|
*/
|
|
9
|
-
export { SPINNER, foldLine, foldWords, visibleWidth, bodySpacing, Container, cellComponent, ROLLUP_NOUN, turnFold, CAP_TASK_LIVE, formatDuration, statusLine, boxTop, boxBottom, terminalPipe, type FrameCtx, type RenderLine, type Component, type BodyCell, } from "./components.js";
|
|
9
|
+
export { SPINNER, foldLine, foldWords, visibleWidth, bodySpacing, Container, cellComponent, focusToken, ROLLUP_NOUN, turnFold, CAP_TASK_LIVE, formatDuration, statusLine, boxTop, boxBottom, terminalPipe, type FrameCtx, type RenderLine, type Component, type BodyCell, } from "./components.js";
|
|
10
10
|
export { editFileDiff, truncateDiff, writeFileDiff, type DiffLine, type DiffResult } from "./diff.js";
|
|
11
11
|
export { pendingQueueRows } from "./components.js";
|
|
12
12
|
export { charWidth, displayWidth, leadWidth, widthOf } from "./width.js";
|
|
13
13
|
export { panelAffordance, panelBlockRows, panelLead, panelLeadPlain, panelLeadWidth, panelStatus, type PanelArgs, type PanelFlavor, type PanelPhase, type PanelSel, type PanelState, type PanelVerdict, type PanelView, type AskAnswer, type AskOption, type AskQuestion, type AskResult, type AskRuntime, type AskSpec, } from "./approval-panel.js";
|
|
14
14
|
export { interactivePrompt, projectTrustRows, projectTrustView, projectUntrustedNote, uncertainView, type TrustArtifact, } from "./strings.js";
|
|
15
15
|
export { extensionsBannerText, helpRows, unansweredAskView, type BannerExtension } from "./strings.js";
|
|
16
|
+
export { displayVerb } from "./strings.js";
|
|
16
17
|
export { bannerLines, COLOR_OFF, COLOR_ON, colorInlineCode, escapeTerminal, foldResult, foldThinking, kUnit, palette, relativeTime, renderResumeList, renderTerminalGap, renderToolSummary, TAGLINE, toolTarget, truncateRow, type Palette, type ResumeMeta, } from "./render.js";
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* package); the cli never imports it directly. Experimental — no
|
|
7
7
|
* API-stability promise yet.
|
|
8
8
|
*/
|
|
9
|
-
export { SPINNER, foldLine, foldWords, visibleWidth, bodySpacing, Container, cellComponent, ROLLUP_NOUN, turnFold, CAP_TASK_LIVE, formatDuration, statusLine, boxTop, boxBottom, terminalPipe, } from "./components.js";
|
|
9
|
+
export { SPINNER, foldLine, foldWords, visibleWidth, bodySpacing, Container, cellComponent, focusToken, ROLLUP_NOUN, turnFold, CAP_TASK_LIVE, formatDuration, statusLine, boxTop, boxBottom, terminalPipe, } from "./components.js";
|
|
10
10
|
export { editFileDiff, truncateDiff, writeFileDiff } from "./diff.js";
|
|
11
11
|
// W22 (the v8 input round): the pending-queue chips — the SAME
|
|
12
12
|
// UserMessage chip with the □ gutter, pre-rendered above the input
|
|
@@ -25,4 +25,7 @@ export { panelAffordance, panelBlockRows, panelLead, panelLeadPlain, panelLeadWi
|
|
|
25
25
|
export { interactivePrompt, projectTrustRows, projectTrustView, projectUntrustedNote, uncertainView, } from "./strings.js";
|
|
26
26
|
// KC3.5: the interrupted-ask copy and the extracted /help table.
|
|
27
27
|
export { extensionsBannerText, helpRows, unansweredAskView } from "./strings.js";
|
|
28
|
+
// TUI2-R2pre ④: the ONE display-verb table — the screen names the act,
|
|
29
|
+
// the tool table names the call.
|
|
30
|
+
export { displayVerb } from "./strings.js";
|
|
28
31
|
export { bannerLines, COLOR_OFF, COLOR_ON, colorInlineCode, escapeTerminal, foldResult, foldThinking, kUnit, palette, relativeTime, renderResumeList, renderTerminalGap, renderToolSummary, TAGLINE, toolTarget, truncateRow, } from "./render.js";
|