@visual-z/xeno 0.8.4 → 0.9.1

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 (350) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +31 -746
  3. package/bin/xeno.exe +26 -0
  4. package/install.cjs +88 -0
  5. package/package.json +18 -48
  6. package/docs/app-server.md +0 -115
  7. package/docs/foundations.md +0 -88
  8. package/docs/install.md +0 -244
  9. package/docs/providers.md +0 -172
  10. package/docs/research/foundation-clients-2026-10-09.md +0 -27
  11. package/docs/research/foundation-editing-2026-10-09.md +0 -56
  12. package/docs/research/foundation-network-2026-10-09.md +0 -37
  13. package/docs/research/foundation-workflow-2026-10-09.md +0 -37
  14. package/src/apps/acp-server.ts +0 -41
  15. package/src/apps/agent-profiles.ts +0 -19
  16. package/src/apps/app-server.ts +0 -30
  17. package/src/apps/approve.ts +0 -49
  18. package/src/apps/cli.ts +0 -99
  19. package/src/apps/commands.ts +0 -319
  20. package/src/apps/completions.ts +0 -87
  21. package/src/apps/control-transport.ts +0 -90
  22. package/src/apps/export-cli.ts +0 -24
  23. package/src/apps/goal-flow.ts +0 -70
  24. package/src/apps/image-draft.ts +0 -46
  25. package/src/apps/input.ts +0 -312
  26. package/src/apps/model-catalog.ts +0 -69
  27. package/src/apps/preferences.ts +0 -20
  28. package/src/apps/project-trust.ts +0 -9
  29. package/src/apps/provider-connect.ts +0 -346
  30. package/src/apps/question-ui.ts +0 -27
  31. package/src/apps/render.ts +0 -181
  32. package/src/apps/resources.ts +0 -66
  33. package/src/apps/review.ts +0 -109
  34. package/src/apps/session-hub.ts +0 -96
  35. package/src/apps/session-lifecycle.ts +0 -64
  36. package/src/apps/shell.ts +0 -2071
  37. package/src/apps/startup-input.ts +0 -135
  38. package/src/apps/startup.ts +0 -1677
  39. package/src/apps/ui/app.ts +0 -1818
  40. package/src/apps/ui/control-model.ts +0 -43
  41. package/src/apps/ui/extension-host.ts +0 -194
  42. package/src/apps/ui/image-cache.ts +0 -121
  43. package/src/apps/ui/palette.ts +0 -88
  44. package/src/apps/ui/project.ts +0 -127
  45. package/src/apps/ui/standalone.ts +0 -148
  46. package/src/apps/ui/status-model.ts +0 -68
  47. package/src/apps/ui/surface.ts +0 -91
  48. package/src/apps/ui/theme.ts +0 -86
  49. package/src/apps/update-check.ts +0 -112
  50. package/src/apps/workflow.ts +0 -58
  51. package/src/core/acceptance.ts +0 -139
  52. package/src/core/action-defaults.ts +0 -58
  53. package/src/core/activity-status.ts +0 -52
  54. package/src/core/agent-profile.ts +0 -130
  55. package/src/core/attachment-input.ts +0 -17
  56. package/src/core/blind-checks.ts +0 -72
  57. package/src/core/boundary.ts +0 -174
  58. package/src/core/branch-receipt.ts +0 -26
  59. package/src/core/catalog-merge.ts +0 -19
  60. package/src/core/checkpoints.ts +0 -85
  61. package/src/core/churn.ts +0 -61
  62. package/src/core/command-artifacts.ts +0 -94
  63. package/src/core/command-contract.ts +0 -151
  64. package/src/core/completion-action.ts +0 -20
  65. package/src/core/completion-review.ts +0 -98
  66. package/src/core/context-detail.ts +0 -37
  67. package/src/core/context-policy.ts +0 -133
  68. package/src/core/context-window.ts +0 -54
  69. package/src/core/continuation.ts +0 -130
  70. package/src/core/control-protocol.ts +0 -69
  71. package/src/core/control-view.ts +0 -39
  72. package/src/core/cycle.ts +0 -17
  73. package/src/core/data-query.ts +0 -201
  74. package/src/core/deadline.ts +0 -40
  75. package/src/core/derive.ts +0 -516
  76. package/src/core/draft-images.ts +0 -56
  77. package/src/core/edit-plan.ts +0 -200
  78. package/src/core/environment.ts +0 -181
  79. package/src/core/event-validation.ts +0 -220
  80. package/src/core/events.ts +0 -670
  81. package/src/core/evidence-ladder.ts +0 -62
  82. package/src/core/evidence-links.ts +0 -68
  83. package/src/core/evidence.ts +0 -240
  84. package/src/core/execution.ts +0 -18
  85. package/src/core/exhaustive.ts +0 -18
  86. package/src/core/export.ts +0 -179
  87. package/src/core/extension-flags.ts +0 -14
  88. package/src/core/external-effects.ts +0 -488
  89. package/src/core/failure-recovery.ts +0 -173
  90. package/src/core/file-precondition.ts +0 -78
  91. package/src/core/file-references.ts +0 -35
  92. package/src/core/frontmatter.ts +0 -213
  93. package/src/core/git-status.ts +0 -29
  94. package/src/core/goal-view.ts +0 -64
  95. package/src/core/goal.ts +0 -200
  96. package/src/core/handoff.ts +0 -479
  97. package/src/core/history-query.ts +0 -139
  98. package/src/core/history.ts +0 -237
  99. package/src/core/image-limits.ts +0 -72
  100. package/src/core/images.ts +0 -46
  101. package/src/core/integration-health.ts +0 -34
  102. package/src/core/json-event.ts +0 -131
  103. package/src/core/json-pointer.ts +0 -46
  104. package/src/core/keymap.ts +0 -153
  105. package/src/core/lsp-framing.ts +0 -34
  106. package/src/core/mcp-config.ts +0 -68
  107. package/src/core/model-picker.ts +0 -64
  108. package/src/core/model-preferences.ts +0 -28
  109. package/src/core/model-replay.ts +0 -22
  110. package/src/core/model-scope.ts +0 -53
  111. package/src/core/note-guidance.ts +0 -34
  112. package/src/core/oauth-fields.ts +0 -44
  113. package/src/core/observation-view.ts +0 -152
  114. package/src/core/outcome.ts +0 -44
  115. package/src/core/overflow.ts +0 -59
  116. package/src/core/packages.ts +0 -47
  117. package/src/core/patch.ts +0 -130
  118. package/src/core/path-suggestions.ts +0 -59
  119. package/src/core/permissions.ts +0 -410
  120. package/src/core/process-receipt.ts +0 -105
  121. package/src/core/process-state.ts +0 -37
  122. package/src/core/program-status.ts +0 -40
  123. package/src/core/progress.ts +0 -150
  124. package/src/core/project.ts +0 -431
  125. package/src/core/prompt-policy.ts +0 -46
  126. package/src/core/prompts.ts +0 -111
  127. package/src/core/question.ts +0 -94
  128. package/src/core/queue.ts +0 -78
  129. package/src/core/read-bounds.ts +0 -24
  130. package/src/core/read-observation.ts +0 -106
  131. package/src/core/recovery-sources.ts +0 -74
  132. package/src/core/regex-dialect.ts +0 -25
  133. package/src/core/request-budget.ts +0 -23
  134. package/src/core/request-progress.ts +0 -87
  135. package/src/core/request-state.ts +0 -273
  136. package/src/core/resource-selection.ts +0 -36
  137. package/src/core/response-recovery.ts +0 -59
  138. package/src/core/resume.ts +0 -59
  139. package/src/core/review.ts +0 -68
  140. package/src/core/ripgrep-release.ts +0 -64
  141. package/src/core/runtime-target.ts +0 -94
  142. package/src/core/scheduling.ts +0 -8
  143. package/src/core/search-observation.ts +0 -97
  144. package/src/core/secrets.ts +0 -188
  145. package/src/core/session-archive.ts +0 -4
  146. package/src/core/session-browser.ts +0 -82
  147. package/src/core/session-entry.ts +0 -61
  148. package/src/core/session-exit.ts +0 -42
  149. package/src/core/session-format.ts +0 -169
  150. package/src/core/session-position.ts +0 -26
  151. package/src/core/session-tree.ts +0 -147
  152. package/src/core/settings.ts +0 -192
  153. package/src/core/skills.ts +0 -165
  154. package/src/core/sql-query.ts +0 -211
  155. package/src/core/stall.ts +0 -128
  156. package/src/core/stop-report.ts +0 -68
  157. package/src/core/text-normalize.ts +0 -81
  158. package/src/core/theme.ts +0 -166
  159. package/src/core/thinking.ts +0 -26
  160. package/src/core/tokens.ts +0 -151
  161. package/src/core/tool-contract.ts +0 -153
  162. package/src/core/tool-discovery.ts +0 -87
  163. package/src/core/tool-failure.ts +0 -81
  164. package/src/core/tool-input.ts +0 -44
  165. package/src/core/transcript.ts +0 -108
  166. package/src/core/tui-mode.ts +0 -29
  167. package/src/core/update-check.ts +0 -157
  168. package/src/core/version.ts +0 -55
  169. package/src/core/web-content.ts +0 -80
  170. package/src/core/work-state.ts +0 -154
  171. package/src/core/workflow-mode.ts +0 -34
  172. package/src/core/working-set.ts +0 -119
  173. package/src/core/yaml.ts +0 -282
  174. package/src/runtime/acp-agent.ts +0 -289
  175. package/src/runtime/agent-profiles.ts +0 -21
  176. package/src/runtime/auth.ts +0 -367
  177. package/src/runtime/blind-check.ts +0 -36
  178. package/src/runtime/branch-jobs.ts +0 -36
  179. package/src/runtime/catalog.ts +0 -285
  180. package/src/runtime/clipboard.ts +0 -125
  181. package/src/runtime/command-artifacts.ts +0 -64
  182. package/src/runtime/connection-reuse.ts +0 -40
  183. package/src/runtime/control-client.ts +0 -149
  184. package/src/runtime/control-connection.ts +0 -139
  185. package/src/runtime/counter-check.ts +0 -74
  186. package/src/runtime/environment-tools.ts +0 -60
  187. package/src/runtime/evidence-notes.ts +0 -35
  188. package/src/runtime/exec-sessions.ts +0 -456
  189. package/src/runtime/execution-control.ts +0 -40
  190. package/src/runtime/extension-loader.ts +0 -37
  191. package/src/runtime/extension-ui.ts +0 -47
  192. package/src/runtime/extensions.ts +0 -370
  193. package/src/runtime/formatter-tool.ts +0 -38
  194. package/src/runtime/formatter.ts +0 -50
  195. package/src/runtime/foundation.ts +0 -116
  196. package/src/runtime/history-cache.ts +0 -57
  197. package/src/runtime/history-tools.ts +0 -168
  198. package/src/runtime/images.ts +0 -80
  199. package/src/runtime/integrations.ts +0 -216
  200. package/src/runtime/loop.ts +0 -3108
  201. package/src/runtime/lsp-client.ts +0 -201
  202. package/src/runtime/mcp-client.ts +0 -264
  203. package/src/runtime/mcp-oauth.ts +0 -168
  204. package/src/runtime/mcp-schema.ts +0 -51
  205. package/src/runtime/model-config.ts +0 -232
  206. package/src/runtime/model-data.LICENSE +0 -27
  207. package/src/runtime/model-data.ts +0 -21
  208. package/src/runtime/model-discovery.ts +0 -208
  209. package/src/runtime/model-replay.ts +0 -10
  210. package/src/runtime/model-scope.ts +0 -20
  211. package/src/runtime/model-sources.ts +0 -151
  212. package/src/runtime/model.ts +0 -686
  213. package/src/runtime/models-fetch.ts +0 -57
  214. package/src/runtime/oauth-device.ts +0 -86
  215. package/src/runtime/oauth.ts +0 -853
  216. package/src/runtime/open-image.ts +0 -42
  217. package/src/runtime/openai-responses.ts +0 -304
  218. package/src/runtime/openai.ts +0 -697
  219. package/src/runtime/output-schema.ts +0 -26
  220. package/src/runtime/package-manager.ts +0 -125
  221. package/src/runtime/patch-tool.ts +0 -38
  222. package/src/runtime/permission-resources.ts +0 -18
  223. package/src/runtime/pi-provider.ts +0 -423
  224. package/src/runtime/program-status.ts +0 -38
  225. package/src/runtime/progress.ts +0 -26
  226. package/src/runtime/provider-access.ts +0 -21
  227. package/src/runtime/provider-connect.ts +0 -109
  228. package/src/runtime/provider-presets.ts +0 -98
  229. package/src/runtime/provider-protocols.ts +0 -29
  230. package/src/runtime/provider-request.ts +0 -87
  231. package/src/runtime/provider-tool-policy.ts +0 -33
  232. package/src/runtime/provider.ts +0 -580
  233. package/src/runtime/question-tool.ts +0 -62
  234. package/src/runtime/retry.ts +0 -447
  235. package/src/runtime/review-tool.ts +0 -48
  236. package/src/runtime/rich-text.ts +0 -36
  237. package/src/runtime/runtime-target.ts +0 -149
  238. package/src/runtime/secrets.ts +0 -79
  239. package/src/runtime/self-update.ts +0 -74
  240. package/src/runtime/session-control.ts +0 -265
  241. package/src/runtime/session-hub.ts +0 -85
  242. package/src/runtime/stream-idle.ts +0 -80
  243. package/src/runtime/tool-broker.ts +0 -137
  244. package/src/runtime/tool-discovery.ts +0 -58
  245. package/src/runtime/tool-queue.ts +0 -77
  246. package/src/runtime/tools.ts +0 -1820
  247. package/src/runtime/turn-effects.ts +0 -31
  248. package/src/runtime/update-check.ts +0 -48
  249. package/src/runtime/usage.ts +0 -253
  250. package/src/runtime/version.ts +0 -12
  251. package/src/runtime/web-http.ts +0 -56
  252. package/src/runtime/web-tools.ts +0 -88
  253. package/src/runtime/working-notes.ts +0 -60
  254. package/src/runtime/workspace-lifecycle.ts +0 -141
  255. package/src/store/acceptance.ts +0 -15
  256. package/src/store/agent-profile-files.ts +0 -13
  257. package/src/store/atomic.ts +0 -31
  258. package/src/store/auth-store.ts +0 -50
  259. package/src/store/blind-checks.ts +0 -32
  260. package/src/store/blobs.ts +0 -143
  261. package/src/store/checkpoint-history.ts +0 -32
  262. package/src/store/command-artifacts.ts +0 -96
  263. package/src/store/command.ts +0 -111
  264. package/src/store/config.ts +0 -69
  265. package/src/store/context-files.ts +0 -436
  266. package/src/store/file-checkpoints.ts +0 -108
  267. package/src/store/file-index.ts +0 -174
  268. package/src/store/foundation-config.ts +0 -24
  269. package/src/store/git-checkpoints.ts +0 -67
  270. package/src/store/git-status.ts +0 -15
  271. package/src/store/history-search.ts +0 -19
  272. package/src/store/log.ts +0 -725
  273. package/src/store/lsp-document.ts +0 -30
  274. package/src/store/mcp-credentials.ts +0 -85
  275. package/src/store/models.ts +0 -60
  276. package/src/store/observations.ts +0 -224
  277. package/src/store/packages.ts +0 -98
  278. package/src/store/patch.ts +0 -92
  279. package/src/store/permission-paths.ts +0 -34
  280. package/src/store/process-receipts.ts +0 -34
  281. package/src/store/query-source.ts +0 -49
  282. package/src/store/recovery.ts +0 -44
  283. package/src/store/replay.ts +0 -76
  284. package/src/store/resource-index.ts +0 -170
  285. package/src/store/resources.ts +0 -285
  286. package/src/store/restore.ts +0 -110
  287. package/src/store/review.ts +0 -109
  288. package/src/store/runtime-target.ts +0 -64
  289. package/src/store/search.ts +0 -728
  290. package/src/store/secrets.ts +0 -44
  291. package/src/store/serial.ts +0 -13
  292. package/src/store/session-archive.ts +0 -165
  293. package/src/store/session-browser.ts +0 -68
  294. package/src/store/session-position.ts +0 -27
  295. package/src/store/sessions.ts +0 -525
  296. package/src/store/source-cancellation.ts +0 -6
  297. package/src/store/tool-binaries.ts +0 -90
  298. package/src/store/trust.ts +0 -82
  299. package/src/store/update-cache.ts +0 -36
  300. package/src/store/user-settings.ts +0 -30
  301. package/src/store/workspace-clone.ts +0 -42
  302. package/src/store/workspace-observation.ts +0 -113
  303. package/src/store/workspace.ts +0 -115
  304. package/src/store/worktree.ts +0 -278
  305. package/src/tui/ansi-spans.ts +0 -126
  306. package/src/tui/diff.ts +0 -160
  307. package/src/tui/exhaustive.ts +0 -14
  308. package/src/tui/fuzzy.ts +0 -75
  309. package/src/tui/style.ts +0 -208
  310. package/src/tui/ui/atomic-draft.ts +0 -121
  311. package/src/tui/ui/branch-panel.ts +0 -47
  312. package/src/tui/ui/canvas.ts +0 -96
  313. package/src/tui/ui/colors.ts +0 -70
  314. package/src/tui/ui/composer.ts +0 -199
  315. package/src/tui/ui/control-panel.ts +0 -25
  316. package/src/tui/ui/control-sidebar.ts +0 -49
  317. package/src/tui/ui/dialog.ts +0 -398
  318. package/src/tui/ui/draft.ts +0 -23
  319. package/src/tui/ui/duration.ts +0 -16
  320. package/src/tui/ui/edge.ts +0 -186
  321. package/src/tui/ui/image-size.ts +0 -44
  322. package/src/tui/ui/ink.ts +0 -210
  323. package/src/tui/ui/items.ts +0 -42
  324. package/src/tui/ui/keys.ts +0 -40
  325. package/src/tui/ui/layout.ts +0 -74
  326. package/src/tui/ui/list-model.ts +0 -86
  327. package/src/tui/ui/list-view.ts +0 -172
  328. package/src/tui/ui/mascot.ts +0 -67
  329. package/src/tui/ui/messages.ts +0 -518
  330. package/src/tui/ui/motion.ts +0 -28
  331. package/src/tui/ui/paths.ts +0 -10
  332. package/src/tui/ui/pictures.ts +0 -108
  333. package/src/tui/ui/prompt-history.ts +0 -60
  334. package/src/tui/ui/screen.ts +0 -579
  335. package/src/tui/ui/status.ts +0 -403
  336. package/src/tui/ui/styled.ts +0 -85
  337. package/src/tui/ui/tool-card.ts +0 -150
  338. package/src/tui/width.ts +0 -567
  339. package/src/vendor/pi-rich/LICENSE.upstream +0 -21
  340. package/src/vendor/pi-rich/UPSTREAM.json +0 -17
  341. package/src/vendor/pi-rich/clipboard-command.ts +0 -44
  342. package/src/vendor/pi-rich/exif-orientation.ts +0 -182
  343. package/src/vendor/pi-rich/html.ts +0 -51
  344. package/src/vendor/pi-rich/image-convert.ts +0 -49
  345. package/src/vendor/pi-rich/image-resize-core.ts +0 -164
  346. package/src/vendor/pi-rich/image-resize-worker.ts +0 -42
  347. package/src/vendor/pi-rich/image-resize.ts +0 -123
  348. package/src/vendor/pi-rich/mime.ts +0 -116
  349. package/src/vendor/pi-rich/photon.ts +0 -139
  350. package/src/vendor/pi-rich/syntax-highlight.ts +0 -214
@@ -1,579 +0,0 @@
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 type { ControlPanelModel, ControlTab } from "./control-panel.ts";
27
- import { ControlSidebar } from "./control-sidebar.ts";
28
- import { clipSegs, edgeLine, segsWidth, type Label, type Seg } from "./edge.ts";
29
- import type { Bindings, KeyPress } from "./keys.ts";
30
- import { screenLayout, sidebarContentWidth, type ScreenLayout } from "./layout.ts";
31
- import { greetingLines, mascotFrame, MASCOT_FRAME_MS, MASCOT_FRAMES, type Greeting } from "./mascot.ts";
32
- import { MessageList, type MessageFeatures } from "./messages.ts";
33
- import { Motion } from "./motion.ts";
34
- import { contextLabel, goalLabel, idleActivityLabel, modeLabels, sidebarLines, speedText, topbarRow, type Line, type StatusModel } from "./status.ts";
35
- import { DEFAULT_CELL, stripImageRows } from "./image-size.ts";
36
- import { formatElapsed } from "./duration.ts";
37
- import { Pictures, type PictureSource } from "./pictures.ts";
38
- import type { ImageView, ViewItem } from "./items.ts";
39
-
40
- export interface DockState {
41
- /** Model display name. */
42
- readonly model: string;
43
- readonly thinking: string;
44
- readonly busy: boolean;
45
- /** What the running turn is doing: "thinking", "streaming", "running command"… */
46
- readonly activity: string;
47
- }
48
-
49
- type Tone = "info" | "success" | "warning" | "error";
50
-
51
- /** Braille spinner, ten frames. */
52
- const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] as const;
53
- /** Spinner cadence: Codex and the research recommendation both use about 100 ms. */
54
- const ACTIVITY_MS = 100;
55
- /**
56
- * After this long without a stream event the spinner slows to IDLE_ACTIVITY_MS:
57
- * a turn waiting on a slow tool still shows it is alive, at a fifth of the
58
- * wakeups (OpenCode #42306 measured a full core spent on a fast spinner).
59
- */
60
- const QUIET_AFTER_MS = 2000;
61
- const IDLE_ACTIVITY_MS = 500;
62
- /** How long a transient message stays on the box edge. Assumed. */
63
- const TOAST_MS = 2500;
64
- /** Rows the transcript keeps even when the box grows (a picker never hides everything above it). Assumed. */
65
- const MIN_TRANSCRIPT_ROWS = 3;
66
- /** Completion rows above the box at most. Assumed. */
67
- const COMPLETION_ROWS = 6;
68
- /** Below this height the greeting would crowd the transcript; it stays hidden. Assumed. */
69
- const GREETING_MIN_ROWS = 14;
70
-
71
- export class Screen {
72
- readonly renderer: CliRenderer;
73
- readonly messages: MessageList;
74
- readonly composer: Composer;
75
- readonly dialogs: DialogHost;
76
- private colors: UiColors;
77
- private readonly topbar: Canvas;
78
- private readonly greeting: Canvas;
79
- private readonly pill: Canvas;
80
- private readonly above: BoxRenderable;
81
- private readonly completion: Canvas;
82
- private readonly queueRows: Canvas;
83
- private readonly widgetRows: Canvas;
84
- private readonly dock: BoxRenderable;
85
- private readonly body: BoxRenderable;
86
- private readonly topEdge: Canvas;
87
- private readonly bottomEdge: Canvas;
88
- /** Branch rows under the box (feat-branch-panel); hidden when there are none, so the box stays on the last row. */
89
- private readonly below: Canvas;
90
- private branchLines: readonly Line[] = [];
91
- /** `branch N · purpose` while the transcript shows a branch instead of the main session. */
92
- private viewingLabel: string | undefined;
93
- private readonly sidebar: BoxRenderable;
94
- private readonly sidebarRows: Canvas;
95
- private readonly middle: BoxRenderable;
96
- private readonly messagesArea: BoxRenderable;
97
- private readonly controlView = new ControlSidebar();
98
- private controlShown = false;
99
- private controlHasFocus = false;
100
- onControlAction: ((action: "refresh" | "interrupt" | "accept") => void) | undefined;
101
- private shape: ScreenLayout;
102
- private sidebarWanted = true;
103
- private status: StatusModel | undefined;
104
- private dockState: DockState = { model: "", thinking: "", busy: false, activity: "" };
105
- private transientActivity: { readonly label: string; readonly startedAt: number; readonly timeoutMs: number; readonly detail?: string } | undefined;
106
- private home: Greeting | undefined;
107
- private greetingPlayed = false;
108
- private greetingFrame = MASCOT_FRAMES;
109
- private readonly entrance = new Motion();
110
- private readonly activity = new Motion();
111
- private spin = 0;
112
- private busySince = 0;
113
- /** The last turn's wall time, shown beside its outcome until the next turn starts. */
114
- private lastTurnMs: number | undefined;
115
- private lastEvent = 0;
116
- private activityMs = ACTIVITY_MS;
117
- private animations = true;
118
- private focused = true;
119
- private toastText: { text: string; tone: Tone } | undefined;
120
- private toastTimer: ReturnType<typeof setTimeout> | undefined;
121
- private queue: readonly string[] = [];
122
- private attachmentImages: readonly ImageView[] = [];
123
- private attachmentsKey = "";
124
- private picturesSource: PictureSource = { mode: "chip", protocol: "blocks", cell: DEFAULT_CELL, widthCells: 60, load: async () => ({ kind: "error", reason: "closed" }) };
125
- private readonly attachments: BoxRenderable;
126
- private widgets: readonly string[] = [];
127
- private unread = 0;
128
- private readonly glyph = () => this.activity.running ? SPINNER[this.spin % SPINNER.length]! : "●";
129
-
130
- constructor(renderer: CliRenderer, colors: UiColors, bindings: () => Bindings, features: MessageFeatures = {}) {
131
- this.renderer = renderer;
132
- this.colors = colors;
133
- renderer.setBackgroundColor(colors.background);
134
- this.shape = screenLayout(renderer.width, renderer.height, true);
135
- const root = new BoxRenderable(renderer, { id: "root", flexDirection: "column", width: "100%", height: "100%" });
136
- this.topbar = new Canvas(renderer, { id: "topbar", height: 1, flexShrink: 0, marginLeft: 1, marginRight: 1, colors });
137
- const middle = new BoxRenderable(renderer, { id: "middle", flexDirection: "row", flexGrow: 1, flexShrink: 1 });
138
- this.middle = middle;
139
- const main = new BoxRenderable(renderer, { id: "main", flexDirection: "column", flexGrow: 1, flexShrink: 1 });
140
- this.greeting = new Canvas(renderer, { id: "greeting", height: 3, flexShrink: 0, marginTop: 1, marginLeft: 3, marginBottom: 1, colors });
141
- const area = new BoxRenderable(renderer, { id: "messages-area", flexDirection: "column", flexGrow: 1, flexShrink: 1 });
142
- this.messagesArea = area;
143
- this.messages = new MessageList(renderer, colors, features, this.glyph);
144
- area.add(this.messages.scroll);
145
- this.pill = new Canvas(renderer, { id: "pill", position: "absolute", zIndex: 40, visible: false, height: 1, width: 1, bottom: 0, right: 3, colors });
146
- area.add(this.pill);
147
- this.above = new BoxRenderable(renderer, { id: "above-dock", flexDirection: "column", flexShrink: 0, marginLeft: 3, marginRight: 3 });
148
- this.completion = new Canvas(renderer, { id: "completion", visible: false, height: 1, flexShrink: 0, colors });
149
- this.queueRows = new Canvas(renderer, { id: "queue", visible: false, height: 1, flexShrink: 0, colors });
150
- this.widgetRows = new Canvas(renderer, { id: "widgets", visible: false, height: 1, flexShrink: 0, colors });
151
- this.attachments = new BoxRenderable(renderer, { id: "attachments", flexDirection: "column", flexShrink: 0, visible: false });
152
- this.above.add(this.widgetRows); this.above.add(this.attachments); this.above.add(this.queueRows); this.above.add(this.completion);
153
- this.dock = new BoxRenderable(renderer, { id: "dock", flexDirection: "column", flexShrink: 0, marginLeft: 1, marginRight: 1 });
154
- this.topEdge = new Canvas(renderer, { id: "dock-top", height: 1, flexShrink: 0, colors, glyph: this.glyph });
155
- this.body = new BoxRenderable(renderer, { id: "dock-body", flexDirection: "column", flexShrink: 0, border: ["left", "right"], borderStyle: "rounded", borderColor: colors.edge, paddingLeft: 1, paddingRight: 1 });
156
- const slot = new BoxRenderable(renderer, { id: "dialog", flexDirection: "column", flexShrink: 0, visible: false });
157
- this.composer = new Composer(renderer, colors);
158
- this.body.add(slot); this.body.add(this.composer.row);
159
- this.bottomEdge = new Canvas(renderer, { id: "dock-bottom", height: 1, flexShrink: 0, colors });
160
- this.dock.add(this.topEdge); this.dock.add(this.body); this.dock.add(this.bottomEdge);
161
- this.below = new Canvas(renderer, { id: "below-dock", visible: false, height: 1, flexShrink: 0, marginLeft: 1, marginRight: 1, colors });
162
- main.add(this.greeting); main.add(area); main.add(this.above); main.add(this.dock); main.add(this.below);
163
- this.sidebar = new BoxRenderable(renderer, { id: "sidebar", flexShrink: 0, border: ["left"], borderColor: colors.line, paddingLeft: 1, paddingRight: 1, paddingTop: 1, flexDirection: "column", overflow: "hidden", backgroundColor: colors.background });
164
- this.sidebarRows = new Canvas(renderer, { id: "sidebar-rows", flexGrow: 1, colors });
165
- this.sidebarRows.onSizeChange = () => this.paintStatus();
166
- this.sidebar.onMouseDown = () => { if (this.controlShown && !this.dialogs.open) this.focusControl(); };
167
- this.sidebar.add(this.sidebarRows);
168
- middle.add(main); middle.add(this.sidebar);
169
- root.add(this.topbar); root.add(middle);
170
- renderer.root.add(root);
171
- this.dialogs = new DialogHost(renderer, colors, bindings, slot, () => this.dialogRows(), this.glyph);
172
- this.dialogs.onOpened = () => { this.composer.setCompletion(undefined); this.composer.area.blur(); this.composer.row.visible = false; this.paintAttachments(); this.layout(); };
173
- this.dialogs.onClosed = () => {
174
- this.composer.row.visible = true; this.paintAttachments(); this.layout();
175
- // Dialogs temporarily own input; restoring the existing owner preserves a
176
- // control-initiated approval without replacing or focusing the editor (Q24).
177
- if (this.controlShown && this.controlHasFocus) this.focusControl(); else this.composer.focus();
178
- };
179
- this.dialogs.onChrome = () => { this.paintEdges(); this.syncActivity(); };
180
- // Native mouse selection can focus the textarea without going through an
181
- // app shortcut. The actual input owner must also release panel actions (Q24).
182
- this.composer.area.on("focused", () => { this.controlHasFocus = false; this.paintStatus(); });
183
- this.composer.onResize = () => this.paintAbove();
184
- this.composer.onOverflow = () => this.paintEdges();
185
- this.composer.onChange = () => this.paintGreeting();
186
- this.messages.onUnread = count => { this.unread = count; this.paintPill(); };
187
- renderer.on("resize", () => this.layout());
188
- renderer.on("blur", () => { this.focused = false; this.finishEntrance(); });
189
- renderer.on("focus", () => { this.focused = true; });
190
- this.layout();
191
- this.composer.focus();
192
- }
193
-
194
- get width(): number { return this.renderer.width; }
195
- get tier(): ScreenLayout["tier"] { return this.shape.tier; }
196
- get sidebarVisible(): boolean { return this.shape.tier === "sidebar"; }
197
- get controlOpen(): boolean { return this.controlShown; }
198
- get controlFocused(): boolean { return this.controlShown && this.controlHasFocus && !this.dialogs.open; }
199
- /** An empty session (the greeting may still be hidden by a draft). */
200
- get homeVisible(): boolean { return this.home !== undefined; }
201
- get greetingVisible(): boolean { return this.greeting.visible; }
202
- get animating(): boolean { return this.activity.running || this.entrance.running; }
203
- /** Live timers owned by the view: none while idle (DoD 9). */
204
- get timers(): number { return (this.activity.running ? 1 : 0) + (this.entrance.running ? 1 : 0) + (this.toastTimer ? 1 : 0); }
205
- /** Plain text of the box edges and status surfaces (tests). */
206
- get chrome(): { top: string; bottom: string; topbar: string; sidebar: string; greeting: string; completion: string } {
207
- return { top: this.topEdge.text, bottom: this.bottomEdge.text, topbar: this.topbar.visible ? this.topbar.text : "", sidebar: this.sidebar.visible ? this.sidebarRows.text : "",
208
- greeting: this.greeting.visible ? this.greeting.text : "", completion: this.completion.visible ? this.completion.text : "" };
209
- }
210
- /** The box's outer rows, for layout tests. */
211
- get dockBox(): BoxRenderable { return this.dock; }
212
-
213
- // -------------------------------------------------------------- appearance
214
- setColors(colors: UiColors): void {
215
- this.colors = colors;
216
- this.renderer.setBackgroundColor(colors.background);
217
- for (const canvas of [this.topbar, this.greeting, this.pill, this.completion, this.queueRows, this.widgetRows, this.topEdge, this.bottomEdge, this.below, this.sidebarRows]) canvas.setColors(colors);
218
- this.sidebar.borderColor = colors.line;
219
- this.sidebar.backgroundColor = colors.background;
220
- this.messages.setColors(colors);
221
- this.composer.setColors(colors);
222
- this.dialogs.setColors(colors);
223
- this.paintStatus();
224
- this.paintEdges();
225
- }
226
-
227
- setAnimations(enabled: boolean): void {
228
- if (enabled === this.animations) return;
229
- this.animations = enabled;
230
- if (!enabled) this.finishEntrance();
231
- this.syncActivity();
232
- }
233
-
234
- toggleSidebar(): void {
235
- if (this.controlShown) { this.closeControl(); return; }
236
- this.sidebarWanted = !this.sidebarWanted; this.layout();
237
- }
238
-
239
- // --------------------------------------------------------------- controls
240
- showControl(model: ControlPanelModel | undefined, tab: ControlTab = "goal"): void {
241
- this.controlView.model = model; this.controlView.tab = tab; this.controlShown = true;
242
- this.composer.setCompletion(undefined);
243
- this.focusControl(); this.layout();
244
- }
245
-
246
- /** Snapshot updates never open the panel or take the user's input focus. */
247
- setControlModel(model: ControlPanelModel | undefined): void { this.controlView.model = model; if (this.controlShown) this.paintStatus(); }
248
-
249
- closeControl(): void { this.controlShown = false; this.focusComposer(); this.layout(); }
250
-
251
- focusComposer(): void {
252
- this.controlHasFocus = false;
253
- if (!this.dialogs.open) this.composer.focus();
254
- this.paintStatus();
255
- }
256
-
257
- private focusControl(): void {
258
- this.controlHasFocus = true;
259
- if (!this.dialogs.open) this.composer.area.blur();
260
- this.paintStatus();
261
- }
262
-
263
- handleControlKey(key: KeyPress): boolean {
264
- if (!this.controlShown || this.dialogs.open) return false;
265
- if (key.chord === "f6") { if (this.controlHasFocus) this.focusComposer(); else this.focusControl(); return true; }
266
- if (!this.controlHasFocus) return false;
267
- if (key.chord === "escape") { this.focusComposer(); return true; }
268
- if (key.chord === "q") { this.closeControl(); return true; }
269
- if (key.chord === "tab" || key.chord === "shift+tab") this.controlView.changeTab(key.shift ? -1 : 1);
270
- else if (key.chord === "left" || key.chord === "right") {
271
- const step = key.chord === "left" ? -1 : 1;
272
- if (this.controlView.tab === "context") this.controlView.changeLens(step); else this.controlView.changeTab(step);
273
- } else if (key.chord === "up" || key.chord === "down") this.controlView.scroll(key.chord === "up" ? -1 : 1);
274
- else if (key.chord === "pageup" || key.chord === "pagedown") this.controlView.scroll(key.chord === "pageup" ? -1 : 1, true);
275
- else if (key.chord === "home" || key.chord === "end") this.controlView.edge(key.chord === "end");
276
- else if (key.chord === "r" || key.chord === "i" || key.chord === "a") this.onControlAction?.(key.chord === "r" ? "refresh" : key.chord === "i" ? "interrupt" : "accept");
277
- else {
278
- // Let the same key reach the original editor after handing focus back.
279
- // Modifier shortcuts remain the app's; panel navigation never eats typing.
280
- if (key.text || key.chord === "enter") this.focusComposer();
281
- return false;
282
- }
283
- this.paintStatus();
284
- return true;
285
- }
286
-
287
- // ------------------------------------------------------------------ layout
288
- private layout(): void {
289
- const w = this.renderer.width, h = this.renderer.height;
290
- this.shape = screenLayout(w, h, this.sidebarWanted || this.controlShown);
291
- this.topbar.visible = this.shape.topbar;
292
- const drawer = this.controlShown && this.shape.tier !== "sidebar";
293
- // One sidebar changes parent at the responsive boundary. The drawer belongs
294
- // to the transcript viewport, so even a growing composer cannot be covered.
295
- const parent = drawer ? this.messagesArea : this.middle;
296
- if (this.sidebar.parent !== parent) { this.sidebar.parent?.remove(this.sidebar); parent.add(this.sidebar); }
297
- this.sidebar.position = drawer ? "absolute" : "relative";
298
- this.sidebar.top = drawer ? 0 : undefined; this.sidebar.bottom = drawer ? 0 : undefined; this.sidebar.right = drawer ? 0 : undefined;
299
- this.sidebar.zIndex = drawer ? 50 : 0;
300
- this.sidebar.visible = (this.shape.tier === "sidebar" || drawer) && w > 0 && h > 3;
301
- if (this.sidebar.visible) this.sidebar.width = drawer ? Math.min(w, sidebarContentWidth(w) + 1) : this.shape.sidebarWidth;
302
- const compact = drawer && w < 6;
303
- this.sidebar.border = compact ? [] : ["left"];
304
- this.sidebar.paddingLeft = compact ? 0 : 1; this.sidebar.paddingRight = compact ? 0 : 1; this.sidebar.paddingTop = compact ? 0 : 1;
305
- // The draft may grow while the transcript keeps its minimum and the box keeps its edges.
306
- this.composer.setMaxLines(Math.min(draftMaxLines(h), h - (this.shape.topbar ? 1 : 0) - 2 - MIN_TRANSCRIPT_ROWS - this.aboveRows()));
307
- this.dialogs.layout();
308
- this.paintStatus();
309
- this.paintEdges();
310
- this.paintAbove();
311
- this.paintBelow();
312
- this.paintGreeting();
313
- this.renderer.requestRender();
314
- }
315
-
316
- /** Rows a dialog may use inside the box. */
317
- private dialogRows(): number {
318
- return Math.max(1, this.renderer.height - (this.shape.topbar ? 1 : 0) - 2 - MIN_TRANSCRIPT_ROWS);
319
- }
320
-
321
- private aboveRows(): number {
322
- 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)
323
- + (this.attachments.visible ? this.attachments.height : 0) + (this.below.visible ? this.below.height as number : 0);
324
- }
325
-
326
- // ------------------------------------------------------------------ status
327
- setStatus(model: StatusModel): void { this.status = model; this.paintStatus(); this.paintEdges(); }
328
-
329
- private paintStatus(): void {
330
- const model = this.status;
331
- if (model && this.topbar.visible) this.topbar.setRows([topbarRow(this.shape.tier === "sidebar" ? { ...model, title: null } : model, Math.max(1, this.renderer.width - 2))]);
332
- if (this.controlShown) {
333
- this.sidebar.borderColor = this.controlFocused ? this.colors.accent : this.colors.line;
334
- this.sidebarRows.setRows(this.controlView.rows(this.sidebarRows.width, this.sidebarRows.height, this.controlFocused));
335
- return;
336
- }
337
- this.sidebar.borderColor = this.colors.line;
338
- if (!model) { this.sidebarRows.setRows([]); return; }
339
- if (this.sidebar.visible) {
340
- const width = Math.max(1, this.shape.sidebarWidth - 3);
341
- this.sidebarRows.setRows(sidebarLines(model, width, Math.max(0, this.renderer.height - (this.shape.topbar ? 1 : 0) - 1)));
342
- }
343
- }
344
-
345
- // -------------------------------------------------------------------- dock
346
- setDock(state: DockState): void {
347
- const wasBusy = this.dockState.busy;
348
- this.dockState = state;
349
- if (state.busy && !wasBusy) { this.busySince = Date.now(); this.lastTurnMs = undefined; }
350
- if (!state.busy && wasBusy) this.lastTurnMs = Date.now() - this.busySince;
351
- this.syncActivity();
352
- this.paintEdges();
353
- }
354
-
355
- toast(text: string, tone: Tone = "info"): void {
356
- this.toastText = { text, tone };
357
- clearTimeout(this.toastTimer);
358
- this.toastTimer = setTimeout(() => { this.toastTimer = undefined; this.toastText = undefined; this.paintEdges(); }, TOAST_MS);
359
- this.toastTimer.unref?.();
360
- this.paintEdges();
361
- }
362
-
363
- /** `startedAt` keeps the elapsed clock running when a long activity only refreshes its detail line. */
364
- setTransientActivity(activity: { readonly label: string; readonly timeoutMs: number; readonly startedAt?: number; readonly detail?: string } | undefined): void {
365
- this.transientActivity = activity ? { ...activity, startedAt: activity.startedAt ?? Date.now() } : undefined;
366
- this.syncActivity();
367
- this.paintEdges();
368
- }
369
-
370
- private paintEdges(): void {
371
- const width = Math.max(2, this.renderer.width - (this.shape.tier === "sidebar" ? this.shape.sidebarWidth : 0) - 2);
372
- const d = this.dockState, dialog = this.dialogs.open;
373
- const role = this.dialogs.tone === "warning" ? "warning" : d.busy && !dialog ? "accent" : "edge";
374
- this.body.borderColor = role === "warning" ? this.colors.warning : role === "accent" ? this.colors.accent : this.colors.edge;
375
- const right: Label[] = [];
376
- if (this.toastText) {
377
- const tone = this.toastText.tone;
378
- right.push({ segs: [{ text: this.toastText.text, role: tone === "error" ? "error" : tone === "warning" ? "warning" : tone === "success" ? "success" : "text" }], priority: 1, min: 12 });
379
- }
380
- let left: Label[] = [];
381
- if (dialog) {
382
- left = [{ segs: [{ text: this.dialogs.title, role: "text", bold: true }], priority: 0, min: 8 }];
383
- if (this.status?.activity?.state === "blocked") right.push({ segs: [{ text: this.status.activity.label, role: "warning" }], priority: 1, min: 8 });
384
- if (this.dialogs.edgeHint) right.push({ segs: [{ text: this.dialogs.edgeHint, role: "muted" }], priority: 3, min: 10 });
385
- } else if (this.controlShown && this.renderer.height <= 3) {
386
- // No transcript rows remain for a drawer. Keep its state and explain how
387
- // to recover on the existing edge, without putting control content in it.
388
- left = [{ segs: [{ text: "Resize for control", role: "accent" }], priority: 0, min: 1 }];
389
- } else if (d.busy) {
390
- const elapsed = formatElapsed(Date.now() - this.busySince);
391
- const activity = this.status?.activity;
392
- const label = activity ? `${activity.label}${activity.label === "Running tool" || activity.label === "Retrying" ? ` · ${activity.detail}` : ""}${activity.queued ? ` · ${activity.queued} queued` : ""}` : d.activity;
393
- const transient = this.transientActivity ? ` · ${this.transientActivityText()}` : "";
394
- left = [{ segs: [{ text: "●", role: "accent", spin: activity?.state !== "blocked" }, { text: ` ${label}${transient}`, role: activity?.state === "blocked" ? "warning" : "muted" }, { text: ` ${elapsed}`, role: "muted" }], priority: 0, min: 1, shrink: 1 }];
395
- // The turn's speed so far, refreshed after each model request (runtime/usage.ts generationSpeed).
396
- const speed = this.status?.usage?.speed;
397
- if (speed !== undefined) right.push({ segs: [{ text: speedText(speed), role: "muted" }], priority: 3 });
398
- right.push({ segs: [{ text: "esc interrupt", role: "muted" }], priority: 4 });
399
- } else if (this.transientActivity) {
400
- left = [{ segs: [{ text: "●", role: "accent", spin: true }, { text: ` ${this.transientActivityText()}`, role: "muted" }], priority: 0, min: 1, shrink: 1 }];
401
- } else {
402
- // A draft taller than the box says what scrolled out of view (fix-tui-composer-growth DoD 4).
403
- const { above, below } = this.composer.overflow;
404
- if (above) left = [{ segs: [{ text: `↑ ${above} more`, role: "muted" }], priority: 1 }];
405
- if (below && !this.toastText) right.push({ segs: [{ text: `↓ ${below} more`, role: "muted" }], priority: 2 });
406
- if (!above && this.status?.activity) left = [idleActivityLabel(this.status.activity, this.lastTurnMs)];
407
- }
408
- if (this.viewingLabel && !dialog) {
409
- left = [{ segs: [{ text: this.viewingLabel, role: "accent", bold: true }], priority: 0, min: 8, shrink: 0 }];
410
- right.push({ segs: [{ text: "esc back", role: "muted" }], priority: 4 });
411
- }
412
- // Which recalled prompt is showing (fix-composer-history). Leads the edge: it describes the text being edited.
413
- const recalled = this.composer.historyPosition;
414
- if (recalled) left = [{ segs: [{ text: `history ${recalled.index}/${recalled.total}`, role: "accent" }], priority: 0, min: 1 }, ...left];
415
- this.topEdge.setRows([edgeLine({ width, corners: ["╭", "╮"], role, left, right })]);
416
- const bottomLeft: Label[] = [...(this.status ? modeLabels(this.status) : []), ...(d.model ? [
417
- { segs: [{ text: d.model, role: "text" as const, bold: true }], priority: 1, min: 8 },
418
- ...(d.thinking ? [{ segs: [{ text: d.thinking, role: "muted" as const }], priority: 2 }] : []),
419
- ] : [])];
420
- // Without a sidebar the goal and the context share ride on this edge (layout.ts placement).
421
- const context = this.shape.tier !== "sidebar" && this.status ? contextLabel(this.status) : undefined;
422
- const goal = this.shape.tier !== "sidebar" && this.status ? goalLabel(this.status) : undefined;
423
- this.bottomEdge.setRows([edgeLine({ width, corners: ["╰", "╯"], role, left: bottomLeft, right: [...(goal ? [goal] : []), ...(context ? [context] : [])] })]);
424
- }
425
-
426
- /** A stream event arrived (delta, tool progress, shell output): the spinner returns to full speed. */
427
- markActivity(): void {
428
- this.lastEvent = Date.now();
429
- if (this.activity.running && this.activityMs !== ACTIVITY_MS) this.startClock(ACTIVITY_MS);
430
- }
431
-
432
- /** The clock runs only while something on screen shows the activity glyph. */
433
- private syncActivity(): void {
434
- const wanted = this.animations && (this.dockState.busy || this.transientActivity !== undefined || this.dialogs.animated);
435
- if (wanted && !this.activity.running) { this.lastEvent = Date.now(); this.startClock(ACTIVITY_MS); }
436
- else if (!wanted && this.activity.running) { this.activity.stop(); this.paintEdges(); }
437
- }
438
-
439
- private startClock(intervalMs: number): void {
440
- this.activityMs = intervalMs;
441
- const base = this.spin;
442
- this.activity.start(frame => {
443
- this.spin = base + frame;
444
- if (this.activityMs === ACTIVITY_MS && Date.now() - this.lastEvent > QUIET_AFTER_MS) { this.startClock(IDLE_ACTIVITY_MS); return; }
445
- this.paintEdges();
446
- this.renderer.requestRender();
447
- }, undefined, intervalMs);
448
- }
449
-
450
- private transientActivityText(): string {
451
- const activity = this.transientActivity!;
452
- // The limit is a whole number of seconds, rounded up so it is never shown shorter than it is.
453
- const deadline = Math.max(1, Math.ceil(activity.timeoutMs / 1000)) * 1000;
454
- return `${activity.label} · ${formatElapsed(Date.now() - activity.startedAt)} / ${formatElapsed(deadline)} max${activity.detail ? ` · ${activity.detail}` : ""}`;
455
- }
456
-
457
- /** The spinner's current interval (tests). */
458
- get activityInterval(): number { return this.activity.running ? this.activityMs : 0; }
459
-
460
- // ------------------------------------------------------------ above the box
461
- setQueue(lines: readonly string[]): void { this.queue = lines; this.paintAbove(); }
462
-
463
- // ------------------------------------------------------------ below the box
464
- /** Rows from branchPanelLines; an empty list hides the region. */
465
- setBranches(lines: readonly Line[]): void {
466
- const shown = this.below.visible ? this.below.height as number : 0;
467
- this.branchLines = lines;
468
- this.paintBelow();
469
- if ((lines.length || 0) !== shown) this.layout();
470
- }
471
- /** The transcript shows a branch: its label leads the top edge (undefined: the main session). */
472
- setViewing(label: string | undefined): void { this.viewingLabel = label; this.paintEdges(); }
473
- private paintBelow(): void {
474
- const width = Math.max(1, this.renderer.width - (this.shape.tier === "sidebar" ? this.shape.sidebarWidth : 0) - 2);
475
- this.below.visible = this.branchLines.length > 0;
476
- if (this.below.visible) { this.below.height = this.branchLines.length; this.below.setRows(this.branchLines.map(row => clipSegs(row, width))); }
477
- this.renderer.requestRender();
478
- }
479
-
480
- /** How pictures are drawn; the transcript and the attachment strip both follow it (feat-tui-inline-images). */
481
- setPictures(source: PictureSource): void {
482
- this.picturesSource = source;
483
- this.messages.setPictures(source);
484
- this.attachmentsKey = "";
485
- this.paintAttachments();
486
- }
487
-
488
- /** The image next to the draft cursor; its identity and sendability remain in the editor. */
489
- setAttachments(images: readonly ImageView[]): void { this.attachmentImages = images; this.paintAttachments(); }
490
-
491
- private paintAttachments(): void {
492
- const images = this.dialogs.open ? [] : this.attachmentImages;
493
- const key = images.map(image => image.sha256).join(",") + `|${this.renderer.width}x${this.renderer.height}`;
494
- if (key === this.attachmentsKey) return;
495
- this.attachmentsKey = key;
496
- for (const child of this.attachments.getChildren()) { this.attachments.remove(child); child.destroyRecursively(); }
497
- this.attachments.visible = images.length > 0;
498
- if (images.length) {
499
- const source = this.picturesSource;
500
- const width = Math.max(10, this.renderer.width - (this.shape.tier === "sidebar" ? this.shape.sidebarWidth : 0) - 6);
501
- this.attachments.add(new Pictures(this.renderer, this.colors, images, source, { maxCols: width, maxRows: stripImageRows(this.renderer.height) }));
502
- }
503
- this.renderer.requestRender();
504
- }
505
- setWidgets(lines: readonly string[]): void { this.widgets = lines; this.paintAbove(); }
506
-
507
- private paintAbove(): void {
508
- const width = Math.max(1, this.renderer.width - (this.shape.tier === "sidebar" ? this.shape.sidebarWidth : 0) - 6);
509
- const set = (canvas: Canvas, rows: readonly Line[]) => {
510
- canvas.visible = rows.length > 0;
511
- if (rows.length) { canvas.height = rows.length; canvas.setRows(rows.map(row => clipSegs(row, width))); }
512
- };
513
- set(this.widgetRows, this.widgets.map(text => [{ text, role: "muted" }]));
514
- set(this.queueRows, this.queue.map(text => [{ text: "↳ ", role: "muted" }, { text, role: "text" }]));
515
- const view = this.composer.completionView;
516
- let rows: Line[] = [];
517
- if (view && !this.dialogs.open) {
518
- 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));
519
- const start = Math.max(0, Math.min(view.selected - Math.floor(limit / 2), view.items.length - limit));
520
- const shown = view.items.slice(start, start + limit);
521
- const labelWidth = Math.min(24, Math.max(...shown.map(item => segsWidth([{ text: item.label, role: "text" }]))) + 2);
522
- rows = shown.map((item, k) => {
523
- const selected = start + k === view.selected;
524
- const pad = Math.max(1, labelWidth - segsWidth([{ text: item.label, role: "text" }]));
525
- return [{ text: selected ? "▸ " : " ", role: "accent", bold: true }, { text: item.label, role: "text", bold: selected },
526
- ...(item.description ? [{ text: " ".repeat(pad), role: "text" as const }, { text: item.description, role: "muted" as const }] : [])] as Seg[];
527
- });
528
- }
529
- set(this.completion, rows);
530
- this.renderer.requestRender();
531
- }
532
-
533
- // ---------------------------------------------------------------- greeting
534
- setGreeting(state: Greeting | undefined): void {
535
- this.home = state;
536
- this.paintGreeting();
537
- }
538
-
539
- private paintGreeting(): void {
540
- const show = this.home !== undefined && !this.controlShown && !this.composer.text && !this.dialogs.open && this.renderer.height >= GREETING_MIN_ROWS;
541
- this.greeting.visible = show;
542
- if (!show) { if (this.entrance.running) this.finishEntrance(); return; }
543
- // The entrance plays once per screen, when the greeting first shows on a focused terminal.
544
- if (!this.greetingPlayed && this.animations && this.focused) {
545
- this.greetingPlayed = true;
546
- this.entrance.start(frame => { this.greetingFrame = frame; this.drawGreeting(); }, MASCOT_FRAMES - 1, MASCOT_FRAME_MS);
547
- }
548
- this.greetingPlayed = true;
549
- this.drawGreeting();
550
- }
551
-
552
- private drawGreeting(): void {
553
- if (!this.home) return;
554
- const width = Math.max(1, this.renderer.width - (this.shape.tier === "sidebar" ? this.shape.sidebarWidth : 0) - 4);
555
- this.greeting.setRows(greetingLines(this.home, mascotFrame(this.greetingFrame)).map(row => clipSegs(row, width)));
556
- }
557
-
558
- private finishEntrance(): void {
559
- this.entrance.stop();
560
- this.greetingFrame = MASCOT_FRAMES;
561
- this.drawGreeting();
562
- }
563
-
564
- // ------------------------------------------------------------- transcript
565
- setItems(items: readonly ViewItem[]): void { this.messages.update(items); }
566
-
567
- private paintPill(): void {
568
- this.pill.visible = this.unread > 0;
569
- const text = ` ↓ ${this.unread} new `;
570
- this.pill.width = segsWidth([{ text, role: "text" }]);
571
- this.pill.setRows([[{ text, role: "accent", bold: true }]]);
572
- }
573
-
574
- dispose(): void {
575
- clearTimeout(this.toastTimer); this.toastTimer = undefined;
576
- this.activity.stop(); this.entrance.stop();
577
- this.dialogs.dispose();
578
- }
579
- }