@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
@@ -0,0 +1,86 @@
1
+ /**
2
+ * What a list dialog shows for a query. Pure.
3
+ *
4
+ * Every picker in the UI (palette, model, login, resume, tree, settings, …)
5
+ * is the same shape: items grouped into sections, a fuzzy query, one
6
+ * selected row, optional checkboxes. One model means one set of keys and
7
+ * one place where "Enter confirms the row under the cursor" is proven.
8
+ *
9
+ * With an empty query, items keep their given order and each new section
10
+ * gets a header row (OpenCode's dialog-model). With a query, sections
11
+ * flatten into one ranked list: grouping a three-hit search only adds noise.
12
+ */
13
+ import { fuzzyMatch } from "../fuzzy.ts";
14
+
15
+ export interface ListItem {
16
+ readonly value: string;
17
+ readonly label: string;
18
+ readonly description?: string;
19
+ readonly section?: string;
20
+ /** Right-aligned hint: a key chord, "current", a count. */
21
+ readonly hint?: string;
22
+ /** Extra text matched by the query but not shown. */
23
+ readonly keywords?: string;
24
+ readonly disabled?: boolean;
25
+ /** Indentation (tree rows). */
26
+ readonly depth?: number;
27
+ }
28
+
29
+ export type ListRow =
30
+ | { readonly kind: "header"; readonly text: string }
31
+ | { readonly kind: "item"; readonly item: ListItem; readonly index: number };
32
+
33
+ export function filterItems(items: readonly ListItem[], query: string): ListItem[] {
34
+ const q = query.trim();
35
+ if (!q) return [...items];
36
+ const ranked: { item: ListItem; score: number; index: number }[] = [];
37
+ items.forEach((item, index) => {
38
+ const label = fuzzyMatch(item.label, q);
39
+ const rest = fuzzyMatch(`${item.label} ${item.description ?? ""} ${item.section ?? ""} ${item.keywords ?? ""}`, q);
40
+ // A hit in the label outranks a hit that needed the description.
41
+ const score = label !== null ? label : rest !== null ? rest + 1000 : null;
42
+ if (score !== null) ranked.push({ item, score, index });
43
+ });
44
+ ranked.sort((a, b) => a.score - b.score || a.index - b.index);
45
+ return ranked.map(entry => entry.item);
46
+ }
47
+
48
+ export function listRows(items: readonly ListItem[], query: string): ListRow[] {
49
+ const filtered = filterItems(items, query);
50
+ const rows: ListRow[] = [];
51
+ let section: string | undefined;
52
+ const grouped = !query.trim();
53
+ filtered.forEach((item, index) => {
54
+ if (grouped && item.section && item.section !== section) rows.push({ kind: "header", text: item.section });
55
+ section = item.section;
56
+ rows.push({ kind: "item", item, index });
57
+ });
58
+ return rows;
59
+ }
60
+
61
+ /** Next selectable index (wrapping), skipping disabled items. -1 when none. */
62
+ export function step(items: readonly ListItem[], from: number, delta: number): number {
63
+ const n = items.length;
64
+ if (n === 0) return -1;
65
+ for (let k = 1; k <= n; k++) {
66
+ const candidate = (((from + delta * k) % n) + n) % n;
67
+ if (!items[candidate]!.disabled) return candidate;
68
+ }
69
+ return -1;
70
+ }
71
+
72
+ /** First enabled index at or after `from`, used when a query changes the list. */
73
+ export function firstEnabled(items: readonly ListItem[], from = 0): number {
74
+ for (let i = Math.max(0, from); i < items.length; i++) if (!items[i]!.disabled) return i;
75
+ for (let i = 0; i < Math.min(from, items.length); i++) if (!items[i]!.disabled) return i;
76
+ return -1;
77
+ }
78
+
79
+ /** Rows to draw so the selected item stays visible, keeping its header when it fits. */
80
+ export function windowRows(rows: readonly ListRow[], selected: number, height: number): { start: number; end: number } {
81
+ if (rows.length <= height) return { start: 0, end: rows.length };
82
+ const at = Math.max(0, rows.findIndex(row => row.kind === "item" && row.index === selected));
83
+ let start = Math.max(0, Math.min(at - Math.floor(height / 2), rows.length - height));
84
+ if (height > 1 && start > 0 && rows[at - 1]?.kind === "header" && at - 1 < start) start = at - 1;
85
+ return { start, end: Math.min(rows.length, start + height) };
86
+ }
@@ -0,0 +1,172 @@
1
+ /**
2
+ * The one list component: grouped rows, selection, optional checkboxes,
3
+ * right-aligned hints, and a search field on the bottom row — the row the
4
+ * draft occupies when no picker is open, so the input box keeps its shape
5
+ * (docs/tasks/refactor-tui-dock-layout.md). State lives in list-model.ts
6
+ * (pure); this class draws it and exposes the moves the dialog host routes.
7
+ *
8
+ * Rows are a Canvas: moving the selection repaints cells, not nodes. The
9
+ * search text is a focused Input so cursor, paste and word deletion stay
10
+ * OpenTUI's.
11
+ */
12
+ import { BoxRenderable, InputRenderable, InputRenderableEvents, type RenderContext } from "@opentui/core";
13
+ import { Canvas } from "./canvas.ts";
14
+ import type { UiColors } from "./colors.ts";
15
+ import { clipText, segsWidth, splitLine, type Seg } from "./edge.ts";
16
+ import { filterItems, firstEnabled, listRows, step, windowRows, type ListItem, type ListRow } from "./list-model.ts";
17
+ import type { Line } from "./status.ts";
18
+
19
+ export interface ListViewOptions {
20
+ readonly colors: UiColors;
21
+ readonly rows?: number;
22
+ readonly search?: boolean;
23
+ readonly multi?: boolean;
24
+ readonly placeholder?: string;
25
+ readonly empty?: string;
26
+ }
27
+
28
+ /** Visible rows. Ten keeps a picker under half of a 30-row terminal. Assumed. */
29
+ const DEFAULT_ROWS = 10;
30
+ let serial = 0;
31
+
32
+ export class ListView extends BoxRenderable {
33
+ private colors: UiColors;
34
+ private maxRows: number;
35
+ private readonly multi: boolean;
36
+ private readonly emptyText: string;
37
+ readonly searchInput: InputRenderable | undefined;
38
+ private readonly searchRow: BoxRenderable | undefined;
39
+ private readonly searchGlyph: Canvas | undefined;
40
+ private readonly body: Canvas;
41
+ private items: ListItem[] = [];
42
+ private filtered: ListItem[] = [];
43
+ private rowsModel: ListRow[] = [];
44
+ private selected = -1;
45
+ readonly checked = new Set<string>();
46
+ onChange: (() => void) | undefined;
47
+
48
+ constructor(ctx: RenderContext, options: ListViewOptions) {
49
+ const id = `list-${serial++}`;
50
+ super(ctx, { id, flexDirection: "column", flexShrink: 0 });
51
+ this.colors = options.colors;
52
+ this.maxRows = options.rows ?? DEFAULT_ROWS;
53
+ this.multi = options.multi ?? false;
54
+ this.emptyText = options.empty ?? "No matches";
55
+ this.body = new Canvas(ctx, { id: `${id}-rows`, height: 1, flexShrink: 0, colors: this.colors });
56
+ this.add(this.body);
57
+ if (options.search !== false) {
58
+ this.searchRow = new BoxRenderable(ctx, { id: `${id}-search`, flexDirection: "row", height: 1, marginTop: 1, flexShrink: 0 });
59
+ this.searchGlyph = new Canvas(ctx, { id: `${id}-glyph`, width: 2, height: 1, flexShrink: 0, colors: this.colors });
60
+ this.searchGlyph.setRows([[{ text: "›", role: "accent", bold: true }]]);
61
+ this.searchInput = new InputRenderable(ctx, { id: `${id}-input`, flexGrow: 1, placeholder: options.placeholder ?? "Search",
62
+ textColor: this.colors.text, focusedTextColor: this.colors.text, placeholderColor: this.colors.muted, backgroundColor: "transparent", focusedBackgroundColor: "transparent" });
63
+ this.searchInput.on(InputRenderableEvents.INPUT, () => this.refilter());
64
+ this.searchRow.add(this.searchGlyph); this.searchRow.add(this.searchInput);
65
+ this.add(this.searchRow);
66
+ }
67
+ this.onSizeChange = () => this.paint();
68
+ }
69
+
70
+ get query(): string { return this.searchInput?.value ?? ""; }
71
+ get current(): ListItem | undefined { return this.selected >= 0 ? this.filtered[this.selected] : undefined; }
72
+ get visibleItems(): readonly ListItem[] { return this.filtered; }
73
+ /** "3/40" when the list scrolls, for the box's top edge; "" when everything is visible. */
74
+ get scrollPosition(): string { return this.rowsModel.length > this.maxRows && this.selected >= 0 ? `${this.selected + 1}/${this.filtered.length}` : ""; }
75
+ /** Plain text of the drawn rows (tests). */
76
+ get text(): string { return this.body.text; }
77
+
78
+ setColors(colors: UiColors): void {
79
+ this.colors = colors;
80
+ this.body.setColors(colors);
81
+ this.searchGlyph?.setColors(colors);
82
+ if (this.searchInput) {
83
+ this.searchInput.textColor = colors.text;
84
+ this.searchInput.focusedTextColor = colors.text;
85
+ this.searchInput.placeholderColor = colors.muted;
86
+ }
87
+ }
88
+
89
+ setRowLimit(rows: number): void {
90
+ const limit = Math.max(1, rows);
91
+ if (limit === this.maxRows) return;
92
+ this.maxRows = limit; this.paint();
93
+ }
94
+
95
+ setItems(items: readonly ListItem[], selectedValue?: string): void {
96
+ const keep = selectedValue ?? this.current?.value;
97
+ this.items = [...items];
98
+ this.refilter(keep);
99
+ }
100
+
101
+ setQuery(query: string): void {
102
+ if (this.searchInput) this.searchInput.value = query;
103
+ this.refilter();
104
+ }
105
+
106
+ move(delta: number): void {
107
+ const next = step(this.filtered, this.selected, delta);
108
+ if (next >= 0) this.selected = next;
109
+ this.paint();
110
+ }
111
+
112
+ page(delta: number): void {
113
+ const target = Math.max(0, Math.min(this.filtered.length - 1, this.selected + delta * this.maxRows));
114
+ this.selected = firstEnabled(this.filtered, target);
115
+ this.paint();
116
+ }
117
+
118
+ toggle(): void {
119
+ const item = this.current;
120
+ if (!this.multi || !item || item.disabled) return;
121
+ if (this.checked.has(item.value)) this.checked.delete(item.value); else this.checked.add(item.value);
122
+ this.paint();
123
+ }
124
+
125
+ toggleAllVisible(): void {
126
+ if (!this.multi) return;
127
+ const enabled = this.filtered.filter(item => !item.disabled);
128
+ const all = enabled.every(item => this.checked.has(item.value));
129
+ for (const item of enabled) { if (all) this.checked.delete(item.value); else this.checked.add(item.value); }
130
+ this.paint();
131
+ }
132
+
133
+ private refilter(keep?: string): void {
134
+ this.filtered = filterItems(this.items, this.query);
135
+ this.rowsModel = listRows(this.items, this.query);
136
+ const at = keep === undefined ? -1 : this.filtered.findIndex(item => item.value === keep);
137
+ this.selected = at >= 0 && !this.filtered[at]!.disabled ? at : firstEnabled(this.filtered);
138
+ this.paint();
139
+ }
140
+
141
+ private paint(): void {
142
+ const width = Math.max(10, this.width || 60);
143
+ const lines: Line[] = [];
144
+ if (this.rowsModel.length === 0) lines.push([{ text: this.emptyText, role: "muted" }]);
145
+ else {
146
+ const { start, end } = windowRows(this.rowsModel, this.selected, this.maxRows);
147
+ for (let r = start; r < end; r++) lines.push(this.line(this.rowsModel[r]!, width));
148
+ }
149
+ this.body.height = lines.length;
150
+ this.body.setRows(lines);
151
+ this.onChange?.();
152
+ }
153
+
154
+ private line(row: ListRow, width: number): Line {
155
+ if (row.kind === "header") return [{ text: row.text.toUpperCase(), role: "muted", bold: true }];
156
+ const item = row.item, selected = row.index === this.selected;
157
+ const lead: Seg[] = [
158
+ { text: selected ? "▸ " : " ", role: "accent", bold: true },
159
+ ...(this.multi ? [{ text: this.checked.has(item.value) ? "◉ " : "○ ", role: this.checked.has(item.value) ? "success" as const : "muted" as const }] : []),
160
+ ...(item.depth ? [{ text: " ".repeat(item.depth), role: "text" as const }] : []),
161
+ ];
162
+ const label: Seg = { text: item.label, role: item.disabled ? "muted" : "text", bold: selected };
163
+ // Keep the actionable identity before optional detail on narrow boxes.
164
+ const detail: Seg[] = item.description && width >= 60 ? [{ text: ` ${item.description}`, role: "muted" }] : [];
165
+ const hint: Seg[] = item.hint ? [{ text: item.hint, role: "muted" }] : [];
166
+ if (!hint.length) return [...lead, label, ...detail];
167
+ const room = Math.max(4, width - segsWidth(hint) - 1);
168
+ const left = [...lead, label, ...detail];
169
+ const fitted = segsWidth(left) <= room ? left : [...lead, { ...label, text: clipText(label.text, Math.max(1, room - segsWidth(lead))) }];
170
+ return splitLine(width, [{ segs: fitted, priority: 0 }], [{ segs: hint, priority: 1 }], 1);
171
+ }
172
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * The empty-session greeting (docs/tasks/refactor-tui-dock-layout.md). Pure.
3
+ *
4
+ * ▝▖ ▗▘ xeno 0.2.1
5
+ * ▐█▙█▟█▌ DeepSeek V4 Pro · high
6
+ * ▀▘ ▝▀ ~/Projects/workspace/xeno-tui ⎇ main
7
+ *
8
+ * An original 7×3 glyph (xeno: the stranger) drawn with ordinary quadrant
9
+ * blocks, so no Nerd Font is needed. Its entrance plays once: antennae rise,
10
+ * wiggle, one blink, then rest. Every frame keeps the same cells, so the
11
+ * terminal never reflows and a selection is never disturbed. Claude Code's
12
+ * fullscreen welcome plays a 0.4–1.3 s entrance the same way; Codex and
13
+ * OpenCode both dropped animations that kept running.
14
+ */
15
+ import type { Line } from "./status.ts";
16
+
17
+ export const MASCOT_WIDTH = 7;
18
+
19
+ const ANTENNAE_UP = "▝▖ ▗▘";
20
+ const ANTENNAE_IN = "▗▘ ▝▖";
21
+ const ANTENNAE_DOWN = " ▖ ▗ ";
22
+ const EYES_OPEN = "▐█▙█▟█▌";
23
+ const EYES_SHUT = "▐█████▌";
24
+ const FEET = " ▀▘ ▝▀ ";
25
+
26
+ /** The resting pose: what a still terminal and every frame after the entrance show. */
27
+ export const MASCOT: readonly string[] = [ANTENNAE_UP, EYES_OPEN, FEET];
28
+
29
+ /** 60 ms per frame: about 16 fps, smooth for blocky glyphs without a frame per paint. Assumed. */
30
+ export const MASCOT_FRAME_MS = 60;
31
+
32
+ /** Asleep, antennae rise, eyes open, wiggle, wiggle, blink, rest. 14 frames = 840 ms, under the 1 s the card allows. */
33
+ const ENTRANCE: readonly (readonly [string, string])[] = [
34
+ [ANTENNAE_DOWN, EYES_SHUT], [ANTENNAE_DOWN, EYES_SHUT], [ANTENNAE_UP, EYES_SHUT], [ANTENNAE_UP, EYES_OPEN],
35
+ [ANTENNAE_IN, EYES_OPEN], [ANTENNAE_UP, EYES_OPEN], [ANTENNAE_IN, EYES_OPEN], [ANTENNAE_UP, EYES_OPEN],
36
+ [ANTENNAE_UP, EYES_OPEN], [ANTENNAE_UP, EYES_SHUT], [ANTENNAE_UP, EYES_OPEN], [ANTENNAE_UP, EYES_OPEN],
37
+ [ANTENNAE_UP, EYES_OPEN], [ANTENNAE_UP, EYES_OPEN],
38
+ ];
39
+ export const MASCOT_FRAMES = ENTRANCE.length;
40
+
41
+ /** Frame `n` of the entrance; anything past the end is the resting pose. */
42
+ export function mascotFrame(n: number): readonly string[] {
43
+ const pose = ENTRANCE[Math.max(0, Math.floor(n))];
44
+ return pose ? [pose[0], pose[1], FEET] : MASCOT;
45
+ }
46
+
47
+ export interface Greeting {
48
+ readonly version: string;
49
+ /** Model display name and thinking level, or null when no provider can authenticate. */
50
+ readonly model: string | null;
51
+ readonly thinking: string;
52
+ readonly workspace: string;
53
+ readonly branch: string | null;
54
+ }
55
+
56
+ /** Three rows: mascot frame, a two-cell gap, then version / model / location. */
57
+ export function greetingLines(greeting: Greeting, frame: readonly string[] = MASCOT): Line[] {
58
+ const art = (row: number) => ({ text: frame[row] ?? "", role: "accent" as const });
59
+ const gap = { text: " ", role: "text" as const };
60
+ return [
61
+ [art(0), gap, { text: "xeno", role: "text", bold: true }, { text: ` ${greeting.version}`, role: "muted" }],
62
+ [art(1), gap, ...(greeting.model
63
+ ? [{ text: greeting.model, role: "text" as const }, { text: ` · ${greeting.thinking}`, role: "muted" as const }]
64
+ : [{ text: "No provider connected yet · /provider", role: "warning" as const }])],
65
+ [art(2), gap, { text: greeting.workspace, role: "muted" }, ...(greeting.branch ? [{ text: ` ⎇ ${greeting.branch}`, role: "muted" as const }] : [])],
66
+ ];
67
+ }