@visual-z/xeno 0.1.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.
Files changed (180) hide show
  1. package/README.md +498 -0
  2. package/docs/install.md +130 -0
  3. package/package.json +43 -0
  4. package/src/apps/approve.ts +49 -0
  5. package/src/apps/cli.ts +37 -0
  6. package/src/apps/commands.ts +226 -0
  7. package/src/apps/completions.ts +28 -0
  8. package/src/apps/input.ts +297 -0
  9. package/src/apps/render.ts +207 -0
  10. package/src/apps/shell.ts +2353 -0
  11. package/src/apps/startup.ts +1302 -0
  12. package/src/core/benchmark.ts +195 -0
  13. package/src/core/boundary.ts +174 -0
  14. package/src/core/churn.ts +59 -0
  15. package/src/core/completion-review.ts +66 -0
  16. package/src/core/context-policy.ts +60 -0
  17. package/src/core/context-window.ts +54 -0
  18. package/src/core/continuation.ts +114 -0
  19. package/src/core/cycle.ts +17 -0
  20. package/src/core/data-query.ts +201 -0
  21. package/src/core/deadline.ts +39 -0
  22. package/src/core/derive.ts +489 -0
  23. package/src/core/edit-plan.ts +200 -0
  24. package/src/core/environment.ts +29 -0
  25. package/src/core/event-validation.ts +99 -0
  26. package/src/core/events.ts +486 -0
  27. package/src/core/evidence-links.ts +71 -0
  28. package/src/core/evidence.ts +89 -0
  29. package/src/core/execution.ts +12 -0
  30. package/src/core/exhaustive.ts +18 -0
  31. package/src/core/experiment.ts +40 -0
  32. package/src/core/export.ts +169 -0
  33. package/src/core/file-references.ts +35 -0
  34. package/src/core/frontmatter.ts +213 -0
  35. package/src/core/handoff.ts +397 -0
  36. package/src/core/harness-comparison.ts +90 -0
  37. package/src/core/history-query.ts +103 -0
  38. package/src/core/history.ts +100 -0
  39. package/src/core/json-event.ts +135 -0
  40. package/src/core/json-pointer.ts +46 -0
  41. package/src/core/keymap.ts +241 -0
  42. package/src/core/observation-view.ts +87 -0
  43. package/src/core/outcome.ts +45 -0
  44. package/src/core/overflow.ts +59 -0
  45. package/src/core/permissions.ts +182 -0
  46. package/src/core/policy-evaluation.ts +55 -0
  47. package/src/core/progress.ts +101 -0
  48. package/src/core/project.ts +372 -0
  49. package/src/core/prompts.ts +83 -0
  50. package/src/core/queue.ts +60 -0
  51. package/src/core/read-bounds.ts +15 -0
  52. package/src/core/recall-analysis.ts +90 -0
  53. package/src/core/recall-trial.ts +153 -0
  54. package/src/core/response-recovery.ts +46 -0
  55. package/src/core/scheduling.ts +8 -0
  56. package/src/core/settings.ts +174 -0
  57. package/src/core/skills.ts +121 -0
  58. package/src/core/stall.ts +48 -0
  59. package/src/core/text-normalize.ts +81 -0
  60. package/src/core/theme.ts +179 -0
  61. package/src/core/tokens.ts +149 -0
  62. package/src/core/tool-contract.ts +102 -0
  63. package/src/core/tool-input.ts +38 -0
  64. package/src/core/transcript.ts +60 -0
  65. package/src/core/window-trial.ts +65 -0
  66. package/src/core/work-state.ts +103 -0
  67. package/src/core/yaml.ts +282 -0
  68. package/src/index.ts +10 -0
  69. package/src/runtime/auth.ts +255 -0
  70. package/src/runtime/benchmark.ts +187 -0
  71. package/src/runtime/catalog.ts +365 -0
  72. package/src/runtime/evidence-notes.ts +33 -0
  73. package/src/runtime/exec-sessions.ts +214 -0
  74. package/src/runtime/execution-control.ts +39 -0
  75. package/src/runtime/harness-comparison.ts +356 -0
  76. package/src/runtime/history-tools.ts +108 -0
  77. package/src/runtime/loop.ts +1983 -0
  78. package/src/runtime/model.ts +614 -0
  79. package/src/runtime/models-fetch.ts +155 -0
  80. package/src/runtime/oauth-device.ts +86 -0
  81. package/src/runtime/oauth.ts +759 -0
  82. package/src/runtime/openai-responses.ts +286 -0
  83. package/src/runtime/openai.ts +675 -0
  84. package/src/runtime/provider.ts +498 -0
  85. package/src/runtime/recall-trial.ts +226 -0
  86. package/src/runtime/resume.ts +66 -0
  87. package/src/runtime/retry.ts +297 -0
  88. package/src/runtime/tools.ts +1193 -0
  89. package/src/runtime/usage.ts +225 -0
  90. package/src/runtime/window-trial.ts +140 -0
  91. package/src/runtime/working-notes.ts +30 -0
  92. package/src/store/atomic.ts +31 -0
  93. package/src/store/auth-store.ts +50 -0
  94. package/src/store/blobs.ts +142 -0
  95. package/src/store/command.ts +75 -0
  96. package/src/store/config.ts +69 -0
  97. package/src/store/context-files.ts +356 -0
  98. package/src/store/file-index.ts +81 -0
  99. package/src/store/git-branch.ts +35 -0
  100. package/src/store/history-search.ts +19 -0
  101. package/src/store/log.ts +642 -0
  102. package/src/store/models.ts +41 -0
  103. package/src/store/observations.ts +181 -0
  104. package/src/store/query-source.ts +44 -0
  105. package/src/store/recovery.ts +39 -0
  106. package/src/store/replay.ts +63 -0
  107. package/src/store/resources.ts +361 -0
  108. package/src/store/restore.ts +302 -0
  109. package/src/store/search.ts +644 -0
  110. package/src/store/serial.ts +13 -0
  111. package/src/store/sessions.ts +463 -0
  112. package/src/store/trust.ts +69 -0
  113. package/src/store/workspace.ts +256 -0
  114. package/src/store/worktree.ts +233 -0
  115. package/src/tui/ansi.ts +67 -0
  116. package/src/tui/autocomplete.ts +29 -0
  117. package/src/tui/complete.ts +87 -0
  118. package/src/tui/diff.ts +160 -0
  119. package/src/tui/editor.ts +893 -0
  120. package/src/tui/exhaustive.ts +14 -0
  121. package/src/tui/fuzzy.ts +75 -0
  122. package/src/tui/host.ts +223 -0
  123. package/src/tui/input.ts +69 -0
  124. package/src/tui/keys.ts +378 -0
  125. package/src/tui/markdown.ts +17 -0
  126. package/src/tui/render.ts +153 -0
  127. package/src/tui/select.ts +240 -0
  128. package/src/tui/settings.ts +40 -0
  129. package/src/tui/statusline.ts +162 -0
  130. package/src/tui/style.ts +205 -0
  131. package/src/tui/terminal.ts +30 -0
  132. package/src/tui/text.ts +52 -0
  133. package/src/tui/toolview.ts +386 -0
  134. package/src/tui/transcript.ts +71 -0
  135. package/src/tui/tree.ts +405 -0
  136. package/src/tui/width.ts +567 -0
  137. package/src/vendor/pi-tui/LICENSE.upstream +21 -0
  138. package/src/vendor/pi-tui/VENDOR.md +23 -0
  139. package/src/vendor/pi-tui/alt-screen-search.ts +327 -0
  140. package/src/vendor/pi-tui/autocomplete.ts +835 -0
  141. package/src/vendor/pi-tui/components/alt-screen-flash.ts +51 -0
  142. package/src/vendor/pi-tui/components/box.ts +167 -0
  143. package/src/vendor/pi-tui/components/cancellable-loader.ts +40 -0
  144. package/src/vendor/pi-tui/components/editor.ts +2470 -0
  145. package/src/vendor/pi-tui/components/h-stack.ts +44 -0
  146. package/src/vendor/pi-tui/components/image.ts +127 -0
  147. package/src/vendor/pi-tui/components/input.ts +494 -0
  148. package/src/vendor/pi-tui/components/loader.ts +101 -0
  149. package/src/vendor/pi-tui/components/markdown.ts +1015 -0
  150. package/src/vendor/pi-tui/components/mouse-region.ts +33 -0
  151. package/src/vendor/pi-tui/components/scroll-view.ts +224 -0
  152. package/src/vendor/pi-tui/components/select-list.ts +273 -0
  153. package/src/vendor/pi-tui/components/settings-list.ts +328 -0
  154. package/src/vendor/pi-tui/components/spacer.ts +28 -0
  155. package/src/vendor/pi-tui/components/stack.ts +154 -0
  156. package/src/vendor/pi-tui/components/text.ts +107 -0
  157. package/src/vendor/pi-tui/components/truncated-text.ts +65 -0
  158. package/src/vendor/pi-tui/components/v-stack.ts +33 -0
  159. package/src/vendor/pi-tui/editor-component.ts +74 -0
  160. package/src/vendor/pi-tui/fuzzy.ts +138 -0
  161. package/src/vendor/pi-tui/index.ts +156 -0
  162. package/src/vendor/pi-tui/keybindings.ts +320 -0
  163. package/src/vendor/pi-tui/keys.ts +1401 -0
  164. package/src/vendor/pi-tui/kill-ring.ts +46 -0
  165. package/src/vendor/pi-tui/latex.ts +1506 -0
  166. package/src/vendor/pi-tui/layout-node.ts +51 -0
  167. package/src/vendor/pi-tui/layout.ts +449 -0
  168. package/src/vendor/pi-tui/native-modifiers.ts +13 -0
  169. package/src/vendor/pi-tui/native-module-path.ts +31 -0
  170. package/src/vendor/pi-tui/native-platform.ts +63 -0
  171. package/src/vendor/pi-tui/stdin-buffer.ts +444 -0
  172. package/src/vendor/pi-tui/terminal-colors.ts +73 -0
  173. package/src/vendor/pi-tui/terminal-image.ts +696 -0
  174. package/src/vendor/pi-tui/terminal.ts +547 -0
  175. package/src/vendor/pi-tui/tui-alt-screen.ts +1742 -0
  176. package/src/vendor/pi-tui/tui-main-screen.ts +655 -0
  177. package/src/vendor/pi-tui/tui.ts +1456 -0
  178. package/src/vendor/pi-tui/undo-stack.ts +28 -0
  179. package/src/vendor/pi-tui/utils.ts +1345 -0
  180. package/src/vendor/pi-tui/word-navigation.ts +117 -0
@@ -0,0 +1,405 @@
1
+ /**
2
+ * Session-tree layout and selection. Pure: renderTree(nodes, selection,
3
+ * width, style) → string[]. Collapse/filter/cursor never reach the log —
4
+ * the thing that happened is rewind, and that already has an event.
5
+ *
6
+ * Connectors (`├─` / `└─` / `│`) are kept before prompt text when the
7
+ * terminal is too narrow. A line that wraps would make the line-diff
8
+ * renderer count one row where the screen used two (PLAYBOOK width rule;
9
+ * same trap as toolview's `" → "` prefix).
10
+ */
11
+
12
+ import { assertNever } from "./exhaustive.ts";
13
+ import type { Key } from "./keys.ts";
14
+ import type { Component } from "./text.ts";
15
+ import { displayWidth } from "./width.ts";
16
+ import { PLAIN_ATTRS, paint, type Style } from "./style.ts";
17
+ import { Input } from "../vendor/pi-tui/components/input.ts";
18
+ import { encodeKey, inputKey, keyMatches } from "./input.ts";
19
+ import { truncateToWidth } from "../vendor/pi-tui/utils.ts";
20
+
21
+ /** Same mark /back uses for a branch the session rewound away from. */
22
+ export const LEFT_BRANCH_MARK = "↩";
23
+
24
+ /**
25
+ * Why the live region is not a blank box. Named in the card: an empty
26
+ * frame looks like a render bug, and there is a real reason (no completed
27
+ * exchange to jump to).
28
+ */
29
+ export const EMPTY_TREE_MESSAGE = "nothing to go back to -- no exchange has completed yet";
30
+
31
+ /** Filter toggle lives here so the key is not a hidden chord. */
32
+ export const TREE_HINT = "tab user/all · enter jump · esc cancel";
33
+
34
+ const BRANCH_MID = "├─ ";
35
+ const BRANCH_END = "└─ ";
36
+ const PIPE = "│ ";
37
+ const BLANK = " ";
38
+ const SELECTED = "→ ";
39
+ const UNSELECTED = " ";
40
+ const CURRENT_MARK = "● ";
41
+ const ON_PATH_MARK = " ";
42
+ const LEFT_MARK = `${LEFT_BRANCH_MARK} `;
43
+
44
+ export type TreeKind = "user" | "other";
45
+ export type TreeFilter = "user" | "all";
46
+
47
+ export interface TreeNode {
48
+ readonly id: string;
49
+ readonly prompt: string;
50
+ readonly onPath: boolean;
51
+ readonly current: boolean;
52
+ readonly kind: TreeKind;
53
+ readonly children: readonly TreeNode[];
54
+ /** Non-user rows navigate to their enclosing complete exchange. */
55
+ readonly target?: string;
56
+ }
57
+
58
+ export interface TreeSelection {
59
+ readonly index: number;
60
+ readonly filter: TreeFilter;
61
+ }
62
+
63
+ export interface TreeState {
64
+ readonly nodes: readonly TreeNode[];
65
+ readonly selected: number;
66
+ readonly filter: TreeFilter;
67
+ readonly confirmed: TreeNode | null;
68
+ readonly cancelled: boolean;
69
+ }
70
+
71
+ export interface CreateTreeOptions {
72
+ readonly filter?: TreeFilter;
73
+ readonly style?: Style;
74
+ }
75
+
76
+ /**
77
+ * A restore point as the log already knows it. parent pointers are a
78
+ * separate map so this file never imports store.
79
+ */
80
+ export interface TreePoint {
81
+ readonly node: number;
82
+ readonly atEvent: string;
83
+ readonly prompt: string;
84
+ readonly onPath: boolean;
85
+ }
86
+
87
+ export type NodeParent = { readonly node: number; readonly at_event?: string } | null;
88
+
89
+ export function createTree(
90
+ nodes: readonly TreeNode[],
91
+ opts: CreateTreeOptions = {},
92
+ ): TreeState {
93
+ return {
94
+ nodes,
95
+ selected: 0,
96
+ filter: opts.filter ?? "all",
97
+ confirmed: null,
98
+ cancelled: false,
99
+ };
100
+ }
101
+
102
+ export function applyTreeKey(state: TreeState, key: Key): TreeState {
103
+ switch (key.type) {
104
+ case "up":
105
+ return move(state, -1);
106
+ case "down":
107
+ return move(state, 1);
108
+ case "enter":
109
+ return confirm(state);
110
+ case "escape":
111
+ return { ...state, cancelled: true, confirmed: null };
112
+ case "tab":
113
+ return toggleFilter(state);
114
+ case "char":
115
+ case "backspace":
116
+ case "delete":
117
+ case "left":
118
+ case "right":
119
+ case "home":
120
+ case "end":
121
+ case "pageup":
122
+ case "pagedown":
123
+ case "ctrl":
124
+ case "alt":
125
+ return state;
126
+ default:
127
+ return assertNever(key, "applyTreeKey");
128
+ }
129
+ }
130
+
131
+ function visibleNodes(nodes: readonly TreeNode[], filter: TreeFilter): TreeNode[] {
132
+ if (filter === "all") return [...nodes];
133
+ const out: TreeNode[] = [];
134
+ for (const n of nodes) {
135
+ const children = visibleNodes(n.children, filter);
136
+ if (n.kind === "user") out.push({ ...n, children });
137
+ else out.push(...children);
138
+ }
139
+ return out;
140
+ }
141
+
142
+ function flatten(nodes: readonly TreeNode[]): TreeNode[] {
143
+ const out: TreeNode[] = [];
144
+ for (const n of nodes) {
145
+ out.push(n);
146
+ out.push(...flatten(n.children));
147
+ }
148
+ return out;
149
+ }
150
+
151
+ function move(state: TreeState, dir: -1 | 1): TreeState {
152
+ const n = flatten(visibleNodes(state.nodes, state.filter)).length;
153
+ if (n === 0) return state;
154
+ const selected = (state.selected + dir + n) % n;
155
+ return { ...state, selected, confirmed: null, cancelled: false };
156
+ }
157
+
158
+ function confirm(state: TreeState): TreeState {
159
+ const item = flatten(visibleNodes(state.nodes, state.filter))[state.selected];
160
+ if (item === undefined) return state;
161
+ return { ...state, confirmed: item, cancelled: false };
162
+ }
163
+
164
+ function toggleFilter(state: TreeState): TreeState {
165
+ const filter: TreeFilter = state.filter === "all" ? "user" : "all";
166
+ const n = flatten(visibleNodes(state.nodes, filter)).length;
167
+ const selected = n === 0 ? 0 : Math.min(state.selected, n - 1);
168
+ return { ...state, filter, selected, confirmed: null, cancelled: false };
169
+ }
170
+
171
+ /**
172
+ * Truncate on display columns. Slicing with s.length would cut a CJK
173
+ * prompt in half and the next cell would paint garbage.
174
+ */
175
+ function truncate(s: string, width: number): string {
176
+ if (width <= 0) return "";
177
+ if (displayWidth(s) <= width) return s;
178
+ let out = "";
179
+ let w = 0;
180
+ for (const ch of s) {
181
+ const cw = displayWidth(ch);
182
+ if (w + cw > width) break;
183
+ out += ch;
184
+ w += cw;
185
+ }
186
+ return out;
187
+ }
188
+
189
+ export function renderTree(
190
+ nodes: readonly TreeNode[],
191
+ selection: TreeSelection,
192
+ width: number,
193
+ style?: Style,
194
+ ): string[] {
195
+ const w = Math.max(0, Math.floor(width));
196
+ const filtered = visibleNodes(nodes, selection.filter);
197
+ if (filtered.length === 0) {
198
+ return [truncate(EMPTY_TREE_MESSAGE, w)];
199
+ }
200
+
201
+ const flat = flatten(filtered);
202
+ const selected = flat[selection.index] ?? flat[0];
203
+ const selectedId = selected?.id;
204
+ const lines: string[] = [];
205
+ walk(filtered, "", true, selectedId, w, style, lines);
206
+ const hint = truncate(TREE_HINT, w);
207
+ if (hint.length > 0) {
208
+ lines.push(style !== undefined ? paint(hint, { ...PLAIN_ATTRS, dim: true }, style) : hint);
209
+ }
210
+ return lines;
211
+ }
212
+
213
+ function walk(
214
+ nodes: readonly TreeNode[],
215
+ prefix: string,
216
+ isRoot: boolean,
217
+ selectedId: string | undefined,
218
+ width: number,
219
+ style: Style | undefined,
220
+ lines: string[],
221
+ ): void {
222
+ for (let i = 0; i < nodes.length; i++) {
223
+ const node = nodes[i];
224
+ if (node === undefined) continue;
225
+ const last = i === nodes.length - 1;
226
+ const branch = isRoot ? "" : last ? BRANCH_END : BRANCH_MID;
227
+ const sel = node.id === selectedId ? SELECTED : UNSELECTED;
228
+ const mark = node.current ? CURRENT_MARK : node.onPath ? ON_PATH_MARK : LEFT_MARK;
229
+ // Left-to-right: connectors stay, prompt is what width drops first.
230
+ const raw = sel + prefix + branch + mark + node.prompt;
231
+ const clipped = truncate(raw, width);
232
+ const chosen = node.id === selectedId;
233
+ lines.push(
234
+ chosen && style !== undefined
235
+ ? paint(clipped, { ...PLAIN_ATTRS, selected: true }, style)
236
+ : clipped,
237
+ );
238
+ const childPrefix = isRoot ? "" : prefix + (last ? BLANK : PIPE);
239
+ walk(node.children, childPrefix, false, selectedId, width, style, lines);
240
+ }
241
+ }
242
+
243
+ /**
244
+ * Restore points plus parent pointers → the tree the view paints.
245
+ *
246
+ * Same-node exchanges stay a chain. A child node whose parent.at_event
247
+ * matches a point hangs off that point as a sibling of the continuation,
248
+ * which is how a rewind becomes a visible fork instead of a missing row.
249
+ */
250
+ export function treeFromPoints(
251
+ points: readonly TreePoint[],
252
+ parents: ReadonlyMap<number, NodeParent>,
253
+ currentNode: number,
254
+ ): TreeNode[] {
255
+ const byNode = new Map<number, TreePoint[]>();
256
+ for (const p of points) {
257
+ const list = byNode.get(p.node) ?? [];
258
+ list.push(p);
259
+ byNode.set(p.node, list);
260
+ }
261
+
262
+ const childrenOf = new Map<number, number[]>();
263
+ const nodeIds = new Set<number>([...parents.keys(), ...byNode.keys()]);
264
+ for (const id of nodeIds) {
265
+ const parent = parents.get(id);
266
+ if (parent === undefined || parent === null) continue;
267
+ const list = childrenOf.get(parent.node) ?? [];
268
+ if (!list.includes(id)) list.push(id);
269
+ childrenOf.set(parent.node, list);
270
+ }
271
+
272
+ const lastHere = [...points].reverse().find((p) => p.onPath && p.node === currentNode);
273
+ const lastOnPath = lastHere ?? [...points].reverse().find((p) => p.onPath);
274
+
275
+ function toNode(p: TreePoint, children: readonly TreeNode[]): TreeNode {
276
+ return {
277
+ id: `${p.node}:${p.atEvent}`,
278
+ prompt: p.prompt,
279
+ onPath: p.onPath,
280
+ current: lastOnPath !== undefined && lastOnPath.node === p.node && lastOnPath.atEvent === p.atEvent,
281
+ kind: "user",
282
+ children,
283
+ };
284
+ }
285
+
286
+ const attached = new Set<number>();
287
+
288
+ function forksAt(nodeId: number, atEvent: string): TreeNode[] {
289
+ const out: TreeNode[] = [];
290
+ for (const childId of childrenOf.get(nodeId) ?? []) {
291
+ const parent = parents.get(childId);
292
+ if (parent === undefined || parent === null) continue;
293
+ if (parent.at_event === atEvent) {
294
+ attached.add(childId);
295
+ out.push(...chain(childId));
296
+ }
297
+ }
298
+ return out;
299
+ }
300
+
301
+ function danglingForks(nodeId: number): TreeNode[] {
302
+ const out: TreeNode[] = [];
303
+ for (const childId of childrenOf.get(nodeId) ?? []) {
304
+ if (attached.has(childId)) continue;
305
+ const parent = parents.get(childId);
306
+ if (parent === undefined || parent === null) continue;
307
+ attached.add(childId);
308
+ out.push(...chain(childId));
309
+ }
310
+ return out;
311
+ }
312
+
313
+ function chain(nodeId: number): TreeNode[] {
314
+ const pts = byNode.get(nodeId) ?? [];
315
+ if (pts.length === 0) return danglingForks(nodeId);
316
+
317
+ function fromIndex(i: number): TreeNode[] {
318
+ const p = pts[i];
319
+ if (p === undefined) return danglingForks(nodeId);
320
+ const forks = forksAt(nodeId, p.atEvent);
321
+ // Continuation (next point in this node) first, then forks at this event.
322
+ const children = i + 1 < pts.length
323
+ ? [...fromIndex(i + 1), ...forks]
324
+ : [...forks, ...danglingForks(nodeId)];
325
+ return [toNode(p, children)];
326
+ }
327
+
328
+ return fromIndex(0);
329
+ }
330
+
331
+ const roots: number[] = [];
332
+ for (const id of nodeIds) {
333
+ const parent = parents.get(id);
334
+ if (parent === undefined || parent === null) roots.push(id);
335
+ }
336
+ roots.sort((a, b) => a - b);
337
+
338
+ const forest: TreeNode[] = [];
339
+ for (const r of roots) forest.push(...chain(r));
340
+ return forest;
341
+ }
342
+
343
+ export class TreeList implements Component {
344
+ private current: TreeState;
345
+ private style: Style | undefined;
346
+ private readonly search = new Input({ prompt: "Search tree: " });
347
+ onInput: ((data: string) => void) | undefined;
348
+
349
+ constructor(nodes: readonly TreeNode[], opts: CreateTreeOptions = {}) {
350
+ this.current = createTree(nodes, opts);
351
+ this.style = opts.style;
352
+ }
353
+
354
+ get state(): TreeState {
355
+ return { ...this.current, nodes: this.filtered() };
356
+ }
357
+
358
+ applyKey(key: Key): TreeState {
359
+ return this.acceptInput(encodeKey(key));
360
+ }
361
+
362
+ get focused(): boolean { return this.search.focused; }
363
+ set focused(value: boolean) { this.search.focused = value; }
364
+ setStyle(style: Style): void { this.style = style; }
365
+ invalidate(): void { this.search.invalidate(); }
366
+ handleInput(data: string): void { if (this.onInput) this.onInput(data); else this.acceptInput(data); }
367
+ private filtered(): readonly TreeNode[] {
368
+ const query = this.search.getValue().toLowerCase();
369
+ const nodes = visibleNodes(this.current.nodes, this.current.filter);
370
+ if (!query) return nodes;
371
+ return flatten(nodes).filter((node) => node.prompt.toLowerCase().includes(query)).map((node) => ({ ...node, children: [] }));
372
+ }
373
+ acceptInput(data: string): TreeState {
374
+ this.current = { ...this.current, confirmed: null, cancelled: false };
375
+ const key = inputKey(data);
376
+ if (keyMatches(data, "ctrl+c")) this.current = { ...this.current, cancelled: true };
377
+ else if (key && ["up", "down", "enter", "escape", "tab"].includes(key.type)) {
378
+ const next = applyTreeKey({ ...this.current, nodes: this.filtered() }, key);
379
+ this.current = { ...next, nodes: this.current.nodes };
380
+ } else if (keyMatches(data, "pageUp") || keyMatches(data, "pageDown")) {
381
+ this.current = { ...this.current, selected: Math.max(0, Math.min(flatten(this.filtered()).length - 1,
382
+ this.current.selected + (keyMatches(data, "pageUp") ? -10 : 10))) };
383
+ } else {
384
+ this.search.handleInput(data);
385
+ this.current = { ...this.current, selected: 0 };
386
+ }
387
+ return this.state;
388
+ }
389
+
390
+ render(width: number): string[] {
391
+ if (width <= 0) return [""];
392
+ const body = renderTree(
393
+ this.filtered(),
394
+ { index: this.current.selected, filter: this.current.filter },
395
+ width,
396
+ this.style,
397
+ );
398
+ // This is a viewport, not a total result cap. Arrows/search can reach every row.
399
+ const start = Math.max(0, this.current.selected - 5);
400
+ const rows = body.slice(start, start + 10);
401
+ return ["Session tree · Enter navigates context at the exchange boundary", ...this.search.render(width),
402
+ ...rows, `Tab ${this.current.filter === "all" ? "all messages" : "user messages"} · ↑↓ navigate · Esc cancel`]
403
+ .map((line) => truncateToWidth(line, width, ""));
404
+ }
405
+ }