@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,421 @@
1
+ /**
2
+ * The whole view, assembled (docs/tasks/refactor-tui-dock-layout.md).
3
+ *
4
+ * ┌ top bar (topbar tier) ───────────────────────────┐┌ sidebar ┐
5
+ * │ greeting (empty session, no draft) ││ (≥ 110 │
6
+ * │ transcript (bottom-sticky) ↓ 3 new ││ cols) │
7
+ * │ completion · queue · attachments · widgets ││ │
8
+ * │ ╭─ transient: activity / dialog title ───────╮ ││ │
9
+ * │ │ › draft, or the open dialog │ ││ │
10
+ * │ ╰─ model · thinking ──────────── ctx ~12% ───╯ ││ │
11
+ * └──────────────────────────────────────────────────┘└─────────┘
12
+ *
13
+ * The input box is the one fixed point: its bottom edge is always the last
14
+ * row and it only grows upward. Status has exactly one place per width tier
15
+ * (layout.ts). Chrome is drawn by Canvas renderables, so the activity clock
16
+ * repaints cells without touching layout.
17
+ *
18
+ * One screen owns the renderer's tree, the dialog host and the editor; the
19
+ * app controller feeds it data and handles keys.
20
+ */
21
+ import { BoxRenderable, type CliRenderer } from "@opentui/core";
22
+ import { Canvas } from "./canvas.ts";
23
+ import type { UiColors } from "./colors.ts";
24
+ import { Composer, draftMaxLines } from "./composer.ts";
25
+ import { DialogHost } from "./dialog.ts";
26
+ import { clipSegs, edgeLine, segsWidth, type Label, type Seg } from "./edge.ts";
27
+ import type { Bindings } from "./keys.ts";
28
+ import { screenLayout, type ScreenLayout } from "./layout.ts";
29
+ import { greetingLines, mascotFrame, MASCOT_FRAME_MS, MASCOT_FRAMES, type Greeting } from "./mascot.ts";
30
+ import { MessageList, type MessageFeatures } from "./messages.ts";
31
+ import { Motion } from "./motion.ts";
32
+ import { contextLabel, sidebarLines, topbarRow, type Line, type StatusModel } from "./status.ts";
33
+ import { DEFAULT_CELL, stripImageRows } from "./image-size.ts";
34
+ import { Pictures, type PictureSource } from "./pictures.ts";
35
+ import type { ImageView, ViewItem } from "./items.ts";
36
+
37
+ export interface DockState {
38
+ /** Model display name. */
39
+ readonly model: string;
40
+ readonly thinking: string;
41
+ readonly busy: boolean;
42
+ /** What the running turn is doing: "thinking", "streaming", "running command"… */
43
+ readonly activity: string;
44
+ }
45
+
46
+ type Tone = "info" | "success" | "warning" | "error";
47
+
48
+ /** Braille spinner, ten frames. */
49
+ const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] as const;
50
+ /** Spinner cadence: Codex and the research recommendation both use about 100 ms. */
51
+ const ACTIVITY_MS = 100;
52
+ /**
53
+ * After this long without a stream event the spinner slows to IDLE_ACTIVITY_MS:
54
+ * a turn waiting on a slow tool still shows it is alive, at a fifth of the
55
+ * wakeups (OpenCode #42306 measured a full core spent on a fast spinner).
56
+ */
57
+ const QUIET_AFTER_MS = 2000;
58
+ const IDLE_ACTIVITY_MS = 500;
59
+ /** How long a transient message stays on the box edge. Assumed. */
60
+ const TOAST_MS = 2500;
61
+ /** Rows the transcript keeps even when the box grows (a picker never hides everything above it). Assumed. */
62
+ const MIN_TRANSCRIPT_ROWS = 3;
63
+ /** Completion rows above the box at most. Assumed. */
64
+ const COMPLETION_ROWS = 6;
65
+ /** Below this height the greeting would crowd the transcript; it stays hidden. Assumed. */
66
+ const GREETING_MIN_ROWS = 14;
67
+
68
+ export class Screen {
69
+ readonly renderer: CliRenderer;
70
+ readonly messages: MessageList;
71
+ readonly composer: Composer;
72
+ readonly dialogs: DialogHost;
73
+ private colors: UiColors;
74
+ private readonly topbar: Canvas;
75
+ private readonly greeting: Canvas;
76
+ private readonly pill: Canvas;
77
+ private readonly above: BoxRenderable;
78
+ private readonly completion: Canvas;
79
+ private readonly queueRows: Canvas;
80
+ private readonly widgetRows: Canvas;
81
+ private readonly dock: BoxRenderable;
82
+ private readonly body: BoxRenderable;
83
+ private readonly topEdge: Canvas;
84
+ private readonly bottomEdge: Canvas;
85
+ private readonly sidebar: BoxRenderable;
86
+ private readonly sidebarRows: Canvas;
87
+ private shape: ScreenLayout;
88
+ private sidebarWanted = true;
89
+ private status: StatusModel | undefined;
90
+ private dockState: DockState = { model: "", thinking: "", busy: false, activity: "" };
91
+ private home: Greeting | undefined;
92
+ private greetingPlayed = false;
93
+ private greetingFrame = MASCOT_FRAMES;
94
+ private readonly entrance = new Motion();
95
+ private readonly activity = new Motion();
96
+ private spin = 0;
97
+ private busySince = 0;
98
+ private lastEvent = 0;
99
+ private activityMs = ACTIVITY_MS;
100
+ private animations = true;
101
+ private focused = true;
102
+ private toastText: { text: string; tone: Tone } | undefined;
103
+ private toastTimer: ReturnType<typeof setTimeout> | undefined;
104
+ private queue: readonly string[] = [];
105
+ private attachmentImages: readonly ImageView[] = [];
106
+ private attachmentsKey = "";
107
+ private picturesSource: PictureSource = { mode: "chip", protocol: "blocks", cell: DEFAULT_CELL, widthCells: 60, load: async () => null };
108
+ private readonly attachments: BoxRenderable;
109
+ private widgets: readonly string[] = [];
110
+ private unread = 0;
111
+ private readonly glyph = () => this.activity.running ? SPINNER[this.spin % SPINNER.length]! : "●";
112
+
113
+ constructor(renderer: CliRenderer, colors: UiColors, bindings: () => Bindings, features: MessageFeatures = {}) {
114
+ this.renderer = renderer;
115
+ this.colors = colors;
116
+ renderer.setBackgroundColor(colors.background);
117
+ this.shape = screenLayout(renderer.width, renderer.height, true);
118
+ const root = new BoxRenderable(renderer, { id: "root", flexDirection: "column", width: "100%", height: "100%" });
119
+ this.topbar = new Canvas(renderer, { id: "topbar", height: 1, flexShrink: 0, marginLeft: 1, marginRight: 1, colors });
120
+ const middle = new BoxRenderable(renderer, { id: "middle", flexDirection: "row", flexGrow: 1, flexShrink: 1 });
121
+ const main = new BoxRenderable(renderer, { id: "main", flexDirection: "column", flexGrow: 1, flexShrink: 1 });
122
+ this.greeting = new Canvas(renderer, { id: "greeting", height: 3, flexShrink: 0, marginTop: 1, marginLeft: 3, marginBottom: 1, colors });
123
+ const area = new BoxRenderable(renderer, { id: "messages-area", flexDirection: "column", flexGrow: 1, flexShrink: 1 });
124
+ this.messages = new MessageList(renderer, colors, features, this.glyph);
125
+ area.add(this.messages.scroll);
126
+ this.pill = new Canvas(renderer, { id: "pill", position: "absolute", zIndex: 40, visible: false, height: 1, width: 1, bottom: 0, right: 3, colors });
127
+ area.add(this.pill);
128
+ this.above = new BoxRenderable(renderer, { id: "above-dock", flexDirection: "column", flexShrink: 0, marginLeft: 3, marginRight: 3 });
129
+ this.completion = new Canvas(renderer, { id: "completion", visible: false, height: 1, flexShrink: 0, colors });
130
+ this.queueRows = new Canvas(renderer, { id: "queue", visible: false, height: 1, flexShrink: 0, colors });
131
+ this.widgetRows = new Canvas(renderer, { id: "widgets", visible: false, height: 1, flexShrink: 0, colors });
132
+ this.attachments = new BoxRenderable(renderer, { id: "attachments", flexDirection: "column", flexShrink: 0, visible: false });
133
+ this.above.add(this.widgetRows); this.above.add(this.attachments); this.above.add(this.queueRows); this.above.add(this.completion);
134
+ this.dock = new BoxRenderable(renderer, { id: "dock", flexDirection: "column", flexShrink: 0, marginLeft: 1, marginRight: 1 });
135
+ this.topEdge = new Canvas(renderer, { id: "dock-top", height: 1, flexShrink: 0, colors, glyph: this.glyph });
136
+ this.body = new BoxRenderable(renderer, { id: "dock-body", flexDirection: "column", flexShrink: 0, border: ["left", "right"], borderStyle: "rounded", borderColor: colors.edge, paddingLeft: 1, paddingRight: 1 });
137
+ const slot = new BoxRenderable(renderer, { id: "dialog", flexDirection: "column", flexShrink: 0, visible: false });
138
+ this.composer = new Composer(renderer, colors);
139
+ this.body.add(slot); this.body.add(this.composer.row);
140
+ this.bottomEdge = new Canvas(renderer, { id: "dock-bottom", height: 1, flexShrink: 0, colors });
141
+ this.dock.add(this.topEdge); this.dock.add(this.body); this.dock.add(this.bottomEdge);
142
+ main.add(this.greeting); main.add(area); main.add(this.above); main.add(this.dock);
143
+ this.sidebar = new BoxRenderable(renderer, { id: "sidebar", flexShrink: 0, border: ["left"], borderColor: colors.line, paddingLeft: 1, paddingRight: 1, paddingTop: 1, flexDirection: "column" });
144
+ this.sidebarRows = new Canvas(renderer, { id: "sidebar-rows", flexGrow: 1, colors });
145
+ this.sidebar.add(this.sidebarRows);
146
+ middle.add(main); middle.add(this.sidebar);
147
+ root.add(this.topbar); root.add(middle);
148
+ renderer.root.add(root);
149
+ this.dialogs = new DialogHost(renderer, colors, bindings, slot, () => this.dialogRows(), this.glyph);
150
+ this.dialogs.onOpened = () => { this.composer.setCompletion(undefined); this.composer.area.blur(); this.composer.row.visible = false; this.paintAttachments(); this.layout(); };
151
+ this.dialogs.onClosed = () => { this.composer.row.visible = true; this.paintAttachments(); this.layout(); this.composer.focus(); };
152
+ this.dialogs.onChrome = () => { this.paintEdges(); this.syncActivity(); };
153
+ this.composer.onResize = () => this.paintAbove();
154
+ this.composer.onOverflow = () => this.paintEdges();
155
+ this.composer.onChange = () => this.paintGreeting();
156
+ this.messages.onUnread = count => { this.unread = count; this.paintPill(); };
157
+ renderer.on("resize", () => this.layout());
158
+ renderer.on("blur", () => { this.focused = false; this.finishEntrance(); });
159
+ renderer.on("focus", () => { this.focused = true; });
160
+ this.layout();
161
+ this.composer.focus();
162
+ }
163
+
164
+ get width(): number { return this.renderer.width; }
165
+ get tier(): ScreenLayout["tier"] { return this.shape.tier; }
166
+ get sidebarVisible(): boolean { return this.shape.tier === "sidebar"; }
167
+ /** An empty session (the greeting may still be hidden by a draft). */
168
+ get homeVisible(): boolean { return this.home !== undefined; }
169
+ get greetingVisible(): boolean { return this.greeting.visible; }
170
+ get animating(): boolean { return this.activity.running || this.entrance.running; }
171
+ /** Live timers owned by the view: none while idle (DoD 9). */
172
+ get timers(): number { return (this.activity.running ? 1 : 0) + (this.entrance.running ? 1 : 0) + (this.toastTimer ? 1 : 0); }
173
+ /** Plain text of the box edges and status surfaces (tests). */
174
+ get chrome(): { top: string; bottom: string; topbar: string; sidebar: string; greeting: string; completion: string } {
175
+ return { top: this.topEdge.text, bottom: this.bottomEdge.text, topbar: this.topbar.visible ? this.topbar.text : "", sidebar: this.sidebar.visible ? this.sidebarRows.text : "",
176
+ greeting: this.greeting.visible ? this.greeting.text : "", completion: this.completion.visible ? this.completion.text : "" };
177
+ }
178
+ /** The box's outer rows, for layout tests. */
179
+ get dockBox(): BoxRenderable { return this.dock; }
180
+
181
+ // -------------------------------------------------------------- appearance
182
+ setColors(colors: UiColors): void {
183
+ this.colors = colors;
184
+ this.renderer.setBackgroundColor(colors.background);
185
+ for (const canvas of [this.topbar, this.greeting, this.pill, this.completion, this.queueRows, this.widgetRows, this.topEdge, this.bottomEdge, this.sidebarRows]) canvas.setColors(colors);
186
+ this.sidebar.borderColor = colors.line;
187
+ this.messages.setColors(colors);
188
+ this.composer.setColors(colors);
189
+ this.dialogs.setColors(colors);
190
+ this.paintEdges();
191
+ }
192
+
193
+ setAnimations(enabled: boolean): void {
194
+ if (enabled === this.animations) return;
195
+ this.animations = enabled;
196
+ if (!enabled) this.finishEntrance();
197
+ this.syncActivity();
198
+ }
199
+
200
+ toggleSidebar(): void { this.sidebarWanted = !this.sidebarWanted; this.layout(); }
201
+
202
+ // ------------------------------------------------------------------ layout
203
+ private layout(): void {
204
+ const w = this.renderer.width, h = this.renderer.height;
205
+ this.shape = screenLayout(w, h, this.sidebarWanted);
206
+ this.topbar.visible = this.shape.topbar;
207
+ this.sidebar.visible = this.shape.tier === "sidebar";
208
+ if (this.sidebar.visible) this.sidebar.width = this.shape.sidebarWidth;
209
+ // The draft may grow while the transcript keeps its minimum and the box keeps its edges.
210
+ this.composer.setMaxLines(Math.min(draftMaxLines(h), h - (this.shape.topbar ? 1 : 0) - 2 - MIN_TRANSCRIPT_ROWS - this.aboveRows()));
211
+ this.dialogs.layout();
212
+ this.paintStatus();
213
+ this.paintEdges();
214
+ this.paintAbove();
215
+ this.paintGreeting();
216
+ this.renderer.requestRender();
217
+ }
218
+
219
+ /** Rows a dialog may use inside the box. */
220
+ private dialogRows(): number {
221
+ return Math.max(1, this.renderer.height - (this.shape.topbar ? 1 : 0) - 2 - MIN_TRANSCRIPT_ROWS);
222
+ }
223
+
224
+ private aboveRows(): number {
225
+ return (this.completion.visible ? this.completion.height as number : 0) + (this.queueRows.visible ? this.queueRows.height as number : 0) + (this.widgetRows.visible ? this.widgetRows.height as number : 0)
226
+ + (this.attachments.visible ? this.attachments.height : 0);
227
+ }
228
+
229
+ // ------------------------------------------------------------------ status
230
+ setStatus(model: StatusModel): void { this.status = model; this.paintStatus(); this.paintEdges(); }
231
+
232
+ private paintStatus(): void {
233
+ const model = this.status;
234
+ if (!model) return;
235
+ if (this.topbar.visible) this.topbar.setRows([topbarRow(model, Math.max(1, this.renderer.width - 2))]);
236
+ if (this.sidebar.visible) {
237
+ const width = Math.max(1, this.shape.sidebarWidth - 3);
238
+ this.sidebarRows.setRows(sidebarLines(model, width, Math.max(0, this.renderer.height - 1)));
239
+ }
240
+ }
241
+
242
+ // -------------------------------------------------------------------- dock
243
+ setDock(state: DockState): void {
244
+ const wasBusy = this.dockState.busy;
245
+ this.dockState = state;
246
+ if (state.busy && !wasBusy) this.busySince = Date.now();
247
+ this.syncActivity();
248
+ this.paintEdges();
249
+ }
250
+
251
+ toast(text: string, tone: Tone = "info"): void {
252
+ this.toastText = { text, tone };
253
+ clearTimeout(this.toastTimer);
254
+ this.toastTimer = setTimeout(() => { this.toastTimer = undefined; this.toastText = undefined; this.paintEdges(); }, TOAST_MS);
255
+ this.toastTimer.unref?.();
256
+ this.paintEdges();
257
+ }
258
+
259
+ private paintEdges(): void {
260
+ const width = Math.max(2, this.renderer.width - (this.shape.tier === "sidebar" ? this.shape.sidebarWidth : 0) - 2);
261
+ const d = this.dockState, dialog = this.dialogs.open;
262
+ const role = this.dialogs.tone === "warning" ? "warning" : d.busy && !dialog ? "accent" : "edge";
263
+ this.body.borderColor = role === "warning" ? this.colors.warning : role === "accent" ? this.colors.accent : this.colors.edge;
264
+ const right: Label[] = [];
265
+ if (this.toastText) {
266
+ const tone = this.toastText.tone;
267
+ right.push({ segs: [{ text: this.toastText.text, role: tone === "error" ? "error" : tone === "warning" ? "warning" : tone === "success" ? "success" : "text" }], priority: 1, min: 12 });
268
+ }
269
+ let left: Label[] = [];
270
+ if (dialog) {
271
+ left = [{ segs: [{ text: this.dialogs.title, role: "text", bold: true }], priority: 0, min: 8 }];
272
+ if (this.dialogs.edgeHint) right.push({ segs: [{ text: this.dialogs.edgeHint, role: "muted" }], priority: 3, min: 10 });
273
+ } else if (d.busy) {
274
+ const seconds = Math.max(0, Math.floor((Date.now() - this.busySince) / 1000));
275
+ left = [{ segs: [{ text: "●", role: "accent", spin: true }, { text: ` ${d.activity}`, role: "muted" }, { text: ` ${seconds}s`, role: "muted" }], priority: 0, min: 1, shrink: 1 }];
276
+ right.push({ segs: [{ text: "esc interrupt", role: "muted" }], priority: 4 });
277
+ } else {
278
+ // A draft taller than the box says what scrolled out of view (fix-tui-composer-growth DoD 4).
279
+ const { above, below } = this.composer.overflow;
280
+ if (above) left = [{ segs: [{ text: `↑ ${above} more`, role: "muted" }], priority: 1 }];
281
+ if (below && !this.toastText) right.push({ segs: [{ text: `↓ ${below} more`, role: "muted" }], priority: 2 });
282
+ }
283
+ this.topEdge.setRows([edgeLine({ width, corners: ["╭", "╮"], role, left, right })]);
284
+ const bottomLeft: Label[] = d.model ? [
285
+ { segs: [{ text: d.model, role: "text", bold: true }], priority: 0, min: 8 },
286
+ ...(d.thinking ? [{ segs: [{ text: d.thinking, role: "muted" as const }], priority: 2 }] : []),
287
+ ] : [];
288
+ const context = this.shape.tier !== "sidebar" && this.status ? contextLabel(this.status) : undefined;
289
+ this.bottomEdge.setRows([edgeLine({ width, corners: ["╰", "╯"], role, left: bottomLeft, right: context ? [context] : [] })]);
290
+ }
291
+
292
+ /** A stream event arrived (delta, tool progress, shell output): the spinner returns to full speed. */
293
+ markActivity(): void {
294
+ this.lastEvent = Date.now();
295
+ if (this.activity.running && this.activityMs !== ACTIVITY_MS) this.startClock(ACTIVITY_MS);
296
+ }
297
+
298
+ /** The clock runs only while something on screen shows the activity glyph. */
299
+ private syncActivity(): void {
300
+ const wanted = this.animations && (this.dockState.busy || this.dialogs.animated);
301
+ if (wanted && !this.activity.running) { this.lastEvent = Date.now(); this.startClock(ACTIVITY_MS); }
302
+ else if (!wanted && this.activity.running) { this.activity.stop(); this.paintEdges(); }
303
+ }
304
+
305
+ private startClock(intervalMs: number): void {
306
+ this.activityMs = intervalMs;
307
+ const base = this.spin;
308
+ this.activity.start(frame => {
309
+ this.spin = base + frame;
310
+ if (this.activityMs === ACTIVITY_MS && Date.now() - this.lastEvent > QUIET_AFTER_MS) { this.startClock(IDLE_ACTIVITY_MS); return; }
311
+ this.paintEdges();
312
+ this.renderer.requestRender();
313
+ }, undefined, intervalMs);
314
+ }
315
+
316
+ /** The spinner's current interval (tests). */
317
+ get activityInterval(): number { return this.activity.running ? this.activityMs : 0; }
318
+
319
+ // ------------------------------------------------------------ above the box
320
+ setQueue(lines: readonly string[]): void { this.queue = lines; this.paintAbove(); }
321
+
322
+ /** How pictures are drawn; the transcript and the attachment strip both follow it (feat-tui-inline-images). */
323
+ setPictures(source: PictureSource): void {
324
+ this.picturesSource = source;
325
+ this.messages.setPictures(source);
326
+ this.attachmentsKey = "";
327
+ this.paintAttachments();
328
+ }
329
+
330
+ /** The image next to the draft cursor; its identity and sendability remain in the editor. */
331
+ setAttachments(images: readonly ImageView[]): void { this.attachmentImages = images; this.paintAttachments(); }
332
+
333
+ private paintAttachments(): void {
334
+ const images = this.dialogs.open ? [] : this.attachmentImages;
335
+ const key = images.map(image => image.sha256).join(",") + `|${this.renderer.width}x${this.renderer.height}`;
336
+ if (key === this.attachmentsKey) return;
337
+ this.attachmentsKey = key;
338
+ for (const child of this.attachments.getChildren()) { this.attachments.remove(child); child.destroyRecursively(); }
339
+ this.attachments.visible = images.length > 0;
340
+ if (images.length) {
341
+ const source = this.picturesSource;
342
+ const width = Math.max(10, this.renderer.width - (this.shape.tier === "sidebar" ? this.shape.sidebarWidth : 0) - 6);
343
+ this.attachments.add(new Pictures(this.renderer, this.colors, images, source, { maxCols: width, maxRows: stripImageRows(this.renderer.height) }));
344
+ }
345
+ this.renderer.requestRender();
346
+ }
347
+ setWidgets(lines: readonly string[]): void { this.widgets = lines; this.paintAbove(); }
348
+
349
+ private paintAbove(): void {
350
+ const width = Math.max(1, this.renderer.width - (this.shape.tier === "sidebar" ? this.shape.sidebarWidth : 0) - 6);
351
+ const set = (canvas: Canvas, rows: readonly Line[]) => {
352
+ canvas.visible = rows.length > 0;
353
+ if (rows.length) { canvas.height = rows.length; canvas.setRows(rows.map(row => clipSegs(row, width))); }
354
+ };
355
+ set(this.widgetRows, this.widgets.map(text => [{ text, role: "muted" }]));
356
+ set(this.queueRows, this.queue.map(text => [{ text: "↳ ", role: "muted" }, { text, role: "text" }]));
357
+ const view = this.composer.completionView;
358
+ let rows: Line[] = [];
359
+ if (view && !this.dialogs.open) {
360
+ const limit = Math.max(1, Math.min(COMPLETION_ROWS, this.renderer.height - (this.shape.topbar ? 1 : 0) - 2 - (this.composer.rows || 1) - MIN_TRANSCRIPT_ROWS - this.widgets.length - this.queue.length));
361
+ const start = Math.max(0, Math.min(view.selected - Math.floor(limit / 2), view.items.length - limit));
362
+ const shown = view.items.slice(start, start + limit);
363
+ const labelWidth = Math.min(24, Math.max(...shown.map(item => segsWidth([{ text: item.label, role: "text" }]))) + 2);
364
+ rows = shown.map((item, k) => {
365
+ const selected = start + k === view.selected;
366
+ const pad = Math.max(1, labelWidth - segsWidth([{ text: item.label, role: "text" }]));
367
+ return [{ text: selected ? "▸ " : " ", role: "accent", bold: true }, { text: item.label, role: "text", bold: selected },
368
+ ...(item.description ? [{ text: " ".repeat(pad), role: "text" as const }, { text: item.description, role: "muted" as const }] : [])] as Seg[];
369
+ });
370
+ }
371
+ set(this.completion, rows);
372
+ this.renderer.requestRender();
373
+ }
374
+
375
+ // ---------------------------------------------------------------- greeting
376
+ setGreeting(state: Greeting | undefined): void {
377
+ this.home = state;
378
+ this.paintGreeting();
379
+ }
380
+
381
+ private paintGreeting(): void {
382
+ const show = this.home !== undefined && !this.composer.text && !this.dialogs.open && this.renderer.height >= GREETING_MIN_ROWS;
383
+ this.greeting.visible = show;
384
+ if (!show) { if (this.entrance.running) this.finishEntrance(); return; }
385
+ // The entrance plays once per screen, when the greeting first shows on a focused terminal.
386
+ if (!this.greetingPlayed && this.animations && this.focused) {
387
+ this.greetingPlayed = true;
388
+ this.entrance.start(frame => { this.greetingFrame = frame; this.drawGreeting(); }, MASCOT_FRAMES - 1, MASCOT_FRAME_MS);
389
+ }
390
+ this.greetingPlayed = true;
391
+ this.drawGreeting();
392
+ }
393
+
394
+ private drawGreeting(): void {
395
+ if (!this.home) return;
396
+ const width = Math.max(1, this.renderer.width - (this.shape.tier === "sidebar" ? this.shape.sidebarWidth : 0) - 4);
397
+ this.greeting.setRows(greetingLines(this.home, mascotFrame(this.greetingFrame)).map(row => clipSegs(row, width)));
398
+ }
399
+
400
+ private finishEntrance(): void {
401
+ this.entrance.stop();
402
+ this.greetingFrame = MASCOT_FRAMES;
403
+ this.drawGreeting();
404
+ }
405
+
406
+ // ------------------------------------------------------------- transcript
407
+ setItems(items: readonly ViewItem[]): void { this.messages.update(items); }
408
+
409
+ private paintPill(): void {
410
+ this.pill.visible = this.unread > 0;
411
+ const text = ` ↓ ${this.unread} new `;
412
+ this.pill.width = segsWidth([{ text, role: "text" }]);
413
+ this.pill.setRows([[{ text, role: "accent", bold: true }]]);
414
+ }
415
+
416
+ dispose(): void {
417
+ clearTimeout(this.toastTimer); this.toastTimer = undefined;
418
+ this.activity.stop(); this.entrance.stop();
419
+ this.dialogs.dispose();
420
+ }
421
+ }