@vincemakes/kiso-tui 0.6.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.
@@ -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
+ }
@@ -0,0 +1,159 @@
1
+ /**
2
+ * KC3 §3 — the @ file picker's PURE half: the subsequence filter and
3
+ * the deterministic rank. No scoring library, no index, no disk. The
4
+ * file list is DATA the CLI feeds in (the tui purity rule: input is
5
+ * data, output is bytes); this module decides which of those paths a
6
+ * query matches, in what order, and which characters to embolden.
7
+ *
8
+ * Determinism is the whole design constraint. A fuzzy finder that
9
+ * reorders on a tie is unusable at speed — the row under the cursor
10
+ * must not move because two paths scored equal. Every comparison here
11
+ * ends in a total order: run length, then path length, then the raw
12
+ * lexical order of the path (never localeCompare, whose result depends
13
+ * on the machine's locale).
14
+ */
15
+ /** The bound source's item — a repo-relative path and nothing else.
16
+ * Structural: the CLI passes whatever it likes as long as it has a
17
+ * path (slice 5 passes exactly this). */
18
+ export interface AtItem {
19
+ readonly path: string;
20
+ }
21
+ /** A matched path plus the indices the panel emboldens. */
22
+ export interface AtMatch {
23
+ readonly path: string;
24
+ /** the matched character positions, ascending — the panel renders
25
+ * these bold-white and the rest dim */
26
+ readonly hit: readonly number[];
27
+ /** the longest CONTIGUOUS run inside `hit` — the rank's first key,
28
+ * carried so the panel and the ranking can never disagree about
29
+ * why a row is where it is */
30
+ readonly run: number;
31
+ }
32
+ /**
33
+ * KC3 §5 — the ONE cap. The file list is computed per open with no
34
+ * index and no watcher, so its cost is bounded here rather than
35
+ * amortized somewhere invisible. The source collects at most CAP + 1
36
+ * entries: the extra one is what makes "there were more" DISTINGUISHABLE
37
+ * from "there were exactly this many", so the counter row can say so
38
+ * honestly instead of guessing.
39
+ */
40
+ export declare const AT_CAP = 2000;
41
+ /**
42
+ * KC3 §5 — the directories the picker never offers, and the other half
43
+ * of its contract with whatever host has to walk a tree to fill it.
44
+ *
45
+ * It lives here beside the cap because the two are the same kind of
46
+ * promise: a host that walks must prune these BEFORE descending (a
47
+ * post-filter would already have walked node_modules, which is the
48
+ * cost the pruning exists to avoid), and must stop at the cap. Hosts
49
+ * that get their list from a VCS ignore this set entirely — the VCS
50
+ * has already applied a better one.
51
+ */
52
+ export declare const AT_SKIP: ReadonlySet<string>;
53
+ /** KC3 §4 — the panel's visible height. A ceiling, not a promise: the
54
+ * compositor clamps further when the terminal is short. */
55
+ export declare const AT_VISIBLE = 5;
56
+ /**
57
+ * The subsequence embedding, TIGHTENED — the two-pass walk every good
58
+ * fuzzy finder uses, and the reason `@ra` emboldens the "ra" of
59
+ * "src/range.js" rather than the r of "src" and the a of "range".
60
+ *
61
+ * Pass 1 walks forward and stops at the EARLIEST index that completes
62
+ * the query — this both answers "does it match at all" and fixes the
63
+ * right-hand edge. Pass 2 walks backward from that edge, taking the
64
+ * LATEST position for each query character in turn, which slides every
65
+ * matched character as far right as it can go without crossing the
66
+ * next one. The result is the most clustered embedding that ends where
67
+ * the earliest match ends.
68
+ *
69
+ * Case-insensitive: both sides are lowercased by the caller once per
70
+ * query rather than once per character.
71
+ *
72
+ * Returns the ascending match indices, or null when the query is not a
73
+ * subsequence of the path at all.
74
+ */
75
+ export declare function atEmbed(lowerPath: string, lowerQuery: string): number[] | null;
76
+ /** The longest run of CONSECUTIVE indices in an ascending list. An
77
+ * empty query has no run — every path ties on it, and the rank falls
78
+ * through to path length. */
79
+ export declare function longestRun(hit: readonly number[]): number;
80
+ /**
81
+ * The filter + the rank. Case-insensitive SUBSEQUENCE over the FULL
82
+ * relative path (so `@tui/ed` finds packages/tui/src/editor.ts — the
83
+ * directory is part of what the user is typing at, not a separate
84
+ * field), ordered by:
85
+ *
86
+ * 1. contiguous-run length DESC — a path where the query appears as
87
+ * a solid stretch beats one where it is scattered across the
88
+ * whole string. This is the key that makes typing feel like
89
+ * aiming rather than fishing.
90
+ * 2. path length ASC — among equally solid hits, the shorter path is
91
+ * the more likely target (src/range.js over a deep vendored copy
92
+ * of the same name).
93
+ * 3. the path itself, lexically — the tiebreak of last resort, and
94
+ * the reason the order NEVER depends on the source's iteration
95
+ * order or on two runs of the same query disagreeing.
96
+ *
97
+ * An EMPTY query matches everything: rule 1 ties at 0 for all, so the
98
+ * listing is shortest-path-first, then lexical. The list is sliced to
99
+ * AT_CAP; `capped` reports whether anything was dropped.
100
+ */
101
+ export declare function atFilter(items: readonly AtItem[], query: string): {
102
+ matches: AtMatch[];
103
+ capped: boolean;
104
+ };
105
+ /**
106
+ * KC3 §4 — the picker's WINDOW: which slice of the ranked list is on
107
+ * screen. The window TRAILS the selection exactly as the composer's
108
+ * own viewport trails the cursor (KC1 §5) — derived per read, never
109
+ * stored, so it can never disagree with the selection it is meant to
110
+ * follow.
111
+ */
112
+ export declare function atWindow(total: number, selected: number, visible?: number): {
113
+ first: number;
114
+ count: number;
115
+ };
116
+ /**
117
+ * KC3 §4 — ONE row of the panel.
118
+ *
119
+ * Two columns: the file's NAME on the left with its matched characters
120
+ * bold, and the DIRECTORY dim on the right, pushed to the far edge.
121
+ * The name is what the user is aiming at; the directory is what tells
122
+ * two same-named files apart, which is why it is present but quiet.
123
+ *
124
+ * The selected row carries `→` on the inverse band (SGR 7, closed with
125
+ * 27 — never SGR 0, so it composes inside the row's own spans).
126
+ *
127
+ * The `hit` indices are over the FULL path, so they are shifted by the
128
+ * directory's length to land on the name. A hit that falls INSIDE the
129
+ * directory is simply not drawn bold — the directory column is
130
+ * uniformly dim by design (a bold fragment in a right-aligned dim
131
+ * column reads as damage, not as information).
132
+ *
133
+ * The row never exceeds W: the name cuts first (it is the flexible
134
+ * column), and the directory is dropped entirely before the name is
135
+ * cut to nothing.
136
+ */
137
+ export declare function atRow(match: AtMatch, selected: boolean, W: number): string;
138
+ /**
139
+ * KC3 §4 — the counter row: `(n/total)`, where n is the 1-based
140
+ * position of the SELECTION in the whole ranked list, not in the
141
+ * visible window. The user needs to know where they are in the list,
142
+ * which the five visible rows cannot tell them.
143
+ *
144
+ * When the source list was truncated the row SAYS SO. A file picker
145
+ * that quietly lists 2,000 of 40,000 files and shows a confident
146
+ * "(3/1998)" is lying by omission; this one admits the horizon.
147
+ */
148
+ export declare function atCounterRow(selected: number, total: number, capped: boolean, W: number): string;
149
+ /**
150
+ * KC3 §4 — the whole band: at most AT_VISIBLE windowed rows, then the
151
+ * counter. Returned as plain strings for the menu-rows channel, which
152
+ * already accounts them in chromeRows — the picker needs no geometry
153
+ * of its own, which is the entire reason it rides that channel.
154
+ */
155
+ export declare function atPanelRows(state: {
156
+ matches: readonly AtMatch[];
157
+ selected: number;
158
+ capped: boolean;
159
+ }, W: number): string[];