@visual-z/xeno 0.2.1 → 0.3.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 (258) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +106 -26
  3. package/docs/install.md +51 -13
  4. package/docs/providers.md +11 -0
  5. package/package.json +5 -4
  6. package/src/apps/approve.ts +3 -3
  7. package/src/apps/commands.ts +38 -5
  8. package/src/apps/completions.ts +73 -19
  9. package/src/apps/image-draft.ts +46 -0
  10. package/src/apps/project-trust.ts +2 -42
  11. package/src/apps/provider-connect.ts +346 -0
  12. package/src/apps/render.ts +22 -47
  13. package/src/apps/resources.ts +5 -2
  14. package/src/apps/shell.ts +298 -1526
  15. package/src/apps/startup-input.ts +45 -9
  16. package/src/apps/startup.ts +128 -86
  17. package/src/apps/ui/app.ts +1383 -0
  18. package/src/apps/ui/extension-host.ts +194 -0
  19. package/src/apps/ui/image-cache.ts +78 -0
  20. package/src/apps/ui/palette.ts +88 -0
  21. package/src/apps/ui/project.ts +127 -0
  22. package/src/apps/ui/standalone.ts +146 -0
  23. package/src/apps/ui/status-model.ts +68 -0
  24. package/src/apps/ui/surface.ts +84 -0
  25. package/src/apps/ui/theme.ts +86 -0
  26. package/src/core/acceptance.ts +107 -0
  27. package/src/core/action-defaults.ts +47 -295
  28. package/src/core/benchmark.ts +2 -2
  29. package/src/core/branch-receipt.ts +24 -0
  30. package/src/core/checkpoints.ts +64 -0
  31. package/src/core/churn.ts +2 -0
  32. package/src/core/command-artifacts.ts +94 -0
  33. package/src/core/command-contract.ts +35 -0
  34. package/src/core/completion-review.ts +21 -5
  35. package/src/core/context-detail.ts +37 -0
  36. package/src/core/context-policy.ts +74 -1
  37. package/src/core/continuation.ts +17 -2
  38. package/src/core/derive.ts +29 -3
  39. package/src/core/draft-images.ts +56 -0
  40. package/src/core/environment.ts +145 -0
  41. package/src/core/evaluation-coverage.ts +74 -0
  42. package/src/core/event-validation.ts +80 -5
  43. package/src/core/events.ts +97 -3
  44. package/src/core/evidence-links.ts +7 -0
  45. package/src/core/evidence.ts +13 -3
  46. package/src/core/execution.ts +8 -2
  47. package/src/core/experiment.ts +133 -0
  48. package/src/core/export.ts +9 -0
  49. package/src/core/failure-recovery.ts +173 -0
  50. package/src/core/file-precondition.ts +72 -0
  51. package/src/core/handoff.ts +65 -4
  52. package/src/core/history-query.ts +38 -2
  53. package/src/core/history.ts +38 -1
  54. package/src/core/integration-health.ts +34 -0
  55. package/src/core/keymap.ts +8 -20
  56. package/src/core/model-picker.ts +64 -0
  57. package/src/core/native-eval.ts +25 -0
  58. package/src/core/note-guidance.ts +34 -0
  59. package/src/core/observation-view.ts +49 -8
  60. package/src/core/permissions.ts +24 -2
  61. package/src/core/process-receipt.ts +105 -0
  62. package/src/core/process-state.ts +37 -0
  63. package/src/core/project.ts +45 -18
  64. package/src/core/prompt-policy.ts +37 -0
  65. package/src/core/query-eval-contract.ts +14 -0
  66. package/src/core/read-observation.ts +82 -0
  67. package/src/core/recovery-corpus.ts +17 -0
  68. package/src/core/recovery-evaluation.ts +60 -0
  69. package/src/core/recovery-sources.ts +74 -0
  70. package/src/core/runtime-target.ts +94 -0
  71. package/src/core/search-observation.ts +97 -0
  72. package/src/core/session-tree.ts +147 -0
  73. package/src/core/settings.ts +26 -10
  74. package/src/core/skills.ts +44 -0
  75. package/src/core/sql-query.ts +211 -0
  76. package/src/core/theme.ts +36 -58
  77. package/src/core/tool-contract.ts +51 -2
  78. package/src/core/tool-discovery.ts +87 -0
  79. package/src/core/tool-failure.ts +60 -0
  80. package/src/core/tool-mode-protocol.ts +25 -0
  81. package/src/core/transcript.ts +19 -4
  82. package/src/core/trial-lifecycle.ts +14 -0
  83. package/src/core/tui-mode.ts +29 -0
  84. package/src/core/update-check.ts +97 -0
  85. package/src/core/version.ts +55 -0
  86. package/src/core/work-state.ts +49 -3
  87. package/src/core/working-set.ts +91 -0
  88. package/src/runtime/auth.ts +2 -2
  89. package/src/runtime/branch-jobs.ts +36 -0
  90. package/src/runtime/clipboard.ts +15 -4
  91. package/src/runtime/command-artifacts.ts +64 -0
  92. package/src/runtime/environment-tools.ts +60 -0
  93. package/src/runtime/evidence-notes.ts +4 -2
  94. package/src/runtime/exec-sessions.ts +206 -61
  95. package/src/runtime/execution-control.ts +1 -0
  96. package/src/runtime/extension-ui.ts +22 -30
  97. package/src/runtime/extensions.ts +95 -16
  98. package/src/runtime/harness-comparison.ts +27 -7
  99. package/src/runtime/harness-eval.ts +460 -0
  100. package/src/runtime/history-cache.ts +57 -0
  101. package/src/runtime/history-tools.ts +62 -12
  102. package/src/runtime/integrations.ts +210 -0
  103. package/src/runtime/loop.ts +1021 -268
  104. package/src/runtime/model-discovery.ts +208 -0
  105. package/src/runtime/native-eval-fixtures.ts +33 -0
  106. package/src/runtime/native-eval.ts +143 -0
  107. package/src/runtime/oauth.ts +1 -1
  108. package/src/runtime/provider-connect.ts +109 -0
  109. package/src/runtime/provider-presets.ts +87 -0
  110. package/src/runtime/query-eval-verifier.ts +28 -0
  111. package/src/runtime/recovery-coverage.ts +71 -0
  112. package/src/runtime/recovery-evidence.ts +76 -0
  113. package/src/runtime/recovery-fixtures.ts +116 -0
  114. package/src/runtime/runtime-target.ts +149 -0
  115. package/src/runtime/tool-broker.ts +137 -0
  116. package/src/runtime/tool-discovery.ts +57 -0
  117. package/src/runtime/tool-mode-eval.ts +146 -0
  118. package/src/runtime/tool-queue.ts +77 -0
  119. package/src/runtime/tools.ts +628 -183
  120. package/src/runtime/trial-control.ts +45 -0
  121. package/src/runtime/update-check.ts +47 -0
  122. package/src/runtime/window-trial.ts +2 -1
  123. package/src/runtime/working-notes.ts +33 -3
  124. package/src/runtime/workspace-lifecycle.ts +97 -0
  125. package/src/store/acceptance.ts +15 -0
  126. package/src/store/checkpoint-history.ts +43 -0
  127. package/src/store/command-artifacts.ts +96 -0
  128. package/src/store/command.ts +65 -37
  129. package/src/store/file-checkpoints.ts +104 -0
  130. package/src/store/file-index.ts +13 -1
  131. package/src/store/git-checkpoints.ts +67 -0
  132. package/src/store/observations.ts +61 -20
  133. package/src/store/process-receipts.ts +34 -0
  134. package/src/store/query-source.ts +13 -8
  135. package/src/store/recovery.ts +2 -3
  136. package/src/store/replay.ts +16 -4
  137. package/src/store/runtime-target.ts +64 -0
  138. package/src/store/search.ts +171 -132
  139. package/src/store/session-archive.ts +4 -0
  140. package/src/store/source-cancellation.ts +6 -0
  141. package/src/store/update-cache.ts +36 -0
  142. package/src/store/workspace-observation.ts +113 -0
  143. package/src/tui/ansi-spans.ts +126 -0
  144. package/src/tui/style.ts +19 -16
  145. package/src/tui/ui/atomic-draft.ts +121 -0
  146. package/src/tui/ui/canvas.ts +96 -0
  147. package/src/tui/ui/colors.ts +70 -0
  148. package/src/tui/ui/composer.ts +199 -0
  149. package/src/tui/ui/dialog.ts +379 -0
  150. package/src/tui/ui/draft.ts +23 -0
  151. package/src/tui/ui/edge.ts +186 -0
  152. package/src/tui/ui/image-size.ts +40 -0
  153. package/src/tui/ui/ink.ts +210 -0
  154. package/src/tui/ui/items.ts +40 -0
  155. package/src/tui/ui/keys.ts +40 -0
  156. package/src/tui/ui/layout.ts +74 -0
  157. package/src/tui/ui/list-model.ts +86 -0
  158. package/src/tui/ui/list-view.ts +172 -0
  159. package/src/tui/ui/mascot.ts +67 -0
  160. package/src/tui/ui/messages.ts +518 -0
  161. package/src/tui/ui/motion.ts +28 -0
  162. package/src/tui/ui/paths.ts +10 -0
  163. package/src/tui/ui/pictures.ts +78 -0
  164. package/src/tui/ui/screen.ts +421 -0
  165. package/src/tui/ui/status.ts +271 -0
  166. package/src/tui/ui/styled.ts +85 -0
  167. package/src/tui/ui/testing.ts +5 -0
  168. package/src/tui/ui/tool-card.ts +150 -0
  169. package/src/apps/extension-ui.ts +0 -330
  170. package/src/apps/resource-browser.ts +0 -75
  171. package/src/apps/session-browser.ts +0 -113
  172. package/src/apps/session-picker.ts +0 -55
  173. package/src/tui/ansi.ts +0 -67
  174. package/src/tui/autocomplete.ts +0 -29
  175. package/src/tui/chat-viewport.ts +0 -47
  176. package/src/tui/complete.ts +0 -87
  177. package/src/tui/editor.ts +0 -940
  178. package/src/tui/host.ts +0 -264
  179. package/src/tui/input.ts +0 -75
  180. package/src/tui/keys.ts +0 -378
  181. package/src/tui/markdown.ts +0 -17
  182. package/src/tui/render.ts +0 -153
  183. package/src/tui/select.ts +0 -258
  184. package/src/tui/settings.ts +0 -44
  185. package/src/tui/statusline.ts +0 -162
  186. package/src/tui/system-theme.ts +0 -700
  187. package/src/tui/terminal.ts +0 -30
  188. package/src/tui/text.ts +0 -52
  189. package/src/tui/toolview.ts +0 -386
  190. package/src/tui/transcript.ts +0 -123
  191. package/src/tui/tree.ts +0 -471
  192. package/src/vendor/pi-tui/LICENSE.upstream +0 -21
  193. package/src/vendor/pi-tui/UPSTREAM.json +0 -50
  194. package/src/vendor/pi-tui/VENDOR.md +0 -25
  195. package/src/vendor/pi-tui/alt-screen-search.ts +0 -327
  196. package/src/vendor/pi-tui/autocomplete.ts +0 -860
  197. package/src/vendor/pi-tui/colors.ts +0 -367
  198. package/src/vendor/pi-tui/components/alt-screen-flash.ts +0 -51
  199. package/src/vendor/pi-tui/components/box.ts +0 -166
  200. package/src/vendor/pi-tui/components/cancellable-loader.ts +0 -40
  201. package/src/vendor/pi-tui/components/editor.ts +0 -2472
  202. package/src/vendor/pi-tui/components/h-stack.ts +0 -44
  203. package/src/vendor/pi-tui/components/image.ts +0 -127
  204. package/src/vendor/pi-tui/components/input.ts +0 -494
  205. package/src/vendor/pi-tui/components/loader.ts +0 -101
  206. package/src/vendor/pi-tui/components/markdown.ts +0 -1021
  207. package/src/vendor/pi-tui/components/mouse-region.ts +0 -33
  208. package/src/vendor/pi-tui/components/scroll-view.ts +0 -224
  209. package/src/vendor/pi-tui/components/select-list.ts +0 -273
  210. package/src/vendor/pi-tui/components/settings-list.ts +0 -328
  211. package/src/vendor/pi-tui/components/spacer.ts +0 -28
  212. package/src/vendor/pi-tui/components/stack.ts +0 -154
  213. package/src/vendor/pi-tui/components/text.ts +0 -107
  214. package/src/vendor/pi-tui/components/truncated-text.ts +0 -65
  215. package/src/vendor/pi-tui/components/v-stack.ts +0 -33
  216. package/src/vendor/pi-tui/editor-component.ts +0 -74
  217. package/src/vendor/pi-tui/fuzzy.ts +0 -138
  218. package/src/vendor/pi-tui/index.ts +0 -186
  219. package/src/vendor/pi-tui/keybindings.ts +0 -320
  220. package/src/vendor/pi-tui/keys.ts +0 -1401
  221. package/src/vendor/pi-tui/kill-ring.ts +0 -46
  222. package/src/vendor/pi-tui/latex.ts +0 -1506
  223. package/src/vendor/pi-tui/layout-node.ts +0 -51
  224. package/src/vendor/pi-tui/layout.ts +0 -449
  225. package/src/vendor/pi-tui/native/UPSTREAM.json +0 -23
  226. package/src/vendor/pi-tui/native/clipboard.h +0 -208
  227. package/src/vendor/pi-tui/native/darwin/README.md +0 -22
  228. package/src/vendor/pi-tui/native/darwin/build.sh +0 -69
  229. package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-arm64/darwin-platform.node +0 -0
  230. package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-x64/darwin-platform.node +0 -0
  231. package/src/vendor/pi-tui/native/darwin/src/darwin-platform.m +0 -132
  232. package/src/vendor/pi-tui/native/linux/README.md +0 -23
  233. package/src/vendor/pi-tui/native/linux/build.sh +0 -65
  234. package/src/vendor/pi-tui/native/linux/prebuilds/linux-arm64/linux-platform-x11.node +0 -0
  235. package/src/vendor/pi-tui/native/linux/prebuilds/linux-x64/linux-platform-x11.node +0 -0
  236. package/src/vendor/pi-tui/native/linux/src/clipboard-worker.h +0 -70
  237. package/src/vendor/pi-tui/native/linux/src/linux-platform-x11.c +0 -274
  238. package/src/vendor/pi-tui/native/napi.h +0 -91
  239. package/src/vendor/pi-tui/native/win32/README.md +0 -31
  240. package/src/vendor/pi-tui/native/win32/build.mjs +0 -232
  241. package/src/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-platform.node +0 -0
  242. package/src/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-platform.node +0 -0
  243. package/src/vendor/pi-tui/native/win32/src/win32-platform.c +0 -241
  244. package/src/vendor/pi-tui/native-modifiers.ts +0 -13
  245. package/src/vendor/pi-tui/native-module-path.ts +0 -31
  246. package/src/vendor/pi-tui/native-platform.ts +0 -65
  247. package/src/vendor/pi-tui/oklab.ts +0 -233
  248. package/src/vendor/pi-tui/stdin-buffer.ts +0 -444
  249. package/src/vendor/pi-tui/terminal-colors.ts +0 -91
  250. package/src/vendor/pi-tui/terminal-image.ts +0 -731
  251. package/src/vendor/pi-tui/terminal.ts +0 -554
  252. package/src/vendor/pi-tui/tui-alt-screen.ts +0 -1750
  253. package/src/vendor/pi-tui/tui-main-screen.ts +0 -655
  254. package/src/vendor/pi-tui/tui.ts +0 -1493
  255. package/src/vendor/pi-tui/undo-stack.ts +0 -28
  256. package/src/vendor/pi-tui/utils.ts +0 -1385
  257. package/src/vendor/pi-tui/wheel-scroll.ts +0 -82
  258. 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 · /login", role: "warning" as const }])],
65
+ [art(2), gap, { text: greeting.workspace, role: "muted" }, ...(greeting.branch ? [{ text: ` ⎇ ${greeting.branch}`, role: "muted" as const }] : [])],
66
+ ];
67
+ }