@vincemakes/kiso-tui 0.1.39 → 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.
- package/dist/approval-panel.d.ts +6 -0
- package/dist/approval-panel.js +6 -0
- package/dist/components.d.ts +4 -222
- package/dist/components.js +4 -884
- package/dist/compositor.d.ts +31 -8
- package/dist/compositor.js +371 -100
- package/dist/diff.d.ts +4 -32
- package/dist/diff.js +4 -122
- package/dist/editor.d.ts +14 -0
- package/dist/editor.js +266 -7
- package/dist/index.d.ts +2 -1
- package/dist/index.js +5 -1
- package/dist/render.d.ts +11 -122
- package/dist/render.js +15 -292
- package/dist/width.d.ts +4 -15
- package/dist/width.js +4 -59
- package/package.json +5 -2
package/dist/diff.d.ts
CHANGED
|
@@ -1,32 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* anti-leak principle), /last has the full data.
|
|
6
|
-
*
|
|
7
|
-
* edit_file diffs IN PLACE (the search→replace windows are known — no
|
|
8
|
-
* general engine needed); write_file does a row-level LCS over the old
|
|
9
|
-
* file (small files are the target). Context: 2 rows each side. The
|
|
10
|
-
* RENDERER truncates (18 head + 18 tail + "… N lines"); the stats come
|
|
11
|
-
* from the full diff.
|
|
12
|
-
*/
|
|
13
|
-
/** The diff block's per-row kind. */
|
|
14
|
-
export type DiffLine = {
|
|
15
|
-
kind: "-" | "+" | " ";
|
|
16
|
-
text: string;
|
|
17
|
-
};
|
|
18
|
-
export interface DiffResult {
|
|
19
|
-
/** The FULL diff (with context, not truncated) — the display truncates. */
|
|
20
|
-
lines: DiffLine[];
|
|
21
|
-
added: number;
|
|
22
|
-
removed: number;
|
|
23
|
-
}
|
|
24
|
-
/** The RENDERER's truncation: head + "… N lines (/last for full)" + tail. */
|
|
25
|
-
export declare function truncateDiff(diff: DiffLine[]): DiffLine[];
|
|
26
|
-
/** edit_file: the search→replace windows replace in place — the changed
|
|
27
|
-
* region is KNOWN, so the diff is the old window vs the new window,
|
|
28
|
-
* context from the surrounding file. */
|
|
29
|
-
export declare function editFileDiff(oldContent: string, search: string, replace: string): DiffResult;
|
|
30
|
-
/** write_file: a new file is all +; an existing file diffs row-level
|
|
31
|
-
* against its old content. */
|
|
32
|
-
export declare function writeFileDiff(oldContent: string | null, newContent: string): DiffResult;
|
|
1
|
+
/** The diff renderer moved to tui-cells (ADR-0043 Amendment 4) —
|
|
2
|
+
* this module is the re-export shim: the compositor's and index.ts's
|
|
3
|
+
* imports (./diff.js) stay verbatim. */
|
|
4
|
+
export * from "@vincemakes/kiso-tui-cells/diff";
|
package/dist/diff.js
CHANGED
|
@@ -1,122 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* anti-leak principle), /last has the full data.
|
|
6
|
-
*
|
|
7
|
-
* edit_file diffs IN PLACE (the search→replace windows are known — no
|
|
8
|
-
* general engine needed); write_file does a row-level LCS over the old
|
|
9
|
-
* file (small files are the target). Context: 2 rows each side. The
|
|
10
|
-
* RENDERER truncates (18 head + 18 tail + "… N lines"); the stats come
|
|
11
|
-
* from the full diff.
|
|
12
|
-
*/
|
|
13
|
-
/** A line-level LCS diff — the classic two-row DP, ~small inputs. */
|
|
14
|
-
function lcsDiff(oldLines, newLines) {
|
|
15
|
-
const n = oldLines.length;
|
|
16
|
-
const m = newLines.length;
|
|
17
|
-
const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
|
|
18
|
-
for (let i = n - 1; i >= 0; i -= 1) {
|
|
19
|
-
for (let j = m - 1; j >= 0; j -= 1) {
|
|
20
|
-
dp[i][j] = oldLines[i] === newLines[j] ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1]);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
const out = [];
|
|
24
|
-
let i = 0;
|
|
25
|
-
let j = 0;
|
|
26
|
-
while (i < n && j < m) {
|
|
27
|
-
if (oldLines[i] === newLines[j]) {
|
|
28
|
-
out.push({ kind: " ", text: oldLines[i] });
|
|
29
|
-
i += 1;
|
|
30
|
-
j += 1;
|
|
31
|
-
}
|
|
32
|
-
else if (dp[i + 1][j] >= dp[i][j + 1]) {
|
|
33
|
-
out.push({ kind: "-", text: oldLines[i] });
|
|
34
|
-
i += 1;
|
|
35
|
-
}
|
|
36
|
-
else {
|
|
37
|
-
out.push({ kind: "+", text: newLines[j] });
|
|
38
|
-
j += 1;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
while (i < n) {
|
|
42
|
-
out.push({ kind: "-", text: oldLines[i] });
|
|
43
|
-
i += 1;
|
|
44
|
-
}
|
|
45
|
-
while (j < m) {
|
|
46
|
-
out.push({ kind: "+", text: newLines[j] });
|
|
47
|
-
j += 1;
|
|
48
|
-
}
|
|
49
|
-
return out;
|
|
50
|
-
}
|
|
51
|
-
/** Keep 2 context rows around each change — the unified-style window. */
|
|
52
|
-
function withContext(diff) {
|
|
53
|
-
const out = [];
|
|
54
|
-
let lastAdded = -10;
|
|
55
|
-
for (let k = 0; k < diff.length; k += 1) {
|
|
56
|
-
if (diff[k].kind === " ")
|
|
57
|
-
continue;
|
|
58
|
-
const from = Math.max(0, k - 2);
|
|
59
|
-
const to = Math.min(diff.length - 1, k + 2);
|
|
60
|
-
for (let c = from; c <= to; c += 1) {
|
|
61
|
-
if (c > lastAdded) {
|
|
62
|
-
out.push(diff[c]);
|
|
63
|
-
lastAdded = c;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
lastAdded = to;
|
|
67
|
-
}
|
|
68
|
-
return out;
|
|
69
|
-
}
|
|
70
|
-
const MAX_DIFF_LINES = 40; // the RENDERED cap
|
|
71
|
-
const TRUNCATE_KEEP = 18;
|
|
72
|
-
/** The RENDERER's truncation: head + "… N lines (/last for full)" + tail. */
|
|
73
|
-
export function truncateDiff(diff) {
|
|
74
|
-
if (diff.length <= MAX_DIFF_LINES)
|
|
75
|
-
return diff;
|
|
76
|
-
const omitted = diff.length - 2 * TRUNCATE_KEEP;
|
|
77
|
-
return [
|
|
78
|
-
...diff.slice(0, TRUNCATE_KEEP),
|
|
79
|
-
{ kind: " ", text: `… ${omitted} lines (/last for full)` },
|
|
80
|
-
...diff.slice(diff.length - TRUNCATE_KEEP),
|
|
81
|
-
];
|
|
82
|
-
}
|
|
83
|
-
function stats(diff) {
|
|
84
|
-
let added = 0;
|
|
85
|
-
let removed = 0;
|
|
86
|
-
for (const d of diff) {
|
|
87
|
-
if (d.kind === "+")
|
|
88
|
-
added += 1;
|
|
89
|
-
else if (d.kind === "-")
|
|
90
|
-
removed += 1;
|
|
91
|
-
}
|
|
92
|
-
return { added, removed };
|
|
93
|
-
}
|
|
94
|
-
/** edit_file: the search→replace windows replace in place — the changed
|
|
95
|
-
* region is KNOWN, so the diff is the old window vs the new window,
|
|
96
|
-
* context from the surrounding file. */
|
|
97
|
-
export function editFileDiff(oldContent, search, replace) {
|
|
98
|
-
const oldLines = oldContent.split("\n");
|
|
99
|
-
const searchLines = search.split("\n");
|
|
100
|
-
const replaceLines = replace.split("\n");
|
|
101
|
-
// Locate the search window (the first occurrence — the edit tool's own
|
|
102
|
-
// semantics); no occurrence → the whole file is the old side.
|
|
103
|
-
let at = -1;
|
|
104
|
-
for (let i = 0; i + searchLines.length <= oldLines.length; i += 1) {
|
|
105
|
-
if (oldLines.slice(i, i + searchLines.length).join("\n") === search) {
|
|
106
|
-
at = i;
|
|
107
|
-
break;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
const lines = at < 0 ? withContext(lcsDiff(oldLines, replaceLines)) : withContext(lcsDiff(oldLines, [...oldLines.slice(0, at), ...replaceLines, ...oldLines.slice(at + searchLines.length)]));
|
|
111
|
-
return { lines, ...stats(lines) };
|
|
112
|
-
}
|
|
113
|
-
/** write_file: a new file is all +; an existing file diffs row-level
|
|
114
|
-
* against its old content. */
|
|
115
|
-
export function writeFileDiff(oldContent, newContent) {
|
|
116
|
-
if (oldContent === null) {
|
|
117
|
-
const lines = newContent.split("\n").map((text) => ({ kind: "+", text }));
|
|
118
|
-
return { lines, added: lines.length, removed: 0 };
|
|
119
|
-
}
|
|
120
|
-
const lines = withContext(lcsDiff(oldContent.split("\n"), newContent.split("\n")));
|
|
121
|
-
return { lines, ...stats(lines) };
|
|
122
|
-
}
|
|
1
|
+
/** The diff renderer moved to tui-cells (ADR-0043 Amendment 4) —
|
|
2
|
+
* this module is the re-export shim: the compositor's and index.ts's
|
|
3
|
+
* imports (./diff.js) stay verbatim. */
|
|
4
|
+
export * from "@vincemakes/kiso-tui-cells/diff";
|
package/dist/editor.d.ts
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { charWidth, displayWidth, widthOf } from "./width.js";
|
|
20
20
|
export { charWidth, displayWidth, widthOf };
|
|
21
|
+
import { type PanelState, type PanelVerdict, type PanelView } from "./approval-panel.js";
|
|
21
22
|
export declare const PROMPT = "\u258C ";
|
|
22
23
|
export declare const PROMPT_WIDTH: number;
|
|
23
24
|
/** v3 §04 — the slash-command menu's command table (English one-liners). */
|
|
@@ -42,6 +43,10 @@ export declare class Editor {
|
|
|
42
43
|
onEot(cb: () => void): void;
|
|
43
44
|
onEscape(cb: () => void): void;
|
|
44
45
|
onExpand(cb: () => void): void;
|
|
46
|
+
/** W22: bind the pending-turn queue — the CLI's live slots. The ↑
|
|
47
|
+
* pop walks them (each pop leaves the queue, cancelling the turn);
|
|
48
|
+
* esc ends the walk after one more pop. */
|
|
49
|
+
bindQueue(state: () => readonly string[], pop: () => string | null): void;
|
|
45
50
|
/** The whole buffer as text (the CLI's line()/clearLine()). */
|
|
46
51
|
line(): string;
|
|
47
52
|
clearLine(): void;
|
|
@@ -61,6 +66,15 @@ export declare class Editor {
|
|
|
61
66
|
/** Cancel a pending question — the buffer stays (its text becomes the
|
|
62
67
|
* next turn on Enter, the readline re-emit equivalent). */
|
|
63
68
|
cancelQuestion(): void;
|
|
69
|
+
/** W21: open the approval panel. The current buffer is stashed
|
|
70
|
+
* (restored at close — commit AND cancel), the panel takes the
|
|
71
|
+
* keys and the input row's lead, the menu closes. */
|
|
72
|
+
beginPanel(view: PanelView, onCommit: (v: PanelVerdict) => void): void;
|
|
73
|
+
/** W21: cancel the panel — the SIGINT path's pair to beginPanel. */
|
|
74
|
+
cancelPanel(): void;
|
|
75
|
+
/** W21: the compositor's bound view — the phase/selection while the
|
|
76
|
+
* panel is up, null otherwise. */
|
|
77
|
+
panelState(): PanelState | null;
|
|
64
78
|
enter(): void;
|
|
65
79
|
exit(): void;
|
|
66
80
|
/** The row's own render when the dock is inactive (a TTY without a
|
package/dist/editor.js
CHANGED
|
@@ -16,11 +16,12 @@
|
|
|
16
16
|
* The editor is a SINGLE line: bracketed paste (?2004h) unwraps and
|
|
17
17
|
* inserts, internal newlines become spaces.
|
|
18
18
|
*/
|
|
19
|
-
import { charWidth, displayWidth, widthOf } from "./width.js";
|
|
19
|
+
import { charWidth, displayWidth, leadWidth, widthOf } from "./width.js";
|
|
20
20
|
// the width primitives moved to width.ts (W1, the single width
|
|
21
21
|
// authority) — re-exported so the editor's public surface is unchanged.
|
|
22
22
|
export { charWidth, displayWidth, widthOf };
|
|
23
23
|
import { palette } from "./render.js";
|
|
24
|
+
import { panelLead } from "./approval-panel.js";
|
|
24
25
|
// TUI v4 #16d: the input row is the blue brick + the edit area — the
|
|
25
26
|
// "you>" text is gone (the brick IS the prompt; the pipe path's readline
|
|
26
27
|
// prompt keeps its own "you> " — v2a line mode, byte-for-byte).
|
|
@@ -47,6 +48,14 @@ export class Editor {
|
|
|
47
48
|
#cursor = 0;
|
|
48
49
|
#scroll = 0; // chars scrolled off the left (width-based reflow)
|
|
49
50
|
#questionCb = null;
|
|
51
|
+
// W21: the panel state machine — the approval/trust panel owns the
|
|
52
|
+
// interaction while up: the digit/y/n/esc/tab routing, the rule
|
|
53
|
+
// input, the tab-amend feedback, the phase/selection the compositor
|
|
54
|
+
// renders. The menu never opens while a panel is up; the pre-panel
|
|
55
|
+
// buffer is stashed at open and restored at close (commit AND
|
|
56
|
+
// cancel) — the panel's rule/feedback text never leaks into the
|
|
57
|
+
// user's next turn.
|
|
58
|
+
#panel = null;
|
|
50
59
|
#pasting = false;
|
|
51
60
|
#lineCb = null;
|
|
52
61
|
#pendingLines = []; // submits before onLine is wired (startup) — never dropped
|
|
@@ -70,6 +79,16 @@ export class Editor {
|
|
|
70
79
|
#history = [];
|
|
71
80
|
#historyIdx = null;
|
|
72
81
|
#preBrowse = [];
|
|
82
|
+
// W22: the pending-turn queue's bound state — the CLI's live slots
|
|
83
|
+
// (chat.ts). ↑ pops the LAST queued message into the buffer and
|
|
84
|
+
// enters the pop-mode (the walk: repeated ↑ pop older ones, each
|
|
85
|
+
// replacing the line); esc in the pop-mode pops once more and ENDS
|
|
86
|
+
// the mode — the next esc at rest rides the escapeCbs (the
|
|
87
|
+
// interrupt survives). The chips themselves are the compositor's
|
|
88
|
+
// bindQueue; this is the keys only.
|
|
89
|
+
#queueState = () => [];
|
|
90
|
+
#queuePop = null;
|
|
91
|
+
#queuePopMode = false;
|
|
73
92
|
#pending = ""; // an incomplete ESC/CSI prefix across chunks
|
|
74
93
|
#decoder = new TextDecoder();
|
|
75
94
|
#entered = false;
|
|
@@ -103,6 +122,13 @@ export class Editor {
|
|
|
103
122
|
onExpand(cb) {
|
|
104
123
|
this.#expandCbs.push(cb);
|
|
105
124
|
}
|
|
125
|
+
/** W22: bind the pending-turn queue — the CLI's live slots. The ↑
|
|
126
|
+
* pop walks them (each pop leaves the queue, cancelling the turn);
|
|
127
|
+
* esc ends the walk after one more pop. */
|
|
128
|
+
bindQueue(state, pop) {
|
|
129
|
+
this.#queueState = state;
|
|
130
|
+
this.#queuePop = pop;
|
|
131
|
+
}
|
|
106
132
|
/** The whole buffer as text (the CLI's line()/clearLine()). */
|
|
107
133
|
line() {
|
|
108
134
|
return String.fromCodePoint(...this.#chars);
|
|
@@ -136,6 +162,8 @@ export class Editor {
|
|
|
136
162
|
return MENU_ITEMS.filter((m) => m.name.startsWith(line));
|
|
137
163
|
}
|
|
138
164
|
#refreshMenu() {
|
|
165
|
+
if (this.#panel !== null)
|
|
166
|
+
return; // W21: the menu never opens while the panel owns the keys
|
|
139
167
|
const f = this.#menuFiltered();
|
|
140
168
|
this.#menuOpen = f.length > 0;
|
|
141
169
|
if (this.#menuSel >= f.length)
|
|
@@ -151,6 +179,38 @@ export class Editor {
|
|
|
151
179
|
cancelQuestion() {
|
|
152
180
|
this.#questionCb = null;
|
|
153
181
|
}
|
|
182
|
+
/** W21: open the approval panel. The current buffer is stashed
|
|
183
|
+
* (restored at close — commit AND cancel), the panel takes the
|
|
184
|
+
* keys and the input row's lead, the menu closes. */
|
|
185
|
+
beginPanel(view, onCommit) {
|
|
186
|
+
this.#panel = {
|
|
187
|
+
view,
|
|
188
|
+
phase: "options",
|
|
189
|
+
sel: 0,
|
|
190
|
+
amend: "yes",
|
|
191
|
+
onCommit,
|
|
192
|
+
stash: { chars: this.#chars, cursor: this.#cursor, scroll: this.#scroll },
|
|
193
|
+
};
|
|
194
|
+
this.#chars = [];
|
|
195
|
+
this.#cursor = 0;
|
|
196
|
+
this.#scroll = 0;
|
|
197
|
+
this.#menuOpen = false;
|
|
198
|
+
this.#menuSel = 0;
|
|
199
|
+
this.#queuePopMode = false; // W22: the panel owns the keys while up
|
|
200
|
+
this.#onRender();
|
|
201
|
+
}
|
|
202
|
+
/** W21: cancel the panel — the SIGINT path's pair to beginPanel. */
|
|
203
|
+
cancelPanel() {
|
|
204
|
+
this.#panelClose({ action: "cancel" });
|
|
205
|
+
}
|
|
206
|
+
/** W21: the compositor's bound view — the phase/selection while the
|
|
207
|
+
* panel is up, null otherwise. */
|
|
208
|
+
panelState() {
|
|
209
|
+
const panel = this.#panel;
|
|
210
|
+
if (panel === null)
|
|
211
|
+
return null;
|
|
212
|
+
return { view: panel.view, phase: panel.phase, sel: panel.sel };
|
|
213
|
+
}
|
|
154
214
|
enter() {
|
|
155
215
|
if (this.#entered)
|
|
156
216
|
return;
|
|
@@ -176,8 +236,17 @@ export class Editor {
|
|
|
176
236
|
const p = palette();
|
|
177
237
|
const st = this.dockState();
|
|
178
238
|
const W = (process.stdout.columns ?? 0) || 80; // a degenerate 0 size (no TIOCSWINSZ) falls back
|
|
179
|
-
|
|
180
|
-
|
|
239
|
+
// W21: the panel's lead owns the row while up (the brick returns
|
|
240
|
+
// when the panel closes).
|
|
241
|
+
const panel = this.#panel;
|
|
242
|
+
const lead = panel !== null ? panelLead(panel.view, panel.phase, panel.sel) : `${p.bold}${PROMPT}${p.reset}`;
|
|
243
|
+
// W23: the ONE width authority — leadWidth(lead), the ANSI-stripped
|
|
244
|
+
// visible width (the styled panel lead / the styled brick measure
|
|
245
|
+
// the same as their plain text — a lead can never measure
|
|
246
|
+
// differently at the editor than at the compositor)
|
|
247
|
+
const leadW = leadWidth(lead);
|
|
248
|
+
const cursorCol = Math.min(1 + leadW + st.cursor, W);
|
|
249
|
+
process.stdout.write(`\r\x1b[0K${lead}${st.line}\x1b[${cursorCol}G`);
|
|
181
250
|
}
|
|
182
251
|
// ---- input ----
|
|
183
252
|
/** Feed raw stdin bytes — the parser. Public for unit tests. */
|
|
@@ -187,6 +256,50 @@ export class Editor {
|
|
|
187
256
|
let i = 0;
|
|
188
257
|
while (i < text.length) {
|
|
189
258
|
const c = text[i];
|
|
259
|
+
if (this.#panel !== null) {
|
|
260
|
+
// W21: the panel owns the keys — the digits/y/n select in
|
|
261
|
+
// the options phase (digit 2 jumps to the rule input), tab
|
|
262
|
+
// opens the amend (approval only), esc backs out (rule/
|
|
263
|
+
// amend → options, selection → rest, rest → cancel), enter
|
|
264
|
+
// commits by phase. CSI/SS3 and the editing keys still ride
|
|
265
|
+
// the normal chain below (the rule/amend lines are free
|
|
266
|
+
// text); ctrl-c still rides the SIGINT handler (which
|
|
267
|
+
// cancels the panel).
|
|
268
|
+
const panel = this.#panel;
|
|
269
|
+
if (c === "\x1b" && !text.slice(i + 1).startsWith("[") && !text.slice(i + 1).startsWith("O")) {
|
|
270
|
+
this.#panelEsc();
|
|
271
|
+
i += 1;
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
if (c === "\t") {
|
|
275
|
+
if (panel.phase === "options")
|
|
276
|
+
this.#panelTab();
|
|
277
|
+
i += 1;
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (c === "\x0d" || c === "\x0a") {
|
|
281
|
+
this.#panelEnter();
|
|
282
|
+
i += 1;
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (c === "1" || c === "y" || c === "Y") {
|
|
286
|
+
if (panel.phase === "options")
|
|
287
|
+
this.#panelSelect(1);
|
|
288
|
+
i += 1;
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
if (c === "2" && panel.phase === "options" && panel.view.flavor === "approval") {
|
|
292
|
+
this.#panelRule();
|
|
293
|
+
i += 1;
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
if (c === "3" || c === "n" || c === "N") {
|
|
297
|
+
if (panel.phase === "options")
|
|
298
|
+
this.#panelSelect(3);
|
|
299
|
+
i += 1;
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
190
303
|
if (c === "\x1b") {
|
|
191
304
|
const rest = text.slice(i + 1);
|
|
192
305
|
if (rest.startsWith("[")) {
|
|
@@ -203,19 +316,32 @@ export class Editor {
|
|
|
203
316
|
}
|
|
204
317
|
else if (this.#menuOpen) {
|
|
205
318
|
// v3 §04: Esc closes the menu and clears the buffer.
|
|
319
|
+
// CA-4: the closing esc consumes its burst (the `i += 1`
|
|
320
|
+
// convention) — a double-esc can never abort the turn.
|
|
206
321
|
this.#chars = [];
|
|
207
322
|
this.#cursor = 0;
|
|
208
323
|
this.#scroll = 0;
|
|
209
324
|
this.#refreshMenu();
|
|
325
|
+
i += 1;
|
|
326
|
+
}
|
|
327
|
+
else if (this.#queuePopMode) {
|
|
328
|
+
// W22: esc in the pop-mode — ONE more pop, then the
|
|
329
|
+
// mode ends: the next esc at rest rides the escapeCbs
|
|
330
|
+
// (the interrupt chain survives the walk).
|
|
331
|
+
this.#queuePopMode = false;
|
|
332
|
+
this.#queuePopIntoBuffer();
|
|
333
|
+
i += 1;
|
|
210
334
|
}
|
|
211
335
|
else if (this.#historyIdx !== null) {
|
|
212
336
|
// A2: Esc exits the history browse — the pre-browse
|
|
213
|
-
// (empty) input returns.
|
|
337
|
+
// (empty) input returns. CA-4: the exiting esc consumes
|
|
338
|
+
// its burst — a double-esc can never abort the turn.
|
|
214
339
|
this.#historyIdx = null;
|
|
215
340
|
this.#chars = [...this.#preBrowse];
|
|
216
341
|
this.#cursor = this.#chars.length;
|
|
217
342
|
this.#reflow();
|
|
218
343
|
this.#onRender();
|
|
344
|
+
i += 1;
|
|
219
345
|
}
|
|
220
346
|
else {
|
|
221
347
|
for (const cb of [...this.#escapeCbs])
|
|
@@ -312,13 +438,27 @@ export class Editor {
|
|
|
312
438
|
// v3 §04: the menu owns ↑↓ while open (the selection, never the
|
|
313
439
|
// cursor). A2 (the feel): otherwise ↑↓ navigate the session history
|
|
314
440
|
// — ONLY from an empty input or while already browsing; mid-edit
|
|
315
|
-
// the cursor semantics are unchanged (↑↓ do nothing).
|
|
316
|
-
|
|
441
|
+
// the cursor semantics are unchanged (↑↓ do nothing). W21: the
|
|
442
|
+
// panel owns the keys while up (↑↓ do nothing — the panel has no
|
|
443
|
+
// ↑↓ role).
|
|
444
|
+
if (this.#panel !== null) {
|
|
445
|
+
/* the panel owns the keys */
|
|
446
|
+
}
|
|
447
|
+
else if (this.#menuOpen) {
|
|
317
448
|
if (final === "A")
|
|
318
449
|
this.#menuSel = Math.max(0, this.#menuSel - 1);
|
|
319
450
|
else
|
|
320
451
|
this.#menuSel = Math.min(this.#menuFiltered().length - 1, this.#menuSel + 1);
|
|
321
452
|
}
|
|
453
|
+
else if (final === "A" && this.#queuePop !== null && (this.#queuePopMode || this.line() === "") && this.#queueState().length > 0) {
|
|
454
|
+
// W22: ↑ pops the LAST queued message into the buffer — the
|
|
455
|
+
// walk: repeated presses pop older ones (each replaces the
|
|
456
|
+
// line, the cursor at the end); esc ends the mode after one
|
|
457
|
+
// more pop. Mid-edit the pop never fires (the A2
|
|
458
|
+
// non-destructive feel, mirroring the history browse).
|
|
459
|
+
this.#queuePopMode = true;
|
|
460
|
+
this.#queuePopIntoBuffer();
|
|
461
|
+
}
|
|
322
462
|
else if (this.#historyIdx !== null || this.line() === "") {
|
|
323
463
|
this.#historyMove(final === "A" ? -1 : 1);
|
|
324
464
|
}
|
|
@@ -339,10 +479,104 @@ export class Editor {
|
|
|
339
479
|
this.#reflow();
|
|
340
480
|
}
|
|
341
481
|
}
|
|
482
|
+
// ---- W21: the panel state machine ----
|
|
483
|
+
#panelSelect(sel) {
|
|
484
|
+
const panel = this.#panel;
|
|
485
|
+
if (panel === null)
|
|
486
|
+
return;
|
|
487
|
+
panel.sel = sel;
|
|
488
|
+
this.#onRender();
|
|
489
|
+
}
|
|
490
|
+
/** digit 2 — the rule input: the buffer prefilled with the tool name
|
|
491
|
+
* (the option-2 prefill; enter commits the rule). */
|
|
492
|
+
#panelRule() {
|
|
493
|
+
const panel = this.#panel;
|
|
494
|
+
if (panel === null)
|
|
495
|
+
return;
|
|
496
|
+
panel.phase = "rule";
|
|
497
|
+
panel.sel = 2;
|
|
498
|
+
this.#chars = [...panel.view.name].map((ch) => ch.codePointAt(0));
|
|
499
|
+
this.#cursor = this.#chars.length;
|
|
500
|
+
this.#scroll = 0;
|
|
501
|
+
this.#onRender();
|
|
502
|
+
}
|
|
503
|
+
/** tab — the amend phase on the selected option (yes/deny); the
|
|
504
|
+
* simple flavor never has it (options 1/3 only, no option 2). */
|
|
505
|
+
#panelTab() {
|
|
506
|
+
const panel = this.#panel;
|
|
507
|
+
if (panel === null || panel.view.flavor !== "approval")
|
|
508
|
+
return;
|
|
509
|
+
panel.amend = panel.sel === 3 ? "no" : "yes";
|
|
510
|
+
panel.phase = "amend";
|
|
511
|
+
this.#chars = [];
|
|
512
|
+
this.#cursor = 0;
|
|
513
|
+
this.#scroll = 0;
|
|
514
|
+
this.#onRender();
|
|
515
|
+
}
|
|
516
|
+
/** esc — back out of the rule/amend to the options (the buffer
|
|
517
|
+
* clears), deselect, or cancel the panel at rest. */
|
|
518
|
+
#panelEsc() {
|
|
519
|
+
const panel = this.#panel;
|
|
520
|
+
if (panel === null)
|
|
521
|
+
return;
|
|
522
|
+
if (panel.phase !== "options") {
|
|
523
|
+
panel.phase = "options";
|
|
524
|
+
panel.sel = 0;
|
|
525
|
+
this.#chars = [];
|
|
526
|
+
this.#cursor = 0;
|
|
527
|
+
this.#scroll = 0;
|
|
528
|
+
this.#onRender();
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
if (panel.sel !== 0) {
|
|
532
|
+
panel.sel = 0;
|
|
533
|
+
this.#onRender();
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
this.#panelClose({ action: "cancel" });
|
|
537
|
+
}
|
|
538
|
+
/** enter — commit by phase: the rule input (the tool name when
|
|
539
|
+
* empty), the amend feedback (the bare verdict when empty), or the
|
|
540
|
+
* selected option (nothing at rest — an accidental enter never
|
|
541
|
+
* approves). Enter on the selected option 2 is the digit-2 key. */
|
|
542
|
+
#panelEnter() {
|
|
543
|
+
const panel = this.#panel;
|
|
544
|
+
if (panel === null)
|
|
545
|
+
return;
|
|
546
|
+
const line = this.line();
|
|
547
|
+
if (panel.phase === "rule") {
|
|
548
|
+
this.#panelClose({ action: "allow-rule", rule: line === "" ? panel.view.name : line });
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
if (panel.phase === "amend") {
|
|
552
|
+
this.#panelClose(panel.amend === "yes" ? { action: "allow", reason: line } : { action: "deny", reason: line });
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
if (panel.sel === 1)
|
|
556
|
+
this.#panelClose({ action: "allow", reason: "" });
|
|
557
|
+
else if (panel.sel === 2)
|
|
558
|
+
this.#panelRule();
|
|
559
|
+
else if (panel.sel === 3)
|
|
560
|
+
this.#panelClose({ action: "deny", reason: "" });
|
|
561
|
+
}
|
|
562
|
+
#panelClose(verdict) {
|
|
563
|
+
const panel = this.#panel;
|
|
564
|
+
if (panel === null)
|
|
565
|
+
return;
|
|
566
|
+
this.#panel = null;
|
|
567
|
+
// the pre-panel buffer returns — the panel's rule/feedback text
|
|
568
|
+
// never leaks into the user's next turn (commit AND cancel).
|
|
569
|
+
this.#chars = [...panel.stash.chars];
|
|
570
|
+
this.#cursor = panel.stash.cursor;
|
|
571
|
+
this.#scroll = panel.stash.scroll;
|
|
572
|
+
this.#onRender();
|
|
573
|
+
panel.onCommit(verdict);
|
|
574
|
+
}
|
|
342
575
|
// ---- editing ----
|
|
343
576
|
#insert(cp) {
|
|
344
577
|
if (this.#historyIdx !== null)
|
|
345
578
|
this.#historyIdx = null; // editing leaves the browse
|
|
579
|
+
this.#queuePopMode = false; // W22: editing leaves the pop-walk too
|
|
346
580
|
this.#chars.splice(this.#cursor, 0, cp);
|
|
347
581
|
this.#cursor += 1;
|
|
348
582
|
this.#reflow();
|
|
@@ -354,6 +588,7 @@ export class Editor {
|
|
|
354
588
|
return;
|
|
355
589
|
if (this.#historyIdx !== null)
|
|
356
590
|
this.#historyIdx = null; // editing leaves the browse
|
|
591
|
+
this.#queuePopMode = false; // W22: editing leaves the pop-walk too
|
|
357
592
|
this.#chars.splice(this.#cursor - 1, 1);
|
|
358
593
|
this.#cursor -= 1;
|
|
359
594
|
this.#reflow();
|
|
@@ -420,6 +655,7 @@ export class Editor {
|
|
|
420
655
|
this.#scroll = 0;
|
|
421
656
|
this.#menuOpen = false;
|
|
422
657
|
this.#menuSel = 0;
|
|
658
|
+
this.#queuePopMode = false; // W22: a submit ends the pop-walk — the next esc at rest interrupts again
|
|
423
659
|
const cb = this.#questionCb;
|
|
424
660
|
this.#questionCb = null;
|
|
425
661
|
if (cb !== null) {
|
|
@@ -467,10 +703,33 @@ export class Editor {
|
|
|
467
703
|
this.#cursor = this.#chars.length;
|
|
468
704
|
this.#reflow();
|
|
469
705
|
}
|
|
706
|
+
/** W22: pop the LAST queued message into the buffer (the walk's
|
|
707
|
+
* step — ↑ enters/stays in the pop-mode, esc's pop ends it). The
|
|
708
|
+
* chip leaves the queue (cancelled in the CLI), the line becomes
|
|
709
|
+
* the popped text, the cursor sits at the end. */
|
|
710
|
+
#queuePopIntoBuffer() {
|
|
711
|
+
if (this.#queuePop === null)
|
|
712
|
+
return;
|
|
713
|
+
const line = this.#queuePop();
|
|
714
|
+
if (line === null)
|
|
715
|
+
return;
|
|
716
|
+
this.#chars = [...line].map((ch) => ch.codePointAt(0));
|
|
717
|
+
this.#cursor = this.#chars.length;
|
|
718
|
+
this.#scroll = 0;
|
|
719
|
+
this.#onRender();
|
|
720
|
+
}
|
|
470
721
|
// ---- width-based horizontal scroll ----
|
|
471
722
|
#reflow() {
|
|
472
723
|
const W = (process.stdout.columns ?? 0) || 80; // degenerate 0 falls back to 80
|
|
473
|
-
|
|
724
|
+
// W21: the panel's phase lead owns the input row while up — the
|
|
725
|
+
// line's max width follows the lead (the rule/amend leads are
|
|
726
|
+
// wider than the brick).
|
|
727
|
+
// W23: the ONE width authority — leadWidth(lead) — the cap follows
|
|
728
|
+
// the lead the editor itself renders (the panel lead when the panel
|
|
729
|
+
// owns the keys, the brick otherwise): maxW = W − walls − lead.
|
|
730
|
+
const lead = this.#panel !== null ? panelLead(this.#panel.view, this.#panel.phase, this.#panel.sel) : PROMPT;
|
|
731
|
+
const leadW = leadWidth(lead);
|
|
732
|
+
const maxW = Math.max(1, W - leadW - 4); // W6: the box's walls (2+2) — the visible line fits the box's inner width; the "…" rides inside
|
|
474
733
|
const curCol = widthOf(this.#chars.slice(0, this.#cursor));
|
|
475
734
|
const scrolledW = widthOf(this.#chars.slice(0, this.#scroll));
|
|
476
735
|
if (curCol < scrolledW) {
|
package/dist/index.d.ts
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* editor, the diff renderer, and the palette.
|
|
7
7
|
*/
|
|
8
8
|
export { Body, Dock, CURSOR_MARKER, type BodyOptions } from "./compositor.js";
|
|
9
|
+
export { panelAffordance, panelBlockRows, panelLead, panelLeadPlain, panelLeadWidth, panelStatus, type PanelArgs, type PanelFlavor, type PanelPhase, type PanelSel, type PanelState, type PanelVerdict, type PanelView, } from "./approval-panel.js";
|
|
9
10
|
export { Container, foldLine, visibleWidth, SPINNER, type Component, type FrameCtx } from "./components.js";
|
|
10
11
|
export { Editor, MENU_ITEMS, PROMPT, PROMPT_WIDTH, displayWidth, charWidth, widthOf, type MenuItem, } from "./editor.js";
|
|
11
|
-
export { bannerLines, COLOR_OFF, COLOR_ON, escapeTerminal, foldResult, foldThinking, kUnit, palette, renderEvent, renderRecap, renderResumeList, renderSessionLine, renderStatusLine, relativeTime, renderTerminalGap, renderToolSummary, TAGLINE, truncateRow, type Palette, type PathResolver, type RecapStats, type ResumeMeta, type RenderInput, type RenderResult, type RunUsage, } from "./render.js";
|
|
12
|
+
export { bannerLines, COLOR_OFF, COLOR_ON, escapeTerminal, foldResult, foldThinking, kUnit, palette, renderEvent, renderRecap, renderResumeList, renderSessionLine, renderStatusLine, relativeTime, renderTerminalGap, renderToolSummary, TAGLINE, toolTarget, truncateRow, type Palette, type PathResolver, type RecapStats, type ResumeMeta, type RenderInput, type RenderResult, type RunUsage, } from "./render.js";
|
|
12
13
|
export { editFileDiff, truncateDiff, writeFileDiff, type DiffLine, type DiffResult } from "./diff.js";
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,11 @@
|
|
|
6
6
|
* editor, the diff renderer, and the palette.
|
|
7
7
|
*/
|
|
8
8
|
export { Body, Dock, CURSOR_MARKER } from "./compositor.js";
|
|
9
|
+
// W21 (the v8 approval round): the approval panel — the bounded block
|
|
10
|
+
// that replaces the running tool's live window while a human-chain
|
|
11
|
+
// approval is pending (the shape authority is the committed preview).
|
|
12
|
+
export { panelAffordance, panelBlockRows, panelLead, panelLeadPlain, panelLeadWidth, panelStatus, } from "./approval-panel.js";
|
|
9
13
|
export { Container, foldLine, visibleWidth, SPINNER } from "./components.js";
|
|
10
14
|
export { Editor, MENU_ITEMS, PROMPT, PROMPT_WIDTH, displayWidth, charWidth, widthOf, } from "./editor.js";
|
|
11
|
-
export { bannerLines, COLOR_OFF, COLOR_ON, escapeTerminal, foldResult, foldThinking, kUnit, palette, renderEvent, renderRecap, renderResumeList, renderSessionLine, renderStatusLine, relativeTime, renderTerminalGap, renderToolSummary, TAGLINE, truncateRow, } from "./render.js";
|
|
15
|
+
export { bannerLines, COLOR_OFF, COLOR_ON, escapeTerminal, foldResult, foldThinking, kUnit, palette, renderEvent, renderRecap, renderResumeList, renderSessionLine, renderStatusLine, relativeTime, renderTerminalGap, renderToolSummary, TAGLINE, toolTarget, truncateRow, } from "./render.js";
|
|
12
16
|
export { editFileDiff, truncateDiff, writeFileDiff } from "./diff.js";
|