@vincemakes/kiso-tui-cells 0.1.41

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.
@@ -0,0 +1,109 @@
1
+ /**
2
+ * W21 (the v8 approval round) — the approval panel: the bounded block
3
+ * that replaces the running tool's live window while a human-chain
4
+ * approval is pending (the v8 design §3.2). The panel is a VIEW only —
5
+ * the verdict mapping (bare No aborts, No+words continues, esc
6
+ * cancels, the allow-amend words ride the next turn) lives in the CLI,
7
+ * never here (the R3 chain ruling).
8
+ *
9
+ * The block's rows:
10
+ * - the rule line — the why-asked line, ONE row (never a fold):
11
+ * `<tool> needs approval — asked by <speaker> · <fix hint>`;
12
+ * - the title — the toolTarget rendering, ONE row;
13
+ * - the divider — "─ the full args — never truncated ─";
14
+ * - the ALWAYS-verbose args (shell = the full command; edit/write =
15
+ * the untruncated ± diff; other = the full JSON — nothing the human
16
+ * is asked to approve is ever cut), FOLDED at W−2 and capped at
17
+ * maxRows−6 with the "└ +N more rows" notice;
18
+ * - the numbered options — "1 Yes / 2 Yes, don't ask again for <tool>
19
+ * / 3 No" (the approval flavor) or "1 Yes / 3 No" (the simple
20
+ * flavor — the trust gate, the uncertain resolutions);
21
+ * - the affordance — the phase's key hint, ONE row;
22
+ * - the └ corner.
23
+ * The single-row lines CUT (never fold — the block's height is its row
24
+ * count, the W20 discipline — the #checked throw demands it); the args
25
+ * FOLD (a bounded block's body — content folds, metadata cuts).
26
+ */
27
+ export type PanelFlavor = "approval" | "simple";
28
+ export type PanelPhase = "options" | "rule" | "amend";
29
+ export type PanelSel = 0 | 1 | 2 | 3;
30
+ /** The ALWAYS-verbose args (the panel's body): the untruncated diff
31
+ * (edit/write), or the full text (shell = the command line, other =
32
+ * the pretty-printed JSON). The CLI composes them UNTRUNCATED — the
33
+ * panel renders the expanded diff path (diffBody(diff, W, true)). */
34
+ export type PanelArgs = {
35
+ readonly kind: "diff";
36
+ readonly diff: import("./diff.js").DiffLine[] | null;
37
+ } | {
38
+ readonly kind: "text";
39
+ readonly lines: readonly string[];
40
+ };
41
+ export interface PanelView {
42
+ /** The flavor — "approval" carries the option-2 rule ("Yes, don't
43
+ * ask again"), "simple" (the trust gate, the uncertain resolutions)
44
+ * carries only 1 Yes / 3 No. */
45
+ readonly flavor: PanelFlavor;
46
+ /** The tool name — the rule line's first word and the option-2
47
+ * rule prefill (the approval flavor). */
48
+ readonly name: string;
49
+ /** The title — the toolTarget rendering ("edit examples/foo.ts"). */
50
+ readonly title: string;
51
+ /** The rule line's "asked by" — the first non-abstain extension
52
+ * (the ask verdict's speaker). */
53
+ readonly speaker: string;
54
+ /** The fix hint per speaker (the v8 design §3.5 table). */
55
+ readonly hint?: string;
56
+ /** The options-phase status-left text — the CLI knows the context
57
+ * ("▸ run paused", the trust gate's line). */
58
+ readonly statusText: string;
59
+ /** The ALWAYS-verbose args — the full command/content/diff. */
60
+ readonly args: PanelArgs;
61
+ /** The simple flavor's full rule line (the trust/uncertain
62
+ * questions) — overrides the why-asked composition. */
63
+ readonly ruleOverride?: string;
64
+ /** The fallback question — the y/n text for the dock-less path
65
+ * (a TTY without a dock, or a pipe). */
66
+ readonly fallbackQuestion: string;
67
+ }
68
+ export type PanelVerdict = {
69
+ readonly action: "allow";
70
+ readonly reason: string;
71
+ } | {
72
+ readonly action: "allow-rule";
73
+ readonly rule: string;
74
+ } | {
75
+ readonly action: "deny";
76
+ readonly reason: string;
77
+ } | {
78
+ readonly action: "cancel";
79
+ };
80
+ /** The bound panel state the compositor reads — the editor owns the
81
+ * phase/selection state machine and the key routing; the compositor
82
+ * renders it (the block rows, the input lead, the status/hint). */
83
+ export interface PanelState {
84
+ readonly view: PanelView;
85
+ readonly phase: PanelPhase;
86
+ readonly sel: PanelSel;
87
+ }
88
+ /** The block's rows — EXACTLY the preview's frame shape, the gutter at
89
+ * the left edge (the preview's two-space mock indent is its own
90
+ * styling; the real rows sit at column 1, like every tool cell).
91
+ * maxRows caps the TOTAL (the args fold; the single-row lines cut). */
92
+ export declare function panelBlockRows(view: PanelView, phase: PanelPhase, sel: PanelSel, W: number, maxRows: number): string[];
93
+ /** The input row's lead for the panel's phase (the preview's chrome
94
+ * rows): the digit leads bold, the rule/feedback leads dim. The rule
95
+ * lead names the tool (the option-2 prefill), the amend lead the
96
+ * denial/allowance feedback ("the words ride the verdict"). */
97
+ export declare function panelLead(view: PanelView, phase: PanelPhase, sel: PanelSel): string;
98
+ /** The lead's plain text — the editor's reflow width (the line must
99
+ * fit the lead + the box's walls). */
100
+ export declare function panelLeadPlain(view: PanelView, phase: PanelPhase, sel: PanelSel): string;
101
+ export declare function panelLeadWidth(view: PanelView, phase: PanelPhase, sel: PanelSel): number;
102
+ /** The status row's left text while the panel is up — the phase, not
103
+ * the CLI's painting status (the compositor derives it from the panel
104
+ * state; the "▸ run paused" etc. ride the options phase). */
105
+ export declare function panelStatus(view: PanelView, phase: PanelPhase, sel: PanelSel): string;
106
+ /** The status row's right-aligned hint while the panel is up — the
107
+ * phase's keys. The approval flavor gains the tab-amend path; the
108
+ * simple flavor (the trust/uncertain gates) never does. */
109
+ export declare function panelAffordance(view: PanelView, phase: PanelPhase, sel: PanelSel): string;
@@ -0,0 +1,144 @@
1
+ /**
2
+ * W21 (the v8 approval round) — the approval panel: the bounded block
3
+ * that replaces the running tool's live window while a human-chain
4
+ * approval is pending (the v8 design §3.2). The panel is a VIEW only —
5
+ * the verdict mapping (bare No aborts, No+words continues, esc
6
+ * cancels, the allow-amend words ride the next turn) lives in the CLI,
7
+ * never here (the R3 chain ruling).
8
+ *
9
+ * The block's rows:
10
+ * - the rule line — the why-asked line, ONE row (never a fold):
11
+ * `<tool> needs approval — asked by <speaker> · <fix hint>`;
12
+ * - the title — the toolTarget rendering, ONE row;
13
+ * - the divider — "─ the full args — never truncated ─";
14
+ * - the ALWAYS-verbose args (shell = the full command; edit/write =
15
+ * the untruncated ± diff; other = the full JSON — nothing the human
16
+ * is asked to approve is ever cut), FOLDED at W−2 and capped at
17
+ * maxRows−6 with the "└ +N more rows" notice;
18
+ * - the numbered options — "1 Yes / 2 Yes, don't ask again for <tool>
19
+ * / 3 No" (the approval flavor) or "1 Yes / 3 No" (the simple
20
+ * flavor — the trust gate, the uncertain resolutions);
21
+ * - the affordance — the phase's key hint, ONE row;
22
+ * - the └ corner.
23
+ * The single-row lines CUT (never fold — the block's height is its row
24
+ * count, the W20 discipline — the #checked throw demands it); the args
25
+ * FOLD (a bounded block's body — content folds, metadata cuts).
26
+ */
27
+ import { displayWidth } from "./width.js";
28
+ import { cutLine, diffBody, gutterFold, visibleWidth, widthCut } from "./components.js";
29
+ import { escapeTerminal, palette } from "./render.js";
30
+ /** The rule line's text — the why-asked line (the R3 chain): the tool
31
+ * name, the first non-abstain speaker, the fix hint (the §3.5 table,
32
+ * code-accented). The simple flavor carries the CLI's own question
33
+ * text instead (the trust gate, the uncertain resolutions) — their
34
+ * titles and args differ, the interaction is identical (§3.6). */
35
+ function panelRuleText(view) {
36
+ const p = palette();
37
+ if (view.ruleOverride !== undefined)
38
+ return escapeTerminal(view.ruleOverride);
39
+ const hint = view.hint;
40
+ const base = `${p.bold}${escapeTerminal(view.name)}${p.reset} ${p.dim}needs approval — asked by${p.reset} ${p.bold}${escapeTerminal(view.speaker)}${p.reset}`;
41
+ return hint ? `${base}${p.dim} ·${p.reset} ${p.code}${escapeTerminal(hint)}${p.reset}` : base;
42
+ }
43
+ /** The numbered options row — "1 Yes 2 Yes, don't ask again for
44
+ * <rule> 3 No" (approval) or "1 Yes 3 No" (simple). The row is a
45
+ * pure SPAN: the block prepends the gutter (the invariant ① test
46
+ * caught the double-gutter — the block and the row both emitted it).
47
+ * The option-2 rule name is the ONLY cuttable span: the row's fixed
48
+ * part (the 1/3 options + the separators + the option-2 prefix) is 45
49
+ * cells, so the name fits W−45 and cuts with a "…" at W−46 (the
50
+ * single-row discipline — the row never folds). */
51
+ function panelOptionsRow(view, sel, W) {
52
+ const p = palette();
53
+ const o1 = sel === 1 ? `${p.bold} 1 Yes${p.reset}` : ` 1 Yes`;
54
+ const o3 = sel === 3 ? `${p.bold} 3 No${p.reset}` : ` 3 No`;
55
+ if (view.flavor === "simple")
56
+ return `${o1} ${o3}`;
57
+ // the option-2 span: " 2 Yes, don't ask again for <name>" — the
58
+ // fixed part is 45 (the 1/3 options + the separators + the 28-cell
59
+ // prefix); the name cuts to W−46 + "…".
60
+ const name = escapeTerminal(view.name);
61
+ const budget = Math.max(1, W - 46);
62
+ const shown = visibleWidth(name) > Math.max(0, W - 45) ? `${widthCut(name, budget)}…` : name;
63
+ const o2 = sel === 2 ? `${p.bold} 2 Yes, don't ask again for ${shown}${p.reset}` : ` 2 Yes, don't ask again for ${shown}`;
64
+ return `${o1} ${o2} ${o3}`;
65
+ }
66
+ /** The block's rows — EXACTLY the preview's frame shape, the gutter at
67
+ * the left edge (the preview's two-space mock indent is its own
68
+ * styling; the real rows sit at column 1, like every tool cell).
69
+ * maxRows caps the TOTAL (the args fold; the single-row lines cut). */
70
+ export function panelBlockRows(view, phase, sel, W, maxRows) {
71
+ const p = palette();
72
+ const gutter = `${p.dim}│${p.reset} `;
73
+ const rows = [];
74
+ rows.push(`${gutter}${cutLine(panelRuleText(view), Math.max(1, W - 2))}`);
75
+ rows.push(`${gutter}${cutLine(`${p.bold}${escapeTerminal(view.title)}${p.reset}`, Math.max(1, W - 2))}`);
76
+ rows.push(`${cutLine(`${p.dim}─ the full args — never truncated ─${p.reset}`, Math.max(1, W - 2))}`);
77
+ // the args — the bounded block's body: fold, then cap. The └ cut is
78
+ // ONE row (the W20 discipline): when the args exceed the budget, one
79
+ // notice row carries the count and where the rest is (the event log).
80
+ const args = view.args.kind === "diff"
81
+ ? diffBody(view.args.diff, W, true) // the expanded path — never the tool cell's capped copy
82
+ : view.args.lines.flatMap((line) => gutterFold(`${p.dim}│${p.reset} `, escapeTerminal(line), W));
83
+ const argsBudget = Math.max(1, maxRows - 6);
84
+ let shown;
85
+ if (args.length > argsBudget) {
86
+ const kept = Math.max(0, argsBudget - 1);
87
+ const n = args.length - kept;
88
+ shown = [...args.slice(0, kept), cutLine(`${p.dim}└ +${n} more rows — the full args are in the event log${p.reset}`, Math.max(1, W - 2))];
89
+ }
90
+ else {
91
+ shown = args;
92
+ }
93
+ rows.push(...shown);
94
+ rows.push(`${gutter}${panelOptionsRow(view, sel, W)}`);
95
+ rows.push(`${gutter}${p.dim}${panelAffordance(view, phase, sel)}${p.reset}`);
96
+ rows.push(`${p.dim}└ ${p.reset}`);
97
+ return rows;
98
+ }
99
+ /** The input row's lead for the panel's phase (the preview's chrome
100
+ * rows): the digit leads bold, the rule/feedback leads dim. The rule
101
+ * lead names the tool (the option-2 prefill), the amend lead the
102
+ * denial/allowance feedback ("the words ride the verdict"). */
103
+ export function panelLead(view, phase, sel) {
104
+ const p = palette();
105
+ if (phase === "rule")
106
+ return `${p.dim}2 Yes, don't ask again for ${p.reset}`;
107
+ if (phase === "amend")
108
+ return `${p.dim}${sel === 3 ? "feedback (deny): " : "feedback (amend): "}${p.reset}`;
109
+ return `${p.bold}${view.flavor === "approval" ? "1-3> " : "1/3> "}${p.reset}`;
110
+ }
111
+ /** The lead's plain text — the editor's reflow width (the line must
112
+ * fit the lead + the box's walls). */
113
+ export function panelLeadPlain(view, phase, sel) {
114
+ if (phase === "rule")
115
+ return "2 Yes, don't ask again for ";
116
+ if (phase === "amend")
117
+ return sel === 3 ? "feedback (deny): " : "feedback (amend): ";
118
+ return view.flavor === "approval" ? "1-3> " : "1/3> ";
119
+ }
120
+ export function panelLeadWidth(view, phase, sel) {
121
+ return displayWidth(panelLeadPlain(view, phase, sel));
122
+ }
123
+ /** The status row's left text while the panel is up — the phase, not
124
+ * the CLI's painting status (the compositor derives it from the panel
125
+ * state; the "▸ run paused" etc. ride the options phase). */
126
+ export function panelStatus(view, phase, sel) {
127
+ if (phase === "rule")
128
+ return "▸ rule input";
129
+ if (phase === "amend")
130
+ return sel === 3 ? "▸ deny · the words become the tool_result" : "▸ amend · the words ride the verdict";
131
+ return view.statusText;
132
+ }
133
+ /** The status row's right-aligned hint while the panel is up — the
134
+ * phase's keys. The approval flavor gains the tab-amend path; the
135
+ * simple flavor (the trust/uncertain gates) never does. */
136
+ export function panelAffordance(view, phase, sel) {
137
+ if (phase === "rule")
138
+ return "enter commits · esc backs out";
139
+ if (phase === "amend")
140
+ return "enter sends";
141
+ if (view.flavor === "approval")
142
+ return sel === 0 ? "tab amend · esc cancel" : "enter sends · esc backs out";
143
+ return sel === 0 ? "esc cancel" : "enter sends";
144
+ }
@@ -0,0 +1,298 @@
1
+ /**
2
+ * TUI v6 (ADR-0046) — the components: EVERY screen line's renderer.
3
+ * Extracted to tui-cells (ADR-0043 Amendment 4): the cell renderer
4
+ * leaves the tui for the 9th package; the tui's shims re-export it.
5
+ *
6
+ * Each component turns one piece of state into display lines (SGR
7
+ * included, raw — the compositor writes them verbatim). The folding
8
+ * lives HERE: every line a component returns must fit the terminal
9
+ * width — the compositor's crash-on-violation invariant backs it up
10
+ * (a component that forgets to fold CRASHES with a diagnostic, never
11
+ * silently truncates — the crash is the contract, not a symptom).
12
+ *
13
+ * The fold is SGR-AWARE: a line whose bold/dim span would straddle a
14
+ * fold boundary closes the span at the break and reopens it on the
15
+ * next row — the #16b contract (no literal "[2m" fragments) survives
16
+ * folding. displayWidth/charWidth (width.ts) are the width primitives
17
+ * (untouched); render.ts supplies the original text (palette, escape,
18
+ * tint, fold wording).
19
+ */
20
+ import { foldThinking, foldResult, renderToolSummary, type ResumeMeta } from "./render.js";
21
+ /** The spinner glyphs, cycled by the compositor's on-demand tick. */
22
+ export declare const SPINNER: string[];
23
+ /** The frame context the compositor passes down — the pieces of time
24
+ * that make a live render non-deterministic (the running tool's glyph
25
+ * and elapsed). Everything else is a pure function of the cell. */
26
+ export interface FrameCtx {
27
+ readonly spinnerI: number;
28
+ readonly now: number;
29
+ /** The terminal height (rows) — the banner cell's tier input (W1:
30
+ * the tier table reads H, so a resize RE-TIERS instead of
31
+ * re-folding frozen rows). */
32
+ readonly height: number;
33
+ }
34
+ /** ONE screen line a component emits (raw, SGR included). */
35
+ export type RenderLine = string;
36
+ /**
37
+ * The fold — split a display-width line into ≤W rows, preserving SGR
38
+ * spans across the break: a span open at the break closes (reset) at
39
+ * the row's end and reopens on the next row. The rows are what the
40
+ * terminal's own soft-wrap would have produced — except the compositor
41
+ * folds FIRST, so the terminal never reflows a component's line (the
42
+ * #17 merge class cannot reach committed content).
43
+ */
44
+ export declare function foldLine(line: string, W: number): string[];
45
+ /** The visible width of a rendered line (SGR stripped — the invariant
46
+ * the compositor enforces on every emitted line). */
47
+ export declare function visibleWidth(line: string): number;
48
+ /** A component: render the display lines for one piece of state. */
49
+ export interface Component {
50
+ render(width: number, ctx: FrameCtx): string[];
51
+ }
52
+ /** The W11 spacing formula — "a row gets one blank line above it when
53
+ * the row is itself a block, or when the previous sibling was taller
54
+ * than one row". One-row siblings pack tight; anything multi-row
55
+ * breathes on both sides. The FIRST cell never gets the blank (it sits
56
+ * at the body's top — the banner would otherwise start one row down).
57
+ * `prev` is the previous sibling's OWN rows (raw — a cell's own blank
58
+ * must never count toward its height). The blank is a JOIN artifact:
59
+ * the cell's own render stays blank-free, so per-cell accounting
60
+ * (heights, the fold cache) never sees a fake row. */
61
+ export declare function bodySpacing(prev: readonly string[] | null, rows: readonly string[]): string[];
62
+ /** The container — vertical concatenation with the W11 formula. No
63
+ * component decides its own spacing: every blank in the body is the
64
+ * container's. */
65
+ export declare class Container implements Component {
66
+ private readonly children;
67
+ constructor(children: Component[]);
68
+ render(width: number, ctx: FrameCtx): string[];
69
+ }
70
+ export type BodyCell = {
71
+ kind: "user";
72
+ text: string;
73
+ done: true;
74
+ turn: number;
75
+ } | {
76
+ kind: "thinking";
77
+ text: string;
78
+ done: boolean;
79
+ turn: number;
80
+ } | {
81
+ kind: "tool";
82
+ name: string;
83
+ input: string;
84
+ /** W15: the FULL input JSON (pretty-printed) — the display
85
+ * summary above is sliced at 60 chars; the expanded block's
86
+ * "--- input ---" section mirrors /last and needs it all. */
87
+ inputFull: string;
88
+ childRoles: string[];
89
+ state: "pending" | "approval" | "running" | "done";
90
+ isError: boolean;
91
+ resultText: string;
92
+ diff: import("./diff.js").DiffLine[] | null;
93
+ added: number;
94
+ removed: number;
95
+ startedAt: number | null;
96
+ doneAt: number | null;
97
+ done: boolean;
98
+ /** W15: the live-region expand toggle — while the cell is live
99
+ * the FULL body renders in place (the compositor owns those
100
+ * rows and redraws them); a committed cell can never toggle
101
+ * (history is never rewritten — ADR-0046). */
102
+ expanded: boolean;
103
+ /** W14: the turn boundary — the index of the turn record that
104
+ * created this cell (the fold-hold's owner; −1 when no turn
105
+ * exists yet — the pre-turn cells never hold). */
106
+ turn: number;
107
+ /** W13: the rolled-up group summary — set at COMMIT time when
108
+ * the head of an N > 2 same-tool run renders the group (the
109
+ * work order's claimed shape: "✓ read 5 files (2.4k lines,
110
+ * 1.1s)" + the target children). The members carry null — the
111
+ * compositor's rolled-heads bookkeeping renders them []. */
112
+ rolled: null | {
113
+ count: number;
114
+ lines: number;
115
+ elapsed: string;
116
+ targets: string[];
117
+ };
118
+ /** W19: a DENIED call's reason (the CLI extracted it from the
119
+ * result's "[Permission denied] " prefix, keyed on the "denied"
120
+ * tag). Non-null renders the pinned row — the full call name,
121
+ * the target, the reason in the W4 parentheses idiom, NO timing
122
+ * metadata (the call never ran). */
123
+ reason: string | null;
124
+ /** A5: the approval verdict — the permission_decided event bound
125
+ * into the cell (no free-standing ` approved` orphan row). The
126
+ * settled head row aggregates name + status + decidedBy in ONE
127
+ * row: a denied call's pinned row gains `· by <decidedBy>`; an
128
+ * extension-approved call's settled row gains `· approved by
129
+ * <decidedBy>` (the human approval needs no marker — the ⏸ →
130
+ * spinner → ✓ sequence told the story). Null until a decision
131
+ * lands (the auto-allowed calls never have one). */
132
+ verdict: {
133
+ decision: "approved" | "denied";
134
+ decidedBy?: string;
135
+ reason?: string;
136
+ } | null;
137
+ } | {
138
+ kind: "text";
139
+ text: string;
140
+ done: boolean;
141
+ } | {
142
+ kind: "notice";
143
+ text: string;
144
+ done: true;
145
+ } | {
146
+ kind: "banner";
147
+ version: string;
148
+ extensionsText: string;
149
+ resume: ResumeMeta[];
150
+ done: true;
151
+ } | {
152
+ kind: "raw";
153
+ lines: string[];
154
+ done: true;
155
+ } | {
156
+ kind: "terminal";
157
+ label: string;
158
+ line: string;
159
+ done: true;
160
+ } | {
161
+ kind: "checklist";
162
+ /** the model-authored header tail (parseChecklist's count line —
163
+ * chat.ts). The compositor's fixed "task" prefix rides BEFORE it
164
+ * (W20 naming ruling: never model-controlled). */
165
+ header: string;
166
+ items: {
167
+ text: string;
168
+ status: "pending" | "active" | "done";
169
+ }[];
170
+ /** W20: false while LIVE — the current turn's ONE in-place block
171
+ * (the commit loop only takes done cells, so it stays in the
172
+ * live region); true once SETTLED — endTurn committed it as the
173
+ * turn's one recap block. */
174
+ done: boolean;
175
+ /** W20: the LIVE block's ctrl+r toggle (W15) — the capped form
176
+ * flips to the full list in place. The settled render ignores
177
+ * it (already full). */
178
+ expanded: boolean;
179
+ /** W20: the wall clock of the block's FIRST call — the settled
180
+ * header's duration is clocked from here, compositor-side (the
181
+ * CLI stays unchanged). */
182
+ startedAt: number;
183
+ /** W20: the run's duration at the settle — the `2h 14m` form. */
184
+ durationSeconds: number;
185
+ turn: number;
186
+ };
187
+ declare const TOOL_SUMMARY_MAX = 60;
188
+ /** The component for one cell — the mapping table lives here so the
189
+ * compositor stays a pure writer. */
190
+ export declare function cellComponent(cell: BodyCell): Component;
191
+ /** W22: the pending-queue chips — queued user lines pre-render above
192
+ * the input row as the SAME UserMessage chip (undimmed: reverse video
193
+ * inverts the CURRENT colours), the dim `□` gutter marking the queued
194
+ * state (the gutter rides EVERY row — the gutterFold precedent: the
195
+ * left edge alone distinguishes the states). Each chip folds at W−3
196
+ * (the gutter's 2 cells), so a long line hard-folds INSIDE the chip
197
+ * and invariant ① holds on the band. */
198
+ export declare function pendingQueueRows(lines: readonly string[], W: number): string[];
199
+ /** Fold a line's CONTENT at W−2 and prefix EVERY row with the gutter
200
+ * (W2: a wrapped tool row keeps its state mark — the left edge alone
201
+ * distinguishes the states at --plain; the UserMessage rail precedent,
202
+ * v5 #16f). The gutter carries its own SGR (e.g. the bold ✓). W21:
203
+ * exported for the approval panel's text args (the same │ gutter). */
204
+ export declare function gutterFold(gutter: string, line: string, W: number): string[];
205
+ /** A6: the tool-header variant — ONE cut row, never a fold. A wide
206
+ * header (a long target path, a wordy denial reason) used to wrap
207
+ * through foldLine — every wrapped row repeated the gutter, the
208
+ * settled row grew past its previewed height. The header names the
209
+ * call — the ellipsis marks the cut, the body below still carries the
210
+ * full content. The budget: the gutter's own visible width + the
211
+ * ellipsis ride the row (the invariant ① cap holds). */
212
+ export declare function gutterCut(gutter: string, line: string, W: number): string[];
213
+ /** W13 — the rollup opt-in table: which tools collapse, and the count
214
+ * NOUN (read_file calls → "5 files", list_dir → "5 dirs", search_text
215
+ * → "5 matches"). Only these tools opt in — a shell burst is never
216
+ * rolled up (its rows carry meaning). The folded-turn line (W14) reuses
217
+ * the plurals for its other-tool terms ("2 dirs", "1 match"). */
218
+ export declare const ROLLUP_NOUN: Readonly<Record<string, string>>;
219
+ /** W14 — the folded-turn line: a whole QUIET turn (no text), once it is
220
+ * scrollback, becomes ONE line — the work order's claimed shape
221
+ * (`▞ thought 19s · 5 reads · no edits`), the counts accumulated at
222
+ * toolStart: read_file → "reads", edit_file → "edits", the other tools
223
+ * as first-call-order terms (the ROLLUP_NOUN plurals when the tool opts
224
+ * in, the verb + "s" otherwise).
225
+ * A9 (ruling R2, mock A): the user chip rides the fold — the human's
226
+ * words LEAD the one line, `▞ <chip> · thought 19s · 5 reads · no
227
+ * edits` — the chip the SAME SGR-7 bracket as the live user row (#16f,
228
+ * side pads included). The words take the fold's width budget: the
229
+ * metadata terms survive (the W14 metadata rule — they give way LAST),
230
+ * the words width-cut at the end with the honest "…" (never a silent
231
+ * truncate — invariant ① holds on the ONE row by construction). */
232
+ export declare function turnFold(t: {
233
+ words: string;
234
+ thoughtSeconds: number;
235
+ reads: number;
236
+ edits: number;
237
+ others: [string, number][];
238
+ }, W: number): string[];
239
+ /** The approval mini-diff (W7): capped at 12 folded rows — the head +
240
+ * the named middle (the renderer cut — what was cut, how to expand) +
241
+ * the tail. The rows are folded at the current width BEFORE the cap —
242
+ * the R1 measured bug: truncateDiff capped at 40 ENTRIES while the
243
+ * fold turned them into 73 SCREEN rows at W≤80 (a 44-row terminal's
244
+ * content cap is H−4 = 40 — the approval force-committed a third of
245
+ * the screen into scrollback inside one frame).
246
+ * W17: the cap is a ROW budget at every width — the └ cut is ONE line
247
+ * (a folded cut pushed the total past 12 at narrow widths), and below
248
+ * a floor of 3 SOURCE lines visible the head/tail pair is noise (each
249
+ * fragment a sliver of a long line): drop to the head only — the head
250
+ * takes the whole budget — and the └ row carries the rest.
251
+ * W21: exported for the approval panel — the expanded path renders
252
+ * the approval's ALWAYS-verbose args (never the capped copy). */
253
+ export declare function diffBody(diff: import("./diff.js").DiffLine[] | null, W: number, expanded?: boolean): string[];
254
+ /** W20 — the task block's fixed-window height: the whole live block
255
+ * (header + rows) in POST-FOLD screen rows at EVERY width: the header,
256
+ * the active row, up to 2 pending, the overflow-pending fold, the
257
+ * done-collapse. Every live row CUTS at W (never folds) — the block's
258
+ * height is its row count. */
259
+ export declare const CAP_TASK_LIVE = 6;
260
+ /** W20 — the live block's fixed-window row cut: an SGR-aware ONE-ROW
261
+ * truncation (foldLine wraps; a wrapped row would break the height
262
+ * cap — every live row is exactly one screen row at every width).
263
+ * A line that fits (≤ W) passes through whole; an overflow cuts the
264
+ * content at W−1 — the ellipsis's slot — and the ellipsis rides AFTER
265
+ * the reset (post-reset — the PTY needles' convention). The cut row
266
+ * never exceeds W (invariant ①). W21: exported for the approval
267
+ * panel's single-row lines (the rule line, the title, the divider,
268
+ * the options/affordance rows). */
269
+ export declare function cutLine(line: string, W: number): string;
270
+ /** W20 — the settled block's duration, the `2h 14m` form (the task
271
+ * narrative's long-horizon idiom): minutes+seconds under an hour,
272
+ * hours+minutes past it. */
273
+ export declare function formatDuration(totalSeconds: number): string;
274
+ /** The status container's row: the status text (+ the tail) with the
275
+ * right-aligned "/ commands · ↑ history" hint in the idle state —
276
+ * the hint CUT FIRST when the width is short (the #16g rule); when
277
+ * the STATUS ITSELF cannot fit, it cuts with a "…" — the last resort,
278
+ * enforced by invariant ① (the old code let the status soft-wrap).
279
+ * W21: the question param is gone — the old question slot retires; a
280
+ * pending approval's status IS the panel's (the compositor derives
281
+ * it from the bound panel state). */
282
+ export declare function statusLine(status: string, tail: string, W: number, hint?: string): string;
283
+ /** The display-width prefix of a plain (SGR-free) text. W21: exported
284
+ * for the approval panel's option-2 rule-name cut. */
285
+ export declare function widthCut(text: string, max: number): string;
286
+ /** W6 — the box: the chrome's top rail. The two ╌ dotted rows become
287
+ * a rounded box (the box already says "input lives here"); the rails
288
+ * stay dim, the width is still the full W (the box is a rail with
289
+ * corners — the menu/gap rows above and the status below are
290
+ * untouched). */
291
+ export declare function boxTop(W: number): string;
292
+ /** W6 — the box: the chrome's bottom rail. */
293
+ export declare function boxBottom(W: number): string;
294
+ /** The terminal label + rhythm gap (the pipe path's v2c bytes — the
295
+ * exact render the passthrough needs). */
296
+ export declare function terminalPipe(label: string, statusLineText: string): string;
297
+ /** The pipe-path pieces the passthrough reuses (byte-identical). */
298
+ export { foldThinking, foldResult, renderToolSummary, TOOL_SUMMARY_MAX };