@visual-z/xeno 0.2.1 → 0.4.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 (282) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +126 -34
  3. package/docs/app-server.md +84 -0
  4. package/docs/install.md +51 -13
  5. package/docs/providers.md +24 -3
  6. package/package.json +7 -5
  7. package/src/apps/app-server.ts +28 -0
  8. package/src/apps/approve.ts +3 -3
  9. package/src/apps/cli.ts +25 -10
  10. package/src/apps/commands.ts +45 -8
  11. package/src/apps/completions.ts +78 -22
  12. package/src/apps/control-transport.ts +89 -0
  13. package/src/apps/image-draft.ts +46 -0
  14. package/src/apps/model-catalog.ts +2 -1
  15. package/src/apps/project-trust.ts +2 -42
  16. package/src/apps/provider-connect.ts +346 -0
  17. package/src/apps/render.ts +22 -47
  18. package/src/apps/resources.ts +5 -2
  19. package/src/apps/shell.ts +333 -1542
  20. package/src/apps/startup-input.ts +45 -9
  21. package/src/apps/startup.ts +149 -94
  22. package/src/apps/ui/app.ts +1553 -0
  23. package/src/apps/ui/control-model.ts +26 -0
  24. package/src/apps/ui/extension-host.ts +194 -0
  25. package/src/apps/ui/image-cache.ts +78 -0
  26. package/src/apps/ui/palette.ts +88 -0
  27. package/src/apps/ui/project.ts +127 -0
  28. package/src/apps/ui/standalone.ts +146 -0
  29. package/src/apps/ui/status-model.ts +68 -0
  30. package/src/apps/ui/surface.ts +86 -0
  31. package/src/apps/ui/theme.ts +86 -0
  32. package/src/core/acceptance.ts +107 -0
  33. package/src/core/action-defaults.ts +47 -295
  34. package/src/core/benchmark.ts +2 -2
  35. package/src/core/branch-receipt.ts +24 -0
  36. package/src/core/checkpoints.ts +64 -0
  37. package/src/core/churn.ts +2 -0
  38. package/src/core/command-artifacts.ts +94 -0
  39. package/src/core/command-contract.ts +35 -0
  40. package/src/core/completion-action.ts +20 -0
  41. package/src/core/completion-review.ts +37 -21
  42. package/src/core/context-detail.ts +37 -0
  43. package/src/core/context-policy.ts +74 -1
  44. package/src/core/continuation.ts +17 -2
  45. package/src/core/control-protocol.ts +62 -0
  46. package/src/core/control-view.ts +42 -0
  47. package/src/core/derive.ts +29 -3
  48. package/src/core/draft-images.ts +56 -0
  49. package/src/core/environment.ts +145 -0
  50. package/src/core/evaluation-coverage.ts +74 -0
  51. package/src/core/event-validation.ts +81 -6
  52. package/src/core/events.ts +99 -3
  53. package/src/core/evidence-links.ts +7 -0
  54. package/src/core/evidence.ts +13 -3
  55. package/src/core/execution.ts +8 -2
  56. package/src/core/experiment.ts +133 -0
  57. package/src/core/export.ts +9 -0
  58. package/src/core/failure-recovery.ts +173 -0
  59. package/src/core/file-precondition.ts +72 -0
  60. package/src/core/handoff.ts +65 -4
  61. package/src/core/harness-diagnostics.ts +206 -0
  62. package/src/core/history-query.ts +38 -2
  63. package/src/core/history.ts +38 -1
  64. package/src/core/integration-health.ts +34 -0
  65. package/src/core/keymap.ts +8 -20
  66. package/src/core/model-picker.ts +64 -0
  67. package/src/core/native-eval.ts +25 -0
  68. package/src/core/note-guidance.ts +34 -0
  69. package/src/core/oauth-fields.ts +44 -0
  70. package/src/core/observation-view.ts +49 -8
  71. package/src/core/permissions.ts +24 -2
  72. package/src/core/process-receipt.ts +105 -0
  73. package/src/core/process-state.ts +37 -0
  74. package/src/core/program-status.ts +31 -0
  75. package/src/core/project.ts +70 -19
  76. package/src/core/prompt-policy.ts +37 -0
  77. package/src/core/query-eval-contract.ts +14 -0
  78. package/src/core/read-observation.ts +82 -0
  79. package/src/core/recovery-corpus.ts +17 -0
  80. package/src/core/recovery-evaluation.ts +60 -0
  81. package/src/core/recovery-sources.ts +74 -0
  82. package/src/core/request-progress.ts +87 -0
  83. package/src/core/request-state.ts +114 -0
  84. package/src/core/runtime-target.ts +94 -0
  85. package/src/core/search-observation.ts +97 -0
  86. package/src/core/session-tree.ts +147 -0
  87. package/src/core/settings.ts +33 -10
  88. package/src/core/skills.ts +44 -0
  89. package/src/core/sql-query.ts +211 -0
  90. package/src/core/theme.ts +36 -58
  91. package/src/core/tool-contract.ts +51 -2
  92. package/src/core/tool-discovery.ts +87 -0
  93. package/src/core/tool-failure.ts +60 -0
  94. package/src/core/tool-input.ts +6 -0
  95. package/src/core/tool-mode-protocol.ts +25 -0
  96. package/src/core/transcript.ts +19 -4
  97. package/src/core/trial-lifecycle.ts +14 -0
  98. package/src/core/tui-mode.ts +29 -0
  99. package/src/core/update-check.ts +97 -0
  100. package/src/core/version.ts +55 -0
  101. package/src/core/work-state.ts +49 -3
  102. package/src/core/working-set.ts +91 -0
  103. package/src/runtime/auth.ts +11 -4
  104. package/src/runtime/branch-jobs.ts +36 -0
  105. package/src/runtime/catalog.ts +13 -3
  106. package/src/runtime/clipboard.ts +15 -4
  107. package/src/runtime/command-artifacts.ts +64 -0
  108. package/src/runtime/control-connection.ts +104 -0
  109. package/src/runtime/environment-tools.ts +60 -0
  110. package/src/runtime/evidence-notes.ts +4 -2
  111. package/src/runtime/exec-sessions.ts +206 -61
  112. package/src/runtime/execution-control.ts +1 -0
  113. package/src/runtime/extension-ui.ts +22 -30
  114. package/src/runtime/extensions.ts +95 -16
  115. package/src/runtime/harness-comparison.ts +27 -7
  116. package/src/runtime/harness-eval.ts +462 -0
  117. package/src/runtime/history-cache.ts +57 -0
  118. package/src/runtime/history-tools.ts +62 -12
  119. package/src/runtime/integrations.ts +210 -0
  120. package/src/runtime/loop.ts +1088 -282
  121. package/src/runtime/model-discovery.ts +208 -0
  122. package/src/runtime/model.ts +9 -0
  123. package/src/runtime/native-eval-fixtures.ts +33 -0
  124. package/src/runtime/native-eval.ts +146 -0
  125. package/src/runtime/oauth.ts +80 -3
  126. package/src/runtime/pi-provider.ts +169 -12
  127. package/src/runtime/program-status.ts +38 -0
  128. package/src/runtime/provider-connect.ts +109 -0
  129. package/src/runtime/provider-presets.ts +87 -0
  130. package/src/runtime/provider-request.ts +13 -2
  131. package/src/runtime/provider.ts +23 -9
  132. package/src/runtime/query-eval-verifier.ts +28 -0
  133. package/src/runtime/recovery-coverage.ts +71 -0
  134. package/src/runtime/recovery-evidence.ts +76 -0
  135. package/src/runtime/recovery-fixtures.ts +116 -0
  136. package/src/runtime/runtime-target.ts +149 -0
  137. package/src/runtime/session-control.ts +198 -0
  138. package/src/runtime/tool-broker.ts +137 -0
  139. package/src/runtime/tool-discovery.ts +57 -0
  140. package/src/runtime/tool-mode-eval.ts +146 -0
  141. package/src/runtime/tool-queue.ts +77 -0
  142. package/src/runtime/tools.ts +628 -183
  143. package/src/runtime/trial-control.ts +45 -0
  144. package/src/runtime/update-check.ts +47 -0
  145. package/src/runtime/window-trial.ts +2 -1
  146. package/src/runtime/working-notes.ts +33 -3
  147. package/src/runtime/workspace-lifecycle.ts +97 -0
  148. package/src/store/acceptance.ts +15 -0
  149. package/src/store/checkpoint-history.ts +43 -0
  150. package/src/store/command-artifacts.ts +96 -0
  151. package/src/store/command.ts +65 -37
  152. package/src/store/file-checkpoints.ts +104 -0
  153. package/src/store/file-index.ts +13 -1
  154. package/src/store/git-checkpoints.ts +67 -0
  155. package/src/store/observations.ts +61 -20
  156. package/src/store/process-receipts.ts +34 -0
  157. package/src/store/query-source.ts +13 -8
  158. package/src/store/recovery.ts +2 -3
  159. package/src/store/replay.ts +16 -4
  160. package/src/store/runtime-target.ts +64 -0
  161. package/src/store/search.ts +171 -132
  162. package/src/store/session-archive.ts +4 -0
  163. package/src/store/source-cancellation.ts +6 -0
  164. package/src/store/update-cache.ts +36 -0
  165. package/src/store/workspace-observation.ts +113 -0
  166. package/src/tui/ansi-spans.ts +126 -0
  167. package/src/tui/style.ts +19 -16
  168. package/src/tui/ui/atomic-draft.ts +121 -0
  169. package/src/tui/ui/canvas.ts +96 -0
  170. package/src/tui/ui/colors.ts +70 -0
  171. package/src/tui/ui/composer.ts +200 -0
  172. package/src/tui/ui/control-panel.ts +23 -0
  173. package/src/tui/ui/dialog.ts +379 -0
  174. package/src/tui/ui/draft.ts +23 -0
  175. package/src/tui/ui/edge.ts +186 -0
  176. package/src/tui/ui/image-size.ts +40 -0
  177. package/src/tui/ui/ink.ts +210 -0
  178. package/src/tui/ui/items.ts +40 -0
  179. package/src/tui/ui/keys.ts +40 -0
  180. package/src/tui/ui/layout.ts +74 -0
  181. package/src/tui/ui/list-model.ts +86 -0
  182. package/src/tui/ui/list-view.ts +172 -0
  183. package/src/tui/ui/mascot.ts +67 -0
  184. package/src/tui/ui/messages.ts +518 -0
  185. package/src/tui/ui/motion.ts +28 -0
  186. package/src/tui/ui/paths.ts +10 -0
  187. package/src/tui/ui/pictures.ts +78 -0
  188. package/src/tui/ui/screen.ts +421 -0
  189. package/src/tui/ui/status.ts +279 -0
  190. package/src/tui/ui/styled.ts +85 -0
  191. package/src/tui/ui/testing.ts +5 -0
  192. package/src/tui/ui/tool-card.ts +150 -0
  193. package/src/apps/extension-ui.ts +0 -330
  194. package/src/apps/resource-browser.ts +0 -75
  195. package/src/apps/session-browser.ts +0 -113
  196. package/src/apps/session-picker.ts +0 -55
  197. package/src/tui/ansi.ts +0 -67
  198. package/src/tui/autocomplete.ts +0 -29
  199. package/src/tui/chat-viewport.ts +0 -47
  200. package/src/tui/complete.ts +0 -87
  201. package/src/tui/editor.ts +0 -940
  202. package/src/tui/host.ts +0 -264
  203. package/src/tui/input.ts +0 -75
  204. package/src/tui/keys.ts +0 -378
  205. package/src/tui/markdown.ts +0 -17
  206. package/src/tui/render.ts +0 -153
  207. package/src/tui/select.ts +0 -258
  208. package/src/tui/settings.ts +0 -44
  209. package/src/tui/statusline.ts +0 -162
  210. package/src/tui/system-theme.ts +0 -700
  211. package/src/tui/terminal.ts +0 -30
  212. package/src/tui/text.ts +0 -52
  213. package/src/tui/toolview.ts +0 -386
  214. package/src/tui/transcript.ts +0 -123
  215. package/src/tui/tree.ts +0 -471
  216. package/src/vendor/pi-tui/LICENSE.upstream +0 -21
  217. package/src/vendor/pi-tui/UPSTREAM.json +0 -50
  218. package/src/vendor/pi-tui/VENDOR.md +0 -25
  219. package/src/vendor/pi-tui/alt-screen-search.ts +0 -327
  220. package/src/vendor/pi-tui/autocomplete.ts +0 -860
  221. package/src/vendor/pi-tui/colors.ts +0 -367
  222. package/src/vendor/pi-tui/components/alt-screen-flash.ts +0 -51
  223. package/src/vendor/pi-tui/components/box.ts +0 -166
  224. package/src/vendor/pi-tui/components/cancellable-loader.ts +0 -40
  225. package/src/vendor/pi-tui/components/editor.ts +0 -2472
  226. package/src/vendor/pi-tui/components/h-stack.ts +0 -44
  227. package/src/vendor/pi-tui/components/image.ts +0 -127
  228. package/src/vendor/pi-tui/components/input.ts +0 -494
  229. package/src/vendor/pi-tui/components/loader.ts +0 -101
  230. package/src/vendor/pi-tui/components/markdown.ts +0 -1021
  231. package/src/vendor/pi-tui/components/mouse-region.ts +0 -33
  232. package/src/vendor/pi-tui/components/scroll-view.ts +0 -224
  233. package/src/vendor/pi-tui/components/select-list.ts +0 -273
  234. package/src/vendor/pi-tui/components/settings-list.ts +0 -328
  235. package/src/vendor/pi-tui/components/spacer.ts +0 -28
  236. package/src/vendor/pi-tui/components/stack.ts +0 -154
  237. package/src/vendor/pi-tui/components/text.ts +0 -107
  238. package/src/vendor/pi-tui/components/truncated-text.ts +0 -65
  239. package/src/vendor/pi-tui/components/v-stack.ts +0 -33
  240. package/src/vendor/pi-tui/editor-component.ts +0 -74
  241. package/src/vendor/pi-tui/fuzzy.ts +0 -138
  242. package/src/vendor/pi-tui/index.ts +0 -186
  243. package/src/vendor/pi-tui/keybindings.ts +0 -320
  244. package/src/vendor/pi-tui/keys.ts +0 -1401
  245. package/src/vendor/pi-tui/kill-ring.ts +0 -46
  246. package/src/vendor/pi-tui/latex.ts +0 -1506
  247. package/src/vendor/pi-tui/layout-node.ts +0 -51
  248. package/src/vendor/pi-tui/layout.ts +0 -449
  249. package/src/vendor/pi-tui/native/UPSTREAM.json +0 -23
  250. package/src/vendor/pi-tui/native/clipboard.h +0 -208
  251. package/src/vendor/pi-tui/native/darwin/README.md +0 -22
  252. package/src/vendor/pi-tui/native/darwin/build.sh +0 -69
  253. package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-arm64/darwin-platform.node +0 -0
  254. package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-x64/darwin-platform.node +0 -0
  255. package/src/vendor/pi-tui/native/darwin/src/darwin-platform.m +0 -132
  256. package/src/vendor/pi-tui/native/linux/README.md +0 -23
  257. package/src/vendor/pi-tui/native/linux/build.sh +0 -65
  258. package/src/vendor/pi-tui/native/linux/prebuilds/linux-arm64/linux-platform-x11.node +0 -0
  259. package/src/vendor/pi-tui/native/linux/prebuilds/linux-x64/linux-platform-x11.node +0 -0
  260. package/src/vendor/pi-tui/native/linux/src/clipboard-worker.h +0 -70
  261. package/src/vendor/pi-tui/native/linux/src/linux-platform-x11.c +0 -274
  262. package/src/vendor/pi-tui/native/napi.h +0 -91
  263. package/src/vendor/pi-tui/native/win32/README.md +0 -31
  264. package/src/vendor/pi-tui/native/win32/build.mjs +0 -232
  265. package/src/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-platform.node +0 -0
  266. package/src/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-platform.node +0 -0
  267. package/src/vendor/pi-tui/native/win32/src/win32-platform.c +0 -241
  268. package/src/vendor/pi-tui/native-modifiers.ts +0 -13
  269. package/src/vendor/pi-tui/native-module-path.ts +0 -31
  270. package/src/vendor/pi-tui/native-platform.ts +0 -65
  271. package/src/vendor/pi-tui/oklab.ts +0 -233
  272. package/src/vendor/pi-tui/stdin-buffer.ts +0 -444
  273. package/src/vendor/pi-tui/terminal-colors.ts +0 -91
  274. package/src/vendor/pi-tui/terminal-image.ts +0 -731
  275. package/src/vendor/pi-tui/terminal.ts +0 -554
  276. package/src/vendor/pi-tui/tui-alt-screen.ts +0 -1750
  277. package/src/vendor/pi-tui/tui-main-screen.ts +0 -655
  278. package/src/vendor/pi-tui/tui.ts +0 -1493
  279. package/src/vendor/pi-tui/undo-stack.ts +0 -28
  280. package/src/vendor/pi-tui/utils.ts +0 -1385
  281. package/src/vendor/pi-tui/wheel-scroll.ts +0 -82
  282. package/src/vendor/pi-tui/word-navigation.ts +0 -117
@@ -1,330 +0,0 @@
1
- /** Pi extension UI ownership, bound to a single resource generation (D19). */
2
- import type { ExtensionRegistry, ExtensionToolResult, ExtensionToolRenderContext } from "../runtime/extensions.ts";
3
- import type { ExtensionUI, ExtensionComponent, ExtensionTheme, WidgetFactory, FooterFactory, EditorFactory, AutocompleteFactory, InputHandler, CustomOptions, DialogOptions } from "../runtime/extension-ui.ts";
4
- import type { Theme } from "../core/theme.ts";
5
- import { builtinTheme } from "../core/theme.ts";
6
- import type { ResourceIndex } from "../store/resource-index.ts";
7
- import { themeResourcePath } from "../store/resource-index.ts";
8
- import { discoverThemes, resolveTheme } from "../store/resources.ts";
9
- import { Container, Lines, PiText, identityEditorTheme, type InteractiveScreen } from "../tui/host.ts";
10
- import type { Editor } from "../tui/editor.ts";
11
- import { SelectList } from "../tui/select.ts";
12
- import { keyMatches } from "../tui/input.ts";
13
- import { Editor as PiEditor } from "../vendor/pi-tui/components/editor.ts";
14
- import { Input } from "../vendor/pi-tui/components/input.ts";
15
- import type { Component, OverlayHandle } from "../vendor/pi-tui/tui.ts";
16
- import type { KeybindingsManager } from "../vendor/pi-tui/keybindings.ts";
17
- import type { AutocompleteProvider } from "../vendor/pi-tui/autocomplete.ts";
18
-
19
- interface Host {
20
- screen(): InteractiveScreen; editor: Editor; theme(): Theme; colored(): boolean;
21
- bindings(): KeybindingsManager; branch(): string | null;
22
- redraw(): void; refreshTranscript(): void; input(data: string): void; notify: ExtensionUI["notify"];
23
- setTheme(theme: Theme): void; expanded(): boolean; expand(value: boolean): void;
24
- cwd(): string; showImages(): boolean; outputPad(): number; saveTheme(name: string): void;
25
- }
26
- type Slot = { factory: WidgetFactory; component: OwnedComponent; below: boolean };
27
- type OwnedComponent = Component & { dispose(): void; focused?: boolean };
28
- type Dialog = { component?: OwnedComponent; raw?: ExtensionComponent; handle?: OverlayHandle; close(value?: unknown): void };
29
-
30
- export class ExtensionUiController {
31
- readonly header = new Container();
32
- readonly above = new Container();
33
- readonly below = new Container();
34
- readonly partials = new Container();
35
- private partialComponents = new Map<string, OwnedComponent>();
36
- private toolStates = new Map<string, Record<string, unknown>>();
37
- private footer: OwnedComponent | undefined;
38
- private headerSlot: Slot | undefined;
39
- private footerFactory: FooterFactory | undefined;
40
- private widgets = new Map<string, Slot>();
41
- private statuses = new Map<string, string>();
42
- private listeners = new Set<InputHandler>();
43
- private branchListeners = new Set<() => void>();
44
- private unlisten: (() => void) | undefined;
45
- private unbind: (() => void) | undefined;
46
- private unwatchTools: (() => void) | undefined;
47
- private footerSubscriptions: (() => void)[] = [];
48
- private editorFactory: EditorFactory | undefined;
49
- private autocomplete: AutocompleteFactory[] = [];
50
- private baseCompletion: AutocompleteProvider | undefined;
51
- private themeCache = new Map<string, { theme: ExtensionTheme; path: string | undefined }>();
52
- private registry: ExtensionRegistry | undefined;
53
- private reported = new Set<string>();
54
- private hasTitle = false;
55
- private generation = 0;
56
- private api: ExtensionUI;
57
- private dialog: Dialog | undefined;
58
- private pendingDialogs = new Set<() => void>();
59
- private tail: Promise<unknown> = Promise.resolve();
60
- private workingMessage: string | undefined;
61
- private workingVisible = true;
62
- private frames: string[] | undefined;
63
- private frame = 0;
64
- private interval = 80;
65
- private timer: ReturnType<typeof setInterval> | undefined;
66
- hiddenThinkingLabel: string | undefined;
67
- private readonly host: Host;
68
- constructor(host: Host) { this.host = host; this.host.editor.onComponentError = error => { this.editorFactory = undefined; this.report(error); }; this.api = this.createApi(); this.rebind(); }
69
- get context(): ExtensionUI { return this.api; }
70
- get modal(): Component | undefined { return this.dialog?.handle ? undefined : this.dialog?.component; }
71
- get overlayFocused(): boolean { return this.dialog?.handle?.isFocused() ?? false; }
72
- get customFooter(): Component | undefined { return this.footer; }
73
- get status(): string { return [...this.statuses.values()].join(" · "); }
74
- private report(error: unknown): void { const message = String(error); if (this.reported.has(message)) return; this.reported.add(message); this.host.notify(`Extension UI: ${message}`, "error"); }
75
- private attempt<T>(fn: () => T): T | undefined { try { return fn(); } catch (error) { this.report(error); return undefined; } }
76
-
77
- /** Errors are isolated at the component boundary, including errors thrown only during render. */
78
- own(component: ExtensionComponent, fallback?: Component): OwnedComponent {
79
- let failed = false, disposed = false;
80
- const fail = (error: unknown) => { const report = !failed; failed = true; if (report) this.report(error); };
81
- return {
82
- render: width => { if (!failed) try { return component.render(width); } catch (error) { fail(error); } return fallback?.render(width) ?? []; },
83
- invalidate: () => { if (!failed) try { component.invalidate?.(); } catch (error) { fail(error); } },
84
- handleInput: data => { if (!failed) try { component.handleInput?.(data); } catch (error) { fail(error); } },
85
- get focused() { return component.focused ?? false; }, set focused(value: boolean) { component.focused = value; },
86
- dispose: () => { if (disposed) return; disposed = true; this.attempt(() => component.dispose?.()); },
87
- };
88
- }
89
-
90
- async bind(registry: ExtensionRegistry | undefined, resources: ResourceIndex | undefined): Promise<void> {
91
- if (registry === this.registry) return;
92
- this.clear(); this.registry = registry; this.api = this.createApi();
93
- this.unbind = registry?.onDispose(() => { this.clear(); this.registry = undefined; this.host.refreshTranscript(); });
94
- this.unwatchTools = registry?.onToolUpdate((name, id, input, result) => {
95
- const tool = registry.tools.get(name); if (!tool?.renderResult) return;
96
- this.partialComponents.get(id)?.dispose();
97
- this.partialComponents.set(id, this.renderTool(name, input, { text: result.content.filter(c => c.type === "text").map(c => c.text).join("\n"), ok: result.isError !== true, data: result.details }, this.host.expanded(), new Lines(() => []), new PiText(result.content.filter(c => c.type === "text").map(c => c.text).join("\n"), 0, 0), id, result));
98
- this.partials.clear(); for (const component of this.partialComponents.values()) this.partials.addChild(component);
99
- this.host.redraw();
100
- });
101
- const generation = this.generation, cache = new Map<string, { theme: ExtensionTheme; path: string | undefined }>();
102
- for (const name of await discoverThemes(resources)) {
103
- const path = resources ? themeResourcePath(resources, name) : undefined, errors: string[] = [];
104
- const theme = await resolveTheme(path ?? name, errors);
105
- if (!errors.length) cache.set(name, { theme: this.theme(theme), path });
106
- }
107
- if (generation === this.generation) this.themeCache = cache;
108
- this.host.refreshTranscript();
109
- }
110
- private theme(theme = this.host.theme()): ExtensionTheme {
111
- const ansi = (color: string) => this.host.colored() && theme.colors[color] ? `\x1b[${theme.colors[color]}m` : "";
112
- const wrap = (open: string, text: string, close = "\x1b[0m") => open ? open + text + close : text;
113
- const attr = (code: number, end: number) => (text: string) => wrap(this.host.colored() ? `\x1b[${code}m` : "", text, `\x1b[${end}m`);
114
- return { ...theme, fg: (color, text) => wrap(ansi(color), text, "\x1b[39m"), bg: (color, text) => wrap(ansi(color), text, "\x1b[49m"),
115
- bold: attr(1, 22), dim: attr(2, 22), italic: attr(3, 23), underline: attr(4, 24), inverse: attr(7, 27), strikethrough: attr(9, 29), getFgAnsi: ansi, getBgAnsi: ansi,
116
- getColorMode: () => "truecolor", getThinkingBorderColor: level => text => wrap(ansi("thinking" + (level[0]?.toUpperCase() ?? "O") + level.slice(1)), text, "\x1b[39m"),
117
- getBashModeBorderColor: () => text => wrap(ansi("bashMode"), text, "\x1b[39m") };
118
- }
119
-
120
- private createApi(): ExtensionUI {
121
- const owner = this, generation = this.generation;
122
- const api: ExtensionUI = {
123
- notify: this.host.notify,
124
- select: (title, choices, opts) => this.choose(title, choices, opts),
125
- confirm: async (title, message, opts) => (await this.choose(`${title}\n${message}`, ["Yes", "No"], opts)) === "Yes",
126
- input: (title, placeholder, opts) => this.textDialog(title, placeholder ?? "", false, opts),
127
- editor: (title, prefill) => this.textDialog(title, prefill ?? "", true),
128
- custom: (factory, opts) => this.custom(factory, opts),
129
- onTerminalInput: handler => { this.listeners.add(handler); return () => this.listeners.delete(handler); },
130
- setStatus: (key, text) => { if (text === undefined) this.statuses.delete(key); else this.statuses.set(key, text); this.host.redraw(); },
131
- setWidget: (key, content, opts) => {
132
- this.widgets.get(key)?.component.dispose(); this.widgets.delete(key);
133
- if (content !== undefined) {
134
- const factory: WidgetFactory = typeof content === "function" ? content : () => new PiText(content.join("\n"), 0, 0);
135
- const component = this.attempt(() => factory(this.host.screen(), this.theme()));
136
- if (component) this.widgets.set(key, { factory, component: this.own(component), below: opts?.placement === "belowEditor" });
137
- }
138
- this.mountWidgets(); this.host.redraw();
139
- },
140
- setHeader: factory => {
141
- this.headerSlot?.component.dispose(); this.headerSlot = undefined; this.header.clear();
142
- const component = factory && this.attempt(() => factory(this.host.screen(), this.theme()));
143
- if (component) { this.headerSlot = { factory: factory!, component: this.own(component), below: false }; this.header.addChild(this.headerSlot.component); }
144
- this.host.redraw();
145
- },
146
- setFooter: factory => {
147
- this.footer?.dispose(); this.footer = undefined; this.footerFactory = factory;
148
- for (const unsubscribe of this.footerSubscriptions.splice(0)) unsubscribe();
149
- const component = factory && this.attempt(() => factory(this.host.screen(), this.theme(), {
150
- getGitBranch: () => this.host.branch() ?? undefined, getExtensionStatuses: () => new Map(this.statuses),
151
- onBranchChange: fn => { this.branchListeners.add(fn); const off = () => { this.branchListeners.delete(fn); }; this.footerSubscriptions.push(off); return off; },
152
- }));
153
- if (component) this.footer = this.own(component);
154
- this.host.redraw();
155
- },
156
- setTitle: title => { this.hasTitle = true; this.host.screen().terminal.setTitle(title); },
157
- setWorkingMessage: message => { this.workingMessage = message; this.host.redraw(); },
158
- setWorkingVisible: visible => { this.workingVisible = visible; this.host.redraw(); },
159
- setWorkingIndicator: options => { this.frames = options?.frames; this.interval = Math.max(16, options?.intervalMs ?? 80); this.frame = 0; clearInterval(this.timer); this.timer = undefined; this.host.redraw(); },
160
- setHiddenThinkingLabel: label => { this.hiddenThinkingLabel = label; this.host.refreshTranscript(); this.host.redraw(); },
161
- getEditorText: () => this.host.editor.state.text,
162
- setEditorText: text => { this.host.editor.applyExternal(text); this.host.redraw(); },
163
- pasteToEditor: text => { this.host.editor.acceptInput(`\x1b[200~${text}\x1b[201~`); this.host.redraw(); },
164
- addAutocompleteProvider: factory => { this.autocomplete.push(factory); this.refreshCompletion(); },
165
- setEditorComponent: factory => {
166
- const component = factory && this.attempt(() => factory(this.host.screen(), identityEditorTheme(), this.host.bindings()));
167
- if (factory && !component) return;
168
- this.editorFactory = factory; this.host.editor.setComponent(component); this.host.redraw();
169
- },
170
- getEditorComponent: () => this.editorFactory,
171
- get theme() { return owner.theme(); },
172
- getAllThemes: () => [...this.themeCache].map(([name, value]) => ({ name, path: value.path })),
173
- getTheme: name => this.themeCache.get(name)?.theme,
174
- setTheme: name => {
175
- const theme = typeof name === "string" ? this.themeCache.get(name)?.theme : name;
176
- if (!theme || typeof theme.colors !== "object") return { success: false, error: `Unknown theme: ${String(name)}` };
177
- this.host.setTheme(theme); if (typeof name === "string") this.host.saveTheme(name); return { success: true };
178
- },
179
- getToolsExpanded: () => this.host.expanded(), setToolsExpanded: value => { this.host.expand(value); this.host.redraw(); },
180
- };
181
- // An asynchronous extension from a disposed session must not mutate its successor's UI.
182
- return new Proxy(api, { get(target, key, receiver) {
183
- const value: unknown = Reflect.get(target, key, receiver);
184
- return typeof value === "function" ? (...args: unknown[]) => generation === owner.generation ? Reflect.apply(value, target, args) : undefined : value;
185
- } });
186
- }
187
-
188
- private mountWidgets(): void {
189
- this.above.clear(); this.below.clear();
190
- for (const slot of this.widgets.values()) (slot.below ? this.below : this.above).addChild(slot.component);
191
- }
192
- setCompletion(provider: AutocompleteProvider): void { this.baseCompletion = provider; this.refreshCompletion(); }
193
- private refreshCompletion(): void {
194
- if (!this.baseCompletion) return;
195
- let provider: AutocompleteProvider = this.baseCompletion;
196
- for (const factory of this.autocomplete) { const base: AutocompleteProvider = provider; provider = this.attempt(() => factory(base)) ?? base; }
197
- this.host.editor.setAutocompleteProvider(provider);
198
- }
199
- filterInput(data: string): string | undefined {
200
- for (const listener of this.listeners) { const value = data, result = this.attempt(() => listener(value)); if (result?.consume) return undefined; data = result?.data ?? data; }
201
- return data;
202
- }
203
- acceptInput(data: string): boolean {
204
- const dialog = this.dialog; if (!dialog || dialog.handle && !dialog.handle.isFocused()) return false;
205
- if (keyMatches(data, "escape") || keyMatches(data, "ctrl+c")) dialog.close();
206
- else this.attempt(() => dialog.raw?.handleInput?.(data));
207
- this.host.redraw(); return true;
208
- }
209
- private choose(title: string, choices: string[], options?: DialogOptions): Promise<string | undefined> {
210
- return this.custom<string | undefined>((_tui, _theme, _bindings, done) => {
211
- const list = new SelectList(choices.map(value => ({ label: value, value })), { title, search: true });
212
- return { render: width => list.render(width), invalidate: () => list.invalidate(), handleInput: data => { const state = list.acceptInput(data); if (state.confirmed) done(state.confirmed.value); else if (state.cancelled) done(undefined); } };
213
- }, options);
214
- }
215
- private textDialog(title: string, value: string, multiline: boolean, options?: DialogOptions): Promise<string | undefined> {
216
- return this.custom<string | undefined>((tui, _theme, _bindings, done) => {
217
- const editor = multiline ? new PiEditor(tui, identityEditorTheme()) : new Input();
218
- if (editor instanceof PiEditor) editor.setText(value);
219
- editor.onSubmit = text => done(text); editor.focused = true;
220
- return { render: width => [title,
221
- ...(editor instanceof Input && !editor.getValue() && value ? [value] : []), ...editor.render(width)], invalidate: () => editor.invalidate(), handleInput: data => editor.handleInput(data) };
222
- }, options);
223
- }
224
- private custom<T>(factory: Parameters<ExtensionUI["custom"]>[0], options?: CustomOptions): Promise<T> {
225
- const generation = this.generation;
226
- let resolve!: (result: T) => void;
227
- const result = new Promise<T>(done => { resolve = done; });
228
- let closed = false, timeout: ReturnType<typeof setTimeout> | undefined, tick: ReturnType<typeof setInterval> | undefined;
229
- const cancel = () => dialog.close();
230
- const dialog: Dialog = { close: value => {
231
- if (closed) return; closed = true;
232
- clearTimeout(timeout); clearInterval(tick); options?.signal?.removeEventListener("abort", cancel); this.pendingDialogs.delete(cancel);
233
- dialog.handle?.hide(); dialog.component?.dispose();
234
- if (this.dialog === dialog) this.dialog = undefined;
235
- resolve(value as T); this.host.redraw();
236
- } };
237
- this.pendingDialogs.add(cancel);
238
- options?.signal?.addEventListener("abort", cancel, { once: true });
239
- if (options?.signal?.aborted) cancel();
240
- const before = this.tail; this.tail = before.then(() => result);
241
- void before.then(async () => {
242
- if (closed || generation !== this.generation) return cancel();
243
- this.dialog = dialog;
244
- dialog.component = this.own(new PiText("Loading…", 0, 0)); this.host.redraw();
245
- if (options?.timeout !== undefined) { timeout = setTimeout(cancel, Math.max(0, options.timeout)); tick = setInterval(() => this.host.redraw(), 200); }
246
- try {
247
- const component = await factory(this.host.screen(), this.theme(), this.host.bindings(), value => dialog.close(value));
248
- if (closed || generation !== this.generation) { this.attempt(() => component.dispose?.()); return; }
249
- dialog.component?.dispose(); dialog.raw = component;
250
- const owner = this.own(component);
251
- // Pi receives raw terminal input through this wrapper; the app loop remains its only dispatcher.
252
- const deadline = options?.timeout === undefined ? undefined : Date.now() + options.timeout;
253
- dialog.component = { ...owner, render: width => [...(deadline === undefined ? [] : [`${Math.max(0, Math.ceil((deadline - Date.now()) / 1000))}s remaining`]), ...owner.render(width)],
254
- get focused() { return component.focused ?? false; }, set focused(value: boolean) { component.focused = value; }, handleInput: data => this.host.input(data) };
255
- if (options?.overlay) {
256
- dialog.handle = this.host.screen().showOverlay(dialog.component!, typeof options.overlayOptions === "function" ? options.overlayOptions() : options.overlayOptions);
257
- options.onHandle?.({ ...dialog.handle, hide: cancel });
258
- }
259
- this.host.redraw();
260
- } catch (error) { this.report(error); cancel(); }
261
- });
262
- return result;
263
- }
264
- working(active: boolean, fallback: string): string | undefined {
265
- if (!active || !this.workingVisible) { clearInterval(this.timer); this.timer = undefined; return undefined; }
266
- const frames = this.frames ?? ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
267
- if (frames.length > 1 && !this.timer) { this.timer = setInterval(() => { this.frame++; this.host.redraw(); }, this.interval); this.timer.unref(); }
268
- return `${frames.length ? frames[this.frame % frames.length] + " " : ""}${this.workingMessage ?? fallback} · Esc to interrupt`;
269
- }
270
- /** Screen replacement cannot leave components pointing at a stopped Pi host. */
271
- rebind(screenChanged = true): void {
272
- if (screenChanged) this.cancelDialogs(); this.unlisten?.();
273
- this.unlisten = this.host.screen().addInputListener(data => { const filtered = this.filterInput(data); return filtered === undefined ? { consume: true } : { data: filtered }; });
274
- for (const [key, slot] of [...this.widgets]) this.api.setWidget(key, slot.factory, { placement: slot.below ? "belowEditor" : "aboveEditor" });
275
- if (this.headerSlot) this.api.setHeader(this.headerSlot.factory);
276
- if (this.footerFactory) this.api.setFooter(this.footerFactory);
277
- if (this.editorFactory) this.api.setEditorComponent(this.editorFactory);
278
- this.branchChanged();
279
- }
280
- branchChanged(): void { for (const listener of this.branchListeners) this.attempt(listener); }
281
- clearPartials(): void { for (const component of this.partialComponents.values()) component.dispose(); this.partialComponents.clear(); this.partials.clear(); }
282
- cancelDialogs(): void { for (const cancel of [...this.pendingDialogs]) cancel(); }
283
- private clear(): void {
284
- this.generation++; this.cancelDialogs(); clearInterval(this.timer); this.timer = undefined;
285
- if (this.hasTitle) { this.host.screen().terminal.setTitle("xeno"); this.hasTitle = false; }
286
- this.unbind?.(); this.unbind = undefined; this.unwatchTools?.(); this.unwatchTools = undefined; this.clearPartials();
287
- this.toolStates.clear();
288
- this.reported.clear();
289
- for (const slot of this.widgets.values()) slot.component.dispose(); this.widgets.clear(); this.mountWidgets();
290
- this.headerSlot?.component.dispose(); this.headerSlot = undefined; this.header.clear();
291
- this.footer?.dispose(); this.footer = undefined; this.footerFactory = undefined;
292
- for (const unsubscribe of this.footerSubscriptions.splice(0)) unsubscribe();
293
- this.editorFactory = undefined; this.attempt(() => this.host.editor.setComponent(undefined));
294
- this.autocomplete = []; this.refreshCompletion(); this.statuses.clear(); this.listeners.clear(); this.branchListeners.clear();
295
- this.workingMessage = undefined; this.workingVisible = true; this.frames = undefined; this.hiddenThinkingLabel = undefined;
296
- this.themeCache = new Map(["dark", "light"].map(name => [name, { theme: this.theme(builtinTheme(name === "dark" ? "dark" : "light")), path: undefined }]));
297
- }
298
- dispose(): void { this.clear(); this.unlisten?.(); this.unlisten = undefined; }
299
- transformMarkdown(text: string, width: number, streaming: boolean, messageType = "assistant"): string {
300
- for (const transform of this.registry?.markdown ?? []) { const input = text; text = this.attempt(() => transform(input, { availableWidth: width, isStreaming: streaming, messageType })) ?? input; }
301
- return text;
302
- }
303
- renderMessage(type: string, text: string, data: unknown, expanded: boolean, fallback: Component, metadata: { id: string; entry: boolean; timestamp: string }): Component {
304
- const render = (metadata.entry ? this.registry?.entryRenderers : this.registry?.renderers)?.get(type);
305
- const message = metadata.entry ? { type: "custom", id: metadata.id, timestamp: metadata.timestamp, customType: type, data }
306
- : { role: "custom", customType: type, content: text, details: data, display: true, timestamp: Date.parse(metadata.timestamp) };
307
- const component = render && this.attempt(() => render(message, { expanded, outputPad: this.host.outputPad() }, this.theme()));
308
- return component ? this.own(component, fallback) : fallback;
309
- }
310
- renderTool(name: string, input: unknown, result: { text: string; ok: boolean; data?: unknown } | null, expanded: boolean, callFallback: Component, resultFallback: Component, id: string, partial?: ExtensionToolResult): OwnedComponent {
311
- const tool = this.registry?.tools.get(name);
312
- const context: ExtensionToolRenderContext = { args: input, toolCallId: id, invalidate: () => this.host.screen().requestRender(), lastComponent: undefined,
313
- state: this.toolStates.get(id) ?? {}, cwd: this.host.cwd(), executionStarted: true, argsComplete: true, isPartial: !!partial, expanded, showImages: this.host.showImages(), isError: result?.ok === false };
314
- const slots: { raw?: ExtensionComponent; owned?: OwnedComponent; failed?: boolean }[] = [{}, {}];
315
- const slot = (index: number, factory: (() => ExtensionComponent) | undefined, fallback: Component): Component => {
316
- const current = slots[index]!;
317
- if (!factory || current.failed) return fallback;
318
- context.lastComponent = current.raw;
319
- try {
320
- const raw = factory();
321
- if (raw !== current.raw) { current.owned?.dispose(); current.raw = raw; current.owned = this.own(raw, fallback); }
322
- return current.owned ?? fallback;
323
- } catch (error) { current.failed = true; this.report(error); return fallback; }
324
- finally { this.toolStates.set(id, context.state); }
325
- };
326
- const components = () => [slot(0, tool?.renderCall ? () => tool.renderCall!(input, this.theme(), context) : undefined, callFallback),
327
- slot(1, result && tool?.renderResult ? () => tool.renderResult!(partial ?? {content:[{type:"text",text:result.text}], ...(result.data === undefined ? {} : { details: result.data as import("../core/tool-contract.ts").JsonValue }),isError:!result.ok}, {expanded,isPartial:!!partial},this.theme(),context) : undefined, resultFallback)];
328
- return { render: width => components().flatMap(c => c.render(width)), invalidate: () => slots.forEach(slot => slot.owned?.invalidate()), dispose: () => slots.forEach(slot => slot.owned?.dispose()) };
329
- }
330
- }
@@ -1,75 +0,0 @@
1
- /** Pi config selector behavior, using the same inventory as runtime loading. */
2
- import { SelectList } from "../tui/select.ts";
3
- import { createInteractiveTui } from "../tui/host.ts";
4
- import { installBindings, keyMatches } from "../tui/input.ts";
5
- import { getKeybindings } from "../vendor/pi-tui/keybindings.ts";
6
- import { truncateToWidth } from "../vendor/pi-tui/utils.ts";
7
- import { nextResourceOverride } from "../core/resource-selection.ts";
8
- import { discoverResources, setResourceOverride, type ResourceIndex, type ResourceOptions } from "../store/resource-index.ts";
9
- import { loadUiResources } from "../store/resources.ts";
10
- import { createStyle, IDENTITY, type Style } from "../tui/style.ts";
11
- import { colorEnabled } from "./render.ts";
12
-
13
- export class ResourceBrowser extends SelectList {
14
- scope: "user" | "project" = "user";
15
- index: ResourceIndex;
16
- notice = "";
17
- private readonly resourceOptions: ResourceOptions;
18
- private readonly redraw: () => void;
19
- private readonly changed: (() => Promise<void>) | undefined;
20
- constructor(options: { resources: ResourceOptions; index: ResourceIndex; style: Style; redraw: () => void; changed?: () => Promise<void> }) {
21
- super([], { title: "Resources", style: options.style, emptyMessage: "No resources found" });
22
- this.resourceOptions = options.resources; this.index = options.index; this.redraw = options.redraw; this.changed = options.changed;
23
- if(options.resources.local)this.scope="project";
24
- this.refresh();
25
- }
26
- private refresh(): void {
27
- this.setItems(this.index.entries.map(entry => ({ value: entry.kind + ":" + entry.path,
28
- label: `[${entry.enabled ? "x" : " "}] ${entry.kind} · ${entry.name}${this.scope === "project" ? ` · ${entry.override}` : ""}`,
29
- description: `${entry.scope}${entry.trusted ? "" : " · untrusted"}${entry.package?" · "+entry.package.source:""} · ${entry.path}` })));
30
- this.redraw();
31
- }
32
- async handleAction(data: string): Promise<boolean> {
33
- const kb = getKeybindings();
34
- if (kb.matches(data, "tui.input.tab")) { this.scope = this.scope === "user" ? "project" : "user"; this.refresh(); return true; }
35
- if (kb.matches(data, "tui.select.confirm")) return true;
36
- if (!keyMatches(data, "space")) return false;
37
- const selected = this.state.items[this.state.selected]?.value;
38
- const entry = this.index.entries.find(row => row.kind + ":" + row.path === selected);
39
- if (!entry) return true;
40
- const state = this.scope === "project" ? nextResourceOverride(entry.override) : entry.inherited ? "skip" : "load";
41
- try {
42
- await setResourceOverride(this.index, entry, this.scope, state);
43
- this.index = await discoverResources(this.resourceOptions);
44
- await this.changed?.();
45
- this.notice = entry.trusted ? "Saved and reloaded" : "Saved; project resources remain blocked until this project is trusted";
46
- } catch (error) { this.notice = String(error); }
47
- this.refresh(); return true;
48
- }
49
- override render(width: number): string[] {
50
- return [`${this.scope === "user" ? "Global" : "Project"} Resources`, this.scope === "user" ? this.index.userDir : this.index.projectDir,
51
- ...super.render(width).slice(1, -1), `${getKeybindings().getKeys("tui.input.tab").join("/")} scope · Space ${this.scope === "project" ? "inherit/+/-" : "toggle"} · ${getKeybindings().getKeys("tui.select.cancel").join("/")} close`, this.notice]
52
- .filter(Boolean).map(line => truncateToWidth(line, width, ""));
53
- }
54
- }
55
-
56
- /** Standalone configuration does not allocate a conversation or require a model credential. */
57
- export async function runResourceConfig(options: ResourceOptions, handoff?: () => void): Promise<void> {
58
- const index = await discoverResources(options);
59
- const ui = await loadUiResources(options, index);
60
- const screen = createInteractiveTui();
61
- const browser = new ResourceBrowser({ resources: options, index, style: colorEnabled() ? createStyle(ui.theme.colors) : IDENTITY, redraw: () => screen.requestRender() });
62
- installBindings(ui.keymap.byAction);
63
- screen.addChild(browser); screen.setFocus(browser);
64
- try {
65
- await new Promise<void>(resolve => {
66
- let pending = Promise.resolve();
67
- browser.onInput = data => { pending = pending.then(async () => {
68
- if (await browser.handleAction(data)) return;
69
- if (browser.acceptInput(data).cancelled) resolve();
70
- screen.requestRender();
71
- }).catch(error => { browser.notice = String(error); screen.requestRender(); }); };
72
- handoff?.(); screen.start();
73
- });
74
- } finally { await screen.terminal.drainInput(); screen.stop(); }
75
- }
@@ -1,113 +0,0 @@
1
- /** Pi selector interaction, backed exclusively by xeno's session store. Shared by startup and /resume. */
2
- import { SelectList, type SelectItem, type SelectState } from "../tui/select.ts";
3
- import { Input } from "../vendor/pi-tui/components/input.ts";
4
- import { fuzzyMatch } from "../vendor/pi-tui/fuzzy.ts";
5
- import { truncateToWidth } from "../vendor/pi-tui/utils.ts";
6
- import { keyMatches } from "../tui/input.ts";
7
- import { defaultKeymap, type Keymap } from "../core/keymap.ts";
8
- import { ACTION_ALIASES } from "../core/action-defaults.ts";
9
- import { parseSessionQuery, selectSessions, type SessionSort } from "../core/session-browser.ts";
10
- import { archiveSession, renameSession, scanSessionRoots, sessionRoots, type SessionRoot } from "../store/session-browser.ts";
11
- import type { ListedSession } from "../store/sessions.ts";
12
- import type { Style } from "../tui/style.ts";
13
-
14
- type Session = Extract<ListedSession, { ok: true }>;
15
- export class SessionBrowser extends SelectList {
16
- private sessions: Session[] = [];
17
- private readonly initial: SessionRoot;
18
- private readonly redraw: () => void;
19
- private readonly active: string | undefined;
20
- private readonly bindings: () => Keymap;
21
- private readonly renamed: ((path: string, name: string) => void) | undefined;
22
- private scan = new AbortController();
23
- private disposed = false;
24
- private editing: { kind: "rename" | "archive"; session: Session; input: Input } | undefined;
25
- scope: "current" | "all" = "current";
26
- sort: SessionSort = "threaded";
27
- named = false;
28
- showPath = false;
29
- progress = "";
30
- notice = "";
31
- constructor(options: { root: SessionRoot; style: Style; redraw: () => void; active?: string; bindings?: () => Keymap; renamed?: (path: string, name: string) => void }) {
32
- super([], { style: options.style, title: "Resume session", emptyMessage: "No matching sessions", filter: (_items, query) => this.visible(query) });
33
- this.initial = options.root; this.redraw = options.redraw; this.active = options.active; this.bindings = options.bindings ?? defaultKeymap;
34
- this.renamed = options.renamed;
35
- }
36
- override get focused(): boolean { return this.editing?.input.focused ?? super.focused; }
37
- override set focused(value: boolean) { super.focused = value; if (this.editing) this.editing.input.focused = value; }
38
- get sessionCount(): number { return this.sessions.length; }
39
- private visible(query: string): SelectItem[] {
40
- return selectSessions(this.sessions, query, this.sort, this.named, fuzzyMatch).map(s => ({ value: s.path,
41
- label: " ".repeat(Math.min(12, s.depth)) + (s.depth ? "↳ " : "") + (s.name?.trim() || s.text?.split("\n").find(Boolean)?.slice(0, 100) || s.id),
42
- description: this.showPath ? s.path : [s.updated_at ?? "", s.model ?? "", this.scope === "all" ? s.workspace ?? "" : ""].filter(Boolean).join(" · "),
43
- }));
44
- }
45
- private refresh(): void { if (!this.disposed) { this.setItems([]); this.redraw(); } }
46
- async load(): Promise<void> {
47
- this.scan.abort(); const scan = this.scan = new AbortController();
48
- this.sessions = []; this.progress = "Loading sessions…"; this.refresh();
49
- try {
50
- const roots = this.scope === "current" ? [this.initial] : await sessionRoots(this.initial);
51
- for await (const batch of scanSessionRoots(roots, scan.signal)) {
52
- if (this.disposed || scan !== this.scan) return;
53
- this.sessions.push(...batch.sessions.filter((s): s is Session => s.ok));
54
- for (const row of batch.sessions) if (!row.ok) this.notice = row.error;
55
- this.progress = `Loading ${batch.loaded}/${batch.total}`; this.refresh();
56
- }
57
- } catch (error) { if (!scan.signal.aborted) this.notice = String(error); }
58
- if (scan !== this.scan || this.disposed) return;
59
- this.progress = ""; this.refresh();
60
- }
61
- dispose(): void { this.disposed = true; this.scan.abort(); }
62
- private matches(data: string, action: string): boolean {
63
- return (this.bindings().byAction[(ACTION_ALIASES[action] ?? action) as keyof Keymap["byAction"]] ?? []).some(chord => keyMatches(data, chord));
64
- }
65
- async handleAction(data: string): Promise<boolean> {
66
- if (this.editing) {
67
- if (this.matches(data, "tui.select.cancel") || keyMatches(data, "ctrl+c")) { this.editing = undefined; this.redraw(); return true; }
68
- if (this.matches(data, "tui.select.confirm")) {
69
- const edit = this.editing;
70
- try {
71
- if (edit.kind === "rename") {
72
- await renameSession(edit.session.path, edit.input.getValue());
73
- edit.session.name = edit.input.getValue().trim();
74
- this.renamed?.(edit.session.path, edit.session.name);
75
- } else {
76
- const destination = await archiveSession(edit.session.path, this.active);
77
- this.sessions = this.sessions.filter(s => s.path !== edit.session.path);
78
- this.notice = `Kept at ${JSON.stringify(destination)}; restore the directory to ${JSON.stringify(edit.session.path)}`;
79
- }
80
- this.editing = undefined; this.refresh();
81
- } catch (error) { this.notice = String(error); this.redraw(); }
82
- } else if (this.editing.kind === "rename") this.editing.input.handleInput(data);
83
- this.redraw(); return true;
84
- }
85
- if (this.matches(data, "tui.input.tab")) { this.scope = this.scope === "current" ? "all" : "current"; void this.load(); return true; }
86
- if (this.matches(data, "app.session.toggleNamedFilter")) this.named = !this.named;
87
- else if (this.matches(data, "app.session.toggleSort")) this.sort = this.sort === "threaded" ? "recent" : this.sort === "recent" ? "relevance" : "threaded";
88
- else if (this.matches(data, "app.session.togglePath")) this.showPath = !this.showPath;
89
- else if (["app.session.rename", "app.session.delete", "app.session.deleteNoninvasive"].some(action => this.matches(data, action))) {
90
- const path = this.state.items[this.state.selected]?.value;
91
- const session = this.sessions.find(s => s.path === path);
92
- if (!session) return true;
93
- const kind = this.matches(data, "app.session.rename") ? "rename" : "archive";
94
- if (kind === "archive" && session.path === this.active) { this.notice = "Cannot remove the currently active session"; this.redraw(); return true; }
95
- const input = new Input(); input.setValue(session.name ?? ""); input.focused = this.focused;
96
- this.editing = { kind, session, input };
97
- } else return false;
98
- this.refresh(); return true;
99
- }
100
- override acceptInput(data: string): SelectState { const result = super.acceptInput(data); this.redraw(); return result; }
101
- override render(width: number): string[] {
102
- const header = this.progress === "Loading sessions…" ? this.progress : `Resume session · ${this.scope === "current" ? "Current folder" : "All workspaces"} · ${this.named ? "Named" : "All names"} · ${this.sort}`;
103
- const hint = (action: string, label: string) => {
104
- const keys = this.bindings().byAction[(ACTION_ALIASES[action] ?? action) as keyof Keymap["byAction"]];
105
- return keys?.length ? `${keys.join("/")} ${label}` : "";
106
- };
107
- const body = this.editing ? [this.editing.kind === "rename" ? "Rename session" : "Remove session from this list? Original log will be kept in .trash.",
108
- this.editing.session.name ?? this.editing.session.id, ...(this.editing.kind === "rename" ? this.editing.input.render(width) : []), "Enter confirm · Esc cancel"] :
109
- [...super.render(width).slice(1, -1), [["tui.input.tab", "scope"], ["app.session.toggleNamedFilter", "named"], ["app.session.toggleSort", "sort"],
110
- ["app.session.togglePath", "path"], ["app.session.rename", "rename"], ["app.session.delete", "remove"]].map(([action, label]) => hint(action!, label!)).filter(Boolean).join(" · "), 're:<pattern> regex · "phrase" exact'];
111
- return [header, ...body, this.progress, parseSessionQuery(this.query).error ?? this.notice].filter(Boolean).map(line => truncateToWidth(line, width, ""));
112
- }
113
- }
@@ -1,55 +0,0 @@
1
- /** A startup picker owns only a terminal, never a provisional durable session. */
2
- import { sessionsDir, type ListedSession } from "../store/sessions.ts";
3
- import { registerSessionRoot, sessionRoots } from "../store/session-browser.ts";
4
- import { createInteractiveTui } from "../tui/host.ts";
5
- import type { SelectItem } from "../tui/select.ts";
6
- import { SessionBrowser } from "./session-browser.ts";
7
- import { installBindings } from "../tui/input.ts";
8
- import { defaultKeymap, type Keymap } from "../core/keymap.ts";
9
- import type { Style } from "../tui/style.ts";
10
-
11
- export function sessionItems(sessions: readonly ListedSession[]): SelectItem[] {
12
- return sessions.flatMap((s) => s.ok ? [{
13
- value: s.path,
14
- label: s.name ?? s.id,
15
- description: [s.model ?? "", s.updated_at ?? "", `${s.events} events`].filter(Boolean).join(" "),
16
- }] : []);
17
- }
18
-
19
- export async function pickStartupSession(opts: {
20
- workspace: string;
21
- sessionDir?: string;
22
- style: Style;
23
- say: (line: string) => void;
24
- keymap?: Keymap;
25
- }, handoff?: () => void): Promise<string | null> {
26
- const root = { path: sessionsDir(opts.workspace, opts.sessionDir), workspace: opts.workspace };
27
- await registerSessionRoot(root).catch(error => opts.say(String(error)));
28
- const screen = createInteractiveTui();
29
- const select = new SessionBrowser({ root, style: opts.style, redraw: () => screen.requestRender(), bindings: () => opts.keymap ?? defaultKeymap() });
30
- installBindings((opts.keymap ?? defaultKeymap()).byAction);
31
- screen.addChild(select);
32
- screen.setFocus(select);
33
- try {
34
- return await new Promise<string | null>((resolve) => {
35
- let pending = Promise.resolve();
36
- select.onInput = (data) => { pending = pending.then(async () => {
37
- if (await select.handleAction(data)) return;
38
- const state = select.acceptInput(data);
39
- if (state.cancelled) resolve(null);
40
- else if (state.confirmed) resolve(state.confirmed.value);
41
- else screen.requestRender();
42
- }).catch(error => { opts.say(String(error)); }); };
43
- handoff?.(); screen.start();
44
- void select.load().then(async () => {
45
- if (select.scope === "current" && !select.sessionCount && !select.notice && (await sessionRoots(root)).length === 1) {
46
- opts.say("No sessions to resume"); resolve(null);
47
- }
48
- }).catch(error => { opts.say(String(error)); });
49
- });
50
- } finally {
51
- select.dispose();
52
- await screen.terminal.drainInput();
53
- screen.stop();
54
- }
55
- }
package/src/tui/ansi.ts DELETED
@@ -1,67 +0,0 @@
1
- /**
2
- * Escape sequences the renderer emits. Each constant names the standard it
3
- * comes from and what breaks without it -- a bare number in a CSI is otherwise
4
- * a magic that nobody can audit.
5
- */
6
-
7
- /**
8
- * DECSET 2026 — begin synchronized output.
9
- * Spec: https://gist.github.com/christianparpart/d8d62cc110656ba92d4e171cec8a547a
10
- * (mode 2026, adopted by xterm, tmux, kitty, iTerm2, Windows Terminal).
11
- * Without this, tmux paints each cursor move as its own frame and the user
12
- * sees the cursor jump and the frame tear while a diff is applied.
13
- */
14
- export const SYNC_BEGIN = "\x1b[?2026h";
15
-
16
- /**
17
- * DECRST 2026 — end synchronized output, the matching close of SYNC_BEGIN.
18
- * Omitting it leaves the terminal buffering subsequent output forever, so
19
- * the next frame (and everything after) never appears.
20
- */
21
- export const SYNC_END = "\x1b[?2026l";
22
-
23
- /**
24
- * CR — carriage return. ASCII 0x0D / ECMA-48. Returns to column 0 of the
25
- * current row so a changed line can be overwritten in place.
26
- */
27
- export const CR = "\r";
28
-
29
- /**
30
- * LF — line feed. ASCII 0x0A / ECMA-48. Advances onto a newly grown row of
31
- * the live region; a CSI CUD at the bottom of the screen is clamped and
32
- * would overwrite the last line instead of extending the region.
33
- */
34
- export const LF = "\n";
35
-
36
- /**
37
- * CSI CUU — cursor up N rows. ECMA-48 §8.3.14. Relative moves are what make
38
- * a line-diff possible without knowing the absolute row of the live region
39
- * (a main-screen TUI does not own the viewport origin).
40
- */
41
- export function cursorUp(n: number): string {
42
- return n > 0 ? `\x1b[${n}A` : "";
43
- }
44
-
45
- /**
46
- * CSI CUD — cursor down N rows. ECMA-48 §8.3.13. Used to skip unchanged
47
- * lines when the cursor sits above them, not to grow the region (see LF).
48
- */
49
- export function cursorDown(n: number): string {
50
- return n > 0 ? `\x1b[${n}B` : "";
51
- }
52
-
53
- /**
54
- * CSI EL 0 — erase from cursor to end of line. ECMA-48 §8.3.41.
55
- * Without it a shorter replacement leaves the tail of the previous longer
56
- * line sitting on screen.
57
- */
58
- export const ERASE_LINE_TAIL = "\x1b[K";
59
-
60
- /**
61
- * CSI ED 0 — erase from cursor to end of display. ECMA-48 §8.3.39.
62
- * Used when the new frame is shorter than the old one. Safe only because
63
- * the live region sits at the bottom of the main screen: anything below it
64
- * is leftover rows of a taller previous frame, not the user's scrollback
65
- * (scrollback is above us).
66
- */
67
- export const ERASE_DOWN = "\x1b[J";