@vincemakes/kiso-tui-cells 0.11.0 → 0.13.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 +205 -16
- package/dist/approval-panel.js +304 -75
- package/dist/components.d.ts +28 -0
- package/dist/components.js +32 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/strings.d.ts +22 -22
- package/dist/strings.js +26 -24
- package/package.json +1 -1
package/dist/approval-panel.d.ts
CHANGED
|
@@ -25,8 +25,136 @@
|
|
|
25
25
|
* FOLD (a bounded block's body — content folds, metadata cuts).
|
|
26
26
|
*/
|
|
27
27
|
export type PanelFlavor = "approval" | "simple";
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
/**
|
|
29
|
+
* TUI2-R3v2 ① — two phases, not three.
|
|
30
|
+
*
|
|
31
|
+
* The "rule" phase is gone. It existed because option 2 used to hand the
|
|
32
|
+
* human a prefilled text box to edit the rule in, which implied the rule
|
|
33
|
+
* could be anything they typed. It could not: the generated extension
|
|
34
|
+
* matches on `call.name` and nothing else, so every character typed
|
|
35
|
+
* beyond the tool name either did nothing or silently produced a rule
|
|
36
|
+
* that never fired. Option 2 now grants exactly what the machinery
|
|
37
|
+
* supports, on the keypress, and the copy says exactly that.
|
|
38
|
+
*/
|
|
39
|
+
export type PanelPhase = "options" | "amend" | "asking" | "safer";
|
|
40
|
+
/**
|
|
41
|
+
* TUI2-R3v2 ③ — one safer alternative the model proposed.
|
|
42
|
+
*
|
|
43
|
+
* Two fields and no more. The command is what would actually run, so it
|
|
44
|
+
* is the row's subject; `why` is the one-line plain-language reason,
|
|
45
|
+
* because a list of three shell commands with no explanation asks the
|
|
46
|
+
* human to diff them in their head — which is the work the feature
|
|
47
|
+
* exists to remove.
|
|
48
|
+
*/
|
|
49
|
+
export interface SaferOption {
|
|
50
|
+
readonly command: string;
|
|
51
|
+
readonly why: string;
|
|
52
|
+
}
|
|
53
|
+
/** TUI2-R3v2 ③: the safer list's own walk — the options the model gave
|
|
54
|
+
* and the bar's place in them. The LAST row (the way back) is not an
|
|
55
|
+
* option and is not in this array; it is rendered after them and its
|
|
56
|
+
* index is `options.length`. */
|
|
57
|
+
export interface SaferRuntime {
|
|
58
|
+
readonly options: readonly SaferOption[];
|
|
59
|
+
readonly cursor: number;
|
|
60
|
+
}
|
|
61
|
+
/** The copy a failed ask owes the human. One line, dim, and it says what
|
|
62
|
+
* is still true rather than what went wrong: the original choices are
|
|
63
|
+
* all still there, which is the only thing they need to know to keep
|
|
64
|
+
* going. */
|
|
65
|
+
export declare const SAFER_DEGRADED = "couldn't get safer options \u2014 the original choices stand";
|
|
66
|
+
/**
|
|
67
|
+
* R3v2-F1 — the same sentence, for the one failure the reply's own text
|
|
68
|
+
* can PROVE.
|
|
69
|
+
*
|
|
70
|
+
* The unqualified line is true of every failure the ask has, which is
|
|
71
|
+
* exactly why it explains nothing when the cause was knowable. A reply
|
|
72
|
+
* the token budget cut in half is knowable: the JSON opens and never
|
|
73
|
+
* closes. So that case gets the cause in a parenthesis and keeps
|
|
74
|
+
* everything else — one line, dim, still leading with what is still
|
|
75
|
+
* true — because the human is mid-approval and the shape of the sentence
|
|
76
|
+
* is what they have already learned to read.
|
|
77
|
+
*/
|
|
78
|
+
export declare const SAFER_DEGRADED_TRUNCATED = "couldn't get safer options (the reply was cut short) \u2014 the original choices stand";
|
|
79
|
+
/**
|
|
80
|
+
* R3v2-F1 — a failure that knows why it failed.
|
|
81
|
+
*
|
|
82
|
+
* `truncated` is the only cause the caller can demonstrate from the
|
|
83
|
+
* text, and it is deliberately the only member: a diagnosis the product
|
|
84
|
+
* cannot prove is worse than no diagnosis, so every other failure stays
|
|
85
|
+
* the unqualified line rather than growing a guess.
|
|
86
|
+
*/
|
|
87
|
+
export interface SaferFailure {
|
|
88
|
+
readonly reason: "truncated";
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* R3v2-F1 — what the safer-options provider hands back.
|
|
92
|
+
*
|
|
93
|
+
* A list is the answer. `null` is a failure with nothing to add, and is
|
|
94
|
+
* still the whole contract for any caller that has nothing to add — the
|
|
95
|
+
* widening is additive, so an existing provider's behaviour is
|
|
96
|
+
* byte-identical. A SaferFailure is a failure that can name its cause.
|
|
97
|
+
*/
|
|
98
|
+
export type SaferAnswer = readonly SaferOption[] | SaferFailure | null;
|
|
99
|
+
/** R3v2-F1: the line a failed ask owes the human — the unqualified one,
|
|
100
|
+
* unless the answer named a cause. The panel asks this instead of
|
|
101
|
+
* reaching for a constant, so the choice of sentence lives with the
|
|
102
|
+
* sentences. */
|
|
103
|
+
export declare function saferDegradedNote(answer: SaferAnswer): string;
|
|
104
|
+
/** The row that returns to state 1. Rendered last, always present — an
|
|
105
|
+
* alternatives list you cannot back out of would be a trap. */
|
|
106
|
+
export declare const SAFER_BACK = "back to the original choices";
|
|
107
|
+
/** What an option DOES — the verdict channel it commits to. The label is
|
|
108
|
+
* what the human reads; the kind is what the editor routes on, so the
|
|
109
|
+
* copy can change without touching a single branch. */
|
|
110
|
+
export type PanelOptionKind = "allow" | "rule" | "safer" | "deny";
|
|
111
|
+
export interface PanelOption {
|
|
112
|
+
readonly kind: PanelOptionKind;
|
|
113
|
+
readonly label: string;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* The options a view offers, IN ROW ORDER — and the order is the whole
|
|
117
|
+
* contract: the index is the digit, the digit is the row, and the row is
|
|
118
|
+
* what a mouse click lands on. One list, read by the renderer, the key
|
|
119
|
+
* router and the hit-test, so those three can never disagree about what
|
|
120
|
+
* option 3 is.
|
|
121
|
+
*
|
|
122
|
+
* The approval flavor's copy is the v4 frames', with ONE correction.
|
|
123
|
+
* The frame said "don't ask again for <tool> this session"; the rule
|
|
124
|
+
* machinery (addDontAskAgainRule) writes a generated extension file that
|
|
125
|
+
* outlives the process and matches on the TOOL NAME. "This session"
|
|
126
|
+
* would have understated a durable grant — the one direction a
|
|
127
|
+
* permission prompt must never be wrong in — so the scope claim is
|
|
128
|
+
* dropped rather than invented. The revocation path is the file itself,
|
|
129
|
+
* which the generated header documents.
|
|
130
|
+
*/
|
|
131
|
+
export declare function panelOptions(view: PanelView): readonly PanelOption[];
|
|
132
|
+
/**
|
|
133
|
+
* TUI2-R3v2 ④ — the deletion-risk hint: four patterns, and nothing else.
|
|
134
|
+
*
|
|
135
|
+
* The owner's ruling narrowed this to commands where UNDO DOES NOT
|
|
136
|
+
* EXIST. That is the whole selection criterion, and it is what makes the
|
|
137
|
+
* line worth reading: a warning on every dangerous command teaches the
|
|
138
|
+
* eye to skip warnings, and the eye is the only thing standing between
|
|
139
|
+
* the human and the side effect.
|
|
140
|
+
*
|
|
141
|
+
* So `dd if=/dev/zero of=/dev/sda` gets nothing. It is more destructive
|
|
142
|
+
* than anything in this table and it is not in it, because the moment
|
|
143
|
+
* the rules start guessing they start being wrong in both directions —
|
|
144
|
+
* missing the real ones and crying wolf on `git checkout main`. Four
|
|
145
|
+
* shapes, matched exactly, no inference.
|
|
146
|
+
*
|
|
147
|
+
* The rm case NAMES ITS TARGETS. "This deletes files" is a sentence
|
|
148
|
+
* about the command's category; "(node_modules, dist)" is the thing the
|
|
149
|
+
* human is actually deciding about, and it is the difference between a
|
|
150
|
+
* hint and a label.
|
|
151
|
+
*
|
|
152
|
+
* Local string rules: zero requests, zero rent, and it never blocks —
|
|
153
|
+
* the hint is a sentence beside the command, never a gate in front of
|
|
154
|
+
* it. The mode moat and the safe-defaults moat are the teeth; this is
|
|
155
|
+
* the eyes.
|
|
156
|
+
*/
|
|
157
|
+
export declare function deletionRiskHint(command: string): string | null;
|
|
30
158
|
/** One option of a question: the label the human picks, plus an
|
|
31
159
|
* optional one-line description (the model's own words). */
|
|
32
160
|
export interface AskOption {
|
|
@@ -144,6 +272,23 @@ export interface PanelView {
|
|
|
144
272
|
/** The fallback question — the y/n text for the dock-less path
|
|
145
273
|
* (a TTY without a dock, or a pipe). */
|
|
146
274
|
readonly fallbackQuestion: string;
|
|
275
|
+
/** TUI2-R3v2 ③: this call is the model's answer to a refusal — the v4
|
|
276
|
+
* frame's "(amended)" marker. It says WHY the call looks different
|
|
277
|
+
* from the one just refused; without it a second approval for the same
|
|
278
|
+
* tool reads as the product asking twice. */
|
|
279
|
+
readonly amended?: boolean;
|
|
280
|
+
/** TUI2-R3v2 ④: the deletion-risk line, when the command matches one
|
|
281
|
+
* of the four irreversible patterns. Composed by the CLI (which owns
|
|
282
|
+
* the tool input) from deletionRiskHint; absent for every other
|
|
283
|
+
* command, which is most of them. */
|
|
284
|
+
readonly riskHint?: string;
|
|
285
|
+
/** TUI2-R3v2 ①: the SIMPLE flavor's two labels. A trust gate answers
|
|
286
|
+
* "Yes / No", but an uncertain execution answers "rerun / abandon"
|
|
287
|
+
* and an unanswered ask "re-ask / drop" — those callers used to
|
|
288
|
+
* smuggle their labels into the rule line as "— 1 rerun · 3 abandon",
|
|
289
|
+
* which stated the digits as well, and the digits have moved. The
|
|
290
|
+
* labels belong on the rows that carry them. */
|
|
291
|
+
readonly simpleOptions?: readonly [string, string];
|
|
147
292
|
/** KC3.5: the questions, when this view is an ASK. Present = the
|
|
148
293
|
* panel renders the ask block and the editor routes the ask keys;
|
|
149
294
|
* absent = the approval/simple panel, unchanged. */
|
|
@@ -184,34 +329,78 @@ export type PanelVerdict = {
|
|
|
184
329
|
export interface PanelState {
|
|
185
330
|
readonly view: PanelView;
|
|
186
331
|
readonly phase: PanelPhase;
|
|
187
|
-
|
|
332
|
+
/** TUI2-R3v2 ①: the highlighted row, 0-based into panelOptions(view).
|
|
333
|
+
* There is no "nothing selected" value any more — the bar opens on
|
|
334
|
+
* the first option, which is what makes a bare ⏎ an approval. */
|
|
335
|
+
readonly cursor: number;
|
|
336
|
+
/** TUI2-R3v2 ①: the one dim line a failed gesture owes the human (the
|
|
337
|
+
* safer-options degradation). Absent when there is nothing to say. */
|
|
338
|
+
readonly note?: string;
|
|
339
|
+
/** TUI2-R3v2 ③: the safer list's walk — present exactly in the
|
|
340
|
+
* "safer" phase. */
|
|
341
|
+
readonly safer?: SaferRuntime;
|
|
188
342
|
/** KC3.5: the ask's walk — present exactly when `view.ask` is. */
|
|
189
343
|
readonly ask?: AskRuntime;
|
|
190
344
|
/** TUI2-R2 \u2463: the pick's walk — present exactly when `view.pick` is. */
|
|
191
345
|
readonly pick?: PickRuntime;
|
|
192
346
|
}
|
|
347
|
+
/**
|
|
348
|
+
* TUI2-R3v2 ② — the block's rows AND where its option rows landed.
|
|
349
|
+
*
|
|
350
|
+
* The click hit-test needs to answer "which option is at screen row N",
|
|
351
|
+
* and the only honest source for that is the arithmetic that placed the
|
|
352
|
+
* rows. Computing it a second time — in the compositor, or in a helper
|
|
353
|
+
* that mirrors the budget — is how a hit-test comes to disagree with the
|
|
354
|
+
* picture: the args cap, the note row and the option window all move the
|
|
355
|
+
* list, and a mirror that misses one sends the click to the wrong
|
|
356
|
+
* verdict. So the renderer reports it, and there is exactly one copy of
|
|
357
|
+
* the sum.
|
|
358
|
+
*
|
|
359
|
+
* `offset` is the index of the first option row INSIDE the returned
|
|
360
|
+
* rows; `first` is which option that row shows (the window's start, non-
|
|
361
|
+
* zero only on a short block).
|
|
362
|
+
*/
|
|
363
|
+
export interface PanelBlockLayout {
|
|
364
|
+
readonly rows: readonly string[];
|
|
365
|
+
readonly offset: number;
|
|
366
|
+
readonly count: number;
|
|
367
|
+
readonly first: number;
|
|
368
|
+
}
|
|
193
369
|
/** The block's rows — EXACTLY the preview's frame shape, the gutter at
|
|
194
370
|
* the left edge (the preview's two-space mock indent is its own
|
|
195
371
|
* styling; the real rows sit at column 1, like every tool cell).
|
|
196
372
|
* maxRows caps the TOTAL (the args fold; the single-row lines cut). */
|
|
197
|
-
export declare function panelBlockRows(view: PanelView, phase: PanelPhase,
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
|
|
373
|
+
export declare function panelBlockRows(view: PanelView, phase: PanelPhase, cursor: number, W: number, maxRows: number, note?: string, safer?: SaferRuntime): string[];
|
|
374
|
+
export declare function panelBlockLayout(view: PanelView, phase: PanelPhase, cursor: number, W: number, maxRows: number, note?: string, safer?: SaferRuntime): PanelBlockLayout;
|
|
375
|
+
/**
|
|
376
|
+
* The input row's lead. In the options phase there is NOTHING to type,
|
|
377
|
+
* so the lead stops pretending there is.
|
|
378
|
+
*
|
|
379
|
+
* "1-3> " was a prompt: it told the human to enter something and press
|
|
380
|
+
* return, which is exactly the interaction this round removed. The row
|
|
381
|
+
* keeps the composer's own quiet lead while the list is up (the keys are
|
|
382
|
+
* on the list and in the hint line), and the typed phase — the one place
|
|
383
|
+
* a human really is writing — leads with the word for what they are
|
|
384
|
+
* writing.
|
|
385
|
+
*/
|
|
386
|
+
export declare function panelLead(view: PanelView, phase: PanelPhase, cursor: number): string;
|
|
203
387
|
/** The lead's plain text — the editor's reflow width (the line must
|
|
204
388
|
* fit the lead + the box's walls). */
|
|
205
|
-
export declare function panelLeadPlain(view: PanelView, phase: PanelPhase,
|
|
206
|
-
export declare function panelLeadWidth(view: PanelView, phase: PanelPhase,
|
|
389
|
+
export declare function panelLeadPlain(view: PanelView, phase: PanelPhase, cursor: number): string;
|
|
390
|
+
export declare function panelLeadWidth(view: PanelView, phase: PanelPhase, cursor: number): number;
|
|
207
391
|
/** The status row's left text while the panel is up — the phase, not
|
|
208
392
|
* the CLI's painting status (the compositor derives it from the panel
|
|
209
393
|
* state; the "▸ run paused" etc. ride the options phase). */
|
|
210
|
-
export declare function panelStatus(view: PanelView, phase: PanelPhase,
|
|
211
|
-
/**
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
|
|
394
|
+
export declare function panelStatus(view: PanelView, phase: PanelPhase, cursor: number): string;
|
|
395
|
+
/**
|
|
396
|
+
* The status row's right-aligned hint — the v4 frame's line, verbatim.
|
|
397
|
+
*
|
|
398
|
+
* It names all four gestures because all four now exist at once and the
|
|
399
|
+
* digit range is the only part that varies: "1-4" on an approval, "1-2"
|
|
400
|
+
* on the simple flavors. The click is advertised for the same reason the
|
|
401
|
+
* arrows are — an affordance nobody is told about is one nobody uses.
|
|
402
|
+
*/
|
|
403
|
+
export declare function panelAffordance(view: PanelView, phase: PanelPhase, cursor: number, safer?: SaferRuntime): string;
|
|
215
404
|
/**
|
|
216
405
|
* The pick block's rows — the prototype's C frame.
|
|
217
406
|
*
|
package/dist/approval-panel.js
CHANGED
|
@@ -25,11 +25,145 @@
|
|
|
25
25
|
* FOLD (a bounded block's body — content folds, metadata cuts).
|
|
26
26
|
*/
|
|
27
27
|
import { displayWidth } from "./width.js";
|
|
28
|
-
import { cutLine, diffBody, gutterFold, visibleWidth, widthCut } from "./components.js";
|
|
28
|
+
import { cutLine, diffBody, gutterFold, selectionBar, visibleWidth, widthCut } from "./components.js";
|
|
29
29
|
// TUI2-R2pre ④: strings.js takes only a TYPE from this module, so the
|
|
30
30
|
// import is erased at compile time and no runtime cycle exists.
|
|
31
31
|
import { displayVerb } from "./strings.js";
|
|
32
32
|
import { escapeTerminal, palette } from "./render.js";
|
|
33
|
+
/** The copy a failed ask owes the human. One line, dim, and it says what
|
|
34
|
+
* is still true rather than what went wrong: the original choices are
|
|
35
|
+
* all still there, which is the only thing they need to know to keep
|
|
36
|
+
* going. */
|
|
37
|
+
export const SAFER_DEGRADED = "couldn't get safer options — the original choices stand";
|
|
38
|
+
/**
|
|
39
|
+
* R3v2-F1 — the same sentence, for the one failure the reply's own text
|
|
40
|
+
* can PROVE.
|
|
41
|
+
*
|
|
42
|
+
* The unqualified line is true of every failure the ask has, which is
|
|
43
|
+
* exactly why it explains nothing when the cause was knowable. A reply
|
|
44
|
+
* the token budget cut in half is knowable: the JSON opens and never
|
|
45
|
+
* closes. So that case gets the cause in a parenthesis and keeps
|
|
46
|
+
* everything else — one line, dim, still leading with what is still
|
|
47
|
+
* true — because the human is mid-approval and the shape of the sentence
|
|
48
|
+
* is what they have already learned to read.
|
|
49
|
+
*/
|
|
50
|
+
export const SAFER_DEGRADED_TRUNCATED = "couldn't get safer options (the reply was cut short) — the original choices stand";
|
|
51
|
+
/** R3v2-F1: the line a failed ask owes the human — the unqualified one,
|
|
52
|
+
* unless the answer named a cause. The panel asks this instead of
|
|
53
|
+
* reaching for a constant, so the choice of sentence lives with the
|
|
54
|
+
* sentences. */
|
|
55
|
+
export function saferDegradedNote(answer) {
|
|
56
|
+
const failure = answer === null || Array.isArray(answer) ? null : answer;
|
|
57
|
+
return failure?.reason === "truncated" ? SAFER_DEGRADED_TRUNCATED : SAFER_DEGRADED;
|
|
58
|
+
}
|
|
59
|
+
/** The row that returns to state 1. Rendered last, always present — an
|
|
60
|
+
* alternatives list you cannot back out of would be a trap. */
|
|
61
|
+
export const SAFER_BACK = "back to the original choices";
|
|
62
|
+
/**
|
|
63
|
+
* The options a view offers, IN ROW ORDER — and the order is the whole
|
|
64
|
+
* contract: the index is the digit, the digit is the row, and the row is
|
|
65
|
+
* what a mouse click lands on. One list, read by the renderer, the key
|
|
66
|
+
* router and the hit-test, so those three can never disagree about what
|
|
67
|
+
* option 3 is.
|
|
68
|
+
*
|
|
69
|
+
* The approval flavor's copy is the v4 frames', with ONE correction.
|
|
70
|
+
* The frame said "don't ask again for <tool> this session"; the rule
|
|
71
|
+
* machinery (addDontAskAgainRule) writes a generated extension file that
|
|
72
|
+
* outlives the process and matches on the TOOL NAME. "This session"
|
|
73
|
+
* would have understated a durable grant — the one direction a
|
|
74
|
+
* permission prompt must never be wrong in — so the scope claim is
|
|
75
|
+
* dropped rather than invented. The revocation path is the file itself,
|
|
76
|
+
* which the generated header documents.
|
|
77
|
+
*/
|
|
78
|
+
export function panelOptions(view) {
|
|
79
|
+
if (view.flavor === "simple") {
|
|
80
|
+
const [yes, no] = view.simpleOptions ?? ["Yes", "No"];
|
|
81
|
+
return [
|
|
82
|
+
{ kind: "allow", label: yes },
|
|
83
|
+
{ kind: "deny", label: no },
|
|
84
|
+
];
|
|
85
|
+
}
|
|
86
|
+
return [
|
|
87
|
+
{ kind: "allow", label: "Yes, run it" },
|
|
88
|
+
{ kind: "rule", label: `Yes, and don't ask again for ${displayVerb(view.name)}` },
|
|
89
|
+
{ kind: "safer", label: "Show me safer ways to do this" },
|
|
90
|
+
{ kind: "deny", label: "No — let me tell it what to do instead" },
|
|
91
|
+
];
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* TUI2-R3v2 ④ — the deletion-risk hint: four patterns, and nothing else.
|
|
95
|
+
*
|
|
96
|
+
* The owner's ruling narrowed this to commands where UNDO DOES NOT
|
|
97
|
+
* EXIST. That is the whole selection criterion, and it is what makes the
|
|
98
|
+
* line worth reading: a warning on every dangerous command teaches the
|
|
99
|
+
* eye to skip warnings, and the eye is the only thing standing between
|
|
100
|
+
* the human and the side effect.
|
|
101
|
+
*
|
|
102
|
+
* So `dd if=/dev/zero of=/dev/sda` gets nothing. It is more destructive
|
|
103
|
+
* than anything in this table and it is not in it, because the moment
|
|
104
|
+
* the rules start guessing they start being wrong in both directions —
|
|
105
|
+
* missing the real ones and crying wolf on `git checkout main`. Four
|
|
106
|
+
* shapes, matched exactly, no inference.
|
|
107
|
+
*
|
|
108
|
+
* The rm case NAMES ITS TARGETS. "This deletes files" is a sentence
|
|
109
|
+
* about the command's category; "(node_modules, dist)" is the thing the
|
|
110
|
+
* human is actually deciding about, and it is the difference between a
|
|
111
|
+
* hint and a label.
|
|
112
|
+
*
|
|
113
|
+
* Local string rules: zero requests, zero rent, and it never blocks —
|
|
114
|
+
* the hint is a sentence beside the command, never a gate in front of
|
|
115
|
+
* it. The mode moat and the safe-defaults moat are the teeth; this is
|
|
116
|
+
* the eyes.
|
|
117
|
+
*/
|
|
118
|
+
export function deletionRiskHint(command) {
|
|
119
|
+
// a compound command's risk can be its SECOND half ("npm run clean &&
|
|
120
|
+
// git clean -fd"), so the segments are scanned in order and the FIRST
|
|
121
|
+
// match wins: one line, never a stack of them.
|
|
122
|
+
for (const raw of command.split(/&&|\|\||[;|]/)) {
|
|
123
|
+
const segment = raw.trim();
|
|
124
|
+
if (segment === "")
|
|
125
|
+
continue;
|
|
126
|
+
const hint = segmentRisk(segment);
|
|
127
|
+
if (hint !== null)
|
|
128
|
+
return hint;
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
function segmentRisk(segment) {
|
|
133
|
+
const words = segment.split(/\s+/);
|
|
134
|
+
const verb = words[0];
|
|
135
|
+
if (verb === "rm") {
|
|
136
|
+
// -rf in any spelling or order (-rf, -fr, -r -f), because the shell
|
|
137
|
+
// accepts all of them and the human meant the same thing by each.
|
|
138
|
+
const flags = words.slice(1).filter((w) => /^-[a-zA-Z]+$/.test(w));
|
|
139
|
+
const letters = flags.join("");
|
|
140
|
+
if (!letters.includes("r") || !letters.includes("f"))
|
|
141
|
+
return null;
|
|
142
|
+
const targets = words.slice(1).filter((w) => !/^-/.test(w));
|
|
143
|
+
return targets.length === 0 ? "⚠ deletes files permanently" : `⚠ deletes files permanently (${targets.join(", ")})`;
|
|
144
|
+
}
|
|
145
|
+
if (verb !== "git")
|
|
146
|
+
return null;
|
|
147
|
+
const sub = words[1];
|
|
148
|
+
// `git checkout -- <paths>` discards; `git checkout <branch>` does not,
|
|
149
|
+
// and conflating them would put a red line on the most ordinary command
|
|
150
|
+
// in the product.
|
|
151
|
+
if (sub === "checkout" && words.includes("--"))
|
|
152
|
+
return "⚠ discards your uncommitted changes — unrecoverable";
|
|
153
|
+
if (sub === "reset" && words.includes("--hard"))
|
|
154
|
+
return "⚠ throws away commits and working changes";
|
|
155
|
+
// `git clean -n` is a DRY RUN and is the reason this checks for the f
|
|
156
|
+
// rather than for the command.
|
|
157
|
+
if (sub === "clean") {
|
|
158
|
+
const letters = words
|
|
159
|
+
.slice(2)
|
|
160
|
+
.filter((w) => /^-[a-zA-Z]+$/.test(w))
|
|
161
|
+
.join("");
|
|
162
|
+
if (letters.includes("f"))
|
|
163
|
+
return "⚠ deletes untracked files permanently";
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
33
167
|
/** The rule line's text — the why-asked line (the R3 chain): the tool
|
|
34
168
|
* name, the first non-abstain speaker, the fix hint (the §3.5 table,
|
|
35
169
|
* code-accented). The simple flavor carries the CLI's own question
|
|
@@ -44,53 +178,55 @@ function panelRuleText(view) {
|
|
|
44
178
|
// ("edit needs approval"). view.name keeps the RAW tool name, which is
|
|
45
179
|
// what the option-2 rule prefill and the fallbackQuestion (the
|
|
46
180
|
// dock-less/pipe path — byte-identical by ruling) still read.
|
|
47
|
-
|
|
181
|
+
// TUI2-R3v2 ③: the marker is SPLICED, and the un-amended line's bytes
|
|
182
|
+
// are left exactly as they were.
|
|
183
|
+
//
|
|
184
|
+
// The first version composed one template for both cases, closing and
|
|
185
|
+
// reopening the dim run around the marker slot. That is invisible on
|
|
186
|
+
// screen and it broke the RAW BYTE run "needs approval — asked by",
|
|
187
|
+
// which four PTY gates use as a frame needle — the driver matches on
|
|
188
|
+
// the byte stream, so the needle stopped matching, the approval was
|
|
189
|
+
// never answered, and the panel hung. An ordinary approval must be
|
|
190
|
+
// byte-identical to what it was; only the amended one differs.
|
|
191
|
+
const head = `${p.bold}${escapeTerminal(displayVerb(view.name))}${p.reset} `;
|
|
192
|
+
const tail = ` ${p.bold}${escapeTerminal(view.speaker)}${p.reset}`;
|
|
193
|
+
const base = view.amended === true
|
|
194
|
+
? `${head}${p.dim}needs approval · (amended) — asked by${p.reset}${tail}`
|
|
195
|
+
: `${head}${p.dim}needs approval — asked by${p.reset}${tail}`;
|
|
48
196
|
return hint ? `${base}${p.dim} ·${p.reset} ${p.code}${escapeTerminal(hint)}${p.reset}` : base;
|
|
49
197
|
}
|
|
50
|
-
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
|
|
198
|
+
/**
|
|
199
|
+
* TUI2-R3v2 ① — ONE ROW PER OPTION, and the cursor's row is a bar.
|
|
200
|
+
*
|
|
201
|
+
* The retired form packed every option onto one line and, below W=47,
|
|
202
|
+
* DROPPED the middle one to make the line fit — a narrow terminal
|
|
203
|
+
* silently lost the ability to grant a durable rule. A list has no such
|
|
204
|
+
* trade to make: each option owns a row, a narrow window cuts LABELS,
|
|
205
|
+
* and every choice stays reachable at every width the product survives.
|
|
206
|
+
*
|
|
207
|
+
* The unselected row carries the block's gutter and a two-space indent;
|
|
208
|
+
* the selected row is the shared selectionBar, which spends its own two
|
|
209
|
+
* cells of frame. Both build their span against W−2, so the digit column
|
|
210
|
+
* does not shift as the bar walks — a column that moves per row reads as
|
|
211
|
+
* damage, which is the R2 picker's finding, inherited.
|
|
212
|
+
*/
|
|
213
|
+
function panelOptionRow(option, n, selected, W) {
|
|
59
214
|
const p = palette();
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if (view.flavor === "simple")
|
|
67
|
-
return `${o1} · ${o3}`;
|
|
68
|
-
// the option-2 span: " 2 Yes, don't ask again for <name>" — the
|
|
69
|
-
// fixed part is 45 (the gutter + the 1/3 options + the separators +
|
|
70
|
-
// the 28-cell prefix); the name cuts to W−46 + "…". The "…" needs
|
|
71
|
-
// its own cell, so the span fits only when W − 46 ≥ 1; below that
|
|
72
|
-
// (W < 47 — incl. the 0.1.42 release-smoke's 40-col winch) the span
|
|
73
|
-
// DROPS: the rule name is the cuttable span, the 1/3 options are
|
|
74
|
-
// the semantics — the approval decision must survive a narrow
|
|
75
|
-
// winch, and invariant ① must never fire on the options row.
|
|
76
|
-
// TUI2-R1.5 ⑪: option 2 states what it DOES; the tool it would do it
|
|
77
|
-
// for is the panel's title, one row above, and repeating it here was
|
|
78
|
-
// what made this row the widest thing in the block. The fixed part is
|
|
79
|
-
// now 33 cells, so the whole row survives far narrower windows than the
|
|
80
|
-
// 47 the rule name used to demand.
|
|
81
|
-
const o2 = sel === 2 ? `${p.bold}2 Yes, don't ask again${p.reset}` : `2 Yes, don't ask again`;
|
|
82
|
-
const full = `${o1} · ${o2} · ${o3}`;
|
|
83
|
-
if (visibleWidth(full) <= W - 2)
|
|
84
|
-
return full;
|
|
85
|
-
// too narrow for the middle option: the 1/3 decision is the semantics
|
|
86
|
-
// and must survive any winch (invariant ① never fires on this row).
|
|
87
|
-
return cutLine(`${o1} · ${o3}`, Math.max(1, W - 2));
|
|
215
|
+
const room = Math.max(1, W - 2);
|
|
216
|
+
const plain = ` ${n} ${option.label}`;
|
|
217
|
+
const text = cutLine(`${selected ? p.bold : ""}${escapeTerminal(plain)}${p.reset}`, room);
|
|
218
|
+
if (!selected)
|
|
219
|
+
return `${p.dim}│${p.reset} ${text}`;
|
|
220
|
+
return selectionBar(text, visibleWidth(text), W);
|
|
88
221
|
}
|
|
89
222
|
/** The block's rows — EXACTLY the preview's frame shape, the gutter at
|
|
90
223
|
* the left edge (the preview's two-space mock indent is its own
|
|
91
224
|
* styling; the real rows sit at column 1, like every tool cell).
|
|
92
225
|
* maxRows caps the TOTAL (the args fold; the single-row lines cut). */
|
|
93
|
-
export function panelBlockRows(view, phase,
|
|
226
|
+
export function panelBlockRows(view, phase, cursor, W, maxRows, note, safer) {
|
|
227
|
+
return panelBlockLayout(view, phase, cursor, W, maxRows, note, safer).rows;
|
|
228
|
+
}
|
|
229
|
+
export function panelBlockLayout(view, phase, cursor, W, maxRows, note, safer) {
|
|
94
230
|
const p = palette();
|
|
95
231
|
const gutter = `${p.dim}│${p.reset} `;
|
|
96
232
|
const rows = [];
|
|
@@ -107,7 +243,23 @@ export function panelBlockRows(view, phase, sel, W, maxRows) {
|
|
|
107
243
|
const args = view.args.kind === "diff"
|
|
108
244
|
? diffBody(view.args.diff, W, true) // the expanded path — never the tool cell's capped copy
|
|
109
245
|
: view.args.lines.flatMap((line) => gutterFold(`${p.dim}│${p.reset} `, escapeTerminal(line), W));
|
|
110
|
-
|
|
246
|
+
// TUI2-R3v2 ①: the block now spends N rows on options instead of one,
|
|
247
|
+
// so the args and the list SHARE what is left after the chrome (the
|
|
248
|
+
// rule, the title, the divider, the affordance, the corner — five
|
|
249
|
+
// rows, plus the note when there is one). The list wins the tie: a
|
|
250
|
+
// human at an approval is choosing, and one more line of a command
|
|
251
|
+
// they can also read in the event log is worth less than the row that
|
|
252
|
+
// carries the choice. The args keep a floor of one row so the block
|
|
253
|
+
// never claims to show what it is asking about and then shows nothing.
|
|
254
|
+
const chrome = 5 +
|
|
255
|
+
(phase === "options" && note !== undefined ? 1 : 0) +
|
|
256
|
+
(view.riskHint !== undefined && view.riskHint !== "" ? 1 : 0) +
|
|
257
|
+
(phase === "asking" ? 1 : 0) +
|
|
258
|
+
// the safer list's rows + its way-back row
|
|
259
|
+
(phase === "safer" && safer !== undefined ? safer.options.length + 1 : 0);
|
|
260
|
+
const optionCount = phase === "options" ? panelOptions(view).length : 0;
|
|
261
|
+
const optionsShown = Math.min(optionCount, Math.max(1, maxRows - chrome - 1));
|
|
262
|
+
const argsBudget = Math.max(1, maxRows - chrome - optionsShown);
|
|
111
263
|
let shown;
|
|
112
264
|
if (args.length > argsBudget) {
|
|
113
265
|
const kept = Math.max(0, argsBudget - 1);
|
|
@@ -118,8 +270,63 @@ export function panelBlockRows(view, phase, sel, W, maxRows) {
|
|
|
118
270
|
shown = args;
|
|
119
271
|
}
|
|
120
272
|
rows.push(...shown);
|
|
121
|
-
|
|
122
|
-
|
|
273
|
+
// TUI2-R3v2 ④: the risk hint sits directly under the args, because it
|
|
274
|
+
// is a sentence ABOUT those args — the v4 frame's placement. The warn
|
|
275
|
+
// tint is the palette's existing functional yellow (no new colour),
|
|
276
|
+
// and under NO_COLOR the ⚠ still carries it.
|
|
277
|
+
const risk = view.riskHint;
|
|
278
|
+
if (risk !== undefined && risk !== "")
|
|
279
|
+
rows.push(`${gutter}${cutLine(`${p.warn}${escapeTerminal(risk)}${p.reset}`, Math.max(1, W - 2))}`);
|
|
280
|
+
// TUI2-R3v2 ①: the option LIST. While the typed phase is open the list
|
|
281
|
+
// stands down — the human is writing prose to the model, and a bar
|
|
282
|
+
// hovering over "Yes, run it" while they do it claims a choice is still
|
|
283
|
+
// live that their next keystroke is not addressing.
|
|
284
|
+
let offset = 0;
|
|
285
|
+
let first = 0;
|
|
286
|
+
// TUI2-R3v2 ③: the in-flight line. A button that goes quiet for two
|
|
287
|
+
// seconds reads as broken, and this one is making a network call —
|
|
288
|
+
// so the panel says what it is doing, and says that esc still works.
|
|
289
|
+
if (phase === "asking") {
|
|
290
|
+
rows.push(`${gutter}${cutLine(`${p.dim}asking the model for safer options…${p.reset}`, Math.max(1, W - 2))}`);
|
|
291
|
+
}
|
|
292
|
+
// TUI2-R3v2 ③: the alternatives, as a list in the SAME shape as the
|
|
293
|
+
// approval's own — the round's one interaction model, applied to the
|
|
294
|
+
// one new surface rather than excepted from it. The way back is the
|
|
295
|
+
// last row and is always present: an alternatives list you cannot back
|
|
296
|
+
// out of would be a trap.
|
|
297
|
+
if (phase === "safer" && safer !== undefined) {
|
|
298
|
+
offset = rows.length;
|
|
299
|
+
for (let i = 0; i < safer.options.length; i += 1) {
|
|
300
|
+
const o = safer.options[i];
|
|
301
|
+
rows.push(panelOptionRow({ kind: "allow", label: `${o.command} — ${o.why}` }, i + 1, i === safer.cursor, W));
|
|
302
|
+
}
|
|
303
|
+
rows.push(panelOptionRow({ kind: "deny", label: SAFER_BACK }, safer.options.length + 1, safer.cursor === safer.options.length, W));
|
|
304
|
+
}
|
|
305
|
+
if (phase === "options") {
|
|
306
|
+
if (note !== undefined)
|
|
307
|
+
rows.push(`${gutter}${cutLine(`${p.dim}${escapeTerminal(note)}${p.reset}`, Math.max(1, W - 2))}`);
|
|
308
|
+
offset = rows.length;
|
|
309
|
+
const options = panelOptions(view);
|
|
310
|
+
// A window, never a truncation. On a screen too short for the whole
|
|
311
|
+
// list the options SCROLL under the bar — the cursor's row is always
|
|
312
|
+
// in view, ↑↓ still reach every option and the digits still address
|
|
313
|
+
// the full list (the affordance says "1-4" whether four rows fit or
|
|
314
|
+
// two do). Dropping the tail instead would make an option that the
|
|
315
|
+
// key still takes invisible, which is the one failure a permission
|
|
316
|
+
// list must not have.
|
|
317
|
+
first = Math.max(0, Math.min(cursor - optionsShown + 1, options.length - optionsShown));
|
|
318
|
+
for (let i = first; i < first + optionsShown; i += 1)
|
|
319
|
+
rows.push(panelOptionRow(options[i], i + 1, i === cursor, W));
|
|
320
|
+
}
|
|
321
|
+
const layout = {
|
|
322
|
+
offset,
|
|
323
|
+
// the safer list is clickable by the same rule the option list is —
|
|
324
|
+
// one interaction model means the click works on every list, and its
|
|
325
|
+
// rows include the way back (hence +1)
|
|
326
|
+
count: phase === "options" ? optionsShown : phase === "safer" && safer !== undefined ? safer.options.length + 1 : 0,
|
|
327
|
+
first,
|
|
328
|
+
};
|
|
329
|
+
rows.push(`${gutter}${p.dim}${cutLine(panelAffordance(view, phase, cursor, safer), Math.max(1, W - 2))}${p.reset}`);
|
|
123
330
|
// TUI2-R1.5 11 (VD-13): a real bottom RULE, in the block's own edge
|
|
124
331
|
// vocabulary — the same box-drawing run its divider already uses —
|
|
125
332
|
// anchored at the gutter column. It used to be `\u2514 `: a two-cell stub
|
|
@@ -129,53 +336,75 @@ export function panelBlockRows(view, phase, sel, W, maxRows) {
|
|
|
129
336
|
// row meaning entirely different things. The rule reads as an edge,
|
|
130
337
|
// and the cut notice above it reads as a notice.
|
|
131
338
|
rows.push(`${p.dim}\u2514${"\u2500".repeat(Math.max(0, W - 1))}${p.reset}`);
|
|
132
|
-
return rows;
|
|
339
|
+
return { rows, ...layout };
|
|
133
340
|
}
|
|
134
|
-
/**
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
|
|
341
|
+
/**
|
|
342
|
+
* The input row's lead. In the options phase there is NOTHING to type,
|
|
343
|
+
* so the lead stops pretending there is.
|
|
344
|
+
*
|
|
345
|
+
* "1-3> " was a prompt: it told the human to enter something and press
|
|
346
|
+
* return, which is exactly the interaction this round removed. The row
|
|
347
|
+
* keeps the composer's own quiet lead while the list is up (the keys are
|
|
348
|
+
* on the list and in the hint line), and the typed phase — the one place
|
|
349
|
+
* a human really is writing — leads with the word for what they are
|
|
350
|
+
* writing.
|
|
351
|
+
*/
|
|
352
|
+
export function panelLead(view, phase, cursor) {
|
|
139
353
|
const p = palette();
|
|
140
|
-
if (phase === "rule")
|
|
141
|
-
return `${p.dim}2 Yes, don't ask again for ${p.reset}`;
|
|
142
354
|
if (phase === "amend")
|
|
143
|
-
return `${p.dim}
|
|
144
|
-
return `${p.
|
|
355
|
+
return `${p.dim}amend› ${p.reset}`;
|
|
356
|
+
return `${p.dim}${PANEL_IDLE_LEAD}${p.reset}`;
|
|
145
357
|
}
|
|
358
|
+
/** The composer's lead while a selection list owns the keys — the quiet
|
|
359
|
+
* chevron, not a prompt for input that is not being asked for. */
|
|
360
|
+
const PANEL_IDLE_LEAD = "› ";
|
|
146
361
|
/** The lead's plain text — the editor's reflow width (the line must
|
|
147
362
|
* fit the lead + the box's walls). */
|
|
148
|
-
export function panelLeadPlain(view, phase,
|
|
149
|
-
|
|
150
|
-
return "2 Yes, don't ask again for ";
|
|
151
|
-
if (phase === "amend")
|
|
152
|
-
return sel === 3 ? "feedback (deny): " : "feedback (amend): ";
|
|
153
|
-
return view.flavor === "approval" ? "1-3> " : "1/3> ";
|
|
363
|
+
export function panelLeadPlain(view, phase, cursor) {
|
|
364
|
+
return phase === "amend" ? "amend› " : PANEL_IDLE_LEAD;
|
|
154
365
|
}
|
|
155
|
-
export function panelLeadWidth(view, phase,
|
|
156
|
-
return displayWidth(panelLeadPlain(view, phase,
|
|
366
|
+
export function panelLeadWidth(view, phase, cursor) {
|
|
367
|
+
return displayWidth(panelLeadPlain(view, phase, cursor));
|
|
157
368
|
}
|
|
158
369
|
/** The status row's left text while the panel is up — the phase, not
|
|
159
370
|
* the CLI's painting status (the compositor derives it from the panel
|
|
160
371
|
* state; the "▸ run paused" etc. ride the options phase). */
|
|
161
|
-
export function panelStatus(view, phase,
|
|
162
|
-
|
|
163
|
-
|
|
372
|
+
export function panelStatus(view, phase, cursor) {
|
|
373
|
+
// TUI2-R3v2 ③: the frames' own words — what the panel is doing, and
|
|
374
|
+
// (in the safer list) what it did.
|
|
375
|
+
if (phase === "asking")
|
|
376
|
+
return "\u25b8 asked the model for safer options";
|
|
377
|
+
if (phase === "safer")
|
|
378
|
+
return "\u25b8 asked the model for safer options";
|
|
379
|
+
// TUI2-R3v2 ①: the typed phase says where the words GO. "the words ride
|
|
380
|
+
// the verdict" described the plumbing to whoever wrote it; the human
|
|
381
|
+
// typing needs to know the model will read this and answer with a new
|
|
382
|
+
// call — which is what the v4 frame says, in those words.
|
|
164
383
|
if (phase === "amend")
|
|
165
|
-
return
|
|
384
|
+
return "▸ your note goes to the model — it will propose a new call";
|
|
166
385
|
return view.statusText;
|
|
167
386
|
}
|
|
168
|
-
/**
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
387
|
+
/**
|
|
388
|
+
* The status row's right-aligned hint — the v4 frame's line, verbatim.
|
|
389
|
+
*
|
|
390
|
+
* It names all four gestures because all four now exist at once and the
|
|
391
|
+
* digit range is the only part that varies: "1-4" on an approval, "1-2"
|
|
392
|
+
* on the simple flavors. The click is advertised for the same reason the
|
|
393
|
+
* arrows are — an affordance nobody is told about is one nobody uses.
|
|
394
|
+
*/
|
|
395
|
+
export function panelAffordance(view, phase, cursor, safer) {
|
|
174
396
|
if (phase === "amend")
|
|
175
|
-
return "
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
397
|
+
return "⏎ send · esc back";
|
|
398
|
+
// TUI2-R3v2 ③: the ask is in flight — the ONE key that still means
|
|
399
|
+
// something is the one that gets you out of it.
|
|
400
|
+
if (phase === "asking")
|
|
401
|
+
return "esc cancels";
|
|
402
|
+
// the same sentence the approval list carries, counting the rows THIS
|
|
403
|
+
// list has (the alternatives plus the way back)
|
|
404
|
+
if (phase === "safer" && safer !== undefined) {
|
|
405
|
+
return `↑↓ move · ⏎ or click confirms · 1-${safer.options.length + 1} instant · esc`;
|
|
406
|
+
}
|
|
407
|
+
return `↑↓ move · ⏎ or click confirms · 1-${panelOptions(view).length} instant · esc`;
|
|
179
408
|
}
|
|
180
409
|
// ── TUI2-R2 ④: the pick block, its lead, its status, its affordance ──
|
|
181
410
|
/**
|
package/dist/components.d.ts
CHANGED
|
@@ -350,6 +350,34 @@ export declare function statusLine(status: string, tail: string, W: number, hint
|
|
|
350
350
|
/** The display-width prefix of a plain (SGR-free) text. W21: exported
|
|
351
351
|
* for the approval panel's option-2 rule-name cut. */
|
|
352
352
|
export declare function widthCut(text: string, max: number): string;
|
|
353
|
+
/**
|
|
354
|
+
* TUI2-R3v2 ① — THE selection bar. One engine, every selection surface.
|
|
355
|
+
*
|
|
356
|
+
* The R1.5 ⑧ ruling settled the shape (a full-row reverse bar, not a
|
|
357
|
+
* two-cell marker you have to hunt for in eighty columns) and the @
|
|
358
|
+
* picker, the user chip and the R2 session picker each grew their own
|
|
359
|
+
* copy of the composition. The approval panel would have been the
|
|
360
|
+
* fourth, so the composition moves HERE and the surfaces call it.
|
|
361
|
+
*
|
|
362
|
+
* Two details are the whole reason this is a function and not four
|
|
363
|
+
* inlined string templates:
|
|
364
|
+
*
|
|
365
|
+
* - the inner `reset`s are rewritten to reset-then-reverse. A plain SGR
|
|
366
|
+
* 0 inside the bar punches a hole in it: the row goes back to normal
|
|
367
|
+
* video mid-span and the bar reads as two bars with a gap. The close
|
|
368
|
+
* is SGR 27 (rvEnd), never SGR 0, for the same reason — the bar
|
|
369
|
+
* composes INSIDE whatever span surrounds it.
|
|
370
|
+
* - the pad is computed from the caller's measured VISIBLE width, never
|
|
371
|
+
* from the styled string's length. A bar that stops short is not a
|
|
372
|
+
* bar, and one that runs past W crashes the compositor's invariant ①
|
|
373
|
+
* rather than truncating quietly — so the arithmetic is stated once,
|
|
374
|
+
* here, and proven once, in the sweep gates.
|
|
375
|
+
*
|
|
376
|
+
* The bar spends one cell of frame at each end, so callers build their
|
|
377
|
+
* spans against W−2 whether the row is selected or not — which is what
|
|
378
|
+
* keeps the columns from moving as the bar walks the list.
|
|
379
|
+
*/
|
|
380
|
+
export declare function selectionBar(styled: string, visible: number, W: number): string;
|
|
353
381
|
/** W6 — the box: the chrome's top rail. The two ╌ dotted rows become
|
|
354
382
|
* a rounded box (the box already says "input lives here"); the rails
|
|
355
383
|
* stay dim, the width is still the full W (the box is a rail with
|
package/dist/components.js
CHANGED
|
@@ -1411,6 +1411,38 @@ export function widthCut(text, max) {
|
|
|
1411
1411
|
}
|
|
1412
1412
|
return text.slice(0, i);
|
|
1413
1413
|
}
|
|
1414
|
+
/**
|
|
1415
|
+
* TUI2-R3v2 ① — THE selection bar. One engine, every selection surface.
|
|
1416
|
+
*
|
|
1417
|
+
* The R1.5 ⑧ ruling settled the shape (a full-row reverse bar, not a
|
|
1418
|
+
* two-cell marker you have to hunt for in eighty columns) and the @
|
|
1419
|
+
* picker, the user chip and the R2 session picker each grew their own
|
|
1420
|
+
* copy of the composition. The approval panel would have been the
|
|
1421
|
+
* fourth, so the composition moves HERE and the surfaces call it.
|
|
1422
|
+
*
|
|
1423
|
+
* Two details are the whole reason this is a function and not four
|
|
1424
|
+
* inlined string templates:
|
|
1425
|
+
*
|
|
1426
|
+
* - the inner `reset`s are rewritten to reset-then-reverse. A plain SGR
|
|
1427
|
+
* 0 inside the bar punches a hole in it: the row goes back to normal
|
|
1428
|
+
* video mid-span and the bar reads as two bars with a gap. The close
|
|
1429
|
+
* is SGR 27 (rvEnd), never SGR 0, for the same reason — the bar
|
|
1430
|
+
* composes INSIDE whatever span surrounds it.
|
|
1431
|
+
* - the pad is computed from the caller's measured VISIBLE width, never
|
|
1432
|
+
* from the styled string's length. A bar that stops short is not a
|
|
1433
|
+
* bar, and one that runs past W crashes the compositor's invariant ①
|
|
1434
|
+
* rather than truncating quietly — so the arithmetic is stated once,
|
|
1435
|
+
* here, and proven once, in the sweep gates.
|
|
1436
|
+
*
|
|
1437
|
+
* The bar spends one cell of frame at each end, so callers build their
|
|
1438
|
+
* spans against W−2 whether the row is selected or not — which is what
|
|
1439
|
+
* keeps the columns from moving as the bar walks the list.
|
|
1440
|
+
*/
|
|
1441
|
+
export function selectionBar(styled, visible, W) {
|
|
1442
|
+
const p = palette();
|
|
1443
|
+
const inner = styled.replaceAll(p.reset, `${p.reset}${p.rv}`);
|
|
1444
|
+
return `${p.rv} ${inner}${" ".repeat(Math.max(0, W - visible - 2))} ${p.rvEnd}`;
|
|
1445
|
+
}
|
|
1414
1446
|
/** W6 — the box: the chrome's top rail. The two ╌ dotted rows become
|
|
1415
1447
|
* a rounded box (the box already says "input lives here"); the rails
|
|
1416
1448
|
* stay dim, the width is still the full W (the box is a rail with
|
package/dist/index.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export { SPINNER, foldLine, foldWords, visibleWidth, bodySpacing, Container, cel
|
|
|
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
|
-
export { panelAffordance, panelBlockRows, panelLead, panelLeadPlain, panelLeadWidth, panelStatus, type PanelArgs, type PanelFlavor, type PanelPhase, type
|
|
13
|
+
export { panelAffordance, panelBlockRows, panelLead, panelLeadPlain, panelLeadWidth, panelStatus, type PanelArgs, type PanelFlavor, type PanelPhase, deletionRiskHint, SAFER_BACK, SAFER_DEGRADED, type SaferOption, type SaferRuntime, panelOptions, type PanelOption, type PanelOptionKind, 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
16
|
export { displayVerb } from "./strings.js";
|
package/dist/index.js
CHANGED
|
@@ -17,7 +17,7 @@ export { charWidth, displayWidth, leadWidth, widthOf } from "./width.js";
|
|
|
17
17
|
// that replaces the running tool's live window while a human-chain
|
|
18
18
|
// approval is pending. Types + the row/lead/status renderers; the
|
|
19
19
|
// verdict mapping lives in the cli, never here.
|
|
20
|
-
export { panelAffordance, panelBlockRows, panelLead, panelLeadPlain, panelLeadWidth, panelStatus, } from "./approval-panel.js";
|
|
20
|
+
export { panelAffordance, panelBlockRows, panelLead, panelLeadPlain, panelLeadWidth, panelStatus, deletionRiskHint, SAFER_BACK, SAFER_DEGRADED, panelOptions, } from "./approval-panel.js";
|
|
21
21
|
// KC3 slice 1 (the extraction): the human-facing strings the CLI's
|
|
22
22
|
// trust-ui.ts used to build inline — the prompt, the project-trust
|
|
23
23
|
// listing/view/note, the uncertain execution's view. The flow stays in
|
package/dist/strings.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* tui-cells — the human-facing STRINGS (KC3 slice 1, the
|
|
3
|
-
*
|
|
2
|
+
* tui-cells — the human-facing STRINGS (KC3 slice 1, the escape hatch
|
|
3
|
+
* of ADR-0043, which supersedes ADR-0041): the readline prompt, the
|
|
4
4
|
* project-trust listing rows and its panel view, the uncertain
|
|
5
5
|
* execution's panel view, and the non-TTY not-trusted note. All five
|
|
6
6
|
* were built inline in the CLI's trust-ui.ts before the move.
|
|
@@ -53,10 +53,14 @@ export declare function projectTrustView(root: string, files: readonly TrustArti
|
|
|
53
53
|
* stderr by the caller; never silent. */
|
|
54
54
|
export declare function projectUntrustedNote(count: number, root: string): string;
|
|
55
55
|
/** rounds 8/10 — the uncertain execution's panel: the SIMPLE flavor
|
|
56
|
-
* again
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
56
|
+
* again. TUI2-R3v2 ①: the option labels used to be smuggled into the
|
|
57
|
+
* rule line ("— 1 rerun · 3 abandon") because the panel only ever
|
|
58
|
+
* rendered "Yes" and "No"; they ride `simpleOptions` now and land on the
|
|
59
|
+
* rows themselves, which is both where they belong and the only way they
|
|
60
|
+
* stay true — the digits moved, and copy naming a digit it does not own
|
|
61
|
+
* goes stale silently. The tool name is escaped for the dock-less
|
|
62
|
+
* fallback question because it reaches the terminal as raw text there;
|
|
63
|
+
* the panel's own rows are escaped by the panel renderer. */
|
|
60
64
|
export declare function uncertainView(name: string, executionId: string): PanelView;
|
|
61
65
|
/**
|
|
62
66
|
* KC3.5 — the SAME uncertainty gate, said honestly for an ask_user call.
|
|
@@ -70,7 +74,7 @@ export declare function uncertainView(name: string, executionId: string): PanelV
|
|
|
70
74
|
* (The round's ① probe pinned why this surface exists at all: the
|
|
71
75
|
* shipped recovery blocks on a started-unreported execution regardless
|
|
72
76
|
* of idempotency, so an interrupted ask meets this gate on the way
|
|
73
|
-
* back. Re-asking is safe — that is what
|
|
77
|
+
* back. Re-asking is safe — that is what the first option says out loud.)
|
|
74
78
|
*/
|
|
75
79
|
export declare function unansweredAskView(executionId: string): PanelView;
|
|
76
80
|
/** An extension as the banner names it — the live `connecting` flag is
|
|
@@ -119,24 +123,20 @@ export declare function displayVerb(name: string): string;
|
|
|
119
123
|
* one dim line rather than four table rows, because they apply only
|
|
120
124
|
* while a panel is up. */
|
|
121
125
|
/**
|
|
122
|
-
* TUI2-R1.5 pin 6 — this row has to be true of BOTH panel flavors
|
|
123
|
-
* "digits select" was wrong in both directions at once.
|
|
126
|
+
* TUI2-R1.5 pin 6 — this row has to be true of BOTH panel flavors.
|
|
124
127
|
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
128
|
+
* The R1.5 wording ("digits pick · ⏎ confirms") was the sentence that
|
|
129
|
+
* covered an approval where a digit only SELECTED and an ask where a
|
|
130
|
+
* digit ANSWERED. TUI2-R3v2 ① removed the disagreement it was papering
|
|
131
|
+
* over: every panel is a list with a bar on it, ↑↓ move the bar, ⏎ (or a
|
|
132
|
+
* click) takes the row under it, and a digit takes its row outright.
|
|
133
|
+
* One sentence, now true of every flavor for the same reason rather than
|
|
134
|
+
* by careful omission.
|
|
129
135
|
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
* undersold it. A multi-select question toggles and waits for enter,
|
|
133
|
-
* like the approval.
|
|
134
|
-
*
|
|
135
|
-
* "digits pick · ⏎ confirms" is the sentence both flavors satisfy. The
|
|
136
|
-
* single-choice fast path — where the confirm is implicit — is the one
|
|
137
|
-
* thing a single row cannot also carry; it is an omission, never a lie.
|
|
136
|
+
* The two ask-only gestures keep their clauses because the ask is the
|
|
137
|
+
* only flavor with a set to build and an answer to write.
|
|
138
138
|
*/
|
|
139
|
-
export declare const PANEL_KEYS_ROW = "panels:
|
|
139
|
+
export declare const PANEL_KEYS_ROW = "panels: \u2191\u2193 move \u00B7 \u23CE or click confirms \u00B7 1-4 instant \u00B7 space toggles \u00B7 t types";
|
|
140
140
|
/**
|
|
141
141
|
* TUI2-R1 (D) — the sheet, one screen, static.
|
|
142
142
|
*
|
package/dist/strings.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* tui-cells — the human-facing STRINGS (KC3 slice 1, the
|
|
3
|
-
*
|
|
2
|
+
* tui-cells — the human-facing STRINGS (KC3 slice 1, the escape hatch
|
|
3
|
+
* of ADR-0043, which supersedes ADR-0041): the readline prompt, the
|
|
4
4
|
* project-trust listing rows and its panel view, the uncertain
|
|
5
5
|
* execution's panel view, and the non-TTY not-trusted note. All five
|
|
6
6
|
* were built inline in the CLI's trust-ui.ts before the move.
|
|
@@ -64,10 +64,14 @@ export function projectUntrustedNote(count, root) {
|
|
|
64
64
|
return `[project .kiso] found ${count} artifact(s) in ${root} — not trusted, not loaded (run kiso interactively once to decide)`;
|
|
65
65
|
}
|
|
66
66
|
/** rounds 8/10 — the uncertain execution's panel: the SIMPLE flavor
|
|
67
|
-
* again
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
67
|
+
* again. TUI2-R3v2 ①: the option labels used to be smuggled into the
|
|
68
|
+
* rule line ("— 1 rerun · 3 abandon") because the panel only ever
|
|
69
|
+
* rendered "Yes" and "No"; they ride `simpleOptions` now and land on the
|
|
70
|
+
* rows themselves, which is both where they belong and the only way they
|
|
71
|
+
* stay true — the digits moved, and copy naming a digit it does not own
|
|
72
|
+
* goes stale silently. The tool name is escaped for the dock-less
|
|
73
|
+
* fallback question because it reaches the terminal as raw text there;
|
|
74
|
+
* the panel's own rows are escaped by the panel renderer. */
|
|
71
75
|
export function uncertainView(name, executionId) {
|
|
72
76
|
return {
|
|
73
77
|
flavor: "simple",
|
|
@@ -76,7 +80,8 @@ export function uncertainView(name, executionId) {
|
|
|
76
80
|
speaker: "kiso",
|
|
77
81
|
statusText: "▸ uncertain execution",
|
|
78
82
|
args: { kind: "text", lines: [executionId] },
|
|
79
|
-
ruleOverride: "did the interrupted execution apply?
|
|
83
|
+
ruleOverride: "did the interrupted execution apply?",
|
|
84
|
+
simpleOptions: ["rerun it", "abandon it"],
|
|
80
85
|
fallbackQuestion: `⚠ interrupted execution: ${escapeTerminal(name)} (${executionId}) — did it apply? (y)es / (n)o `,
|
|
81
86
|
};
|
|
82
87
|
}
|
|
@@ -92,7 +97,7 @@ export function uncertainView(name, executionId) {
|
|
|
92
97
|
* (The round's ① probe pinned why this surface exists at all: the
|
|
93
98
|
* shipped recovery blocks on a started-unreported execution regardless
|
|
94
99
|
* of idempotency, so an interrupted ask meets this gate on the way
|
|
95
|
-
* back. Re-asking is safe — that is what
|
|
100
|
+
* back. Re-asking is safe — that is what the first option says out loud.)
|
|
96
101
|
*/
|
|
97
102
|
export function unansweredAskView(executionId) {
|
|
98
103
|
return {
|
|
@@ -102,7 +107,8 @@ export function unansweredAskView(executionId) {
|
|
|
102
107
|
speaker: "kiso",
|
|
103
108
|
statusText: "▸ unanswered question",
|
|
104
109
|
args: { kind: "text", lines: [executionId] },
|
|
105
|
-
ruleOverride: "an unanswered question was interrupted — ask it again?
|
|
110
|
+
ruleOverride: "an unanswered question was interrupted — ask it again?",
|
|
111
|
+
simpleOptions: ["ask it again", "drop it"],
|
|
106
112
|
fallbackQuestion: `⚠ an unanswered question was interrupted (${executionId}) — ask it again? (y)es / (n)o `,
|
|
107
113
|
};
|
|
108
114
|
}
|
|
@@ -193,24 +199,20 @@ export function displayVerb(name) {
|
|
|
193
199
|
* one dim line rather than four table rows, because they apply only
|
|
194
200
|
* while a panel is up. */
|
|
195
201
|
/**
|
|
196
|
-
* TUI2-R1.5 pin 6 — this row has to be true of BOTH panel flavors
|
|
197
|
-
* "digits select" was wrong in both directions at once.
|
|
202
|
+
* TUI2-R1.5 pin 6 — this row has to be true of BOTH panel flavors.
|
|
198
203
|
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
204
|
+
* The R1.5 wording ("digits pick · ⏎ confirms") was the sentence that
|
|
205
|
+
* covered an approval where a digit only SELECTED and an ask where a
|
|
206
|
+
* digit ANSWERED. TUI2-R3v2 ① removed the disagreement it was papering
|
|
207
|
+
* over: every panel is a list with a bar on it, ↑↓ move the bar, ⏎ (or a
|
|
208
|
+
* click) takes the row under it, and a digit takes its row outright.
|
|
209
|
+
* One sentence, now true of every flavor for the same reason rather than
|
|
210
|
+
* by careful omission.
|
|
203
211
|
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
* undersold it. A multi-select question toggles and waits for enter,
|
|
207
|
-
* like the approval.
|
|
208
|
-
*
|
|
209
|
-
* "digits pick · ⏎ confirms" is the sentence both flavors satisfy. The
|
|
210
|
-
* single-choice fast path — where the confirm is implicit — is the one
|
|
211
|
-
* thing a single row cannot also carry; it is an omission, never a lie.
|
|
212
|
+
* The two ask-only gestures keep their clauses because the ask is the
|
|
213
|
+
* only flavor with a set to build and an answer to write.
|
|
212
214
|
*/
|
|
213
|
-
export const PANEL_KEYS_ROW = "panels:
|
|
215
|
+
export const PANEL_KEYS_ROW = "panels: ↑↓ move · ⏎ or click confirms · 1-4 instant · space toggles · t types";
|
|
214
216
|
/** The sheet's grid: the first six bindings in two 3-column rows, the
|
|
215
217
|
* last four in two 2-column rows (the wide entries get the room). The
|
|
216
218
|
* COLUMN STOPS are the prototype's absolute positions, floored by the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-tui-cells",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
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",
|