@vincemakes/kiso-tui 0.8.0 → 0.10.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/ask-panel.d.ts +8 -7
- package/dist/ask-panel.js +31 -12
- package/dist/at-picker.d.ts +4 -0
- package/dist/at-picker.js +41 -8
- package/dist/compositor.d.ts +28 -10
- package/dist/compositor.js +405 -53
- package/dist/context-ledger.d.ts +66 -0
- package/dist/context-ledger.js +87 -0
- package/dist/editor.d.ts +11 -1
- package/dist/editor.js +301 -17
- package/dist/index.d.ts +7 -5
- package/dist/index.js +16 -5
- package/dist/session-picker.d.ts +113 -0
- package/dist/session-picker.js +249 -0
- package/dist/status.d.ts +32 -2
- package/dist/status.js +24 -3
- package/package.json +2 -2
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TUI2-R1 (E) — /context's attribution rows.
|
|
3
|
+
*
|
|
4
|
+
* The question "where did my context go?" has had an answer since E3:
|
|
5
|
+
* the trace sidecar's rent ledger records, per request, exactly what
|
|
6
|
+
* each static surface costs, and the context manifest records what the
|
|
7
|
+
* conversation costs. Until now that answer was only readable by
|
|
8
|
+
* someone willing to parse JSONL.
|
|
9
|
+
*
|
|
10
|
+
* This module is the presentation half and nothing else — a pure
|
|
11
|
+
* function from counts to rows, with no idea where the counts came
|
|
12
|
+
* from. That matters for the purity gate: the trace surface is an
|
|
13
|
+
* OBSERVATION surface (ADR-0051 §6), correctness never reads it, and
|
|
14
|
+
* keeping the reader in the CLI and the renderer here means this module
|
|
15
|
+
* cannot accidentally become a second correctness path.
|
|
16
|
+
*
|
|
17
|
+
* Every number is a count the ledger already carries. Nothing here
|
|
18
|
+
* estimates, projects, or predicts.
|
|
19
|
+
*/
|
|
20
|
+
/** The counts one request's ledger yields, already grouped by surface.
|
|
21
|
+
* Estimated tokens throughout (the rent ledger's own chars/4 convention
|
|
22
|
+
* — R6), because that is the unit the ledger records. */
|
|
23
|
+
export interface ContextLedger {
|
|
24
|
+
/** The model's context window, as the session is configured. */
|
|
25
|
+
readonly window: number;
|
|
26
|
+
/** system:base + every system:ext:* append EXCEPT skills. */
|
|
27
|
+
readonly systemPrompt: number;
|
|
28
|
+
/** system:base alone — the detail behind the row. */
|
|
29
|
+
readonly systemBase: number;
|
|
30
|
+
/** how many extensions appended (the detail's count). */
|
|
31
|
+
readonly appends: number;
|
|
32
|
+
/** the sum of the tool:* lines. */
|
|
33
|
+
readonly toolTable: number;
|
|
34
|
+
readonly tools: number;
|
|
35
|
+
/** system:ext:skills — broken out because it is an INDEX of content
|
|
36
|
+
* rather than an instruction, and it grows with the workspace rather
|
|
37
|
+
* than with the build. 0 when the extension is not loaded. */
|
|
38
|
+
readonly skillsIndex: number;
|
|
39
|
+
/** how many skills the index lists — 0 when the caller cannot know
|
|
40
|
+
* (the rent ledger records surfaces, never their contents). */
|
|
41
|
+
readonly skills: number;
|
|
42
|
+
/** the per-request skeleton (the `envelope` rent line). */
|
|
43
|
+
readonly envelope: number;
|
|
44
|
+
/** the context manifest's turn segments — the conversation itself. */
|
|
45
|
+
readonly messages: number;
|
|
46
|
+
readonly turns: number;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The rows: the header, the bar, one row per surface that EXISTS, and
|
|
50
|
+
* the free remainder.
|
|
51
|
+
*
|
|
52
|
+
* An absent surface is an absent row — the rent ledger's own R9 rule
|
|
53
|
+
* ("not paid = no rent"), carried into the display: a session with no
|
|
54
|
+
* skills extension should not read a "skills index 0" row, because the
|
|
55
|
+
* zero would look like a measurement rather than an absence.
|
|
56
|
+
*
|
|
57
|
+
* The columns are fixed so the numbers line up as a column of numbers;
|
|
58
|
+
* the detail text rides after them, dim, and is cut by the caller's
|
|
59
|
+
* width if it must be.
|
|
60
|
+
*/
|
|
61
|
+
export declare function contextRows(ledger: ContextLedger): string[];
|
|
62
|
+
/** TUI2-R1 (E) — the honest fallback. The ledger is written PER REQUEST:
|
|
63
|
+
* a session that has not called the model yet has no sidecar, and the
|
|
64
|
+
* right thing to show is that fact and the one step that produces one.
|
|
65
|
+
* Never an empty bar — an empty bar reads as "measured zero". */
|
|
66
|
+
export declare function contextUnavailableRows(reason: string): string[];
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TUI2-R1 (E) — /context's attribution rows.
|
|
3
|
+
*
|
|
4
|
+
* The question "where did my context go?" has had an answer since E3:
|
|
5
|
+
* the trace sidecar's rent ledger records, per request, exactly what
|
|
6
|
+
* each static surface costs, and the context manifest records what the
|
|
7
|
+
* conversation costs. Until now that answer was only readable by
|
|
8
|
+
* someone willing to parse JSONL.
|
|
9
|
+
*
|
|
10
|
+
* This module is the presentation half and nothing else — a pure
|
|
11
|
+
* function from counts to rows, with no idea where the counts came
|
|
12
|
+
* from. That matters for the purity gate: the trace surface is an
|
|
13
|
+
* OBSERVATION surface (ADR-0051 §6), correctness never reads it, and
|
|
14
|
+
* keeping the reader in the CLI and the renderer here means this module
|
|
15
|
+
* cannot accidentally become a second correctness path.
|
|
16
|
+
*
|
|
17
|
+
* Every number is a count the ledger already carries. Nothing here
|
|
18
|
+
* estimates, projects, or predicts.
|
|
19
|
+
*/
|
|
20
|
+
import { palette } from "./render.js";
|
|
21
|
+
const BAR_CELLS = 12;
|
|
22
|
+
/** k-units for the ledger's columns: 25700 → 25.7k, 300 → 300, 11 → 11.
|
|
23
|
+
*
|
|
24
|
+
* TUI2-R1.5 ⑤ (VD-15): the floor was 100, which put `11`, `0.3k` and
|
|
25
|
+
* `25.7k` in one right-aligned column — two unit systems stacked, and
|
|
26
|
+
* the reader has to switch between them row by row to compare. The
|
|
27
|
+
* repo already had a k-formatter with a 1000 floor (render.ts's kUnit,
|
|
28
|
+
* which the status row and every settled card use); this now agrees
|
|
29
|
+
* with it, so /context speaks the same number language as the rest of
|
|
30
|
+
* the product. It still differs from kUnit in never having a null to
|
|
31
|
+
* report — every ledger figure is a measured count. */
|
|
32
|
+
function k(n) {
|
|
33
|
+
return n >= 1000 ? `${(n / 1000).toFixed(1).replace(/\.0$/, "")}k` : String(Math.round(n));
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The rows: the header, the bar, one row per surface that EXISTS, and
|
|
37
|
+
* the free remainder.
|
|
38
|
+
*
|
|
39
|
+
* An absent surface is an absent row — the rent ledger's own R9 rule
|
|
40
|
+
* ("not paid = no rent"), carried into the display: a session with no
|
|
41
|
+
* skills extension should not read a "skills index 0" row, because the
|
|
42
|
+
* zero would look like a measurement rather than an absence.
|
|
43
|
+
*
|
|
44
|
+
* The columns are fixed so the numbers line up as a column of numbers;
|
|
45
|
+
* the detail text rides after them, dim, and is cut by the caller's
|
|
46
|
+
* width if it must be.
|
|
47
|
+
*/
|
|
48
|
+
export function contextRows(ledger) {
|
|
49
|
+
const p = palette();
|
|
50
|
+
const used = ledger.systemPrompt + ledger.toolTable + ledger.skillsIndex + ledger.envelope + ledger.messages;
|
|
51
|
+
const free = Math.max(0, ledger.window - used);
|
|
52
|
+
const ratio = ledger.window > 0 ? Math.min(1, used / ledger.window) : 1;
|
|
53
|
+
const filled = Math.max(0, Math.min(BAR_CELLS, Math.round(ratio * BAR_CELLS)));
|
|
54
|
+
const rows = [
|
|
55
|
+
`${p.bold}context — ${k(used)} / ${k(ledger.window)} tokens (${Math.round(ratio * 100)}%)${p.reset}`,
|
|
56
|
+
`${p.bold}${"▰".repeat(filled)}${p.reset}${p.dim}${"▱".repeat(BAR_CELLS - filled)}${p.reset}`,
|
|
57
|
+
];
|
|
58
|
+
/** One surface row: the label at 14 columns, the count right-aligned
|
|
59
|
+
* at 5, then the dim detail. */
|
|
60
|
+
const row = (label, value, detail) => ` ${p.bold}▰${p.reset} ${label.padEnd(14)}${p.bold}${k(value).padStart(5)}${p.reset}${detail === "" ? "" : ` ${p.dim}${detail}${p.reset}`}`;
|
|
61
|
+
if (ledger.systemPrompt > 0) {
|
|
62
|
+
rows.push(row("system prompt", ledger.systemPrompt, `(base ${k(ledger.systemBase)}${ledger.appends > 0 ? ` + ${ledger.appends} extension append${ledger.appends === 1 ? "" : "s"}` : ""})`));
|
|
63
|
+
}
|
|
64
|
+
if (ledger.toolTable > 0)
|
|
65
|
+
rows.push(row("tool table", ledger.toolTable, `${ledger.tools} tool${ledger.tools === 1 ? "" : "s"}`));
|
|
66
|
+
if (ledger.skillsIndex > 0) {
|
|
67
|
+
// the skill COUNT is not in the ledger (rent records surfaces, not
|
|
68
|
+
// their contents) — a caller that knows it passes it, and a caller
|
|
69
|
+
// that does not gets the honest half of the sentence rather than a
|
|
70
|
+
// fabricated number.
|
|
71
|
+
rows.push(row("skills index", ledger.skillsIndex, `${ledger.skills > 0 ? `${ledger.skills} skill${ledger.skills === 1 ? "" : "s"}, ` : ""}tier-1 lines only`));
|
|
72
|
+
}
|
|
73
|
+
if (ledger.envelope > 0)
|
|
74
|
+
rows.push(row("envelope", ledger.envelope, ""));
|
|
75
|
+
if (ledger.messages > 0)
|
|
76
|
+
rows.push(row("messages", ledger.messages, `${ledger.turns} turn${ledger.turns === 1 ? "" : "s"}`));
|
|
77
|
+
rows.push(` ${p.dim}▱ ${"free".padEnd(14)}${k(free).padStart(5)}${p.reset}`);
|
|
78
|
+
return rows;
|
|
79
|
+
}
|
|
80
|
+
/** TUI2-R1 (E) — the honest fallback. The ledger is written PER REQUEST:
|
|
81
|
+
* a session that has not called the model yet has no sidecar, and the
|
|
82
|
+
* right thing to show is that fact and the one step that produces one.
|
|
83
|
+
* Never an empty bar — an empty bar reads as "measured zero". */
|
|
84
|
+
export function contextUnavailableRows(reason) {
|
|
85
|
+
const p = palette();
|
|
86
|
+
return [`${p.bold}context — no ledger yet${p.reset}`, ` ${p.dim}${reason}${p.reset}`];
|
|
87
|
+
}
|
package/dist/editor.d.ts
CHANGED
|
@@ -23,8 +23,9 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import { charWidth, displayWidth, widthOf } from "./width.js";
|
|
25
25
|
export { charWidth, displayWidth, widthOf };
|
|
26
|
-
import type
|
|
26
|
+
import { type PanelState, type PanelVerdict, type PanelView } from "./approval-panel.js";
|
|
27
27
|
import { type AtItem, type AtMatch } from "./at-picker.js";
|
|
28
|
+
import { type SessionCardView, type SessionPickState } from "./session-picker.js";
|
|
28
29
|
export declare const PROMPT = "\u258C ";
|
|
29
30
|
export declare const PROMPT_WIDTH: number;
|
|
30
31
|
/** v3 §04 — the slash-command menu's command table (English one-liners). */
|
|
@@ -60,6 +61,9 @@ export declare class Editor {
|
|
|
60
61
|
bindQueue(state: () => readonly string[], pop: () => string | null): void;
|
|
61
62
|
/** The whole buffer as text (the CLI's line()/clearLine()). */
|
|
62
63
|
line(): string;
|
|
64
|
+
/** TUI2-R1 (D): whether the keys sheet is up — the compositor's slot
|
|
65
|
+
* read (bound like the menu and the picker). */
|
|
66
|
+
sheetOpen(): boolean;
|
|
63
67
|
clearLine(): void;
|
|
64
68
|
/** The dock's input-row state — ADDITIVE (§5): `line` + `cursor` keep
|
|
65
69
|
* their legacy meaning (the CURSOR LINE's visible slice and the
|
|
@@ -98,6 +102,12 @@ export declare class Editor {
|
|
|
98
102
|
selected: number;
|
|
99
103
|
capped: boolean;
|
|
100
104
|
} | null;
|
|
105
|
+
/** Open the picker on a bound card source. The composer is cleared
|
|
106
|
+
* (the buffer becomes the filter query) and `onPick` receives the
|
|
107
|
+
* chosen id — or null when the human leaves without picking, which
|
|
108
|
+
* is a first-class outcome and not an error. */
|
|
109
|
+
beginPick(cards: () => readonly SessionCardView[], onPick: (id: string | null) => void): void;
|
|
110
|
+
pickState(): SessionPickState | null;
|
|
101
111
|
/** One-shot question mode: the NEXT submit answers, not a turn. */
|
|
102
112
|
question(_query: string, cb: (answer: string) => void): void;
|
|
103
113
|
/** Cancel a pending question — the buffer stays (its text becomes the
|
package/dist/editor.js
CHANGED
|
@@ -26,10 +26,15 @@ import { charWidth, displayWidth, leadWidth, widthOf } from "./width.js";
|
|
|
26
26
|
// authority) — re-exported so the editor's public surface is unchanged.
|
|
27
27
|
export { charWidth, displayWidth, widthOf };
|
|
28
28
|
import { palette } from "./render.js";
|
|
29
|
+
import { PICK_MAX } from "./approval-panel.js";
|
|
29
30
|
// KC3.5: the panel-slot dispatchers — the ask branch folded into the
|
|
30
31
|
// W21 lead/rows, so this file keeps ONE panel and one key owner.
|
|
31
32
|
import { askCommitCustom, askKey, askStart, panelLead } from "./ask-panel.js";
|
|
32
33
|
import { AT_VISIBLE, atFilter } from "./at-picker.js";
|
|
34
|
+
// TUI2-R2 ②: the session picker — the band's THIRD occupant. Its filter
|
|
35
|
+
// is the @ picker's rank aimed at the session id; the editor owns the
|
|
36
|
+
// keys, the compositor draws the rows.
|
|
37
|
+
import { sessionFilter } from "./session-picker.js";
|
|
33
38
|
// TUI v4 #16d: the input row is the blue brick + the edit area — the
|
|
34
39
|
// "you>" text is gone (the brick IS the prompt; the pipe path's readline
|
|
35
40
|
// prompt keeps its own "you> " — v2a line mode, byte-for-byte).
|
|
@@ -42,6 +47,8 @@ export const MENU_ITEMS = [
|
|
|
42
47
|
{ name: "/think", desc: "show the last full thinking block" },
|
|
43
48
|
{ name: "/last", desc: "show the most recent tool call's input and output" },
|
|
44
49
|
{ name: "/status", desc: "show session id, event count, and context estimate" },
|
|
50
|
+
// TUI2-R1 (E): the rent-ledger attribution — where the context went
|
|
51
|
+
{ name: "/context", desc: "show where the context went — the last request's rent ledger" },
|
|
45
52
|
{ name: "/help", desc: "print this list of commands" },
|
|
46
53
|
];
|
|
47
54
|
/** KC1 §3 — the newline code point. Every source (paste, Ctrl+J, the
|
|
@@ -109,6 +116,12 @@ export class Editor {
|
|
|
109
116
|
// coexist; the editor never interprets the key itself.
|
|
110
117
|
#expandCbs = [];
|
|
111
118
|
#onRender;
|
|
119
|
+
/** TUI2-R1 (D): the keys sheet — a static one-screen overlay opened by
|
|
120
|
+
* `?` on an empty composer and closed by the next key, whatever it
|
|
121
|
+
* is. Deliberately a BOOLEAN and not a panel: the panel machinery
|
|
122
|
+
* exists for interactions (a lead, a status, a reducer, a stashed
|
|
123
|
+
* buffer), and the sheet has no interaction to speak of. */
|
|
124
|
+
#sheetOpen = false;
|
|
112
125
|
#menuOpen = false; // v3 §04: the slash-command menu
|
|
113
126
|
#menuSel = 0;
|
|
114
127
|
// KC3 §3 — the @ file picker. THREE fields and no more: the armed
|
|
@@ -127,6 +140,20 @@ export class Editor {
|
|
|
127
140
|
// next open re-snapshots, so a stale list can never be shown.
|
|
128
141
|
#atList = null;
|
|
129
142
|
#atItems = null;
|
|
143
|
+
// TUI2-R2 ② — the session picker. Two fields: the bound source (its
|
|
144
|
+
// presence IS "the picker is up") and the selection. The query, like
|
|
145
|
+
// the @ picker's, is DERIVED from the buffer on every read rather
|
|
146
|
+
// than stored — so every existing buffer op (backspace, the kills,
|
|
147
|
+
// paste) filters correctly with no handler of its own.
|
|
148
|
+
//
|
|
149
|
+
// The picker is MODAL in a way the @ picker is not: it opens before
|
|
150
|
+
// a session exists, owns the whole composer, and the only ways out
|
|
151
|
+
// are a pick and an esc. That is why the commit callback lives here
|
|
152
|
+
// rather than on the line channel — the caller is waiting for an id,
|
|
153
|
+
// not for a turn.
|
|
154
|
+
#pickCards = null;
|
|
155
|
+
#pickCommit = null;
|
|
156
|
+
#pickSel = 0;
|
|
130
157
|
// A2 (the feel): the session-scoped input history — every submitted TURN
|
|
131
158
|
// line (never a question answer), capped at 100, never persisted. ↑↓
|
|
132
159
|
// navigate it ONLY from an empty input or while already browsing.
|
|
@@ -194,6 +221,11 @@ export class Editor {
|
|
|
194
221
|
line() {
|
|
195
222
|
return String.fromCodePoint(...this.#chars);
|
|
196
223
|
}
|
|
224
|
+
/** TUI2-R1 (D): whether the keys sheet is up — the compositor's slot
|
|
225
|
+
* read (bound like the menu and the picker). */
|
|
226
|
+
sheetOpen() {
|
|
227
|
+
return this.#sheetOpen;
|
|
228
|
+
}
|
|
197
229
|
clearLine() {
|
|
198
230
|
this.#chars = [];
|
|
199
231
|
this.#cursor = 0;
|
|
@@ -241,7 +273,7 @@ export class Editor {
|
|
|
241
273
|
* the frame's clamp is the authority. */
|
|
242
274
|
#visibleRows(lineCount) {
|
|
243
275
|
const H = process.stdout.rows ?? 24;
|
|
244
|
-
const bands = (this.#menuOpen ? this.#menuFiltered().length : 0) + this.#atRows() + this.#queueState().length;
|
|
276
|
+
const bands = (this.#menuOpen ? this.#menuFiltered().length : 0) + this.#atRows() + this.#pickRows() + this.#queueState().length;
|
|
245
277
|
return Math.max(1, Math.min(lineCount, N_MAX, Math.max(1, H - 3 - bands)));
|
|
246
278
|
}
|
|
247
279
|
/** The dock's input-row state — ADDITIVE (§5): `line` + `cursor` keep
|
|
@@ -379,6 +411,11 @@ export class Editor {
|
|
|
379
411
|
#atArm() {
|
|
380
412
|
if (this.#atItems === null)
|
|
381
413
|
return;
|
|
414
|
+
// TUI2-R2 ②: not inside a session filter. An `@` typed into the
|
|
415
|
+
// picker's query is a character in a session id, and a file picker
|
|
416
|
+
// opening over a session picker would put two bands in one slot.
|
|
417
|
+
if (this.#pickUp())
|
|
418
|
+
return;
|
|
382
419
|
if (this.#panel !== null || this.#menuOpen || this.#questionCb !== null)
|
|
383
420
|
return;
|
|
384
421
|
if (this.#atToken() === null)
|
|
@@ -424,6 +461,71 @@ export class Editor {
|
|
|
424
461
|
const view = this.#atView();
|
|
425
462
|
return view === null ? 0 : Math.min(view.matches.length, AT_VISIBLE) + 1;
|
|
426
463
|
}
|
|
464
|
+
// ── TUI2-R2 ② — the session picker ───────────────────────────────
|
|
465
|
+
/** Open the picker on a bound card source. The composer is cleared
|
|
466
|
+
* (the buffer becomes the filter query) and `onPick` receives the
|
|
467
|
+
* chosen id — or null when the human leaves without picking, which
|
|
468
|
+
* is a first-class outcome and not an error. */
|
|
469
|
+
beginPick(cards, onPick) {
|
|
470
|
+
this.#pickCards = cards;
|
|
471
|
+
this.#pickCommit = onPick;
|
|
472
|
+
this.#pickSel = 0;
|
|
473
|
+
this.#chars = [];
|
|
474
|
+
this.#cursor = 0;
|
|
475
|
+
this.#reflow();
|
|
476
|
+
this.#onRender();
|
|
477
|
+
}
|
|
478
|
+
/** The picker's state, derived: the full card list (the id column
|
|
479
|
+
* measures over ALL of them, so the columns never jump), the
|
|
480
|
+
* filtered matches, and the selection CLAMPED at read time — the
|
|
481
|
+
* same correction discipline the @ picker uses, for the same
|
|
482
|
+
* reason: narrowing can only ever shrink the list. */
|
|
483
|
+
#pickView() {
|
|
484
|
+
if (this.#pickCards === null)
|
|
485
|
+
return null;
|
|
486
|
+
const cards = this.#pickCards();
|
|
487
|
+
const matches = sessionFilter(cards, this.line());
|
|
488
|
+
return { cards, matches, selected: Math.max(0, Math.min(this.#pickSel, matches.length - 1)) };
|
|
489
|
+
}
|
|
490
|
+
pickState() {
|
|
491
|
+
return this.#pickView();
|
|
492
|
+
}
|
|
493
|
+
#pickUp() {
|
|
494
|
+
return this.#pickCards !== null;
|
|
495
|
+
}
|
|
496
|
+
/** The band's height estimate: the header + the windowed rows (or
|
|
497
|
+
* the one "no match" row) + the counter. */
|
|
498
|
+
#pickRows() {
|
|
499
|
+
const view = this.#pickView();
|
|
500
|
+
return view === null ? 0 : Math.min(Math.max(view.matches.length, 1), AT_VISIBLE) + 2;
|
|
501
|
+
}
|
|
502
|
+
/** Close and hand the verdict back. The callback fires AFTER the
|
|
503
|
+
* state is cleared, so a caller that re-enters (a second picker, a
|
|
504
|
+
* session that starts) never sees the closing picker's rows. */
|
|
505
|
+
#pickClose(id) {
|
|
506
|
+
const cb = this.#pickCommit;
|
|
507
|
+
this.#pickCards = null;
|
|
508
|
+
this.#pickCommit = null;
|
|
509
|
+
this.#pickSel = 0;
|
|
510
|
+
this.#chars = [];
|
|
511
|
+
this.#cursor = 0;
|
|
512
|
+
this.#reflow();
|
|
513
|
+
cb?.(id);
|
|
514
|
+
this.#onRender();
|
|
515
|
+
}
|
|
516
|
+
/** Enter takes the SELECTED session. An empty match set takes
|
|
517
|
+
* nothing and leaves the picker up: a picker that invented a pick
|
|
518
|
+
* when the query matched nothing would resume the wrong session,
|
|
519
|
+
* which is the one failure this surface must never have. */
|
|
520
|
+
#pickAccept() {
|
|
521
|
+
const view = this.#pickView();
|
|
522
|
+
if (view === null)
|
|
523
|
+
return;
|
|
524
|
+
const card = view.matches[view.selected];
|
|
525
|
+
if (card === undefined)
|
|
526
|
+
return;
|
|
527
|
+
this.#pickClose(card.id);
|
|
528
|
+
}
|
|
427
529
|
/** One-shot question mode: the NEXT submit answers, not a turn. */
|
|
428
530
|
question(_query, cb) {
|
|
429
531
|
this.#questionCb = cb;
|
|
@@ -442,6 +544,9 @@ export class Editor {
|
|
|
442
544
|
phase: "options",
|
|
443
545
|
sel: 0,
|
|
444
546
|
ask: view.ask === undefined ? null : askStart(view.ask),
|
|
547
|
+
// TUI2-R2 ④: the pick's walk — present exactly when the view is
|
|
548
|
+
// a pick, the same contract the ask's runtime has.
|
|
549
|
+
pick: view.pick === undefined ? null : { cursor: 0, phase: "options" },
|
|
445
550
|
amend: "yes",
|
|
446
551
|
onCommit,
|
|
447
552
|
stash: { chars: this.#chars, cursor: this.#cursor, scroll: this.#scroll },
|
|
@@ -466,7 +571,13 @@ export class Editor {
|
|
|
466
571
|
const panel = this.#panel;
|
|
467
572
|
if (panel === null)
|
|
468
573
|
return null;
|
|
469
|
-
return {
|
|
574
|
+
return {
|
|
575
|
+
view: panel.view,
|
|
576
|
+
phase: panel.phase,
|
|
577
|
+
sel: panel.sel,
|
|
578
|
+
...(panel.ask === null ? {} : { ask: panel.ask }),
|
|
579
|
+
...(panel.pick === null ? {} : { pick: panel.pick }),
|
|
580
|
+
};
|
|
470
581
|
}
|
|
471
582
|
enter() {
|
|
472
583
|
if (this.#entered)
|
|
@@ -510,6 +621,17 @@ export class Editor {
|
|
|
510
621
|
feed(raw) {
|
|
511
622
|
const text = this.#pending + this.#decoder.decode(raw, { stream: true });
|
|
512
623
|
this.#pending = "";
|
|
624
|
+
// TUI2-R1 (D): the sheet is up — ANY key closes it, and the key
|
|
625
|
+
// that closed it is CONSUMED. The whole chunk goes, deliberately:
|
|
626
|
+
// an arrow key is three bytes, and closing on the first while
|
|
627
|
+
// letting `[A` fall through as literal text would be a sheet that
|
|
628
|
+
// types into your composer on the way out. A dismissal costs one
|
|
629
|
+
// keystroke; that is the entire contract.
|
|
630
|
+
if (this.#sheetOpen) {
|
|
631
|
+
this.#sheetOpen = false;
|
|
632
|
+
this.#onRender();
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
513
635
|
let i = 0;
|
|
514
636
|
while (i < text.length) {
|
|
515
637
|
const c = text[i];
|
|
@@ -530,6 +652,44 @@ export class Editor {
|
|
|
530
652
|
// only esc and enter are intercepted while typing), esc
|
|
531
653
|
// declines the whole call. Everything else falls through to
|
|
532
654
|
// the ordinary editing chain below.
|
|
655
|
+
// TUI2-R2 ④: a PICK panel routes its own keys — a digit moves
|
|
656
|
+
// the cursor to that option (never commits: the choice is
|
|
657
|
+
// CONFIRMED, so a mistyped digit is a mistake you can see
|
|
658
|
+
// before it takes effect), `t` opens the type-it line, enter
|
|
659
|
+
// commits, esc backs out then cancels. The swallow rule below
|
|
660
|
+
// is the ask's, for the ask's reason: a typed `/` must not arm
|
|
661
|
+
// the menu under a panel that owns the keys.
|
|
662
|
+
if (panel.pick !== null) {
|
|
663
|
+
const typing = panel.pick.phase === "custom";
|
|
664
|
+
if (c === "\x1b" && !text.slice(i + 1).startsWith("[") && !text.slice(i + 1).startsWith("O")) {
|
|
665
|
+
this.#pickPanelEsc();
|
|
666
|
+
i += 1;
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
if (c === "\x0d" || c === "\x0a") {
|
|
670
|
+
this.#pickPanelEnter();
|
|
671
|
+
i += 1;
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
if (!typing && c !== undefined && c >= "1" && c <= "9") {
|
|
675
|
+
this.#pickPanelDigit(Number(c) - 1);
|
|
676
|
+
i += 1;
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
679
|
+
if (!typing && (c === "t" || c === "T")) {
|
|
680
|
+
panel.pick = { cursor: panel.pick.cursor, phase: "custom" };
|
|
681
|
+
this.#chars = [];
|
|
682
|
+
this.#cursor = 0;
|
|
683
|
+
this.#scroll = 0;
|
|
684
|
+
this.#onRender();
|
|
685
|
+
i += 1;
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
if (!typing && c !== undefined && c >= " " && c !== "\x7f") {
|
|
689
|
+
i += 1;
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
533
693
|
if (panel.ask !== null) {
|
|
534
694
|
const typing = panel.ask.phase === "custom";
|
|
535
695
|
if (c === "\x1b" && !text.slice(i + 1).startsWith("[") && !text.slice(i + 1).startsWith("O")) {
|
|
@@ -578,22 +738,49 @@ export class Editor {
|
|
|
578
738
|
i += 1;
|
|
579
739
|
continue;
|
|
580
740
|
}
|
|
581
|
-
|
|
582
|
-
|
|
741
|
+
// TUI2-R2 ⑧ — the shortcut keys belong to the OPTIONS phase and
|
|
742
|
+
// to it alone.
|
|
743
|
+
//
|
|
744
|
+
// `1`/`y` select yes and `3`/`n` select no, and they used to be
|
|
745
|
+
// applied in every phase of every flavour: the `i += 1;
|
|
746
|
+
// continue;` sat OUTSIDE the phase check, so a phase where the
|
|
747
|
+
// key meant nothing swallowed it anyway. A phase where a letter
|
|
748
|
+
// means nothing is exactly a phase where a human is typing
|
|
749
|
+
// prose — so every y, n, 1 and 3 vanished from the line,
|
|
750
|
+
// silently, with no error and no visible cause. "yes, run it
|
|
751
|
+
// now 13" committed as "es, ru it ow ".
|
|
752
|
+
//
|
|
753
|
+
// Three typed phases were affected: the ask's custom answer,
|
|
754
|
+
// the approval panel's rule input, and its amend/feedback line.
|
|
755
|
+
// The rule input is the one that mattered most — it writes a
|
|
756
|
+
// DURABLE don't-ask-again rule, so a dropped character persists
|
|
757
|
+
// a rule the human never typed.
|
|
758
|
+
//
|
|
759
|
+
// Slice ④ met the same mechanism on the new pick panel
|
|
760
|
+
// ("openai/deepseek-reasoner" -> "opeai/deepseek-reasoer") and
|
|
761
|
+
// guarded pick alone, because the rest was a behaviour change
|
|
762
|
+
// owed its own red. This is that guard, stated once for every
|
|
763
|
+
// flavour: the options phase keeps its keys, and every typed
|
|
764
|
+
// phase keeps its text.
|
|
765
|
+
const optionsPhase = panel.pick === null && // a pick has no yes and no no
|
|
766
|
+
panel.phase === "options" && // the rule / amend lines are prose
|
|
767
|
+
(panel.ask === null || panel.ask.phase === "options"); // and so is a typed ask answer
|
|
768
|
+
if (optionsPhase) {
|
|
769
|
+
if (c === "1" || c === "y" || c === "Y") {
|
|
583
770
|
this.#panelSelect(1);
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
if (panel.phase === "options")
|
|
771
|
+
i += 1;
|
|
772
|
+
continue;
|
|
773
|
+
}
|
|
774
|
+
if (c === "2" && panel.view.flavor === "approval") {
|
|
775
|
+
this.#panelRule();
|
|
776
|
+
i += 1;
|
|
777
|
+
continue;
|
|
778
|
+
}
|
|
779
|
+
if (c === "3" || c === "n" || c === "N") {
|
|
594
780
|
this.#panelSelect(3);
|
|
595
|
-
|
|
596
|
-
|
|
781
|
+
i += 1;
|
|
782
|
+
continue;
|
|
783
|
+
}
|
|
597
784
|
}
|
|
598
785
|
}
|
|
599
786
|
if (c === "\x1b") {
|
|
@@ -632,6 +819,15 @@ export class Editor {
|
|
|
632
819
|
this.#refreshMenu();
|
|
633
820
|
i += 1;
|
|
634
821
|
}
|
|
822
|
+
else if (this.#pickUp()) {
|
|
823
|
+
// TUI2-R2 ②: esc leaves the picker with nothing picked.
|
|
824
|
+
// The caller reads null and exits 0 — declining to resume
|
|
825
|
+
// is a normal thing to do, not a failure, so it must not
|
|
826
|
+
// fall through to the escapeCbs (which mean "abort the
|
|
827
|
+
// run" and there is no run yet).
|
|
828
|
+
this.#pickClose(null);
|
|
829
|
+
i += 1;
|
|
830
|
+
}
|
|
635
831
|
else if (this.#atUp()) {
|
|
636
832
|
// KC3 §3: esc closes the picker and leaves the BUFFER
|
|
637
833
|
// ALONE — unlike the menu's esc, which clears it. The
|
|
@@ -752,6 +948,17 @@ export class Editor {
|
|
|
752
948
|
cb();
|
|
753
949
|
i += 1;
|
|
754
950
|
}
|
|
951
|
+
else if (c === "?" && this.#composerIdle() && this.#chars.length === 0) {
|
|
952
|
+
// TUI2-R1 (D): `?` opens the keys sheet — but ONLY on an
|
|
953
|
+
// empty composer with nobody else holding the keys. Mid-text
|
|
954
|
+
// it is the question mark a human is typing, and #composerIdle
|
|
955
|
+
// already encodes "no panel, no menu, no picker, no browse".
|
|
956
|
+
// The precedence can only ever ADD: every state that used to
|
|
957
|
+
// insert a `?` still inserts one.
|
|
958
|
+
this.#sheetOpen = true;
|
|
959
|
+
this.#onRender();
|
|
960
|
+
i += 1;
|
|
961
|
+
}
|
|
755
962
|
else if (c !== undefined && c < " ") {
|
|
756
963
|
i += 1; // other control — ignored
|
|
757
964
|
}
|
|
@@ -804,9 +1011,25 @@ export class Editor {
|
|
|
804
1011
|
if (this.#panel !== null) {
|
|
805
1012
|
// W21: the panel owns the keys. KC3.5: an ask uses ↑↓ for
|
|
806
1013
|
// the option cursor (the approval panel still has no ↑↓ role).
|
|
807
|
-
if (this.#panel.
|
|
1014
|
+
if (this.#panel.pick !== null && this.#panel.pick.phase === "options") {
|
|
1015
|
+
// TUI2-R2 ④: ↑↓ walk the pick's cursor — the same list the
|
|
1016
|
+
// digits address, the other muscle.
|
|
1017
|
+
const n = Math.min(this.#panel.view.pick.options.length, PICK_MAX);
|
|
1018
|
+
const cur = this.#panel.pick.cursor;
|
|
1019
|
+
this.#panel.pick = { cursor: final === "A" ? Math.max(0, cur - 1) : Math.min(Math.max(0, n - 1), cur + 1), phase: "options" };
|
|
1020
|
+
}
|
|
1021
|
+
else if (this.#panel.ask !== null && this.#panel.ask.phase === "options")
|
|
808
1022
|
this.#askStep(final === "A" ? "up" : "down");
|
|
809
1023
|
}
|
|
1024
|
+
else if (this.#pickUp()) {
|
|
1025
|
+
// TUI2-R2 ②: the session picker owns ↑↓ while up — the
|
|
1026
|
+
// SELECTION, never the composer's line walk and never the
|
|
1027
|
+
// history browse. It sits above both because the picker is
|
|
1028
|
+
// modal: there is no turn to recall and no second line to
|
|
1029
|
+
// walk to while it is open.
|
|
1030
|
+
const view = this.#pickView();
|
|
1031
|
+
this.#pickSel = final === "A" ? Math.max(0, view.selected - 1) : Math.min(Math.max(0, view.matches.length - 1), view.selected + 1);
|
|
1032
|
+
}
|
|
810
1033
|
else if (this.#menuOpen) {
|
|
811
1034
|
if (final === "A")
|
|
812
1035
|
this.#menuSel = Math.max(0, this.#menuSel - 1);
|
|
@@ -990,6 +1213,60 @@ export class Editor {
|
|
|
990
1213
|
}
|
|
991
1214
|
this.#onRender();
|
|
992
1215
|
}
|
|
1216
|
+
/**
|
|
1217
|
+
* TUI2-R2 ④ — the pick panel's three keys.
|
|
1218
|
+
*
|
|
1219
|
+
* A digit MOVES the cursor rather than committing. The list is short
|
|
1220
|
+
* and the digits are adjacent on the keyboard; a picker that acted on
|
|
1221
|
+
* the keypress would make a mistyped 3 a model switch, and the whole
|
|
1222
|
+
* point of a confirm step is that the choice is visible before it is
|
|
1223
|
+
* taken.
|
|
1224
|
+
*/
|
|
1225
|
+
#pickPanelDigit(index) {
|
|
1226
|
+
const panel = this.#panel;
|
|
1227
|
+
if (panel === null || panel.pick === null)
|
|
1228
|
+
return;
|
|
1229
|
+
// a digit past the list is INERT — an option nobody has is never
|
|
1230
|
+
// selected, and the cursor stays where the human left it
|
|
1231
|
+
if (index < 0 || index >= Math.min(panel.view.pick.options.length, PICK_MAX))
|
|
1232
|
+
return;
|
|
1233
|
+
panel.pick = { cursor: index, phase: "options" };
|
|
1234
|
+
this.#onRender();
|
|
1235
|
+
}
|
|
1236
|
+
/** enter — the typed line when there is one (an EMPTY line is not a
|
|
1237
|
+
* choice and commits nothing), else the option under the cursor. */
|
|
1238
|
+
#pickPanelEnter() {
|
|
1239
|
+
const panel = this.#panel;
|
|
1240
|
+
if (panel === null || panel.pick === null)
|
|
1241
|
+
return;
|
|
1242
|
+
if (panel.pick.phase === "custom") {
|
|
1243
|
+
const line = this.line().trim();
|
|
1244
|
+
if (line === "")
|
|
1245
|
+
return;
|
|
1246
|
+
this.#panelClose({ action: "picked", result: { custom: line } });
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
if (panel.view.pick.options.length === 0)
|
|
1250
|
+
return; // nothing to take
|
|
1251
|
+
this.#panelClose({ action: "picked", result: { index: panel.pick.cursor } });
|
|
1252
|
+
}
|
|
1253
|
+
/** esc — back out of the type-it line first, then cancel the panel.
|
|
1254
|
+
* Two escapes, two meanings, exactly as the approval panel's
|
|
1255
|
+
* rule/amend phases already work. */
|
|
1256
|
+
#pickPanelEsc() {
|
|
1257
|
+
const panel = this.#panel;
|
|
1258
|
+
if (panel === null || panel.pick === null)
|
|
1259
|
+
return;
|
|
1260
|
+
if (panel.pick.phase === "custom") {
|
|
1261
|
+
panel.pick = { cursor: panel.pick.cursor, phase: "options" };
|
|
1262
|
+
this.#chars = [];
|
|
1263
|
+
this.#cursor = 0;
|
|
1264
|
+
this.#scroll = 0;
|
|
1265
|
+
this.#onRender();
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
this.#panelClose({ action: "cancel" });
|
|
1269
|
+
}
|
|
993
1270
|
#panelClose(verdict) {
|
|
994
1271
|
const panel = this.#panel;
|
|
995
1272
|
if (panel === null)
|
|
@@ -1113,6 +1390,7 @@ export class Editor {
|
|
|
1113
1390
|
return (this.#panel === null &&
|
|
1114
1391
|
!this.#menuOpen &&
|
|
1115
1392
|
!this.#atUp() && // KC3 §3: the @ picker owns the keys while up, exactly like the menu
|
|
1393
|
+
!this.#pickUp() && // TUI2-R2 ②: and so does the session picker — `?` is a query character there
|
|
1116
1394
|
this.#historyIdx === null &&
|
|
1117
1395
|
!this.#queuePopMode &&
|
|
1118
1396
|
!this.#pasting &&
|
|
@@ -1145,6 +1423,12 @@ export class Editor {
|
|
|
1145
1423
|
this.#onRender();
|
|
1146
1424
|
}
|
|
1147
1425
|
#submit() {
|
|
1426
|
+
// TUI2-R2 ②: the session picker takes Enter before anything else —
|
|
1427
|
+
// while it is up there is no turn to submit and no line to send.
|
|
1428
|
+
if (this.#pickUp()) {
|
|
1429
|
+
this.#pickAccept();
|
|
1430
|
+
return;
|
|
1431
|
+
}
|
|
1148
1432
|
// KC3 §3: Enter ACCEPTS while the picker is up — the same rule the
|
|
1149
1433
|
// menu's A1 feel established (complete first, let the user read
|
|
1150
1434
|
// what they got, and let the NEXT Enter send it). An @ reference
|
package/dist/index.d.ts
CHANGED
|
@@ -6,13 +6,15 @@
|
|
|
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";
|
|
10
|
-
export { Container, foldLine, visibleWidth, SPINNER, type Component, type FrameCtx } from "./components.js";
|
|
9
|
+
export { panelAffordance, panelBlockRows, panelLead, panelLeadPlain, panelLeadWidth, panelStatus, PICK_MAX, modelPickView, pickAffordance, pickBlockRows, pickLeadPlain, type PickOption, type PickResult, type PickRuntime, type PickSpec, type PanelArgs, type PanelFlavor, type PanelPhase, type PanelSel, type PanelState, type PanelVerdict, type PanelView, } from "./approval-panel.js";
|
|
10
|
+
export { Container, foldLine, foldWords, visibleWidth, SPINNER, type Component, type FrameCtx } from "./components.js";
|
|
11
11
|
export { Editor, MENU_ITEMS, PROMPT, PROMPT_WIDTH, displayWidth, charWidth, widthOf, type MenuItem, } from "./editor.js";
|
|
12
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";
|
|
13
13
|
export { editFileDiff, truncateDiff, writeFileDiff, type DiffLine, type DiffResult } from "./diff.js";
|
|
14
|
-
export { STATUS_GLYPHS, idleStatus, runningStatus } from "./status.js";
|
|
14
|
+
export { STATUS_GLYPHS, cacheHitPct, idleStatus, runningStatus, type StatusMeter } from "./status.js";
|
|
15
|
+
export { contextRows, contextUnavailableRows, type ContextLedger } from "./context-ledger.js";
|
|
15
16
|
export { interactivePrompt, projectTrustRows, projectTrustView, projectUntrustedNote, uncertainView, type TrustArtifact } from "./strings.js";
|
|
16
|
-
export { AT_CAP, AT_SKIP, AT_VISIBLE, atEmbed, atFilter, atPanelRows, atWindow, longestRun, type AtItem, type AtMatch } from "./at-picker.js";
|
|
17
|
+
export { AT_CAP, AT_SKIP, AT_VISIBLE, atEmbed, atFilter, atPanelRows, atWindow, bandHeader, longestRun, type AtItem, type AtMatch } from "./at-picker.js";
|
|
18
|
+
export { BADGE_GLYPH, idColumn, sessionAge, sessionBadge, sessionCounterRow, sessionFilter, sessionListFooter, sessionListRow, sessionNote, sessionPickerRows, sessionRow, type SessionCardView, type SessionPickState, } from "./session-picker.js";
|
|
17
19
|
export { ASK_HEADER_CAP, ASK_MAX_OPTIONS, ASK_MAX_QUESTIONS, ASK_MIN_OPTIONS, askAffordance, askAnswers, askBlockRows, askCommitCustom, askDeclineAll, askDeclineList, askKey, askLeadPlain, askStart, askStatus, askView, type AskAnswer, type AskOption, type AskQuestion, type AskResult, type AskRuntime, type AskSpec, type AskStep, } from "./ask-panel.js";
|
|
18
|
-
export { extensionsBannerText, helpRows, unansweredAskView, type BannerExtension } from "./strings.js";
|
|
20
|
+
export { KEY_BINDINGS, PANEL_KEYS_ROW, displayVerb, extensionsBannerText, helpRows, keysHelpRow, keysSheetRows, unansweredAskView, type BannerExtension, type KeyBinding } from "./strings.js";
|