@vincemakes/kiso-tui 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -27,7 +27,7 @@
27
27
  * durability would need a new durable mechanism), no "chat about this"
28
28
  * hand-off, no timeout and no countdown.
29
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";
30
+ import { type AskAnswer, type AskOption, type AskQuestion, type AskResult, type AskRuntime, type AskSpec, type PanelPhase, type PanelSel, type PanelState, type PanelView, type PickRuntime } from "./approval-panel.js";
31
31
  /** The schema's own bounds — the registry refuses anything outside them
32
32
  * (extensions/ask validates; these are the numbers it validates to). */
33
33
  export declare const ASK_MAX_QUESTIONS = 4;
@@ -77,13 +77,14 @@ export declare function askStatus(view: PanelView, state: AskRuntime): string;
77
77
  /** The input row's lead: the digit lead while picking, the typing lead
78
78
  * in the custom phase (the rule-input phase's shape, reused). */
79
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;
80
+ export declare function panelBlockRows(view: PanelView, phase: PanelPhase, sel: PanelSel, W: number, maxRows: number, ask?: AskRuntime, pick?: PickRuntime): string[];
81
+ export declare function panelLead(view: PanelView, phase: PanelPhase, sel: PanelSel, ask?: AskRuntime, pick?: PickRuntime): string;
82
+ export declare function panelLeadPlain(view: PanelView, phase: PanelPhase, sel: PanelSel, ask?: AskRuntime, pick?: PickRuntime): string;
83
+ export declare function panelStatus(view: PanelView, phase: PanelPhase, sel: PanelSel, ask?: AskRuntime, pick?: PickRuntime): string;
84
+ export declare function panelAffordance(view: PanelView, phase: PanelPhase, sel: PanelSel, ask?: AskRuntime, pick?: PickRuntime): string;
85
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. */
86
+ * one source, so an ask can never render half as an approval (TUI2-R2
87
+ * ④: nor a pick as either). */
87
88
  export declare const panelRowsOf: (s: PanelState, W: number, maxRows: number) => string[];
88
89
  export declare const panelLeadOf: (s: PanelState) => string;
89
90
  export declare const panelStatusOf: (s: PanelState) => string;
package/dist/ask-panel.js CHANGED
@@ -27,7 +27,7 @@
27
27
  * durability would need a new durable mechanism), no "chat about this"
28
28
  * hand-off, no timeout and no countdown.
29
29
  */
30
- import { panelAffordance as basePanelAffordance, panelBlockRows as basePanelBlockRows, panelLead as basePanelLead, panelLeadPlain as basePanelLeadPlain, panelStatus as basePanelStatus, } from "./approval-panel.js";
30
+ import { 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
31
  import { cutLine } 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
@@ -197,7 +197,15 @@ export function askBlockRows(view, state, W, maxRows) {
197
197
  rows.push(...body);
198
198
  }
199
199
  rows.push(`${gutter}${p.dim}${askAffordance(state)}${p.reset}`);
200
- rows.push(`${p.dim}└ ${p.reset}`);
200
+ // TUI2-R1.5 11 (VD-13), shared with the approval panel: a real bottom RULE, in the block's own edge
201
+ // vocabulary — the same box-drawing run its divider already uses —
202
+ // anchored at the gutter column. It used to be `\u2514 `: a two-cell stub
203
+ // floating at column 1, with no rule running from it and no corner
204
+ // above it to answer. Worse, `\u2514 ` is the cut-notice prefix everywhere
205
+ // else in the product, so a CAPPED panel emitted two elbow rows in a
206
+ // row meaning entirely different things. The rule reads as an edge,
207
+ // and the cut notice above it reads as a notice.
208
+ rows.push(`${p.dim}\u2514${"\u2500".repeat(Math.max(0, W - 1))}${p.reset}`);
201
209
  return rows;
202
210
  }
203
211
  /** The status row's right-hand hint — the phase's keys. */
@@ -217,38 +225,49 @@ export function askLeadPlain(state) {
217
225
  return state.phase === "custom" ? "your answer: " : "1-4> ";
218
226
  }
219
227
  // ── the dispatchers: the panel slot, with the ask branch folded in ────
220
- export function panelBlockRows(view, phase, sel, W, maxRows, ask) {
228
+ export function panelBlockRows(view, phase, sel, W, maxRows, ask, pick) {
229
+ if (view.pick !== undefined && pick !== undefined)
230
+ return pickBlockRows(view, pick, W, maxRows);
221
231
  if (view.ask !== undefined && ask !== undefined)
222
232
  return askBlockRows(view, ask, W, maxRows);
223
233
  return basePanelBlockRows(view, phase, sel, W, maxRows);
224
234
  }
225
- export function panelLead(view, phase, sel, ask) {
235
+ export function panelLead(view, phase, sel, ask, pick) {
226
236
  const p = palette();
237
+ if (view.pick !== undefined && pick !== undefined)
238
+ return pickLead(view, pick);
227
239
  if (view.ask !== undefined && ask !== undefined)
228
240
  return `${p.bold}${askLeadPlain(ask)}${p.reset}`;
229
241
  return basePanelLead(view, phase, sel);
230
242
  }
231
- export function panelLeadPlain(view, phase, sel, ask) {
243
+ export function panelLeadPlain(view, phase, sel, ask, pick) {
244
+ if (view.pick !== undefined && pick !== undefined)
245
+ return pickLeadPlain(view, pick);
232
246
  if (view.ask !== undefined && ask !== undefined)
233
247
  return askLeadPlain(ask);
234
248
  return basePanelLeadPlain(view, phase, sel);
235
249
  }
236
- export function panelStatus(view, phase, sel, ask) {
250
+ export function panelStatus(view, phase, sel, ask, pick) {
251
+ if (view.pick !== undefined && pick !== undefined)
252
+ return pickStatus(view);
237
253
  if (view.ask !== undefined && ask !== undefined)
238
254
  return askStatus(view, ask);
239
255
  return basePanelStatus(view, phase, sel);
240
256
  }
241
- export function panelAffordance(view, phase, sel, ask) {
257
+ export function panelAffordance(view, phase, sel, ask, pick) {
258
+ if (view.pick !== undefined && pick !== undefined)
259
+ return pickAffordance(pick);
242
260
  if (view.ask !== undefined && ask !== undefined)
243
261
  return askAffordance(ask);
244
262
  return basePanelAffordance(view, phase, sel);
245
263
  }
246
264
  /** 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);
265
+ * one source, so an ask can never render half as an approval (TUI2-R2
266
+ * ④: nor a pick as either). */
267
+ export const panelRowsOf = (s, W, maxRows) => panelBlockRows(s.view, s.phase, s.sel, W, maxRows, s.ask, s.pick);
268
+ export const panelLeadOf = (s) => panelLead(s.view, s.phase, s.sel, s.ask, s.pick);
269
+ export const panelStatusOf = (s) => panelStatus(s.view, s.phase, s.sel, s.ask, s.pick);
270
+ export const panelAffordanceOf = (s) => panelAffordance(s.view, s.phase, s.sel, s.ask, s.pick);
252
271
  // ── the view: what the human reads when the model asks ────────────────
253
272
  /** The ask's PanelView. The dock-less fallback question is HONEST: a
254
273
  * terminal without a panel cannot walk options, so it says the ask is
@@ -157,3 +157,7 @@ export declare function atPanelRows(state: {
157
157
  selected: number;
158
158
  capped: boolean;
159
159
  }, W: number): string[];
160
+ /** TUI2-R1.5 ⑦(b) — the one-row dim header that turns a band into a
161
+ * surface. Shared by the @ picker and the / menu so the two read the
162
+ * same way. */
163
+ export declare function bandHeader(label: string, W: number): string;
package/dist/at-picker.js CHANGED
@@ -182,20 +182,41 @@ function splitPath(path) {
182
182
  export function atRow(match, selected, W) {
183
183
  const p = palette();
184
184
  const { dir, name } = splitPath(escapeTerminal(match.path));
185
- const lead = selected ? `${p.rv}→ ${p.rvEnd}` : " ";
186
185
  // the name's own matched positions, mapped out of the full path
187
186
  const marks = new Set(match.hit.filter((i) => i >= dir.length).map((i) => i - dir.length));
188
- // room: W the 2-cell lead 1 separating space before the directory
189
- const nameRoom = Math.max(1, W - 2 - (dir === "" ? 0 : visibleWidth(dir) + 1));
187
+ // TUI2-R1.5 (VD-9): the directory rides NEXT TO the name it
188
+ // qualifies. It used to be right-aligned to the band's far edge, which
189
+ // on a 100-column terminal put `src/` some eighty columns from the
190
+ // `parser.ts` it belongs to: the eye had to cross the whole row to
191
+ // learn which parser.ts this was, and the column read as a second list.
192
+ // Adjacent and dim, it is what it always meant to be — a qualifier.
193
+ // the name is the flexible column and the qualifier gives way first: a
194
+ // narrow window keeps the thing being aimed at.
195
+ const room = Math.max(1, W - 2);
196
+ const suffix = dir === "" ? "" : widthCut(` — ${dir}`, Math.max(0, room - 4));
197
+ const nameRoom = Math.max(1, room - visibleWidth(suffix));
190
198
  const shownName = widthCut(name, nameRoom);
191
199
  let painted = "";
192
200
  for (let i = 0; i < shownName.length; i += 1) {
193
201
  painted += marks.has(i) ? `${p.bold}${shownName[i]}${p.reset}` : shownName[i];
194
202
  }
195
- if (dir === "")
196
- return `${lead}${painted}`;
197
- const pad = Math.max(1, W - 2 - visibleWidth(shownName) - visibleWidth(dir));
198
- return `${lead}${painted}${" ".repeat(pad)}${p.dim}${dir}${p.reset}`;
203
+ const text = `${painted}${suffix === "" ? "" : `${p.dim}${suffix}${p.reset}`}`;
204
+ const width = visibleWidth(shownName) + visibleWidth(suffix);
205
+ if (!selected)
206
+ return ` ${text}`;
207
+ // TUI2-R1.5 ⑧ (VD-9): the selection is a FULL-ROW bar — the W16 chip
208
+ // mechanism, which the user chip and the turn fold already use. A
209
+ // two-cell `→ ` marker on the inverse band is one character of
210
+ // highlight in an eighty-column row, and the walkthrough could barely
211
+ // find it. The bar spans the row's whole width so the selection is
212
+ // visible from anywhere on the line. Mono discipline: reverse video,
213
+ // no new colours.
214
+ //
215
+ // The inner spans close with rvEnd (SGR 27), never SGR 0 — a reset
216
+ // inside the bar would punch a hole in it. `painted`'s bold marks and
217
+ // the dim suffix both end in SGR 0, so the bar is re-opened after each.
218
+ const inner = `${painted.replaceAll(p.reset, `${p.reset}${p.rv}`)}${suffix === "" ? "" : `${p.dim}${suffix}${p.reset}${p.rv}`}`;
219
+ return `${p.rv} ${inner}${" ".repeat(Math.max(0, W - width - 2))} ${p.rvEnd}`;
199
220
  }
200
221
  /**
201
222
  * KC3 §4 — the counter row: `(n/total)`, where n is the 1-based
@@ -220,9 +241,21 @@ export function atCounterRow(selected, total, capped, W) {
220
241
  */
221
242
  export function atPanelRows(state, W) {
222
243
  const { first, count } = atWindow(state.matches.length, state.selected);
223
- const rows = [];
244
+ // TUI2-R1.5 ⑦(b) (VD-8): the band NAMES itself. It renders frameless
245
+ // directly above the composer, so with scrollback behind it there was
246
+ // nothing to say where the surface began — the rows read as more
247
+ // history. One dim row is enough to make it UI. Mono discipline: dim
248
+ // text, no rule, no colour.
249
+ const rows = [bandHeader("files", W)];
224
250
  for (let i = first; i < first + count; i += 1)
225
251
  rows.push(atRow(state.matches[i], i === state.selected, W));
226
252
  rows.push(atCounterRow(state.selected, state.matches.length, state.capped, W));
227
253
  return rows;
228
254
  }
255
+ /** TUI2-R1.5 ⑦(b) — the one-row dim header that turns a band into a
256
+ * surface. Shared by the @ picker and the / menu so the two read the
257
+ * same way. */
258
+ export function bandHeader(label, W) {
259
+ const p = palette();
260
+ return `${p.dim}${widthCut(label, Math.max(1, W))}${p.reset}`;
261
+ }
@@ -45,6 +45,7 @@
45
45
  import { type MenuItem } from "./editor.js";
46
46
  import type { PanelState } from "./approval-panel.js";
47
47
  import { type AtMatch } from "./at-picker.js";
48
+ import { type SessionPickState } from "./session-picker.js";
48
49
  /** KC3 §4 — the @ picker's bound state (the editor's atState()). */
49
50
  export interface AtPanelState {
50
51
  readonly matches: readonly AtMatch[];
@@ -109,6 +110,20 @@ export declare class Body {
109
110
  * outcome. */
110
111
  toolVerdict(callId: string, decision: "approved" | "denied", decidedBy?: string, reason?: string): void;
111
112
  toolRunning(callId: string): void;
113
+ /**
114
+ * TUI2-R1 (C) — the RUNNING call's observed output.
115
+ *
116
+ * The CLI tails the shell tool's progress sidecar and hands what it
117
+ * read to the cell. Deliberately narrow: only a cell that is still
118
+ * RUNNING accepts it, so an observation can never overwrite a real
119
+ * result, and an unchanged read costs no frame at all (a poller
120
+ * fires far more often than the output changes).
121
+ *
122
+ * This adds no event and no durable state. The text lands in the
123
+ * cell's live rendering and is replaced wholesale by the tool's own
124
+ * result at settle — which is the only text anything else ever reads.
125
+ */
126
+ toolProgress(callId: string, text: string): void;
112
127
  toolSucceeded(callId: string): void;
113
128
  toolFailed(callId: string, error: string): void;
114
129
  toolResult(callId: string, result: {
@@ -152,7 +167,7 @@ export declare class Body {
152
167
  * frame. The inactive path keeps the historical bytes (no resume —
153
168
  * the pipe contract). */
154
169
  banner(version: string, extensionsText: string, resume?: ResumeMeta[]): void;
155
- raw(lines: string[]): void;
170
+ raw(lines: string[], wrap?: "words"): void;
156
171
  /** The last COMPLETE thinking block, for /think. */
157
172
  lastThinking(): string | null;
158
173
  /** The last completed tool call, for /last. */
@@ -164,15 +179,6 @@ export declare class Body {
164
179
  isError: boolean;
165
180
  };
166
181
  } | null;
167
- /** W15 — the expand key's target (ctrl+r). A cell still in the LIVE
168
- * region (the newest live tool) TOGGLES in place — the compositor
169
- * owns those rows and redraws them (the body flips to the full
170
- * form, no cap). A committed cell can never toggle — history is
171
- * never rewritten (ADR-0046) — so the key APPENDS a fresh expanded
172
- * block at the bottom instead, the /last idiom aimed at a chosen
173
- * cell: the pointer cycles the collapsed history, newest first, and
174
- * the header names the target ("N turns back" — the user cells
175
- * after it), so every press tells the user what they got. */
176
182
  expandNext(): {
177
183
  kind: "toggled";
178
184
  } | {
@@ -208,6 +214,8 @@ export declare class Body {
208
214
  bindInput(state: () => InputState, prompt: string): void;
209
215
  /** Bind the editor's slash-command menu state — the MenuSelect slot
210
216
  * occupant (the menu replaces the editor's view while open). */
217
+ /** TUI2-R1 (D): bind the editor's keys-sheet flag. */
218
+ bindSheet(state: () => boolean): void;
211
219
  bindMenu(state: () => {
212
220
  items: readonly MenuItem[];
213
221
  selected: number;
@@ -216,6 +224,9 @@ export declare class Body {
216
224
  * band — see #menuRows for why that is a decision and not a
217
225
  * shortcut. */
218
226
  bindAt(state: () => AtPanelState | null): void;
227
+ /** TUI2-R2 ②: bind the editor's session picker — the band's third
228
+ * occupant (see #menuRows for why they share one). */
229
+ bindPick(state: () => SessionPickState | null): void;
219
230
  /** Bind the pending-turn queue — the CLI's live slots (chat.ts):
220
231
  * the chips render in the menu-rows family, the live caps shrink
221
232
  * by their rows, and the +N queued hint rides the status row. */
@@ -252,6 +263,9 @@ export declare class Dock {
252
263
  * occupant (the panel replaces the live region + the input lead
253
264
  * while up; the old ApprovalPrompt's question slot retires). */
254
265
  bindApproval(state: () => PanelState | null): void;
266
+ /** TUI2-R1 (D): bind the editor's keys-sheet flag — the slot read for
267
+ * the ? overlay (the menu/picker binding pattern). */
268
+ bindSheet(state: () => boolean): void;
255
269
  bindInput(state: () => InputState, prompt: string): void;
256
270
  bindMenu(state: () => {
257
271
  items: readonly MenuItem[];
@@ -261,6 +275,10 @@ export declare class Dock {
261
275
  * menu (see Body#menuRows). Unbound, the picker cannot render, and
262
276
  * every frame is byte-identical to before the round. */
263
277
  bindAt(state: () => AtPanelState | null): void;
278
+ /** TUI2-R2 ②: bind the editor's session picker — the band's third
279
+ * occupant. Unbound (every path but bare `kiso resume`), the picker
280
+ * cannot render and every frame is byte-identical to before. */
281
+ bindPick(state: () => SessionPickState | null): void;
264
282
  /** W22: bind the pending-turn queue — the chips + the +N queued
265
283
  * hint (the CLI binds it from chat(); the editor's pop keys ride
266
284
  * the LineInput's own bindQueue). */