@vincemakes/kiso-tui 0.16.2 → 0.16.4
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 +7 -2
- package/dist/ask-panel.js +111 -32
- package/dist/at-picker.d.ts +11 -3
- package/dist/at-picker.js +19 -10
- package/dist/compositor.d.ts +18 -2
- package/dist/compositor.js +50 -19
- package/dist/editor.d.ts +9 -0
- package/dist/editor.js +53 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/render.js +4 -4
- package/dist/session-picker.d.ts +33 -7
- package/dist/session-picker.js +72 -32
- package/package.json +3 -3
package/dist/ask-panel.d.ts
CHANGED
|
@@ -63,10 +63,15 @@ export declare function askKey(spec: AskSpec, state: AskRuntime, key: string): A
|
|
|
63
63
|
* (clearing its picks) and the walk advances. An empty line is a
|
|
64
64
|
* no-op back to the options — nothing is recorded. */
|
|
65
65
|
export declare function askCommitCustom(spec: AskSpec, state: AskRuntime, text: string): AskStep;
|
|
66
|
+
/** The description column, computed over the WHOLE list: two cells past
|
|
67
|
+
* the widest label, never past the half-width, and 0 (meaning "no
|
|
68
|
+
* column, use the em dash") when what is left would not hold a
|
|
69
|
+
* readable description. */
|
|
70
|
+
export declare function askDescriptionStop(q: AskQuestion, W: number): number;
|
|
66
71
|
/** The ask block's rows — the question as the rule line, the header (or
|
|
67
72
|
* the counter) as the title, the options as the body, and the
|
|
68
|
-
* type-your-own line last.
|
|
69
|
-
* title,
|
|
73
|
+
* type-your-own line last. R2: the shape is the RULE's — rule,
|
|
74
|
+
* title, header, body, affordance, rule. */
|
|
70
75
|
export declare function askBlockRows(view: PanelView, state: AskRuntime, W: number, maxRows: number): string[];
|
|
71
76
|
/** The status row's right-hand hint — the phase's keys. */
|
|
72
77
|
export declare function askAffordance(state: AskRuntime): string;
|
package/dist/ask-panel.js
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
* hand-off, no timeout and no countdown.
|
|
29
29
|
*/
|
|
30
30
|
import { panelBlockLayout, panelAffordance as basePanelAffordance, panelBlockRows as basePanelBlockRows, panelLead as basePanelLead, panelLeadPlain as basePanelLeadPlain, panelStatus as basePanelStatus, pickAffordance, pickBlockRows, pickLead, pickLeadPlain, pickStatus, } from "./approval-panel.js";
|
|
31
|
-
import { cutLine } from "@vincemakes/kiso-tui-cells/components";
|
|
31
|
+
import { cutLine, selectionBar, visibleWidth, widthCut } from "@vincemakes/kiso-tui-cells/components";
|
|
32
32
|
import { escapeTerminal, palette } from "./render.js";
|
|
33
33
|
/** The schema's own bounds — the registry refuses anything outside them
|
|
34
34
|
* (extensions/ask validates; these are the numbers it validates to). */
|
|
@@ -181,34 +181,101 @@ export function askCommitCustom(spec, state, text) {
|
|
|
181
181
|
return advance(spec, { ...state, custom, picks, phase: "options" });
|
|
182
182
|
}
|
|
183
183
|
// ── the rows ─────────────────────────────────────────────────────────
|
|
184
|
-
/**
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
|
|
184
|
+
/**
|
|
185
|
+
* One option row: the cursor arrow, the number, the selection mark, the
|
|
186
|
+
* label, and the description in its own RIGHT COLUMN. The row CUTS
|
|
187
|
+
* (never folds) — the block's height is its row count, the W20
|
|
188
|
+
* discipline.
|
|
189
|
+
*
|
|
190
|
+
* R2 (design §7.5) — two changes, both about what survives:
|
|
191
|
+
*
|
|
192
|
+
* - the cursor carries `→` as well as the bar. The bar is the loud
|
|
193
|
+
* signal and the arrow is the durable one: strip every escape and the
|
|
194
|
+
* row still says which option the cursor is on, which is law 1.3's
|
|
195
|
+
* test applied to a selection rather than to an outcome.
|
|
196
|
+
* - the description leaves the em dash and takes a column. Run-on
|
|
197
|
+
* `label — description` makes the labels — the thing being chosen
|
|
198
|
+
* between — unscannable, because each one starts at a column the
|
|
199
|
+
* previous row's length decided. `stop` is computed ONCE over the
|
|
200
|
+
* whole option list by the caller, so the column is a property of
|
|
201
|
+
* the list and not of the row.
|
|
202
|
+
*
|
|
203
|
+
* A narrow block has no room for two columns; `stop` arrives as 0 and
|
|
204
|
+
* the em-dash form is what it degrades to.
|
|
205
|
+
*/
|
|
206
|
+
function optionRow(o, n, picked, cursor, multi, W, stop = 0) {
|
|
188
207
|
const p = palette();
|
|
189
208
|
const mark = multi ? (picked ? "◉" : "◯") : picked ? "◉" : " ";
|
|
190
|
-
const
|
|
191
|
-
const
|
|
192
|
-
|
|
209
|
+
const lead = askOptionLead(o, n, mark, cursor);
|
|
210
|
+
const head = `${cursor ? p.bold : ""}${lead}${p.reset}`;
|
|
211
|
+
const room = Math.max(1, W - 2);
|
|
212
|
+
let body;
|
|
213
|
+
if (o.description === undefined)
|
|
214
|
+
body = "";
|
|
215
|
+
else if (stop > 0) {
|
|
216
|
+
const desc = widthCut(escapeTerminal(o.description), Math.max(0, room - stop));
|
|
217
|
+
body = `${" ".repeat(Math.max(1, stop - visibleWidth(lead)))}${p.dim}${desc}${p.reset}`;
|
|
218
|
+
}
|
|
219
|
+
else
|
|
220
|
+
body = `${p.dim} — ${escapeTerminal(o.description)}${p.reset}`;
|
|
221
|
+
const text = cutLine(`${head}${body}`, Math.max(1, W - 2));
|
|
222
|
+
// R2: the cursor is a FULL-ROW bar, the same one the approval panel
|
|
223
|
+
// has had since R1.5 ⑧ — "a two-cell marker in an eighty-column row is
|
|
224
|
+
// a selection you have to hunt for". This panel was carrying bold and
|
|
225
|
+
// nothing else, which on a white terminal is close to invisible. One
|
|
226
|
+
// ruling, applied in the second place it was always about.
|
|
227
|
+
return cursor ? selectionBar(text, visibleWidth(text), W) : ` ${text}`; // R2: the frame is a rule, so a row is just indented
|
|
228
|
+
}
|
|
229
|
+
/** The row's left span, PLAIN — the one place its shape is written, so
|
|
230
|
+
* the column arithmetic below and the row above cannot disagree about
|
|
231
|
+
* how wide it is. The arrow's cell is spent on every row so the digit
|
|
232
|
+
* column does not move as the cursor walks. */
|
|
233
|
+
function askOptionLead(o, n, mark, cursor) {
|
|
234
|
+
return `${cursor ? "→" : " "} ${n} ${mark} ${escapeTerminal(o.label)}`;
|
|
235
|
+
}
|
|
236
|
+
/** Below this many cells a right column is not a column, it is a
|
|
237
|
+
* three-word stub — the em-dash form carries more of the sentence. */
|
|
238
|
+
const ASK_DESC_MIN = 18;
|
|
239
|
+
/** The description column, computed over the WHOLE list: two cells past
|
|
240
|
+
* the widest label, never past the half-width, and 0 (meaning "no
|
|
241
|
+
* column, use the em dash") when what is left would not hold a
|
|
242
|
+
* readable description. */
|
|
243
|
+
export function askDescriptionStop(q, W) {
|
|
244
|
+
const multi = q.multiSelect === true;
|
|
245
|
+
if (!q.options.some((o) => o.description !== undefined))
|
|
246
|
+
return 0;
|
|
247
|
+
const widest = Math.max(...q.options.map((o, i) => visibleWidth(askOptionLead(o, i + 1, multi ? "◯" : " ", false))));
|
|
248
|
+
const room = Math.max(1, W - 2);
|
|
249
|
+
const stop = widest + 2;
|
|
250
|
+
if (stop > Math.floor(room / 2) || room - stop < ASK_DESC_MIN)
|
|
251
|
+
return 0;
|
|
252
|
+
return stop;
|
|
193
253
|
}
|
|
194
254
|
/** The ask block's rows — the question as the rule line, the header (or
|
|
195
255
|
* the counter) as the title, the options as the body, and the
|
|
196
|
-
* type-your-own line last.
|
|
197
|
-
* title,
|
|
256
|
+
* type-your-own line last. R2: the shape is the RULE's — rule,
|
|
257
|
+
* title, header, body, affordance, rule. */
|
|
198
258
|
export function askBlockRows(view, state, W, maxRows) {
|
|
199
259
|
const p = palette();
|
|
200
260
|
const spec = view.ask;
|
|
201
261
|
const q = spec.questions[state.qIndex];
|
|
202
262
|
const multi = q.multiSelect === true;
|
|
203
|
-
const gutter = `${p.dim}│${p.reset} `;
|
|
204
263
|
const counter = spec.questions.length > 1 ? `${p.dim} ‹ ${state.qIndex + 1}/${spec.questions.length} ›${p.reset}` : "";
|
|
205
264
|
const rows = [];
|
|
206
|
-
|
|
265
|
+
// R2: the same dashed rule the composer and the approval panel use.
|
|
266
|
+
rows.push(`${p.dim}${"\u254c".repeat(Math.max(0, W))}${p.reset}`);
|
|
267
|
+
rows.push(` ${cutLine(`${p.bold}${escapeTerminal(q.question)}${p.reset}${counter}`, Math.max(1, W - 2))}`);
|
|
207
268
|
const header = q.header === undefined ? "the question" : escapeTerminal(q.header.slice(0, ASK_HEADER_CAP));
|
|
208
|
-
|
|
209
|
-
|
|
269
|
+
// R2: the divider row is gone (the opening rule says a block starts
|
|
270
|
+
// here), but the multi-select gesture it carried is INFORMATION and
|
|
271
|
+
// rides the header instead — dropping it would have been a regression
|
|
272
|
+
// wearing a restyle's clothes.
|
|
273
|
+
const gesture = multi ? `${p.dim} · pick any — space toggles${p.reset}` : "";
|
|
274
|
+
rows.push(` ${cutLine(`${p.dim}${header}${p.reset}${gesture}`, Math.max(1, W - 2))}`);
|
|
275
|
+
rows.push("");
|
|
210
276
|
const picks = state.picks[state.qIndex] ?? [];
|
|
211
|
-
const
|
|
277
|
+
const stop = askDescriptionStop(q, W);
|
|
278
|
+
const body = q.options.map((o, i) => optionRow(o, i + 1, picks.includes(i), state.cursor === i, multi, W, stop));
|
|
212
279
|
// REL-0152-D3: the row is part of the list, so it carries the same
|
|
213
280
|
// cursor affordance the options do. Dim-always made a reachable row
|
|
214
281
|
// look like a footnote.
|
|
@@ -220,14 +287,22 @@ export function askBlockRows(view, state, W, maxRows) {
|
|
|
220
287
|
// like nothing happened. The placeholder is dim and the answer is
|
|
221
288
|
// not: the two can never be mistaken for each other.
|
|
222
289
|
const typingHere = state.phase === "custom";
|
|
223
|
-
|
|
224
|
-
|
|
290
|
+
// R2: the custom row is one of the list's rows, so it wears the
|
|
291
|
+
// list's cursor — the arrow AND the bar. It was carrying bold alone,
|
|
292
|
+
// which is the exact invisibility §7.5 was written about, on the one
|
|
293
|
+
// row a keyboard user reaches last.
|
|
294
|
+
const customLead = `${onCustom ? "→" : " "} t `;
|
|
295
|
+
const customText = cutLine(typed !== null && typed !== undefined
|
|
296
|
+
? `${onCustom || typingHere ? p.bold : ""}${customLead}◉ ${escapeTerminal(typed)}${p.reset}`
|
|
225
297
|
: typingHere
|
|
226
|
-
? `${p.bold}
|
|
227
|
-
: `${onCustom ? p.bold : p.dim}
|
|
298
|
+
? `${p.bold}${customLead}▸${p.reset} ${p.dim}type your answer — enter sends, esc backs out${p.reset}`
|
|
299
|
+
: `${onCustom ? p.bold : p.dim}${customLead} type your own answer${p.reset}`, Math.max(1, W - 2));
|
|
300
|
+
body.push(onCustom ? selectionBar(customText, visibleWidth(customText), W) : ` ${customText}`);
|
|
228
301
|
// the bounded block: the options fold nothing and cut individually,
|
|
229
302
|
// so the cap drops whole rows with the W21 notice row.
|
|
230
|
-
|
|
303
|
+
// R2: SIX rows of frame — the block opens with a rule as well as
|
|
304
|
+
// closing with one, and the divider became a blank.
|
|
305
|
+
const budget = Math.max(1, maxRows - 6);
|
|
231
306
|
if (body.length > budget) {
|
|
232
307
|
const kept = Math.max(0, budget - 1);
|
|
233
308
|
rows.push(...body.slice(0, kept));
|
|
@@ -236,16 +311,13 @@ export function askBlockRows(view, state, W, maxRows) {
|
|
|
236
311
|
else {
|
|
237
312
|
rows.push(...body);
|
|
238
313
|
}
|
|
239
|
-
rows.push(
|
|
240
|
-
//
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
|
|
246
|
-
// row meaning entirely different things. The rule reads as an edge,
|
|
247
|
-
// and the cut notice above it reads as a notice.
|
|
248
|
-
rows.push(`${p.dim}\u2514${"\u2500".repeat(Math.max(0, W - 1))}${p.reset}`);
|
|
314
|
+
rows.push(` ${p.dim}${askAffordance(state)}${p.reset}`);
|
|
315
|
+
// R2, shared with the approval panel: the block closes with the SAME
|
|
316
|
+
// dashed rule it opened with, and the same one the composer uses.
|
|
317
|
+
// TUI2-R1.5 ⑪ had already replaced a two-cell `\u2514 ` stub with a
|
|
318
|
+
// real rule for the reason that stub read as the cut-notice prefix it
|
|
319
|
+
// collides with; this keeps that finding and only changes which rule.
|
|
320
|
+
rows.push(`${p.dim}${"\u254c".repeat(Math.max(0, W))}${p.reset}`);
|
|
249
321
|
return rows;
|
|
250
322
|
}
|
|
251
323
|
/** The status row's right-hand hint — the phase's keys. */
|
|
@@ -256,8 +328,15 @@ export function askAffordance(state) {
|
|
|
256
328
|
}
|
|
257
329
|
/** The status row's left text — the ask's own line, with the walk. */
|
|
258
330
|
export function askStatus(view, state) {
|
|
331
|
+
// R2: the status says the DURABLE thing. Every other agent's option
|
|
332
|
+
// panel dies with its process; this one is a fact in the event log, so
|
|
333
|
+
// killing kiso and coming back brings the question with it and never
|
|
334
|
+
// re-asks an answered one (ADR-0051 §8). The screen had never said so.
|
|
335
|
+
// It costs a status string and it is the cheapest claim in the product
|
|
336
|
+
// that no competitor can copy without building the log first.
|
|
259
337
|
const total = view.ask.questions.length;
|
|
260
|
-
|
|
338
|
+
const where = total > 1 ? `question ${state.qIndex + 1} of ${total}` : "a question for you";
|
|
339
|
+
return `⏸ ${where} · answers are durable facts`;
|
|
261
340
|
}
|
|
262
341
|
/** The input row's lead: the digit lead while picking, the typing lead
|
|
263
342
|
* in the custom phase (the rule-input phase's shape, reused). */
|
|
@@ -347,7 +426,7 @@ export function askView(spec) {
|
|
|
347
426
|
name: "ask_user",
|
|
348
427
|
title: first.header ?? first.question,
|
|
349
428
|
speaker: "kiso",
|
|
350
|
-
statusText: "
|
|
429
|
+
statusText: "⏸ a question for you",
|
|
351
430
|
args: { kind: "text", lines: askDeclineList(spec) },
|
|
352
431
|
fallbackQuestion: `⚠ ${escapeTerminal(first.question)} — this terminal cannot show the option panel; the question is declined `,
|
|
353
432
|
ask: spec,
|
package/dist/at-picker.d.ts
CHANGED
|
@@ -157,7 +157,15 @@ export declare function atPanelRows(state: {
|
|
|
157
157
|
selected: number;
|
|
158
158
|
capped: boolean;
|
|
159
159
|
}, W: number): string[];
|
|
160
|
-
/**
|
|
161
|
-
*
|
|
162
|
-
*
|
|
160
|
+
/**
|
|
161
|
+
* TUI2-R1.5 ⑦(b) — the one row that turns a band into a surface. Shared
|
|
162
|
+
* by the @ picker, the / menu and the session picker so the three read
|
|
163
|
+
* the same way.
|
|
164
|
+
*
|
|
165
|
+
* R2: the label rides the RULE. It was a bare dim word on its own row —
|
|
166
|
+
* which said "a surface starts here" only if you already knew that, and
|
|
167
|
+
* it spent a row saying it. The dashed rule is the edge vocabulary the
|
|
168
|
+
* composer and every panel now share, so a band opens the way everything
|
|
169
|
+
* else does and the label tells you WHICH band in the same row.
|
|
170
|
+
*/
|
|
163
171
|
export declare function bandHeader(label: string, W: number): string;
|
package/dist/at-picker.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* on the machine's locale).
|
|
14
14
|
*/
|
|
15
15
|
import { escapeTerminal, palette } from "./render.js";
|
|
16
|
-
import { visibleWidth, widthCut } from "./components.js";
|
|
16
|
+
import { selectionBar, visibleWidth, widthCut } from "./components.js";
|
|
17
17
|
/**
|
|
18
18
|
* KC3 §5 — the ONE cap. The file list is computed per open with no
|
|
19
19
|
* index and no watcher, so its cost is bounded here rather than
|
|
@@ -212,11 +212,11 @@ export function atRow(match, selected, W) {
|
|
|
212
212
|
// visible from anywhere on the line. Mono discipline: reverse video,
|
|
213
213
|
// no new colours.
|
|
214
214
|
//
|
|
215
|
-
//
|
|
216
|
-
//
|
|
217
|
-
//
|
|
218
|
-
|
|
219
|
-
return `${
|
|
215
|
+
// R2: the composition is selectionBar's — one copy, and with it the
|
|
216
|
+
// §2.1 rule that dim never sits on the wash (the directory column was
|
|
217
|
+
// rendering grey-on-grey inside the bar, i.e. the half of the row the
|
|
218
|
+
// selection was meant to help you read).
|
|
219
|
+
return selectionBar(`${painted}${suffix === "" ? "" : `${p.dim}${suffix}${p.reset}`}`, width, W);
|
|
220
220
|
}
|
|
221
221
|
/**
|
|
222
222
|
* KC3 §4 — the counter row: `(n/total)`, where n is the 1-based
|
|
@@ -252,10 +252,19 @@ export function atPanelRows(state, W) {
|
|
|
252
252
|
rows.push(atCounterRow(state.selected, state.matches.length, state.capped, W));
|
|
253
253
|
return rows;
|
|
254
254
|
}
|
|
255
|
-
/**
|
|
256
|
-
*
|
|
257
|
-
*
|
|
255
|
+
/**
|
|
256
|
+
* TUI2-R1.5 ⑦(b) — the one row that turns a band into a surface. Shared
|
|
257
|
+
* by the @ picker, the / menu and the session picker so the three read
|
|
258
|
+
* the same way.
|
|
259
|
+
*
|
|
260
|
+
* R2: the label rides the RULE. It was a bare dim word on its own row —
|
|
261
|
+
* which said "a surface starts here" only if you already knew that, and
|
|
262
|
+
* it spent a row saying it. The dashed rule is the edge vocabulary the
|
|
263
|
+
* composer and every panel now share, so a band opens the way everything
|
|
264
|
+
* else does and the label tells you WHICH band in the same row.
|
|
265
|
+
*/
|
|
258
266
|
export function bandHeader(label, W) {
|
|
259
267
|
const p = palette();
|
|
260
|
-
|
|
268
|
+
const head = `\u254c\u254c\u254c ${label} `;
|
|
269
|
+
return `${p.dim}${widthCut(`${head}${"\u254c".repeat(Math.max(1, W - head.length))}`, Math.max(1, W))}${p.reset}`;
|
|
261
270
|
}
|
package/dist/compositor.d.ts
CHANGED
|
@@ -52,7 +52,7 @@ export interface AtPanelState {
|
|
|
52
52
|
readonly selected: number;
|
|
53
53
|
readonly capped: boolean;
|
|
54
54
|
}
|
|
55
|
-
import { type ResumeMeta } from "./render.js";
|
|
55
|
+
import { type BannerMeta, type ResumeMeta } from "./render.js";
|
|
56
56
|
/** The cursor marker — an APC private sequence the focus component
|
|
57
57
|
* embeds at the edit position; the compositor strips it and moves
|
|
58
58
|
* relatively (it never reaches the terminal). */
|
|
@@ -171,7 +171,7 @@ export declare class Body {
|
|
|
171
171
|
* list re-gates with the tier (BIG only) and re-times with the
|
|
172
172
|
* frame. The inactive path keeps the historical bytes (no resume —
|
|
173
173
|
* the pipe contract). */
|
|
174
|
-
banner(version: string, extensionsText: string, resume?: ResumeMeta[]): void;
|
|
174
|
+
banner(version: string, extensionsText: string, resume?: ResumeMeta[], meta?: BannerMeta | undefined): void;
|
|
175
175
|
raw(lines: string[], wrap?: "words"): void;
|
|
176
176
|
/** The last COMPLETE thinking block, for /think. */
|
|
177
177
|
lastThinking(): string | null;
|
|
@@ -239,6 +239,20 @@ export declare class Body {
|
|
|
239
239
|
* that choice here, and has to pay carefully.
|
|
240
240
|
*/
|
|
241
241
|
onResize(): void;
|
|
242
|
+
/**
|
|
243
|
+
* DC-3 — the ground arrived, so every colour on the held screen is
|
|
244
|
+
* stale.
|
|
245
|
+
*
|
|
246
|
+
* The terminal answers `OSC 11` a few milliseconds after startup, by
|
|
247
|
+
* which time the first frame is already painted with the no-ground
|
|
248
|
+
* palette. Rather than delay the opening to wait for an answer that
|
|
249
|
+
* may never come, the frame is painted at once and repainted when the
|
|
250
|
+
* answer lands. Invalidating the held screen is exactly what a resize
|
|
251
|
+
* does, for exactly the same reason — every row's bytes are wrong —
|
|
252
|
+
* so it rides the settle the resize already owns and costs one
|
|
253
|
+
* repaint, once per session.
|
|
254
|
+
*/
|
|
255
|
+
onGroundChange(): void;
|
|
242
256
|
/** W18: the status row's right-aligned hint is part of the status
|
|
243
257
|
* state — the compacting row passes "esc to cancel" (the affordance
|
|
244
258
|
* must survive repaints). */
|
|
@@ -295,6 +309,8 @@ export declare class Dock {
|
|
|
295
309
|
enter(): void;
|
|
296
310
|
exit(): void;
|
|
297
311
|
onResize(): void;
|
|
312
|
+
/** DC-3 — the terminal answered what its background is; repaint. */
|
|
313
|
+
onGroundChange(): void;
|
|
298
314
|
/** TUI2-R3v2 ②: where the last frame put the panel's clickable option
|
|
299
315
|
* rows (absolute screen rows). The editor binds this and does no row
|
|
300
316
|
* arithmetic of its own. */
|
package/dist/compositor.js
CHANGED
|
@@ -667,7 +667,7 @@ export class Body {
|
|
|
667
667
|
this.#closeOpenThinking();
|
|
668
668
|
this.#closeOpenText();
|
|
669
669
|
const p = palette();
|
|
670
|
-
this.#write(`${p.bold}
|
|
670
|
+
this.#write(`${p.bold}✦${p.reset} ${escapeTerminal(header)}\n`);
|
|
671
671
|
const glyphOf = (status) => (status === "pending" ? "□" : status === "active" ? "▖" : "▣");
|
|
672
672
|
for (const item of items)
|
|
673
673
|
this.#write(` ${glyphOf(item.status)} ${escapeTerminal(item.text)}\n`);
|
|
@@ -706,21 +706,21 @@ export class Body {
|
|
|
706
706
|
* list re-gates with the tier (BIG only) and re-times with the
|
|
707
707
|
* frame. The inactive path keeps the historical bytes (no resume —
|
|
708
708
|
* the pipe contract). */
|
|
709
|
-
banner(version, extensionsText, resume = []) {
|
|
709
|
+
banner(version, extensionsText, resume = [], meta) {
|
|
710
710
|
if (!this.#isActive()) {
|
|
711
711
|
this.#closeOpenThinking();
|
|
712
712
|
this.#closeOpenText();
|
|
713
713
|
const W = this.#opts.width() || 80; // a 0-size pty falls back
|
|
714
714
|
const H = this.#opts.height();
|
|
715
715
|
const p = palette();
|
|
716
|
-
for (const r of bannerLines(W, H, version, extensionsText))
|
|
716
|
+
for (const r of bannerLines(W, H, version, extensionsText, [], Date.now(), meta))
|
|
717
717
|
this.#write(`${p.dim}${r}${p.reset}\n`);
|
|
718
718
|
this.#write("\n");
|
|
719
719
|
return;
|
|
720
720
|
}
|
|
721
721
|
this.#closeOpenThinking();
|
|
722
722
|
this.#closeOpenText();
|
|
723
|
-
this.#cells.push({ kind: "banner", version, extensionsText, resume, done: true });
|
|
723
|
+
this.#cells.push({ kind: "banner", version, extensionsText, resume, meta, done: true });
|
|
724
724
|
this.#mark();
|
|
725
725
|
}
|
|
726
726
|
raw(lines, wrap) {
|
|
@@ -815,11 +815,11 @@ export class Body {
|
|
|
815
815
|
// the row showed, then one row per tool with its subjects. The
|
|
816
816
|
// header keeps W15's shape; only the subject changes.
|
|
817
817
|
if (cell.rolled.parts !== undefined) {
|
|
818
|
-
const header = `${p.bold}
|
|
818
|
+
const header = `${p.bold}✦${p.reset} expanded · ${escapeTerminal(`explored ${exploreCounts(cell.rolled.parts)}`)} · ${back}`;
|
|
819
819
|
return { kind: "appended", lines: [header, ...exploreRows(cell.rolled.parts, this.#opts.width())] };
|
|
820
820
|
}
|
|
821
821
|
const noun = ROLLUP_NOUN[cell.name] ?? "calls";
|
|
822
|
-
const header = `${p.bold}
|
|
822
|
+
const header = `${p.bold}✦${p.reset} expanded · ${escapeTerminal(`${displayVerb(cell.name)} ${cell.rolled.count} ${noun}`)} · ${back}`;
|
|
823
823
|
return {
|
|
824
824
|
kind: "appended",
|
|
825
825
|
lines: [header, ...cell.rolled.targets.map((t) => ` ${p.dim}└ ${escapeTerminal(t)}${p.reset}`)],
|
|
@@ -835,7 +835,7 @@ export class Body {
|
|
|
835
835
|
}
|
|
836
836
|
const turnsBack = this.#cells.slice(idx + 1).filter((c) => c.kind === "user").length;
|
|
837
837
|
const p = palette();
|
|
838
|
-
const header = `${p.bold}
|
|
838
|
+
const header = `${p.bold}✦${p.reset} expanded · ${escapeTerminal(`${displayVerb(cell.name)} ${toolTarget(cell.name, input)}`)} · ${turnsBack} ${turnsBack === 1 ? "turn" : "turns"} back`;
|
|
839
839
|
return {
|
|
840
840
|
kind: "appended",
|
|
841
841
|
lines: [
|
|
@@ -1092,6 +1092,23 @@ export class Body {
|
|
|
1092
1092
|
if (this.#resizeTimer.unref !== undefined)
|
|
1093
1093
|
this.#resizeTimer.unref();
|
|
1094
1094
|
}
|
|
1095
|
+
/**
|
|
1096
|
+
* DC-3 — the ground arrived, so every colour on the held screen is
|
|
1097
|
+
* stale.
|
|
1098
|
+
*
|
|
1099
|
+
* The terminal answers `OSC 11` a few milliseconds after startup, by
|
|
1100
|
+
* which time the first frame is already painted with the no-ground
|
|
1101
|
+
* palette. Rather than delay the opening to wait for an answer that
|
|
1102
|
+
* may never come, the frame is painted at once and repainted when the
|
|
1103
|
+
* answer lands. Invalidating the held screen is exactly what a resize
|
|
1104
|
+
* does, for exactly the same reason — every row's bytes are wrong —
|
|
1105
|
+
* so it rides the settle the resize already owns and costs one
|
|
1106
|
+
* repaint, once per session.
|
|
1107
|
+
*/
|
|
1108
|
+
onGroundChange() {
|
|
1109
|
+
this.#screen = [];
|
|
1110
|
+
this.onResize();
|
|
1111
|
+
}
|
|
1095
1112
|
/** The one repaint a drag earns, once its signals have stopped. */
|
|
1096
1113
|
#settleResize() {
|
|
1097
1114
|
if (!this.#resizePending || !this.#isActive())
|
|
@@ -1182,9 +1199,11 @@ export class Body {
|
|
|
1182
1199
|
// W23: the frame-derived column — wallL + leadWidth(lead) + cells
|
|
1183
1200
|
// + 1 — the SAME formula the marker embeds at (the panel lead when
|
|
1184
1201
|
// the panel owns the row; the old prompt-only math desynced the
|
|
1185
|
-
// panel rows' edit column; leadWidth is the ONE authority)
|
|
1202
|
+
// panel rows' edit column; leadWidth is the ONE authority).
|
|
1203
|
+
// R2: wallL is 0 — the box is retired, so the row starts at column
|
|
1204
|
+
// one and the frame's marker and this formula share the constant.
|
|
1186
1205
|
const lead = panel !== null ? panelLeadOf(panel) : this.#inputPrompt;
|
|
1187
|
-
return
|
|
1206
|
+
return 1 + leadWidth(lead) + st.cursor;
|
|
1188
1207
|
}
|
|
1189
1208
|
/** The old dock's redraw — the editor's onRender target: mark + the
|
|
1190
1209
|
* scheduler (16ms coalescing — the old sync draw coalesces the same). */
|
|
@@ -1903,8 +1922,14 @@ export class Body {
|
|
|
1903
1922
|
inserted = true;
|
|
1904
1923
|
}
|
|
1905
1924
|
const cw = displayWidth(row[i]);
|
|
1906
|
-
|
|
1907
|
-
|
|
1925
|
+
// R2: no walls to pay for — but ONE column stays reserved. When
|
|
1926
|
+
// the cursor rests past the content the drawn cell is taken out
|
|
1927
|
+
// of the pad, and a walk that filled to exactly W leaves no pad
|
|
1928
|
+
// to take it from: the row becomes W+1 and invariant ① throws.
|
|
1929
|
+
// The old cap paid for two walls and a space; this pays for the
|
|
1930
|
+
// cursor, which is the only thing still owed a cell.
|
|
1931
|
+
if (w + cw > W - 1)
|
|
1932
|
+
break;
|
|
1908
1933
|
markerLine += row[i];
|
|
1909
1934
|
w += cw;
|
|
1910
1935
|
i += 1;
|
|
@@ -1938,8 +1963,8 @@ export class Body {
|
|
|
1938
1963
|
const after = markerLine.slice(mi + CURSOR_MARKER.length);
|
|
1939
1964
|
if (after.length === 0) {
|
|
1940
1965
|
// the cursor rests past the content — an inverse space,
|
|
1941
|
-
// taken OUT of the pad (the walk capped content at W
|
|
1942
|
-
//
|
|
1966
|
+
// taken OUT of the pad (the walk capped content at W, so
|
|
1967
|
+
// the pad absorbs it and the row still totals W)
|
|
1943
1968
|
stripped0 = `${before}\x1b[7m \x1b[27m`;
|
|
1944
1969
|
cursorPad = 1;
|
|
1945
1970
|
}
|
|
@@ -1954,10 +1979,12 @@ export class Body {
|
|
|
1954
1979
|
stripped0 = `${before}\x1b[7m${glyph}\x1b[27m${after.slice(glyph.length)}`;
|
|
1955
1980
|
}
|
|
1956
1981
|
}
|
|
1957
|
-
//
|
|
1958
|
-
//
|
|
1959
|
-
|
|
1960
|
-
|
|
1982
|
+
// R2 — the box is retired (see boxTop): the composer is two dashed
|
|
1983
|
+
// rules and the row between them, so there are no walls to pay for
|
|
1984
|
+
// and no pad to hold them off. The row is exactly W, as it always
|
|
1985
|
+
// was; what changed is that all W columns belong to the input.
|
|
1986
|
+
const padW = Math.max(0, W - w - cursorPad);
|
|
1987
|
+
return { stripped: `${stripped0}${" ".repeat(padW)}`, markerCell };
|
|
1961
1988
|
}
|
|
1962
1989
|
/** KC1 §6 — the focus component's input ROWS (N = 1 today's single
|
|
1963
1990
|
* row, byte for byte). The lead rides the FIRST row and the
|
|
@@ -1994,7 +2021,7 @@ export class Body {
|
|
|
1994
2021
|
cursorRow -= first;
|
|
1995
2022
|
}
|
|
1996
2023
|
const out = [];
|
|
1997
|
-
let markerCol =
|
|
2024
|
+
let markerCol = 1; // R2: no wall to skip — the row starts at column 1
|
|
1998
2025
|
for (let r = 0; r < rows.length; r += 1) {
|
|
1999
2026
|
const text = `${r === 0 ? lead : " ".repeat(leadW)}${rows[r]}`;
|
|
2000
2027
|
const bytes = this.#inputRowBytes(text, W, r === cursorRow ? leadW + cursorCol : null);
|
|
@@ -2002,7 +2029,7 @@ export class Body {
|
|
|
2002
2029
|
// W23: the frame-derived column — wallL (2) + the marker's
|
|
2003
2030
|
// cell + 1 — the CHA lands the cursor AT the marker from ANY base
|
|
2004
2031
|
if (r === cursorRow)
|
|
2005
|
-
markerCol =
|
|
2032
|
+
markerCol = 1 + bytes.markerCell;
|
|
2006
2033
|
}
|
|
2007
2034
|
return { rows: out, markerRow: cursorRow, markerCol };
|
|
2008
2035
|
}
|
|
@@ -2323,6 +2350,10 @@ export class Dock {
|
|
|
2323
2350
|
onResize() {
|
|
2324
2351
|
compositorRef?.onResize();
|
|
2325
2352
|
}
|
|
2353
|
+
/** DC-3 — the terminal answered what its background is; repaint. */
|
|
2354
|
+
onGroundChange() {
|
|
2355
|
+
compositorRef?.onGroundChange();
|
|
2356
|
+
}
|
|
2326
2357
|
/** TUI2-R3v2 ②: where the last frame put the panel's clickable option
|
|
2327
2358
|
* rows (absolute screen rows). The editor binds this and does no row
|
|
2328
2359
|
* arithmetic of its own. */
|
package/dist/editor.d.ts
CHANGED
|
@@ -56,6 +56,15 @@ export declare class Editor {
|
|
|
56
56
|
#private;
|
|
57
57
|
readonly closed: Promise<void>;
|
|
58
58
|
constructor(onRender: () => void);
|
|
59
|
+
/**
|
|
60
|
+
* DC-7 — the terminal answering a question kiso asked it.
|
|
61
|
+
*
|
|
62
|
+
* The body is everything between `ESC ]` and the terminator, verbatim
|
|
63
|
+
* and unparsed (`11;rgb:ffff/ffff/ffff`). The editor's job ends at
|
|
64
|
+
* keeping it out of the draft; deciding what a report MEANS belongs to
|
|
65
|
+
* whoever asked the question.
|
|
66
|
+
*/
|
|
67
|
+
onOsc(cb: (body: string) => void): void;
|
|
59
68
|
onLine(cb: (line: string) => void): void;
|
|
60
69
|
onSigint(cb: () => void): void;
|
|
61
70
|
onEot(cb: () => void): void;
|
package/dist/editor.js
CHANGED
|
@@ -81,6 +81,11 @@ const TAB = 0x09;
|
|
|
81
81
|
* the geometry stays legal down to the compositor's enter gate (H = 4
|
|
82
82
|
* ⇒ one row, exactly today's minimum). */
|
|
83
83
|
const N_MAX = 6;
|
|
84
|
+
/** DC-7 — the longest OSC kiso will hold while waiting for a terminator.
|
|
85
|
+
* The reports it reads are tens of bytes; anything past this is a
|
|
86
|
+
* payload for someone else, and holding it is how the editor goes
|
|
87
|
+
* deaf. */
|
|
88
|
+
const OSC_MAX = 1024;
|
|
84
89
|
/** The dim "…" — the ONE truncation mark: the horizontal scroll's
|
|
85
90
|
* prefix (unchanged) and the viewport's hidden-rows markers. */
|
|
86
91
|
const ELLIPSIS = "\x1b[2m…\x1b[0m";
|
|
@@ -293,7 +298,9 @@ export class Editor {
|
|
|
293
298
|
#queueState = () => [];
|
|
294
299
|
#queuePop = null;
|
|
295
300
|
#queuePopMode = false;
|
|
296
|
-
#pending = ""; // an incomplete ESC/CSI prefix across chunks
|
|
301
|
+
#pending = ""; // an incomplete ESC/CSI/OSC prefix across chunks
|
|
302
|
+
/** DC-7: the terminal's own reports (OSC). Never a keystroke. */
|
|
303
|
+
#oscCb = null;
|
|
297
304
|
#decoder = new TextDecoder();
|
|
298
305
|
#entered = false;
|
|
299
306
|
#onData;
|
|
@@ -306,6 +313,17 @@ export class Editor {
|
|
|
306
313
|
this.#closedResolve = resolve;
|
|
307
314
|
});
|
|
308
315
|
}
|
|
316
|
+
/**
|
|
317
|
+
* DC-7 — the terminal answering a question kiso asked it.
|
|
318
|
+
*
|
|
319
|
+
* The body is everything between `ESC ]` and the terminator, verbatim
|
|
320
|
+
* and unparsed (`11;rgb:ffff/ffff/ffff`). The editor's job ends at
|
|
321
|
+
* keeping it out of the draft; deciding what a report MEANS belongs to
|
|
322
|
+
* whoever asked the question.
|
|
323
|
+
*/
|
|
324
|
+
onOsc(cb) {
|
|
325
|
+
this.#oscCb = cb;
|
|
326
|
+
}
|
|
309
327
|
onLine(cb) {
|
|
310
328
|
this.#lineCb = cb;
|
|
311
329
|
// Flush submits that arrived before the handler was wired (typed
|
|
@@ -1101,6 +1119,32 @@ export class Editor {
|
|
|
1101
1119
|
this.#csi(m[1], m[2]);
|
|
1102
1120
|
i += m[0].length + 1;
|
|
1103
1121
|
}
|
|
1122
|
+
else if (rest.startsWith("]")) {
|
|
1123
|
+
// DC-7: an OSC is a message FROM the terminal — a background
|
|
1124
|
+
// colour answer, a theme-change notice, a clipboard report.
|
|
1125
|
+
// There was no branch for it, so the bytes fell through to
|
|
1126
|
+
// the literal-text path and the answer was typed into the
|
|
1127
|
+
// draft. The terminator is BEL **or** ST: Apple Terminal
|
|
1128
|
+
// answers `ESC ] 11 ; rgb:… BEL`, and ST is the standard.
|
|
1129
|
+
const end = /\x07|\x1b\\/.exec(rest);
|
|
1130
|
+
if (end === null) {
|
|
1131
|
+
const tail = text.slice(i);
|
|
1132
|
+
// The unterminated case is the SGR-1006 hazard by another
|
|
1133
|
+
// door: park it and a stream that never terminates grows
|
|
1134
|
+
// #pending forever until the editor goes deaf. A report
|
|
1135
|
+
// long enough to be a payload (OSC 52 carries a whole
|
|
1136
|
+
// clipboard) is not one we read, so past the cap it is
|
|
1137
|
+
// dropped rather than held.
|
|
1138
|
+
if (tail.length > OSC_MAX) {
|
|
1139
|
+
i = text.length;
|
|
1140
|
+
break;
|
|
1141
|
+
}
|
|
1142
|
+
this.#pending = tail; // incomplete OSC — wait for more
|
|
1143
|
+
break;
|
|
1144
|
+
}
|
|
1145
|
+
this.#oscCb?.(rest.slice(1, end.index));
|
|
1146
|
+
i += 1 + end.index + end[0].length;
|
|
1147
|
+
}
|
|
1104
1148
|
else if (rest.startsWith("O")) {
|
|
1105
1149
|
i += 3; // SS3 (function keys) — ignored
|
|
1106
1150
|
}
|
|
@@ -2309,7 +2353,14 @@ export class Editor {
|
|
|
2309
2353
|
// owns the keys, the brick otherwise): maxW = W − walls − lead.
|
|
2310
2354
|
const lead = this.#panel !== null ? panelLead(this.#panel.view, this.#panel.phase, this.#panel.cursor, this.#panel.ask ?? undefined) : PROMPT;
|
|
2311
2355
|
const leadW = leadWidth(lead);
|
|
2312
|
-
|
|
2356
|
+
// DC-17: ONE column, not four. W6's box took 2+2 and this kept
|
|
2357
|
+
// reserving them after law 1.1 retired it — so the horizontal
|
|
2358
|
+
// scroll fired three columns early, the cursor could never reach
|
|
2359
|
+
// the row's last three cells, and the two width authorities the
|
|
2360
|
+
// W23 contract says must never disagree disagreed by 3. The
|
|
2361
|
+
// compositor's walk caps at W−1 (the drawn cursor's own cell);
|
|
2362
|
+
// this is the same one column, on the same row.
|
|
2363
|
+
const maxW = Math.max(1, W - leadW - 1);
|
|
2313
2364
|
// KC1: the scroll is the CURSOR LINE's own offset — a single-line
|
|
2314
2365
|
// buffer's line starts at 0, so the math is today's exactly. The
|
|
2315
2366
|
// clamp catches a walk onto a line SHORTER than the old offset.
|
package/dist/index.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export { Body, Dock, CURSOR_MARKER, type BodyOptions } from "./compositor.js";
|
|
|
10
10
|
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, deletionRiskHint, SAFER_BACK, SAFER_DEGRADED, SAFER_DEGRADED_TRUNCATED, saferDegradedNote, type SaferAnswer, type SaferFailure, type SaferOption, type SaferRuntime, panelOptions, type PanelOption, type PanelOptionKind, type PanelState, type PanelVerdict, type PanelView, } from "./approval-panel.js";
|
|
11
11
|
export { Container, foldLine, foldWords, visibleWidth, SPINNER, type Component, type FrameCtx } from "./components.js";
|
|
12
12
|
export { Editor, MENU_ITEMS, PROMPT, PROMPT_WIDTH, displayWidth, charWidth, widthOf, type MenuItem, } from "./editor.js";
|
|
13
|
-
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
|
+
export { bannerLines, COLOR_OFF, COLOR_ON, currentGround, setGround, 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";
|
|
14
14
|
export { editFileDiff, truncateDiff, writeFileDiff, type DiffLine, type DiffResult } from "./diff.js";
|
|
15
15
|
export { STATUS_GLYPHS, cacheHitPct, idleStatus, runningStatus, type StatusMeter } from "./status.js";
|
|
16
16
|
export { contextRows, contextUnavailableRows, type ContextLedger } from "./context-ledger.js";
|
|
@@ -18,4 +18,5 @@ export { interactivePrompt, projectTrustRows, projectTrustView, projectUntrusted
|
|
|
18
18
|
export { AT_CAP, AT_SKIP, AT_VISIBLE, atEmbed, atFilter, atPanelRows, atWindow, bandHeader, longestRun, type AtItem, type AtMatch } from "./at-picker.js";
|
|
19
19
|
export { BADGE_GLYPH, idColumn, sessionAge, sessionBadge, sessionCounterRow, sessionFilter, sessionListFooter, sessionListRow, sessionNote, sessionPickerRows, sessionRow, type SessionCardView, type SessionPickState, } from "./session-picker.js";
|
|
20
20
|
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";
|
|
21
|
+
export { resolveGround, type Ground } from "@vincemakes/kiso-tui-cells";
|
|
21
22
|
export { KEY_BINDINGS, PANEL_KEYS_ROW, displayVerb, extensionsBannerText, helpRows, keysHelpRow, keysSheetRows, unansweredAskView, type BannerExtension, type KeyBinding } from "./strings.js";
|
package/dist/index.js
CHANGED
|
@@ -15,7 +15,7 @@ export { panelAffordance, panelBlockRows, panelLead, panelLeadPlain, panelLeadWi
|
|
|
15
15
|
PICK_MAX, modelPickView, pickAffordance, pickBlockRows, pickLeadPlain, deletionRiskHint, SAFER_BACK, SAFER_DEGRADED, SAFER_DEGRADED_TRUNCATED, saferDegradedNote, panelOptions, } from "./approval-panel.js";
|
|
16
16
|
export { Container, foldLine, foldWords, visibleWidth, SPINNER } from "./components.js";
|
|
17
17
|
export { Editor, MENU_ITEMS, PROMPT, PROMPT_WIDTH, displayWidth, charWidth, widthOf, } from "./editor.js";
|
|
18
|
-
export { bannerLines, COLOR_OFF, COLOR_ON, escapeTerminal, foldResult, foldThinking, kUnit, palette, renderEvent, renderRecap, renderResumeList, renderSessionLine, renderStatusLine, relativeTime, renderTerminalGap, renderToolSummary, TAGLINE, toolTarget, truncateRow, } from "./render.js";
|
|
18
|
+
export { bannerLines, COLOR_OFF, COLOR_ON, currentGround, setGround, escapeTerminal, foldResult, foldThinking, kUnit, palette, renderEvent, renderRecap, renderResumeList, renderSessionLine, renderStatusLine, relativeTime, renderTerminalGap, renderToolSummary, TAGLINE, toolTarget, truncateRow, } from "./render.js";
|
|
19
19
|
export { editFileDiff, truncateDiff, writeFileDiff } from "./diff.js";
|
|
20
20
|
// KC2 §5: the status rows' formatters — the CLI keeps the state and the
|
|
21
21
|
// repaint, the terminal layer owns what the row says.
|
|
@@ -42,4 +42,5 @@ export { ASK_HEADER_CAP, ASK_MAX_OPTIONS, ASK_MAX_QUESTIONS, ASK_MIN_OPTIONS, as
|
|
|
42
42
|
// honestly for a question nobody answered (the ① probe's surface).
|
|
43
43
|
// TUI2-R1 (D): the keys sheet + THE key table — one source for the ?
|
|
44
44
|
// overlay and /help's keys row.
|
|
45
|
+
export { resolveGround } from "@vincemakes/kiso-tui-cells";
|
|
45
46
|
export { KEY_BINDINGS, PANEL_KEYS_ROW, displayVerb, extensionsBannerText, helpRows, keysHelpRow, keysSheetRows, unansweredAskView } from "./strings.js";
|
package/dist/render.js
CHANGED
|
@@ -103,10 +103,10 @@ export function renderEvent(ev, prevThinking = false, resolvePath = (p) => p) {
|
|
|
103
103
|
prompt: false,
|
|
104
104
|
};
|
|
105
105
|
case "checklist": {
|
|
106
|
-
// ⑥: the durable checklist — the
|
|
106
|
+
// ⑥: the durable checklist — the ✦ header accent (the recap's
|
|
107
107
|
// brick) + one brick-glyph line per item. Static line content:
|
|
108
108
|
// byte-identical in pipes and NO_COLOR.
|
|
109
|
-
const lines = [`${p.bold}
|
|
109
|
+
const lines = [`${p.bold}✦${p.reset} ${escapeTerminal(ev.header)}`];
|
|
110
110
|
for (const item of ev.items) {
|
|
111
111
|
const glyph = item.status === "pending" ? "□" : item.status === "active" ? "▖" : "▣";
|
|
112
112
|
lines.push(` ${glyph} ${escapeTerminal(item.text)}`);
|
|
@@ -174,7 +174,7 @@ export function renderRecap(s) {
|
|
|
174
174
|
// row's right side.
|
|
175
175
|
if (s.mode === "plan") {
|
|
176
176
|
const parts = ["plan ready", "/mode default executes", "/mode accept-edits auto-approves edits"];
|
|
177
|
-
return `${p.bold}
|
|
177
|
+
return `${p.bold}✦${p.reset} ${parts.join(" · ")}\n`;
|
|
178
178
|
}
|
|
179
179
|
const parts = [`${s.seconds}s`, `${s.tools} tool${s.tools === 1 ? "" : "s"}${s.edits > 0 ? ` (${s.edits} edit${s.edits === 1 ? "" : "s"})` : ""}`];
|
|
180
180
|
if (s.usage.known) {
|
|
@@ -193,7 +193,7 @@ export function renderRecap(s) {
|
|
|
193
193
|
}
|
|
194
194
|
if (s.ctxLeftPct !== null)
|
|
195
195
|
parts.push(`ctx left ~${Math.round(s.ctxLeftPct)}%`);
|
|
196
|
-
return `${p.bold}
|
|
196
|
+
return `${p.bold}✦${p.reset} ${parts.join(" · ")}\n`;
|
|
197
197
|
}
|
|
198
198
|
/** One-line summary of a session, for `kiso sessions`. */
|
|
199
199
|
export function renderSessionLine(meta) {
|
package/dist/session-picker.d.ts
CHANGED
|
@@ -68,10 +68,23 @@ export declare function sessionAge(updatedAt: number, now: number): string;
|
|
|
68
68
|
* (the whole reason a filter-as-you-type picker is usable). */
|
|
69
69
|
export declare function idColumn(cards: readonly SessionCardView[]): number;
|
|
70
70
|
/**
|
|
71
|
-
* The filter — the @ picker's muscle, aimed at the
|
|
71
|
+
* The filter — the @ picker's muscle, aimed at what the ROW SHOWS: a
|
|
72
72
|
* case-insensitive SUBSEQUENCE, ranked by the longest contiguous run,
|
|
73
|
-
* then by
|
|
74
|
-
* feel; a row under the cursor never moves because two
|
|
73
|
+
* then by the haystack's length, then lexically. Identical determinism,
|
|
74
|
+
* identical feel; a row under the cursor never moves because two
|
|
75
|
+
* candidates tied.
|
|
76
|
+
*
|
|
77
|
+
* DC-13 (R2) — it used to search the ID and nothing else. That was
|
|
78
|
+
* coherent while the id was the row's leading column; the owner's
|
|
79
|
+
* ruling moved the TITLE there and retired the id from the row, and the
|
|
80
|
+
* filter did not follow. The result is the worst kind of search: typing
|
|
81
|
+
* what you can SEE returns nothing, and typing an id you cannot see
|
|
82
|
+
* narrows the list for a reason the screen never explains.
|
|
83
|
+
*
|
|
84
|
+
* Both are searched, title FIRST. The id stays a haystack because
|
|
85
|
+
* `kiso sessions` prints ids and a human who copied one must be able to
|
|
86
|
+
* paste it here; a title hit outranks an id hit at equal run length,
|
|
87
|
+
* because the title is what the person was reading.
|
|
75
88
|
*
|
|
76
89
|
* An empty query matches everything and keeps the caller's order (the
|
|
77
90
|
* listing's newest-first), because "no query" is not a search — it is
|
|
@@ -107,10 +120,23 @@ export interface SessionPickState {
|
|
|
107
120
|
* channel.
|
|
108
121
|
*/
|
|
109
122
|
export declare function sessionPickerRows(state: SessionPickState, W: number, now: number): string[];
|
|
110
|
-
/**
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
123
|
+
/**
|
|
124
|
+
* Slice ③ — the `kiso sessions` TTY row: the same projection, printed
|
|
125
|
+
* rather than picked. No selection bar (nothing is selected on a
|
|
126
|
+
* listing) and no leading indent: this row starts at column 1 like
|
|
127
|
+
* every other line a shell command prints.
|
|
128
|
+
*
|
|
129
|
+
* DC-16: it keeps the ID, and the picker row does not. Sharing one
|
|
130
|
+
* projection is what stops the two surfaces drifting — that is this
|
|
131
|
+
* module's whole design and it stays — but "share the projection" is
|
|
132
|
+
* not "be the same row". The owner's ruling was about the PICKER, where
|
|
133
|
+
* the id was four characters of machine identity in the column the eye
|
|
134
|
+
* lands on first. A LISTING is the surface you read to copy an id OUT
|
|
135
|
+
* of: it is what `/resume <id>` and the filter's id haystack both
|
|
136
|
+
* assume exists, and deleting the id here quietly falsified both. The
|
|
137
|
+
* id goes LAST and dim — present for the hand that needs it, out of the
|
|
138
|
+
* way of the eye that does not.
|
|
139
|
+
*/
|
|
114
140
|
export declare function sessionListRow(card: SessionCardView, W: number, now: number, idCol: number): string;
|
|
115
141
|
/** Slice ③ — the listing's last line: the count, and the one thing the
|
|
116
142
|
* user can do next. */
|
package/dist/session-picker.js
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* listing render from ONE definition instead of two that drift.
|
|
25
25
|
*/
|
|
26
26
|
import { escapeTerminal, palette } from "./render.js";
|
|
27
|
-
import { visibleWidth, widthCut } from "./components.js";
|
|
27
|
+
import { selectionBar, visibleWidth, widthCut } from "./components.js";
|
|
28
28
|
import { atEmbed, bandHeader, longestRun, AT_VISIBLE, atWindow } from "./at-picker.js";
|
|
29
29
|
/** The glyph per state — one cell each, so the badge column never
|
|
30
30
|
* shifts the id column (a column that moves per row reads as damage). */
|
|
@@ -105,10 +105,23 @@ export function idColumn(cards) {
|
|
|
105
105
|
return Math.min(Math.max(w, 1), 24);
|
|
106
106
|
}
|
|
107
107
|
/**
|
|
108
|
-
* The filter — the @ picker's muscle, aimed at the
|
|
108
|
+
* The filter — the @ picker's muscle, aimed at what the ROW SHOWS: a
|
|
109
109
|
* case-insensitive SUBSEQUENCE, ranked by the longest contiguous run,
|
|
110
|
-
* then by
|
|
111
|
-
* feel; a row under the cursor never moves because two
|
|
110
|
+
* then by the haystack's length, then lexically. Identical determinism,
|
|
111
|
+
* identical feel; a row under the cursor never moves because two
|
|
112
|
+
* candidates tied.
|
|
113
|
+
*
|
|
114
|
+
* DC-13 (R2) — it used to search the ID and nothing else. That was
|
|
115
|
+
* coherent while the id was the row's leading column; the owner's
|
|
116
|
+
* ruling moved the TITLE there and retired the id from the row, and the
|
|
117
|
+
* filter did not follow. The result is the worst kind of search: typing
|
|
118
|
+
* what you can SEE returns nothing, and typing an id you cannot see
|
|
119
|
+
* narrows the list for a reason the screen never explains.
|
|
120
|
+
*
|
|
121
|
+
* Both are searched, title FIRST. The id stays a haystack because
|
|
122
|
+
* `kiso sessions` prints ids and a human who copied one must be able to
|
|
123
|
+
* paste it here; a title hit outranks an id hit at equal run length,
|
|
124
|
+
* because the title is what the person was reading.
|
|
112
125
|
*
|
|
113
126
|
* An empty query matches everything and keeps the caller's order (the
|
|
114
127
|
* listing's newest-first), because "no query" is not a search — it is
|
|
@@ -120,17 +133,22 @@ export function sessionFilter(cards, query) {
|
|
|
120
133
|
const lower = query.toLowerCase();
|
|
121
134
|
const scored = [];
|
|
122
135
|
for (const card of cards) {
|
|
123
|
-
const
|
|
136
|
+
const shown = (card.title ?? card.id).toLowerCase();
|
|
137
|
+
const onTitle = atEmbed(shown, lower);
|
|
138
|
+
const hit = onTitle ?? atEmbed(card.id.toLowerCase(), lower);
|
|
124
139
|
if (hit === null)
|
|
125
140
|
continue;
|
|
126
|
-
|
|
141
|
+
const key = onTitle !== null ? shown : card.id;
|
|
142
|
+
scored.push({ card, run: longestRun(hit), onTitle: onTitle !== null, key });
|
|
127
143
|
}
|
|
128
144
|
scored.sort((a, b) => {
|
|
129
145
|
if (a.run !== b.run)
|
|
130
146
|
return b.run - a.run;
|
|
131
|
-
if (a.
|
|
132
|
-
return a.
|
|
133
|
-
|
|
147
|
+
if (a.onTitle !== b.onTitle)
|
|
148
|
+
return a.onTitle ? -1 : 1;
|
|
149
|
+
if (a.key.length !== b.key.length)
|
|
150
|
+
return a.key.length - b.key.length;
|
|
151
|
+
return a.key < b.key ? -1 : a.key > b.key ? 1 : 0;
|
|
134
152
|
});
|
|
135
153
|
return scored.map((s) => s.card);
|
|
136
154
|
}
|
|
@@ -173,27 +191,29 @@ function rowSpans(card, budget, now, idCol) {
|
|
|
173
191
|
text += `${sessionBadge(card.badge)} `;
|
|
174
192
|
w += 2;
|
|
175
193
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
//
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
|
|
194
|
+
// R2 (owner, 2026-08-27) — the TITLE LEADS and the id is gone.
|
|
195
|
+
//
|
|
196
|
+
// The id was four characters of machine identity sitting in the column
|
|
197
|
+
// the eye lands on first, and the title — the only span that answers
|
|
198
|
+
// "which conversation is this?" — came after the meta. The order is
|
|
199
|
+
// reversed: the title takes the left edge, the age and turn count go
|
|
200
|
+
// right and dim, and the note keeps its reserve. The id is still
|
|
201
|
+
// reachable where it is USED (`kiso sessions`, `/status`) — it left
|
|
202
|
+
// the row it was never the subject of.
|
|
203
|
+
// a row with no title at all falls back to the id: the id left the
|
|
204
|
+
// row it was never the subject of, but a row must still IDENTIFY
|
|
205
|
+
// something — an anonymous row is not a picker row. (In practice
|
|
206
|
+
// `sessionTitle` returns "(no prompt)" rather than "", so this is
|
|
207
|
+
// the seam for callers that build a card without records.)
|
|
208
|
+
const title = card.title ?? card.id;
|
|
189
209
|
if (title !== "") {
|
|
190
210
|
const room = Math.max(0, Math.min(budget - w - 3 - NOTE_RESERVE, TITLE_MAX));
|
|
191
211
|
const cut = widthCut(escapeTerminal(title), room);
|
|
192
|
-
if (cut !== "")
|
|
193
|
-
put(" ", " ");
|
|
212
|
+
if (cut !== "")
|
|
194
213
|
put(cut, `${p.bold}${cut}${p.reset}`);
|
|
195
|
-
}
|
|
196
214
|
}
|
|
215
|
+
const meta = ` ${sessionAge(card.updatedAt, now)} · ${card.turns} turn${card.turns === 1 ? "" : "s"}`;
|
|
216
|
+
put(meta, `${p.dim}${meta}${p.reset}`);
|
|
197
217
|
const note = widthCut(sessionNote(card), Math.max(0, budget - w - 3));
|
|
198
218
|
if (note !== "") {
|
|
199
219
|
// the ? note carries the warn tint — the row's own words are what
|
|
@@ -221,8 +241,10 @@ export function sessionRow(card, selected, W, now, idCol) {
|
|
|
221
241
|
const { text, width } = rowSpans(card, Math.max(0, W - 2), now, idCol);
|
|
222
242
|
if (!selected)
|
|
223
243
|
return ` ${text}`;
|
|
224
|
-
|
|
225
|
-
|
|
244
|
+
// R2: one bar, in one place — and with it §2.1's rule that dim never
|
|
245
|
+
// sits on the wash. The age/turns/note spans were dim INSIDE the bar,
|
|
246
|
+
// grey on grey, on the row the cursor was pointing at.
|
|
247
|
+
return selectionBar(text, width, W);
|
|
226
248
|
}
|
|
227
249
|
/** The counter row — the SELECTION's 1-based place in the whole
|
|
228
250
|
* filtered list, which the visible window cannot tell the user. */
|
|
@@ -253,12 +275,30 @@ export function sessionPickerRows(state, W, now) {
|
|
|
253
275
|
rows.push(sessionCounterRow(state.selected, state.matches.length, W));
|
|
254
276
|
return rows;
|
|
255
277
|
}
|
|
256
|
-
/**
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
278
|
+
/**
|
|
279
|
+
* Slice ③ — the `kiso sessions` TTY row: the same projection, printed
|
|
280
|
+
* rather than picked. No selection bar (nothing is selected on a
|
|
281
|
+
* listing) and no leading indent: this row starts at column 1 like
|
|
282
|
+
* every other line a shell command prints.
|
|
283
|
+
*
|
|
284
|
+
* DC-16: it keeps the ID, and the picker row does not. Sharing one
|
|
285
|
+
* projection is what stops the two surfaces drifting — that is this
|
|
286
|
+
* module's whole design and it stays — but "share the projection" is
|
|
287
|
+
* not "be the same row". The owner's ruling was about the PICKER, where
|
|
288
|
+
* the id was four characters of machine identity in the column the eye
|
|
289
|
+
* lands on first. A LISTING is the surface you read to copy an id OUT
|
|
290
|
+
* of: it is what `/resume <id>` and the filter's id haystack both
|
|
291
|
+
* assume exists, and deleting the id here quietly falsified both. The
|
|
292
|
+
* id goes LAST and dim — present for the hand that needs it, out of the
|
|
293
|
+
* way of the eye that does not.
|
|
294
|
+
*/
|
|
260
295
|
export function sessionListRow(card, W, now, idCol) {
|
|
261
|
-
|
|
296
|
+
const p = palette();
|
|
297
|
+
const tail = ` ${card.id}`;
|
|
298
|
+
const { text, width } = rowSpans(card, Math.max(1, W - visibleWidth(tail)), now, idCol);
|
|
299
|
+
if (width + visibleWidth(tail) > W)
|
|
300
|
+
return text; // a terminal too narrow for both keeps the words
|
|
301
|
+
return `${text}${p.dim}${tail}${p.reset}`;
|
|
262
302
|
}
|
|
263
303
|
/** Slice ③ — the listing's last line: the count, and the one thing the
|
|
264
304
|
* user can do next. */
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-tui",
|
|
3
|
-
"version": "0.16.
|
|
4
|
-
"description": "kiso tui
|
|
3
|
+
"version": "0.16.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",
|
|
7
7
|
"exports": {
|
|
@@ -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.16.
|
|
38
|
+
"@vincemakes/kiso-tui-cells": "0.16.4"
|
|
39
39
|
}
|
|
40
40
|
}
|