@vincemakes/kiso-tui 0.7.0 → 0.8.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 +95 -0
- package/dist/ask-panel.js +268 -0
- package/dist/compositor.d.ts +1 -1
- package/dist/compositor.js +8 -6
- package/dist/editor.d.ts +1 -1
- package/dist/editor.js +83 -6
- package/dist/index.d.ts +2 -0
- package/dist/index.js +7 -0
- package/package.json +2 -2
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KC3.5 — the ask view: the model asks the human a real question.
|
|
3
|
+
*
|
|
4
|
+
* The W21 approval panel generalizes rather than duplicates. An ask is a
|
|
5
|
+
* PanelView carrying `ask`, so it rides the EXISTING panel slot: the
|
|
6
|
+
* compositor's live region, the input row's lead, the status/affordance
|
|
7
|
+
* pair, and — the part that matters — the editor's buffer stash/restore
|
|
8
|
+
* and its key precedence (the panel owns the keys; the menu, the history
|
|
9
|
+
* walk and the @ picker never open under it).
|
|
10
|
+
*
|
|
11
|
+
* This module owns three things and nothing else:
|
|
12
|
+
*
|
|
13
|
+
* 1. the ROWS — the question, the ‹ n/m › counter, the numbered
|
|
14
|
+
* options with their descriptions and selection marks, the
|
|
15
|
+
* type-your-own line;
|
|
16
|
+
* 2. the pure REDUCER — a key plus a state gives the next state (and,
|
|
17
|
+
* when the walk ends, the result). No I/O, no editor internals: the
|
|
18
|
+
* editor feeds it keys and renders what comes back, which is what
|
|
19
|
+
* makes the whole interaction unit-testable without a terminal;
|
|
20
|
+
* 3. the DISPATCHERS — panelBlockRows/panelLead/panelStatus/
|
|
21
|
+
* panelAffordance re-exported with the ask branch folded in, so the
|
|
22
|
+
* compositor and the editor change ONE import line between them and
|
|
23
|
+
* the panel slot itself stays exactly as W21 built it.
|
|
24
|
+
*
|
|
25
|
+
* What is deliberately NOT here (the round's stop clauses): no partial
|
|
26
|
+
* answer durability (a crash re-presents the WHOLE call — per-toggle
|
|
27
|
+
* durability would need a new durable mechanism), no "chat about this"
|
|
28
|
+
* hand-off, no timeout and no countdown.
|
|
29
|
+
*/
|
|
30
|
+
import { type AskAnswer, type AskOption, type AskQuestion, type AskResult, type AskRuntime, type AskSpec, type PanelPhase, type PanelSel, type PanelState, type PanelView } from "./approval-panel.js";
|
|
31
|
+
/** The schema's own bounds — the registry refuses anything outside them
|
|
32
|
+
* (extensions/ask validates; these are the numbers it validates to). */
|
|
33
|
+
export declare const ASK_MAX_QUESTIONS = 4;
|
|
34
|
+
export declare const ASK_MIN_OPTIONS = 2;
|
|
35
|
+
export declare const ASK_MAX_OPTIONS = 4;
|
|
36
|
+
export declare const ASK_HEADER_CAP = 12;
|
|
37
|
+
/** The ask panel's opening state: nothing picked, the cursor on the
|
|
38
|
+
* first option of the first question. */
|
|
39
|
+
export declare function askStart(spec: AskSpec): AskRuntime;
|
|
40
|
+
/** The decline's honest record: every question with its options, so the
|
|
41
|
+
* model reads what it did NOT get an answer to (frame 4). The SAME
|
|
42
|
+
* list whether the human pressed esc on question one or question four
|
|
43
|
+
* — the round declines the CALL, never half of it. */
|
|
44
|
+
export declare function askDeclineList(spec: AskSpec): string[];
|
|
45
|
+
export declare function askDeclineAll(spec: AskSpec): AskResult;
|
|
46
|
+
/** The answers collected so far, in the tool_result's own shapes: a
|
|
47
|
+
* typed answer wins over the picks (the human typed it last), a
|
|
48
|
+
* multi-select question yields `choices`, a single one `choice`. */
|
|
49
|
+
export declare function askAnswers(spec: AskSpec, state: AskRuntime): AskAnswer[];
|
|
50
|
+
/** The reducer's outcome: the next state, plus the RESULT when the walk
|
|
51
|
+
* ended (the last question answered, or the decline). */
|
|
52
|
+
export interface AskStep {
|
|
53
|
+
readonly state: AskRuntime;
|
|
54
|
+
readonly result?: AskResult;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The pure key reducer. `key` is a single logical key: a digit "1".."4",
|
|
58
|
+
* "space", "up"/"down", "left" (walk back), "enter", "t" (type your own),
|
|
59
|
+
* or "esc". The custom phase's TEXT is not routed here — the editor owns
|
|
60
|
+
* the buffer exactly as it does for the rule-input phase, and hands the
|
|
61
|
+
* committed line to `askCommitCustom`.
|
|
62
|
+
*/
|
|
63
|
+
export declare function askKey(spec: AskSpec, state: AskRuntime, key: string): AskStep;
|
|
64
|
+
/** The typed answer commits: it becomes THE answer for this question
|
|
65
|
+
* (clearing its picks) and the walk advances. An empty line is a
|
|
66
|
+
* no-op back to the options — nothing is recorded. */
|
|
67
|
+
export declare function askCommitCustom(spec: AskSpec, state: AskRuntime, text: string): AskStep;
|
|
68
|
+
/** The ask block's rows — the question as the rule line, the header (or
|
|
69
|
+
* the counter) as the title, the options as the body, and the
|
|
70
|
+
* type-your-own line last. The shape is the W21 block's: gutter, rule,
|
|
71
|
+
* title, divider, body, affordance, corner. */
|
|
72
|
+
export declare function askBlockRows(view: PanelView, state: AskRuntime, W: number, maxRows: number): string[];
|
|
73
|
+
/** The status row's right-hand hint — the phase's keys. */
|
|
74
|
+
export declare function askAffordance(state: AskRuntime): string;
|
|
75
|
+
/** The status row's left text — the ask's own line, with the walk. */
|
|
76
|
+
export declare function askStatus(view: PanelView, state: AskRuntime): string;
|
|
77
|
+
/** The input row's lead: the digit lead while picking, the typing lead
|
|
78
|
+
* in the custom phase (the rule-input phase's shape, reused). */
|
|
79
|
+
export declare function askLeadPlain(state: AskRuntime): string;
|
|
80
|
+
export declare function panelBlockRows(view: PanelView, phase: PanelPhase, sel: PanelSel, W: number, maxRows: number, ask?: AskRuntime): string[];
|
|
81
|
+
export declare function panelLead(view: PanelView, phase: PanelPhase, sel: PanelSel, ask?: AskRuntime): string;
|
|
82
|
+
export declare function panelLeadPlain(view: PanelView, phase: PanelPhase, sel: PanelSel, ask?: AskRuntime): string;
|
|
83
|
+
export declare function panelStatus(view: PanelView, phase: PanelPhase, sel: PanelSel, ask?: AskRuntime): string;
|
|
84
|
+
export declare function panelAffordance(view: PanelView, phase: PanelPhase, sel: PanelSel, ask?: AskRuntime): string;
|
|
85
|
+
/** The whole panel state in one call — the compositor's four reads share
|
|
86
|
+
* one source, so an ask can never render half as an approval. */
|
|
87
|
+
export declare const panelRowsOf: (s: PanelState, W: number, maxRows: number) => string[];
|
|
88
|
+
export declare const panelLeadOf: (s: PanelState) => string;
|
|
89
|
+
export declare const panelStatusOf: (s: PanelState) => string;
|
|
90
|
+
export declare const panelAffordanceOf: (s: PanelState) => string;
|
|
91
|
+
/** The ask's PanelView. The dock-less fallback question is HONEST: a
|
|
92
|
+
* terminal without a panel cannot walk options, so it says the ask is
|
|
93
|
+
* being declined rather than pretending y/n answered it. */
|
|
94
|
+
export declare function askView(spec: AskSpec): PanelView;
|
|
95
|
+
export type { AskAnswer, AskOption, AskQuestion, AskResult, AskRuntime, AskSpec };
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KC3.5 — the ask view: the model asks the human a real question.
|
|
3
|
+
*
|
|
4
|
+
* The W21 approval panel generalizes rather than duplicates. An ask is a
|
|
5
|
+
* PanelView carrying `ask`, so it rides the EXISTING panel slot: the
|
|
6
|
+
* compositor's live region, the input row's lead, the status/affordance
|
|
7
|
+
* pair, and — the part that matters — the editor's buffer stash/restore
|
|
8
|
+
* and its key precedence (the panel owns the keys; the menu, the history
|
|
9
|
+
* walk and the @ picker never open under it).
|
|
10
|
+
*
|
|
11
|
+
* This module owns three things and nothing else:
|
|
12
|
+
*
|
|
13
|
+
* 1. the ROWS — the question, the ‹ n/m › counter, the numbered
|
|
14
|
+
* options with their descriptions and selection marks, the
|
|
15
|
+
* type-your-own line;
|
|
16
|
+
* 2. the pure REDUCER — a key plus a state gives the next state (and,
|
|
17
|
+
* when the walk ends, the result). No I/O, no editor internals: the
|
|
18
|
+
* editor feeds it keys and renders what comes back, which is what
|
|
19
|
+
* makes the whole interaction unit-testable without a terminal;
|
|
20
|
+
* 3. the DISPATCHERS — panelBlockRows/panelLead/panelStatus/
|
|
21
|
+
* panelAffordance re-exported with the ask branch folded in, so the
|
|
22
|
+
* compositor and the editor change ONE import line between them and
|
|
23
|
+
* the panel slot itself stays exactly as W21 built it.
|
|
24
|
+
*
|
|
25
|
+
* What is deliberately NOT here (the round's stop clauses): no partial
|
|
26
|
+
* answer durability (a crash re-presents the WHOLE call — per-toggle
|
|
27
|
+
* durability would need a new durable mechanism), no "chat about this"
|
|
28
|
+
* hand-off, no timeout and no countdown.
|
|
29
|
+
*/
|
|
30
|
+
import { panelAffordance as basePanelAffordance, panelBlockRows as basePanelBlockRows, panelLead as basePanelLead, panelLeadPlain as basePanelLeadPlain, panelStatus as basePanelStatus, } from "./approval-panel.js";
|
|
31
|
+
import { cutLine } from "@vincemakes/kiso-tui-cells/components";
|
|
32
|
+
import { escapeTerminal, palette } from "./render.js";
|
|
33
|
+
/** The schema's own bounds — the registry refuses anything outside them
|
|
34
|
+
* (extensions/ask validates; these are the numbers it validates to). */
|
|
35
|
+
export const ASK_MAX_QUESTIONS = 4;
|
|
36
|
+
export const ASK_MIN_OPTIONS = 2;
|
|
37
|
+
export const ASK_MAX_OPTIONS = 4;
|
|
38
|
+
export const ASK_HEADER_CAP = 12;
|
|
39
|
+
/** The ask panel's opening state: nothing picked, the cursor on the
|
|
40
|
+
* first option of the first question. */
|
|
41
|
+
export function askStart(spec) {
|
|
42
|
+
return {
|
|
43
|
+
qIndex: 0,
|
|
44
|
+
cursor: 0,
|
|
45
|
+
picks: spec.questions.map(() => []),
|
|
46
|
+
custom: spec.questions.map(() => null),
|
|
47
|
+
phase: "options",
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** The decline's honest record: every question with its options, so the
|
|
51
|
+
* model reads what it did NOT get an answer to (frame 4). The SAME
|
|
52
|
+
* list whether the human pressed esc on question one or question four
|
|
53
|
+
* — the round declines the CALL, never half of it. */
|
|
54
|
+
export function askDeclineList(spec) {
|
|
55
|
+
return spec.questions.map((q) => `${q.question} (${q.options.map((o) => o.label).join(", ")})`);
|
|
56
|
+
}
|
|
57
|
+
export function askDeclineAll(spec) {
|
|
58
|
+
return { declined: askDeclineList(spec) };
|
|
59
|
+
}
|
|
60
|
+
/** The answers collected so far, in the tool_result's own shapes: a
|
|
61
|
+
* typed answer wins over the picks (the human typed it last), a
|
|
62
|
+
* multi-select question yields `choices`, a single one `choice`. */
|
|
63
|
+
export function askAnswers(spec, state) {
|
|
64
|
+
return spec.questions.map((q, i) => {
|
|
65
|
+
const typed = state.custom[i];
|
|
66
|
+
if (typed !== null && typed !== undefined && typed !== "")
|
|
67
|
+
return { q: q.question, custom: typed };
|
|
68
|
+
const picked = (state.picks[i] ?? []).map((n) => q.options[n]?.label ?? "");
|
|
69
|
+
return q.multiSelect === true ? { q: q.question, choices: picked } : { q: q.question, choice: picked[0] ?? "" };
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
/** Whether the CURRENT question has something to submit — a pick or a
|
|
73
|
+
* typed answer. Enter on an empty question is a no-op: the panel never
|
|
74
|
+
* invents an answer, and never silently skips one. */
|
|
75
|
+
function answered(state, i) {
|
|
76
|
+
const typed = state.custom[i];
|
|
77
|
+
return (state.picks[i] ?? []).length > 0 || (typed !== null && typed !== undefined && typed !== "");
|
|
78
|
+
}
|
|
79
|
+
/** Advance past the current question — the next one, or the end. */
|
|
80
|
+
function advance(spec, state) {
|
|
81
|
+
const next = state.qIndex + 1;
|
|
82
|
+
if (next >= spec.questions.length)
|
|
83
|
+
return { state, result: { answers: askAnswers(spec, state) } };
|
|
84
|
+
return { state: { ...state, qIndex: next, cursor: 0, phase: "options" } };
|
|
85
|
+
}
|
|
86
|
+
function toggle(state, option, multi) {
|
|
87
|
+
const current = state.picks[state.qIndex] ?? [];
|
|
88
|
+
const next = multi
|
|
89
|
+
? current.includes(option)
|
|
90
|
+
? current.filter((n) => n !== option)
|
|
91
|
+
: [...current, option].sort((a, b) => a - b)
|
|
92
|
+
: [option];
|
|
93
|
+
const picks = state.picks.map((p, i) => (i === state.qIndex ? next : p));
|
|
94
|
+
// a pick supersedes a typed answer for the same question — one
|
|
95
|
+
// question, one answer, and the human's last gesture is the one.
|
|
96
|
+
const custom = state.custom.map((c, i) => (i === state.qIndex ? null : c));
|
|
97
|
+
return { ...state, picks, custom, cursor: option };
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The pure key reducer. `key` is a single logical key: a digit "1".."4",
|
|
101
|
+
* "space", "up"/"down", "left" (walk back), "enter", "t" (type your own),
|
|
102
|
+
* or "esc". The custom phase's TEXT is not routed here — the editor owns
|
|
103
|
+
* the buffer exactly as it does for the rule-input phase, and hands the
|
|
104
|
+
* committed line to `askCommitCustom`.
|
|
105
|
+
*/
|
|
106
|
+
export function askKey(spec, state, key) {
|
|
107
|
+
const q = spec.questions[state.qIndex];
|
|
108
|
+
const multi = q.multiSelect === true;
|
|
109
|
+
if (state.phase === "custom") {
|
|
110
|
+
// esc backs out of the typing line; enter is the editor's (it
|
|
111
|
+
// carries the text and calls askCommitCustom).
|
|
112
|
+
if (key === "esc")
|
|
113
|
+
return { state: { ...state, phase: "options" } };
|
|
114
|
+
return { state };
|
|
115
|
+
}
|
|
116
|
+
if (key === "esc")
|
|
117
|
+
return { state, result: askDeclineAll(spec) };
|
|
118
|
+
if (key === "t")
|
|
119
|
+
return { state: { ...state, phase: "custom" } };
|
|
120
|
+
if (key === "left")
|
|
121
|
+
return { state: state.qIndex === 0 ? state : { ...state, qIndex: state.qIndex - 1, cursor: 0 } };
|
|
122
|
+
if (key === "up")
|
|
123
|
+
return { state: { ...state, cursor: Math.max(0, state.cursor - 1) } };
|
|
124
|
+
if (key === "down")
|
|
125
|
+
return { state: { ...state, cursor: Math.min(q.options.length - 1, state.cursor + 1) } };
|
|
126
|
+
if (key === "enter")
|
|
127
|
+
return answered(state, state.qIndex) ? advance(spec, state) : { state };
|
|
128
|
+
// SPACE selects at the cursor and NEVER commits — in either mode. It
|
|
129
|
+
// used to answer-and-advance a single-select question, which made a
|
|
130
|
+
// stray space (the most pressable key there is) an instant answer of
|
|
131
|
+
// whatever the cursor happened to be on. Enter and the digits are the
|
|
132
|
+
// only gestures that commit; space is how you point at something.
|
|
133
|
+
if (key === "space")
|
|
134
|
+
return { state: toggle(state, state.cursor, multi) };
|
|
135
|
+
const digit = Number.parseInt(key, 10);
|
|
136
|
+
if (Number.isInteger(digit) && digit >= 1 && digit <= q.options.length) {
|
|
137
|
+
const next = toggle(state, digit - 1, multi);
|
|
138
|
+
// single-select answers AND advances — the fast path a human
|
|
139
|
+
// expects; multi-select toggles and waits for enter.
|
|
140
|
+
return multi ? { state: next } : advance(spec, next);
|
|
141
|
+
}
|
|
142
|
+
return { state };
|
|
143
|
+
}
|
|
144
|
+
/** The typed answer commits: it becomes THE answer for this question
|
|
145
|
+
* (clearing its picks) and the walk advances. An empty line is a
|
|
146
|
+
* no-op back to the options — nothing is recorded. */
|
|
147
|
+
export function askCommitCustom(spec, state, text) {
|
|
148
|
+
const trimmed = text.trim();
|
|
149
|
+
if (trimmed === "")
|
|
150
|
+
return { state: { ...state, phase: "options" } };
|
|
151
|
+
const custom = state.custom.map((c, i) => (i === state.qIndex ? trimmed : c));
|
|
152
|
+
const picks = state.picks.map((p, i) => (i === state.qIndex ? [] : p));
|
|
153
|
+
return advance(spec, { ...state, custom, picks, phase: "options" });
|
|
154
|
+
}
|
|
155
|
+
// ── the rows ─────────────────────────────────────────────────────────
|
|
156
|
+
/** One option row: the number, the selection mark, the label, and the
|
|
157
|
+
* description after an em dash. The row CUTS (never folds) — the
|
|
158
|
+
* block's height is its row count, the W20 discipline. */
|
|
159
|
+
function optionRow(o, n, picked, cursor, multi, W) {
|
|
160
|
+
const p = palette();
|
|
161
|
+
const mark = multi ? (picked ? "◉" : "◯") : picked ? "◉" : " ";
|
|
162
|
+
const head = `${cursor ? p.bold : ""} ${n} ${mark} ${escapeTerminal(o.label)}${p.reset}`;
|
|
163
|
+
const body = o.description === undefined ? "" : `${p.dim} — ${escapeTerminal(o.description)}${p.reset}`;
|
|
164
|
+
return cutLine(`${head}${body}`, Math.max(1, W - 2));
|
|
165
|
+
}
|
|
166
|
+
/** The ask block's rows — the question as the rule line, the header (or
|
|
167
|
+
* the counter) as the title, the options as the body, and the
|
|
168
|
+
* type-your-own line last. The shape is the W21 block's: gutter, rule,
|
|
169
|
+
* title, divider, body, affordance, corner. */
|
|
170
|
+
export function askBlockRows(view, state, W, maxRows) {
|
|
171
|
+
const p = palette();
|
|
172
|
+
const spec = view.ask;
|
|
173
|
+
const q = spec.questions[state.qIndex];
|
|
174
|
+
const multi = q.multiSelect === true;
|
|
175
|
+
const gutter = `${p.dim}│${p.reset} `;
|
|
176
|
+
const counter = spec.questions.length > 1 ? `${p.dim} ‹ ${state.qIndex + 1}/${spec.questions.length} ›${p.reset}` : "";
|
|
177
|
+
const rows = [];
|
|
178
|
+
rows.push(`${gutter}${cutLine(`${p.bold}${escapeTerminal(q.question)}${p.reset}${counter}`, Math.max(1, W - 2))}`);
|
|
179
|
+
const header = q.header === undefined ? "the question" : escapeTerminal(q.header.slice(0, ASK_HEADER_CAP));
|
|
180
|
+
rows.push(`${gutter}${cutLine(`${p.dim}${header}${p.reset}`, Math.max(1, W - 2))}`);
|
|
181
|
+
rows.push(cutLine(`${p.dim}─ ${multi ? "pick any — space toggles" : "pick one"} ─${p.reset}`, Math.max(1, W - 2)));
|
|
182
|
+
const picks = state.picks[state.qIndex] ?? [];
|
|
183
|
+
const body = q.options.map((o, i) => `${gutter}${optionRow(o, i + 1, picks.includes(i), state.cursor === i, multi, W)}`);
|
|
184
|
+
const typed = state.custom[state.qIndex];
|
|
185
|
+
body.push(`${gutter}${cutLine(typed === null || typed === undefined
|
|
186
|
+
? `${p.dim} t type your own answer${p.reset}`
|
|
187
|
+
: ` t ◉ ${escapeTerminal(typed)}`, Math.max(1, W - 2))}`);
|
|
188
|
+
// the bounded block: the options fold nothing and cut individually,
|
|
189
|
+
// so the cap drops whole rows with the W21 notice row.
|
|
190
|
+
const budget = Math.max(1, maxRows - 5);
|
|
191
|
+
if (body.length > budget) {
|
|
192
|
+
const kept = Math.max(0, budget - 1);
|
|
193
|
+
rows.push(...body.slice(0, kept));
|
|
194
|
+
rows.push(cutLine(`${p.dim}└ +${body.length - kept} more rows — the full question is in the event log${p.reset}`, Math.max(1, W - 2)));
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
rows.push(...body);
|
|
198
|
+
}
|
|
199
|
+
rows.push(`${gutter}${p.dim}${askAffordance(state)}${p.reset}`);
|
|
200
|
+
rows.push(`${p.dim}└ ${p.reset}`);
|
|
201
|
+
return rows;
|
|
202
|
+
}
|
|
203
|
+
/** The status row's right-hand hint — the phase's keys. */
|
|
204
|
+
export function askAffordance(state) {
|
|
205
|
+
if (state.phase === "custom")
|
|
206
|
+
return "enter answers · esc backs out";
|
|
207
|
+
return state.qIndex > 0 ? "1-4 pick · t type · ← back · esc decline" : "1-4 pick · t type · esc decline";
|
|
208
|
+
}
|
|
209
|
+
/** The status row's left text — the ask's own line, with the walk. */
|
|
210
|
+
export function askStatus(view, state) {
|
|
211
|
+
const total = view.ask.questions.length;
|
|
212
|
+
return total > 1 ? `▸ question ${state.qIndex + 1} of ${total}` : "▸ a question for you";
|
|
213
|
+
}
|
|
214
|
+
/** The input row's lead: the digit lead while picking, the typing lead
|
|
215
|
+
* in the custom phase (the rule-input phase's shape, reused). */
|
|
216
|
+
export function askLeadPlain(state) {
|
|
217
|
+
return state.phase === "custom" ? "your answer: " : "1-4> ";
|
|
218
|
+
}
|
|
219
|
+
// ── the dispatchers: the panel slot, with the ask branch folded in ────
|
|
220
|
+
export function panelBlockRows(view, phase, sel, W, maxRows, ask) {
|
|
221
|
+
if (view.ask !== undefined && ask !== undefined)
|
|
222
|
+
return askBlockRows(view, ask, W, maxRows);
|
|
223
|
+
return basePanelBlockRows(view, phase, sel, W, maxRows);
|
|
224
|
+
}
|
|
225
|
+
export function panelLead(view, phase, sel, ask) {
|
|
226
|
+
const p = palette();
|
|
227
|
+
if (view.ask !== undefined && ask !== undefined)
|
|
228
|
+
return `${p.bold}${askLeadPlain(ask)}${p.reset}`;
|
|
229
|
+
return basePanelLead(view, phase, sel);
|
|
230
|
+
}
|
|
231
|
+
export function panelLeadPlain(view, phase, sel, ask) {
|
|
232
|
+
if (view.ask !== undefined && ask !== undefined)
|
|
233
|
+
return askLeadPlain(ask);
|
|
234
|
+
return basePanelLeadPlain(view, phase, sel);
|
|
235
|
+
}
|
|
236
|
+
export function panelStatus(view, phase, sel, ask) {
|
|
237
|
+
if (view.ask !== undefined && ask !== undefined)
|
|
238
|
+
return askStatus(view, ask);
|
|
239
|
+
return basePanelStatus(view, phase, sel);
|
|
240
|
+
}
|
|
241
|
+
export function panelAffordance(view, phase, sel, ask) {
|
|
242
|
+
if (view.ask !== undefined && ask !== undefined)
|
|
243
|
+
return askAffordance(ask);
|
|
244
|
+
return basePanelAffordance(view, phase, sel);
|
|
245
|
+
}
|
|
246
|
+
/** The whole panel state in one call — the compositor's four reads share
|
|
247
|
+
* one source, so an ask can never render half as an approval. */
|
|
248
|
+
export const panelRowsOf = (s, W, maxRows) => panelBlockRows(s.view, s.phase, s.sel, W, maxRows, s.ask);
|
|
249
|
+
export const panelLeadOf = (s) => panelLead(s.view, s.phase, s.sel, s.ask);
|
|
250
|
+
export const panelStatusOf = (s) => panelStatus(s.view, s.phase, s.sel, s.ask);
|
|
251
|
+
export const panelAffordanceOf = (s) => panelAffordance(s.view, s.phase, s.sel, s.ask);
|
|
252
|
+
// ── the view: what the human reads when the model asks ────────────────
|
|
253
|
+
/** The ask's PanelView. The dock-less fallback question is HONEST: a
|
|
254
|
+
* terminal without a panel cannot walk options, so it says the ask is
|
|
255
|
+
* being declined rather than pretending y/n answered it. */
|
|
256
|
+
export function askView(spec) {
|
|
257
|
+
const first = spec.questions[0];
|
|
258
|
+
return {
|
|
259
|
+
flavor: "simple",
|
|
260
|
+
name: "ask_user",
|
|
261
|
+
title: first.header ?? first.question,
|
|
262
|
+
speaker: "kiso",
|
|
263
|
+
statusText: "▸ a question for you",
|
|
264
|
+
args: { kind: "text", lines: askDeclineList(spec) },
|
|
265
|
+
fallbackQuestion: `⚠ ${escapeTerminal(first.question)} — this terminal cannot show the option panel; the question is declined `,
|
|
266
|
+
ask: spec,
|
|
267
|
+
};
|
|
268
|
+
}
|
package/dist/compositor.d.ts
CHANGED
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
* line-mode bytes byte-for-byte (the e2e guards them).
|
|
44
44
|
*/
|
|
45
45
|
import { type MenuItem } from "./editor.js";
|
|
46
|
-
import {
|
|
46
|
+
import type { PanelState } from "./approval-panel.js";
|
|
47
47
|
import { type AtMatch } from "./at-picker.js";
|
|
48
48
|
/** KC3 §4 — the @ picker's bound state (the editor's atState()). */
|
|
49
49
|
export interface AtPanelState {
|
package/dist/compositor.js
CHANGED
|
@@ -45,7 +45,9 @@
|
|
|
45
45
|
import { truncateDiff } from "./diff.js";
|
|
46
46
|
import { displayWidth } from "./editor.js";
|
|
47
47
|
import { leadWidth } from "./width.js"; // W23: the ONE width authority (the editor, #inputRow, and editCol share it)
|
|
48
|
-
|
|
48
|
+
// KC3.5: the panel-slot reads come from the DISPATCHERS — one source
|
|
49
|
+
// for four reads, so an ask can never render half as an approval.
|
|
50
|
+
import { panelAffordanceOf, panelLeadOf, panelRowsOf, panelStatusOf } from "./ask-panel.js";
|
|
49
51
|
import { atPanelRows } from "./at-picker.js";
|
|
50
52
|
import { Container, ROLLUP_NOUN, SPINNER, bodySpacing, boxBottom, boxTop, cellComponent, foldLine, pendingQueueRows, statusLine, turnFold, visibleWidth, } from "./components.js";
|
|
51
53
|
import { bannerLines, escapeTerminal, foldResult, foldThinking, palette, renderTerminalGap, renderToolSummary, toolTarget } from "./render.js";
|
|
@@ -657,7 +659,7 @@ export class Body {
|
|
|
657
659
|
#statusSource() {
|
|
658
660
|
const panel = this.#panelState?.() ?? null;
|
|
659
661
|
if (panel !== null)
|
|
660
|
-
return { status:
|
|
662
|
+
return { status: panelStatusOf(panel), hint: panelAffordanceOf(panel) };
|
|
661
663
|
// W22: while turns wait in the queue, the right hint shows the
|
|
662
664
|
// count — the chips below carry the lines themselves.
|
|
663
665
|
const queued = this.#queueState?.().length ?? 0;
|
|
@@ -704,7 +706,7 @@ export class Body {
|
|
|
704
706
|
// + 1 — the SAME formula the marker embeds at (the panel lead when
|
|
705
707
|
// the panel owns the row; the old prompt-only math desynced the
|
|
706
708
|
// panel rows' edit column; leadWidth is the ONE authority)
|
|
707
|
-
const lead = panel !== null ?
|
|
709
|
+
const lead = panel !== null ? panelLeadOf(panel) : this.#inputPrompt;
|
|
708
710
|
return 3 + leadWidth(lead) + st.cursor;
|
|
709
711
|
}
|
|
710
712
|
/** The old dock's redraw — the editor's onRender target: mark + the
|
|
@@ -778,7 +780,7 @@ export class Body {
|
|
|
778
780
|
// W21: the panel's own rows (the cap is exact — the scalar
|
|
779
781
|
// reflects the screen). W22: the queue chips occupy their
|
|
780
782
|
// own band — the panel's cap shrinks by their rows.
|
|
781
|
-
return (
|
|
783
|
+
return (panelRowsOf(panel, this.#opts.width(), Math.max(1, this.#opts.height() - 4 - inputExtra - queueRows.length)).length +
|
|
782
784
|
CHROME_ROWS +
|
|
783
785
|
inputExtra +
|
|
784
786
|
queueRows.length);
|
|
@@ -865,7 +867,7 @@ export class Body {
|
|
|
865
867
|
// the W11 blank would separate it from the frozen content).
|
|
866
868
|
// The cap is exact, so the force-commit loop never fires. W22:
|
|
867
869
|
// the queue band sits below the panel — the cap shrinks by it.
|
|
868
|
-
liveLines =
|
|
870
|
+
liveLines = panelRowsOf(panel, W, Math.max(1, H - 4 - inputExtra - queueRows.length));
|
|
869
871
|
}
|
|
870
872
|
else {
|
|
871
873
|
let prev = this.#committed > 0 ? this.#lineCache[this.#committed - 1] : null;
|
|
@@ -1224,7 +1226,7 @@ export class Body {
|
|
|
1224
1226
|
// the lead — the panel's phase lead when the panel owns the row
|
|
1225
1227
|
// (1-3> / the rule input's "2 Yes, don't ask again for " / the
|
|
1226
1228
|
// amend "feedback (deny): "), the bound prompt otherwise
|
|
1227
|
-
const lead = panel !== null ?
|
|
1229
|
+
const lead = panel !== null ? panelLeadOf(panel) : this.#inputPrompt;
|
|
1228
1230
|
const leadW = leadWidth(lead);
|
|
1229
1231
|
// a LEGACY one-row provider (the old {line, cursor} shape) keeps
|
|
1230
1232
|
// working: its single line is the composer's single row
|
package/dist/editor.d.ts
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import { charWidth, displayWidth, widthOf } from "./width.js";
|
|
25
25
|
export { charWidth, displayWidth, widthOf };
|
|
26
|
-
import {
|
|
26
|
+
import type { PanelState, PanelVerdict, PanelView } from "./approval-panel.js";
|
|
27
27
|
import { type AtItem, type AtMatch } from "./at-picker.js";
|
|
28
28
|
export declare const PROMPT = "\u258C ";
|
|
29
29
|
export declare const PROMPT_WIDTH: number;
|
package/dist/editor.js
CHANGED
|
@@ -26,7 +26,9 @@ 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
|
-
|
|
29
|
+
// KC3.5: the panel-slot dispatchers — the ask branch folded into the
|
|
30
|
+
// W21 lead/rows, so this file keeps ONE panel and one key owner.
|
|
31
|
+
import { askCommitCustom, askKey, askStart, panelLead } from "./ask-panel.js";
|
|
30
32
|
import { AT_VISIBLE, atFilter } from "./at-picker.js";
|
|
31
33
|
// TUI v4 #16d: the input row is the blue brick + the edit area — the
|
|
32
34
|
// "you>" text is gone (the brick IS the prompt; the pipe path's readline
|
|
@@ -439,6 +441,7 @@ export class Editor {
|
|
|
439
441
|
view,
|
|
440
442
|
phase: "options",
|
|
441
443
|
sel: 0,
|
|
444
|
+
ask: view.ask === undefined ? null : askStart(view.ask),
|
|
442
445
|
amend: "yes",
|
|
443
446
|
onCommit,
|
|
444
447
|
stash: { chars: this.#chars, cursor: this.#cursor, scroll: this.#scroll },
|
|
@@ -463,7 +466,7 @@ export class Editor {
|
|
|
463
466
|
const panel = this.#panel;
|
|
464
467
|
if (panel === null)
|
|
465
468
|
return null;
|
|
466
|
-
return { view: panel.view, phase: panel.phase, sel: panel.sel };
|
|
469
|
+
return { view: panel.view, phase: panel.phase, sel: panel.sel, ...(panel.ask === null ? {} : { ask: panel.ask }) };
|
|
467
470
|
}
|
|
468
471
|
enter() {
|
|
469
472
|
if (this.#entered)
|
|
@@ -493,7 +496,7 @@ export class Editor {
|
|
|
493
496
|
// W21: the panel's lead owns the row while up (the brick returns
|
|
494
497
|
// when the panel closes).
|
|
495
498
|
const panel = this.#panel;
|
|
496
|
-
const lead = panel !== null ? panelLead(panel.view, panel.phase, panel.sel) : `${p.bold}${PROMPT}${p.reset}`;
|
|
499
|
+
const lead = panel !== null ? panelLead(panel.view, panel.phase, panel.sel, panel.ask ?? undefined) : `${p.bold}${PROMPT}${p.reset}`;
|
|
497
500
|
// W23: the ONE width authority — leadWidth(lead), the ANSI-stripped
|
|
498
501
|
// visible width (the styled panel lead / the styled brick measure
|
|
499
502
|
// the same as their plain text — a lead can never measure
|
|
@@ -520,6 +523,45 @@ export class Editor {
|
|
|
520
523
|
// text); ctrl-c still rides the SIGINT handler (which
|
|
521
524
|
// cancels the panel).
|
|
522
525
|
const panel = this.#panel;
|
|
526
|
+
// KC3.5: an ASK panel routes its own keys — the digits pick
|
|
527
|
+
// (single-select advances, multi toggles), space toggles at
|
|
528
|
+
// the cursor, `t` opens the type-your-own line (the
|
|
529
|
+
// rule-input phase's shape: the buffer is the editor's, so
|
|
530
|
+
// only esc and enter are intercepted while typing), esc
|
|
531
|
+
// declines the whole call. Everything else falls through to
|
|
532
|
+
// the ordinary editing chain below.
|
|
533
|
+
if (panel.ask !== null) {
|
|
534
|
+
const typing = panel.ask.phase === "custom";
|
|
535
|
+
if (c === "\x1b" && !text.slice(i + 1).startsWith("[") && !text.slice(i + 1).startsWith("O")) {
|
|
536
|
+
this.#askStep("esc");
|
|
537
|
+
i += 1;
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
if (c === "\x0d" || c === "\x0a") {
|
|
541
|
+
this.#askStep(typing ? "commit" : "enter");
|
|
542
|
+
i += 1;
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
if (!typing && (c === " " || (c !== undefined && c >= "1" && c <= "4") || c === "t" || c === "T")) {
|
|
546
|
+
this.#askStep(c === " " ? "space" : c === "T" ? "t" : c);
|
|
547
|
+
i += 1;
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
// an ask at rest swallows stray PRINTABLE keys — the panel
|
|
551
|
+
// owns them, and a typed "/" or "@" must not arm the menu
|
|
552
|
+
// or the picker underneath. Two things are never
|
|
553
|
+
// swallowed: the CSI/SS3 introducer, because ←/↑/↓ are
|
|
554
|
+
// the ask's own keys and the parser below routes them
|
|
555
|
+
// (the T-Q1 red), and the CONTROL characters, because
|
|
556
|
+
// ctrl-c must still reach the SIGINT handler that
|
|
557
|
+
// cancels the panel — W21's own rule, and what the T-Q6
|
|
558
|
+
// race red caught: an abort with the panel up did
|
|
559
|
+
// nothing at all.
|
|
560
|
+
if (!typing && c !== undefined && c >= " " && c !== "\x7f") {
|
|
561
|
+
i += 1;
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
523
565
|
if (c === "\x1b" && !text.slice(i + 1).startsWith("[") && !text.slice(i + 1).startsWith("O")) {
|
|
524
566
|
this.#panelEsc();
|
|
525
567
|
i += 1;
|
|
@@ -760,7 +802,10 @@ export class Editor {
|
|
|
760
802
|
// panel owns the keys while up (↑↓ do nothing — the panel has no
|
|
761
803
|
// ↑↓ role).
|
|
762
804
|
if (this.#panel !== null) {
|
|
763
|
-
|
|
805
|
+
// W21: the panel owns the keys. KC3.5: an ask uses ↑↓ for
|
|
806
|
+
// the option cursor (the approval panel still has no ↑↓ role).
|
|
807
|
+
if (this.#panel.ask !== null && this.#panel.ask.phase === "options")
|
|
808
|
+
this.#askStep(final === "A" ? "up" : "down");
|
|
764
809
|
}
|
|
765
810
|
else if (this.#menuOpen) {
|
|
766
811
|
if (final === "A")
|
|
@@ -798,7 +843,12 @@ export class Editor {
|
|
|
798
843
|
this.#onRender();
|
|
799
844
|
}
|
|
800
845
|
else if (final === "D") {
|
|
801
|
-
|
|
846
|
+
// KC3.5: ← walks the ask BACK a question (the ‹ n/m › walk); at
|
|
847
|
+
// question one it stays put — esc is the decline, never ←.
|
|
848
|
+
if (this.#panel?.ask != null && this.#panel.ask.phase === "options")
|
|
849
|
+
this.#askStep("left");
|
|
850
|
+
else
|
|
851
|
+
this.#move(-1);
|
|
802
852
|
}
|
|
803
853
|
else if (final === "C") {
|
|
804
854
|
this.#move(1);
|
|
@@ -913,6 +963,33 @@ export class Editor {
|
|
|
913
963
|
else if (panel.sel === 3)
|
|
914
964
|
this.#panelClose({ action: "deny", reason: "" });
|
|
915
965
|
}
|
|
966
|
+
/**
|
|
967
|
+
* KC3.5 — one ask key: the pure reducer decides, this method applies.
|
|
968
|
+
* The buffer is cleared on every phase change so the type-your-own
|
|
969
|
+
* line starts empty and its text never leaks back into the options
|
|
970
|
+
* (the rule-input phase's own discipline). A step that produced a
|
|
971
|
+
* RESULT closes the panel with it — the stash/restore is the W21
|
|
972
|
+
* path, identical for an answer and for a decline.
|
|
973
|
+
*/
|
|
974
|
+
#askStep(key) {
|
|
975
|
+
const panel = this.#panel;
|
|
976
|
+
if (panel === null || panel.ask === null)
|
|
977
|
+
return;
|
|
978
|
+
const spec = panel.view.ask;
|
|
979
|
+
const before = panel.ask.phase;
|
|
980
|
+
const step = key === "commit" ? askCommitCustom(spec, panel.ask, this.line()) : askKey(spec, panel.ask, key);
|
|
981
|
+
panel.ask = step.state;
|
|
982
|
+
if (step.state.phase !== before) {
|
|
983
|
+
this.#chars = [];
|
|
984
|
+
this.#cursor = 0;
|
|
985
|
+
this.#scroll = 0;
|
|
986
|
+
}
|
|
987
|
+
if (step.result !== undefined) {
|
|
988
|
+
this.#panelClose({ action: "answers", result: step.result });
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
this.#onRender();
|
|
992
|
+
}
|
|
916
993
|
#panelClose(verdict) {
|
|
917
994
|
const panel = this.#panel;
|
|
918
995
|
if (panel === null)
|
|
@@ -1162,7 +1239,7 @@ export class Editor {
|
|
|
1162
1239
|
// W23: the ONE width authority — leadWidth(lead) — the cap follows
|
|
1163
1240
|
// the lead the editor itself renders (the panel lead when the panel
|
|
1164
1241
|
// owns the keys, the brick otherwise): maxW = W − walls − lead.
|
|
1165
|
-
const lead = this.#panel !== null ? panelLead(this.#panel.view, this.#panel.phase, this.#panel.sel) : PROMPT;
|
|
1242
|
+
const lead = this.#panel !== null ? panelLead(this.#panel.view, this.#panel.phase, this.#panel.sel, this.#panel.ask ?? undefined) : PROMPT;
|
|
1166
1243
|
const leadW = leadWidth(lead);
|
|
1167
1244
|
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
|
|
1168
1245
|
// KC1: the scroll is the CURSOR LINE's own offset — a single-line
|
package/dist/index.d.ts
CHANGED
|
@@ -14,3 +14,5 @@ export { editFileDiff, truncateDiff, writeFileDiff, type DiffLine, type DiffResu
|
|
|
14
14
|
export { STATUS_GLYPHS, idleStatus, runningStatus } from "./status.js";
|
|
15
15
|
export { interactivePrompt, projectTrustRows, projectTrustView, projectUntrustedNote, uncertainView, type TrustArtifact } from "./strings.js";
|
|
16
16
|
export { AT_CAP, AT_SKIP, AT_VISIBLE, atEmbed, atFilter, atPanelRows, atWindow, longestRun, type AtItem, type AtMatch } from "./at-picker.js";
|
|
17
|
+
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";
|
package/dist/index.js
CHANGED
|
@@ -24,3 +24,10 @@ export { interactivePrompt, projectTrustRows, projectTrustView, projectUntrusted
|
|
|
24
24
|
// KC3 §3/§5: the @ file picker's pure half — the subsequence filter, the
|
|
25
25
|
// deterministic rank, and the ONE cap the CLI's file source shares.
|
|
26
26
|
export { AT_CAP, AT_SKIP, AT_VISIBLE, atEmbed, atFilter, atPanelRows, atWindow, longestRun } from "./at-picker.js";
|
|
27
|
+
// KC3.5 (the ask round): the ask view — the panel machinery generalized.
|
|
28
|
+
// The cli composes the view and hands the answers to the tool; the keys,
|
|
29
|
+
// the rows and the walk are the terminal layer's.
|
|
30
|
+
export { ASK_HEADER_CAP, ASK_MAX_OPTIONS, ASK_MAX_QUESTIONS, ASK_MIN_OPTIONS, askAffordance, askAnswers, askBlockRows, askCommitCustom, askDeclineAll, askDeclineList, askKey, askLeadPlain, askStart, askStatus, askView, } from "./ask-panel.js";
|
|
31
|
+
// KC3.5 §4: the interrupted-ask copy — the SAME uncertainty gate, said
|
|
32
|
+
// honestly for a question nobody answered (the ① probe's surface).
|
|
33
|
+
export { extensionsBannerText, helpRows, unansweredAskView } from "./strings.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-tui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "kiso tui — the pure terminal layer (cell renderer, dock, raw editor, diff, palette). Zero runtime dependencies: input is data, output is bytes.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -35,6 +35,6 @@
|
|
|
35
35
|
},
|
|
36
36
|
"homepage": "https://github.com/vincemakes/kiso/tree/main/packages/tui#readme",
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@vincemakes/kiso-tui-cells": "0.
|
|
38
|
+
"@vincemakes/kiso-tui-cells": "0.8.0"
|
|
39
39
|
}
|
|
40
40
|
}
|