@visual-z/xeno 0.8.3 → 0.9.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 (348) hide show
  1. package/CHANGELOG.md +24 -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 -2066
  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 -1713
  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 -3001
  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/canvas.ts +0 -96
  312. package/src/tui/ui/colors.ts +0 -70
  313. package/src/tui/ui/composer.ts +0 -200
  314. package/src/tui/ui/control-panel.ts +0 -25
  315. package/src/tui/ui/control-sidebar.ts +0 -49
  316. package/src/tui/ui/dialog.ts +0 -398
  317. package/src/tui/ui/draft.ts +0 -23
  318. package/src/tui/ui/duration.ts +0 -16
  319. package/src/tui/ui/edge.ts +0 -186
  320. package/src/tui/ui/image-size.ts +0 -44
  321. package/src/tui/ui/ink.ts +0 -210
  322. package/src/tui/ui/items.ts +0 -42
  323. package/src/tui/ui/keys.ts +0 -40
  324. package/src/tui/ui/layout.ts +0 -74
  325. package/src/tui/ui/list-model.ts +0 -86
  326. package/src/tui/ui/list-view.ts +0 -172
  327. package/src/tui/ui/mascot.ts +0 -67
  328. package/src/tui/ui/messages.ts +0 -518
  329. package/src/tui/ui/motion.ts +0 -28
  330. package/src/tui/ui/paths.ts +0 -10
  331. package/src/tui/ui/pictures.ts +0 -108
  332. package/src/tui/ui/screen.ts +0 -548
  333. package/src/tui/ui/status.ts +0 -403
  334. package/src/tui/ui/styled.ts +0 -85
  335. package/src/tui/ui/tool-card.ts +0 -150
  336. package/src/tui/width.ts +0 -567
  337. package/src/vendor/pi-rich/LICENSE.upstream +0 -21
  338. package/src/vendor/pi-rich/UPSTREAM.json +0 -17
  339. package/src/vendor/pi-rich/clipboard-command.ts +0 -44
  340. package/src/vendor/pi-rich/exif-orientation.ts +0 -182
  341. package/src/vendor/pi-rich/html.ts +0 -51
  342. package/src/vendor/pi-rich/image-convert.ts +0 -49
  343. package/src/vendor/pi-rich/image-resize-core.ts +0 -164
  344. package/src/vendor/pi-rich/image-resize-worker.ts +0 -42
  345. package/src/vendor/pi-rich/image-resize.ts +0 -123
  346. package/src/vendor/pi-rich/mime.ts +0 -116
  347. package/src/vendor/pi-rich/photon.ts +0 -139
  348. package/src/vendor/pi-rich/syntax-highlight.ts +0 -214
@@ -1,43 +0,0 @@
1
- import type { ControlSnapshot } from "../../runtime/session-control.ts";
2
- import type { ControlPanelModel } from "../../tui/ui/control-panel.ts";
3
- import type { RequestState } from "../../core/request-state.ts";
4
- import { evidenceLadder, ladderGlyph, RUNGS } from "../../core/evidence-ladder.ts";
5
-
6
- /** The sidebar's compact ladder: rung names with their glyphs, then the reading. */
7
- export function ladderView(state: RequestState): { ladder?: { glyphs: string; summary: string } } {
8
- const ladder = evidenceLadder(state);
9
- if (!ladder) return {};
10
- const names = { ran: "ran", discriminating: "tests", independent: "blind", human: "human" } as const;
11
- return { ladder: { glyphs: RUNGS.map(rung => `${names[rung]} ${ladderGlyph(ladder[rung])}`).join(" "), summary: ladder.summary } };
12
- }
13
-
14
- export function controlModel(snapshot: ControlSnapshot): ControlPanelModel {
15
- const goal = snapshot.goal, state = goal?.state, context = snapshot.context;
16
- const progress = (label: string, sources: readonly { node: number; event: string }[]) => ({ label, count: sources.length, sources: sources.map(source => `${source.node}/${source.event}`) });
17
- return { thread: snapshot.threadId, turn: snapshot.activeTurnId ?? state?.request.event ?? null,
18
- goal: goal?.text ?? "Send a request to establish a goal.", status: state?.status ?? "idle",
19
- execution: snapshot.waiting ? `waiting for ${snapshot.waiting}` : snapshot.activeTurnId ? "running" : state?.execution?.reason ?? "idle",
20
- gaps: [...ladderLines(state), ...goal?.gaps ?? []], checks: state?.commands.map(command => `${command.status} · exit ${command.exit} · ${command.command} · ${command.source.node}/${command.source.event}`) ?? [],
21
- progress: state ? [
22
- progress("New observations", state.progress.observations),
23
- progress("Artifact changes", state.progress.artifact_changes),
24
- progress("Acceptance advances", state.progress.acceptance_advances),
25
- progress("New diagnostics", state.progress.new_diagnostics),
26
- progress("Unchanged retries", state.progress.unchanged_retries),
27
- progress("Unknown diagnostics", state.progress.unknown_diagnostics),
28
- ] : [],
29
- context: [`${context.used} tokens (${context.countKind}); ${context.messages.length} messages; ${context.policy}`,
30
- ...Object.entries(context.components).map(([name, count]) => `${name}: ~${count}`),
31
- ...context.messages.map(message => `#${message.index} ${message.role}: ${message.sources.join(", ")}`)],
32
- omitted: context.omitted.map(event => `${event.id}: ${event.type} (original stays in log)`),
33
- history: context.sections.filter(section => section.total > 0).map(section => `${section.section}: ${section.retained}/${section.total}` +
34
- (section.recovery ? ` · history(${JSON.stringify(section.recovery)})` : "")),
35
- };
36
- }
37
-
38
- /** The control panel's goal page spells each rung out on its own line. */
39
- function ladderLines(state: RequestState | undefined): string[] {
40
- const ladder = state ? evidenceLadder(state) : null;
41
- if (!ladder) return [];
42
- return [`Evidence: ${ladder.summary}`, ...RUNGS.map(rung => ` ${ladderGlyph(ladder[rung])} ${rung}: ${ladder[rung]}`), ""];
43
- }
@@ -1,194 +0,0 @@
1
- /**
2
- * ExtensionUI for the interactive view. Dialog requests are serialized (one
3
- * extension question at a time, in arrival order) and all cancel on
4
- * shutdown or session switch, resolving as "dismissed". A call arriving from
5
- * a disposed generation (an extension of the previous session finishing
6
- * late) is ignored rather than mutating the new session's UI.
7
- */
8
- import type { Theme } from "../../core/theme.ts";
9
- import type { DialogOptions, ExtensionComponent, ExtensionTheme, ExtensionUI } from "../../runtime/extension-ui.ts";
10
- import type { ExtensionRegistry, ExtensionToolResult } from "../../runtime/extensions.ts";
11
- import type { DialogSpec } from "../../tui/ui/dialog.ts";
12
-
13
- export interface ExtensionHostDeps {
14
- dialog(spec: DialogSpec): void;
15
- closeDialog(spec: DialogSpec): void;
16
- notify(message: string, tone: "info" | "warning" | "error"): void;
17
- redraw(): void;
18
- theme(): Theme;
19
- colored(): boolean;
20
- editorText(): string;
21
- setEditorText(text: string): void;
22
- insertEditorText(text: string): void;
23
- toolsExpanded(): boolean;
24
- setToolsExpanded(value: boolean): void;
25
- setTitle(title: string): void;
26
- cwd(): string;
27
- }
28
-
29
- export class ExtensionHost {
30
- private readonly deps: ExtensionHostDeps;
31
- private generation = 0;
32
- private queue: Promise<unknown> = Promise.resolve();
33
- private readonly pending = new Set<() => void>();
34
- private readonly statuses = new Map<string, string>();
35
- private readonly widgets = new Map<string, string[]>();
36
- private readonly toolState = new Map<string, Record<string, unknown>>();
37
- private readonly partials = new Map<string, ExtensionToolResult>();
38
- private registry: ExtensionRegistry | undefined;
39
- private unwatch: (() => void) | undefined;
40
- workingMessage: string | undefined;
41
- hiddenThinkingLabel: string | undefined;
42
- private api: ExtensionUI;
43
-
44
- constructor(deps: ExtensionHostDeps) {
45
- this.deps = deps;
46
- this.api = this.createApi();
47
- }
48
-
49
- get context(): ExtensionUI { return this.api; }
50
- get status(): string { return [...this.statuses.values()].join(" · "); }
51
- get widgetLines(): string[] { return [...this.widgets.values()].flat(); }
52
-
53
- bind(registry: ExtensionRegistry | undefined): void {
54
- if (registry === this.registry && this.unwatch) return;
55
- if (registry !== this.registry) this.reset();
56
- this.registry = registry;
57
- this.unwatch = registry?.onToolUpdate((_name, id, _input, result) => { this.partials.set(id, result); this.deps.redraw(); });
58
- }
59
-
60
- /** New generation: cancel dialogs, drop widgets and statuses. Late calls from the old one become no-ops. */
61
- reset(): void {
62
- this.generation++;
63
- this.cancelDialogs();
64
- this.unwatch?.(); this.unwatch = undefined; this.registry = undefined;
65
- this.statuses.clear(); this.widgets.clear(); this.toolState.clear(); this.partials.clear();
66
- this.workingMessage = undefined; this.hiddenThinkingLabel = undefined;
67
- this.api = this.createApi();
68
- }
69
-
70
- cancelDialogs(): void { for (const cancel of [...this.pending]) cancel(); }
71
-
72
- partial(id: string): ExtensionToolResult | undefined { return this.partials.get(id); }
73
- clearPartials(): void { this.partials.clear(); }
74
-
75
- theme(): ExtensionTheme {
76
- const theme = this.deps.theme();
77
- const colored = this.deps.colored();
78
- const sgr = (color: string) => colored && theme.colors[color] ? `\x1b[${theme.colors[color]}m` : "";
79
- const wrap = (open: string, text: string, close: string) => open ? open + text + close : text;
80
- const attr = (on: number, off: number) => (text: string) => wrap(colored ? `\x1b[${on}m` : "", text, `\x1b[${off}m`);
81
- return { ...theme, fg: (color, text) => wrap(sgr(color), text, "\x1b[39m"), bg: (color, text) => wrap(sgr(color), text, "\x1b[49m"),
82
- bold: attr(1, 22), dim: attr(2, 22), italic: attr(3, 23), underline: attr(4, 24), inverse: attr(7, 27), strikethrough: attr(9, 29) };
83
- }
84
-
85
- /** Render an extension component safely: a throwing renderer shows its error, not a crash. */
86
- lines(component: ExtensionComponent | undefined, width: number): string[] {
87
- if (!component) return [];
88
- try { return component.render(width); } catch (error) { this.deps.notify(`Extension renderer: ${String(error)}`, "error"); return [`(extension renderer failed: ${String(error)})`]; }
89
- }
90
-
91
- toolRenderer(name: string, input: unknown, result: { text: string; ok: boolean; data?: unknown } | null, id: string): ((width: number) => string[]) | undefined {
92
- const tool = this.registry?.tools.get(name);
93
- if (!tool?.renderCall && !tool?.renderResult) return undefined;
94
- const state = this.toolState.get(id) ?? {};
95
- this.toolState.set(id, state);
96
- const partial = this.partials.get(id);
97
- const expanded = this.deps.toolsExpanded();
98
- return width => {
99
- const context = { args: input, toolCallId: id, invalidate: () => this.deps.redraw(), lastComponent: undefined, state, cwd: this.deps.cwd(),
100
- executionStarted: result !== null || partial !== undefined, argsComplete: true, isPartial: result === null, expanded, showImages: false, isError: result?.ok === false };
101
- const call = tool.renderCall ? this.lines(tool.renderCall(input, this.theme(), context), width) : [];
102
- const payload: ExtensionToolResult | undefined = result
103
- ? resultPayload(result)
104
- : partial;
105
- const body = payload && tool.renderResult ? this.lines(tool.renderResult(payload, { expanded, isPartial: result === null }, this.theme(), context), width) : [];
106
- return [...call, ...body];
107
- };
108
- }
109
-
110
- /** Extension Markdown transformers, in registration order; a throwing one is skipped. Display only. */
111
- transformMarkdown(text: string, context: { streaming: boolean; messageType: "assistant" | "assistant-thinking"; width: number }): string {
112
- for (const transform of this.registry?.markdown ?? []) {
113
- // Context keys are the ones extensions already read (availableWidth / isStreaming / messageType).
114
- try { text = transform(text, { availableWidth: context.width, isStreaming: context.streaming, messageType: context.messageType }) ?? text; } catch (error) { this.deps.notify(`Markdown transformer: ${String(error)}`, "error"); }
115
- }
116
- return text;
117
- }
118
-
119
- messageRenderer(type: string, text: string, data: unknown, entry: boolean): ((width: number) => string[]) | undefined {
120
- const renderer = (entry ? this.registry?.entryRenderers.get(type) : undefined) ?? this.registry?.renderers.get(type);
121
- if (!renderer) return undefined;
122
- return width => {
123
- try {
124
- const component = renderer({ customType: type, content: text, ...(data === undefined ? {} : { details: data }) }, { expanded: this.deps.toolsExpanded(), outputPad: 1 }, this.theme());
125
- return component ? this.lines(component, width) : [text];
126
- } catch (error) { return [`(extension renderer failed: ${String(error)})`, text]; }
127
- };
128
- }
129
-
130
- private createApi(): ExtensionUI {
131
- const generation = this.generation;
132
- const live = () => generation === this.generation;
133
- const api: ExtensionUI = {
134
- select: (title, options, opts) => this.ask<string | undefined>(undefined, opts, resolve => ({
135
- kind: "list", title, items: options.map(option => ({ value: option, label: option })), onPick: item => resolve(item.value), onCancel: () => resolve(undefined),
136
- })),
137
- confirm: (title, message, opts) => this.ask<boolean>(false, opts, resolve => ({ kind: "confirm", title, message, onAnswer: yes => resolve(yes) })),
138
- input: (title, placeholder, opts) => this.ask<string | undefined>(undefined, opts, resolve => ({
139
- kind: "form", title, label: placeholder || "value", onSubmit: value => resolve(value), onCancel: () => resolve(undefined),
140
- })),
141
- show: (title, component) => this.ask<void>(undefined, undefined, resolve => ({
142
- kind: "lines", title, render: width => this.lines(component, width), onCancel: () => { component.dispose?.(); resolve(undefined); },
143
- })),
144
- notify: (message, type) => { if (live()) this.deps.notify(message, type ?? "info"); },
145
- setStatus: (key, text) => { if (!live()) return; if (text === undefined) this.statuses.delete(key); else this.statuses.set(key, text); this.deps.redraw(); },
146
- setWidget: (key, lines) => { if (!live()) return; if (lines === undefined) this.widgets.delete(key); else this.widgets.set(key, lines); this.deps.redraw(); },
147
- setWorkingMessage: message => { if (!live()) return; this.workingMessage = message; this.deps.redraw(); },
148
- setHiddenThinkingLabel: label => { if (!live()) return; this.hiddenThinkingLabel = label; this.deps.redraw(); },
149
- setTitle: title => { if (live()) this.deps.setTitle(title); },
150
- getEditorText: () => this.deps.editorText(),
151
- setEditorText: text => { if (live()) this.deps.setEditorText(text); },
152
- pasteToEditor: text => { if (live()) this.deps.insertEditorText(text); },
153
- get theme() { return hostTheme(); },
154
- getToolsExpanded: () => this.deps.toolsExpanded(),
155
- setToolsExpanded: value => { if (live()) this.deps.setToolsExpanded(value); },
156
- };
157
- const hostTheme = () => this.theme();
158
- return api;
159
- }
160
-
161
- /** One extension dialog at a time; signal/timeout/shutdown resolve with `dismissed`. */
162
- private ask<T>(dismissed: T, opts: DialogOptions | undefined, spec: (resolve: (value: T) => void) => DialogSpec): Promise<T> {
163
- const generation = this.generation;
164
- const run = () => new Promise<T>(resolve => {
165
- if (generation !== this.generation || opts?.signal?.aborted) { resolve(dismissed); return; }
166
- let done = false, timer: ReturnType<typeof setTimeout> | undefined, shown: DialogSpec | undefined;
167
- const finish = (value: T, close: boolean) => {
168
- if (done) return;
169
- done = true;
170
- clearTimeout(timer);
171
- opts?.signal?.removeEventListener("abort", abort);
172
- this.pending.delete(cancel);
173
- if (close && shown) this.deps.closeDialog(shown);
174
- resolve(value);
175
- };
176
- const cancel = () => finish(dismissed, true);
177
- const abort = () => finish(dismissed, true);
178
- this.pending.add(cancel);
179
- opts?.signal?.addEventListener("abort", abort, { once: true });
180
- if (opts?.timeout) timer = setTimeout(abort, opts.timeout);
181
- shown = spec(value => finish(value, false));
182
- this.deps.dialog(shown);
183
- });
184
- const next = this.queue.then(run, run);
185
- this.queue = next.catch(() => undefined);
186
- return next;
187
- }
188
- }
189
-
190
- function resultPayload(result: { text: string; ok: boolean; data?: unknown }): ExtensionToolResult {
191
- const payload: ExtensionToolResult = { content: [{ type: "text", text: result.text }], isError: !result.ok };
192
- if (result.data !== undefined) payload.details = result.data as NonNullable<ExtensionToolResult["details"]>;
193
- return payload;
194
- }
@@ -1,121 +0,0 @@
1
- /**
2
- * Decoded pictures, shared and bounded (docs/tasks/feat-tui-inline-images.md DoD 4).
3
- *
4
- * The log stores each image once as a content-addressed blob; this keeps one
5
- * decoded NativeImage per blob for every node that shows it. Each picture
6
- * node retains its own reference (OpenTUI counts them), so evicting an entry
7
- * here only drops the cache's reference: a picture still on screen keeps
8
- * its pixels, and memory is returned once the last node goes.
9
- *
10
- * Bytes come from the blob store, which checks the hash; a missing or
11
- * corrupt blob, or one that will not decode, has a typed failure so the view
12
- * retains the caption and shows the actual preparation error.
13
- */
14
- import { NativeImage, imageInfo } from "@opentui/core";
15
- import { getBlobBytes } from "../../store/blobs.ts";
16
- import { modelImageDimensions, MAX_IMAGE_SOURCE_BYTES, MAX_IMAGE_SOURCE_PIXELS } from "../../core/image-limits.ts";
17
- import type { PictureLoad } from "../../tui/ui/pictures.ts";
18
-
19
- /** Two full previews fit; mounted ImageRenderables retain their own references. */
20
- export const IMAGE_CACHE_BYTES = 64 * 1024 * 1024;
21
- /** Bounds failed entries and queued requests as well as successful pixels. */
22
- export const IMAGE_CACHE_ENTRIES = 128;
23
- /** Limit simultaneous native allocations; remaining admitted loads wait in the bounded cache. */
24
- const IMAGE_DECODE_CONCURRENCY = 2;
25
- interface Entry { readonly result: Promise<PictureLoad>; bytes: number; settled: boolean }
26
-
27
- export class ImageCache {
28
- private readonly entries = new Map<string, Entry>();
29
- private used = 0;
30
- private active = 0;
31
- private closed = false;
32
- private readonly waiting: (() => void)[] = [];
33
- private readonly blobDir: () => string;
34
- private readonly budget: number;
35
-
36
- constructor(blobDir: () => string, budget = IMAGE_CACHE_BYTES) {
37
- this.blobDir = blobDir;
38
- this.budget = budget;
39
- }
40
-
41
- /** Bytes of decoded pixels held now (tests). */
42
- get size(): number { return this.used; }
43
- get count(): number { return this.entries.size; }
44
-
45
- async load(sha256: string): Promise<NativeImage | null> {
46
- const result = await this.loadResult(sha256);
47
- return result.kind === "ready" ? result.image : null;
48
- }
49
-
50
- loadResult(sha256: string): Promise<PictureLoad> {
51
- if (this.closed) return Promise.resolve({ kind: "error", reason: "closed" });
52
- const hit = this.entries.get(sha256);
53
- if (hit) {
54
- // Most recently used goes last; eviction starts from the front.
55
- this.entries.delete(sha256); this.entries.set(sha256, hit);
56
- return hit.result;
57
- }
58
- if (this.entries.size >= IMAGE_CACHE_ENTRIES) {
59
- const oldest = [...this.entries].find(([, entry]) => entry.settled);
60
- if (oldest) this.drop(oldest[0], oldest[1]);
61
- else return Promise.resolve({ kind: "error", reason: "busy" });
62
- }
63
- // Capture the blob owner before awaiting: session switches cannot redirect an in-flight read.
64
- const entry: Entry = { result: this.decode(this.blobDir(), sha256), bytes: 0, settled: false };
65
- this.entries.set(sha256, entry);
66
- void entry.result.then(result => {
67
- entry.settled = true;
68
- if (this.entries.get(sha256) !== entry) { if (result.kind === "ready") result.image.dispose(); return; }
69
- entry.bytes = result.kind === "ready" ? result.image.width * result.image.height * 4 : 0;
70
- this.used += entry.bytes;
71
- for (const [sha, old] of this.entries) {
72
- if (this.used <= this.budget) break;
73
- if (sha !== sha256 && old.settled) this.drop(sha, old);
74
- }
75
- });
76
- return entry.result;
77
- }
78
-
79
- private async decode(dir: string, sha: string): Promise<PictureLoad> {
80
- if (this.active >= IMAGE_DECODE_CONCURRENCY) await new Promise<void>(resolve => this.waiting.push(resolve));
81
- else this.active++;
82
- if (this.closed) return { kind: "error", reason: "closed" };
83
- let native: NativeImage | undefined;
84
- try {
85
- let bytes: Uint8Array | null;
86
- try { bytes = await getBlobBytes(dir, sha, MAX_IMAGE_SOURCE_BYTES); }
87
- catch (error) { return { kind: "error", reason: error instanceof RangeError ? "limit" : "corrupt" }; }
88
- if (!bytes) return { kind: "error", reason: "missing" };
89
- if (bytes.length > MAX_IMAGE_SOURCE_BYTES) return { kind: "error", reason: "limit" };
90
- const info = imageInfo(bytes);
91
- if (info.width * info.height > MAX_IMAGE_SOURCE_PIXELS) return { kind: "error", reason: "limit" };
92
- native = await NativeImage.load(bytes);
93
- const target = modelImageDimensions(native);
94
- if (target.width !== native.width || target.height !== native.height) {
95
- const resized = native.resize(target); native.dispose(); native = resized;
96
- }
97
- if (this.closed) return { kind: "error", reason: "closed" };
98
- if (native.width * native.height * 4 > this.budget) return { kind: "error", reason: "limit" };
99
- const image = native; native = undefined;
100
- return { kind: "ready", image };
101
- } catch { return { kind: "error", reason: "decode" }; }
102
- finally {
103
- native?.dispose();
104
- const next = this.waiting.shift();
105
- if (next) next(); else this.active--;
106
- }
107
- }
108
-
109
- private drop(sha: string, entry: Entry): void {
110
- this.entries.delete(sha); this.used -= entry.bytes;
111
- void entry.result.then(result => { if (result.kind === "ready") result.image.dispose(); });
112
- }
113
-
114
- dispose(): void {
115
- this.closed = true;
116
- for (const entry of this.entries.values()) if (entry.settled) void entry.result.then(result => { if (result.kind === "ready") result.image.dispose(); });
117
- this.entries.clear();
118
- this.used = 0;
119
- this.waiting.splice(0).forEach(resolve => resolve());
120
- }
121
- }
@@ -1,88 +0,0 @@
1
- /**
2
- * Ctrl+P: every command and action in one searchable list.
3
- *
4
- * Sources: the slash-command catalog (COMMANDS, prompt templates, skills,
5
- * extension commands) plus actions that have no slash spelling (toggle tools,
6
- * scroll, sidebar) and the ready models. The `/` completion menu reads the
7
- * same command entries, so a command is discoverable exactly one way
8
- * (OpenCode's command registry). Pure: the controller supplies state.
9
- */
10
- import type { ListItem } from "../../tui/ui/list-model.ts";
11
- import { COMMANDS } from "../commands.ts";
12
-
13
- export interface PaletteAction { readonly id: string; readonly label: string; readonly description: string; readonly section: string; readonly chord?: string; readonly when?: boolean }
14
-
15
- export interface PaletteInput {
16
- readonly bindings: Readonly<Record<string, readonly string[]>>;
17
- readonly busy: boolean;
18
- readonly extraCommands: readonly { name: string; description: string; section: string }[];
19
- readonly models: readonly { key: string; label: string; description: string; current: boolean }[];
20
- readonly toolsExpanded: boolean;
21
- readonly thinkingExpanded: boolean;
22
- readonly sidebar: boolean;
23
- }
24
-
25
- /** Which section a built-in command belongs to; unlisted ones go under "Commands". */
26
- const SECTION: Readonly<Record<string, string>> = {
27
- model: "Model", "scoped-models": "Model", thinking: "Model", login: "Model", logout: "Model",
28
- new: "Session", resume: "Session", tree: "Session", fork: "Session", clone: "Session", back: "Session", name: "Session", session: "Session",
29
- export: "Session", import: "Session", copy: "Session",
30
- usage: "Inspect", state: "Inspect", context: "Inspect", acceptance: "Inspect", tasks: "Inspect", branches: "Inspect", processes: "Inspect", debug: "Inspect",
31
- settings: "Configure", reload: "Configure", config: "Configure", trust: "Configure", hotkeys: "Configure", models: "Configure",
32
- };
33
-
34
- function chord(bindings: PaletteInput["bindings"], action: string): string | undefined {
35
- return bindings[action]?.[0];
36
- }
37
-
38
- export function paletteActions(input: PaletteInput): PaletteAction[] {
39
- const b = input.bindings;
40
- const actions: PaletteAction[] = [
41
- { id: "action:toggleTools", label: input.toolsExpanded ? "Collapse tool output" : "Expand tool output", description: "all tool cards", section: "View", ...opt(chord(b, "app.toggleTools")) },
42
- { id: "action:toggleThinking", label: input.thinkingExpanded ? "Hide thinking" : "Show thinking", description: "reasoning blocks", section: "View", ...opt(chord(b, "app.toggleThinking")) },
43
- { id: "action:toggleSidebar", label: input.sidebar ? "Hide sidebar" : "Show sidebar", description: "segment state (≥120 columns)", section: "View", ...opt(chord(b, "app.toggleSidebar")) },
44
- { id: "action:appearance", label: "Appearance", description: "dark, light or follow terminal", section: "View" },
45
- { id: "action:animations", label: "Animations", description: "enable or reduce motion", section: "View" },
46
- { id: "action:bottom", label: "Jump to latest", description: "resume following new output", section: "View", ...opt(chord(b, "app.scroll.bottom")) },
47
- { id: "action:top", label: "Jump to start", description: "first message of this session", section: "View", ...opt(chord(b, "app.scroll.top")) },
48
- { id: "action:editor", label: "Open draft in $EDITOR", description: "edit the prompt externally", section: "Compose", ...opt(chord(b, "app.externalEditor")) },
49
- { id: "action:pasteImage", label: "Paste image from clipboard", description: "attach to the draft", section: "Compose", ...opt(chord(b, "app.clipboard.pasteImage")) },
50
- { id: "action:cycleThinking", label: "Cycle thinking level", description: "next supported level", section: "Model", ...opt(chord(b, "app.cycleThinking")) },
51
- { id: "action:interrupt", label: "Interrupt", description: "stop the running turn", section: "Run", when: input.busy, ...opt(chord(b, "app.interrupt")) },
52
- { id: "action:quit", label: "Quit", description: "exit xeno", section: "Run", ...opt(chord(b, "app.exit")) },
53
- ];
54
- return actions.filter(action => action.when !== false);
55
- }
56
-
57
- function opt(value: string | undefined): { chord?: string } { return value ? { chord: value } : {}; }
58
-
59
- export function paletteItems(input: PaletteInput): ListItem[] {
60
- const items: ListItem[] = [];
61
- const model = input.models.find(entry => entry.current);
62
- for (const action of paletteActions(input)) items.push({ value: action.id, label: action.label, description: action.description, section: action.section, ...(action.chord ? { hint: action.chord } : {}) });
63
- for (const command of COMMANDS) {
64
- if (command.name === "exit" || command.name === "quit") continue;
65
- const blocked = input.busy && command.busy === "idle";
66
- items.push({ value: `command:${command.name}`, label: `/${command.name}`, description: blocked ? `${command.description} · needs an idle session` : command.description,
67
- section: SECTION[command.name] ?? "Commands", disabled: blocked,
68
- ...(command.name === "model" && model ? { hint: model.label } : command.name === "model" && input.bindings["app.model"]?.[0] ? { hint: input.bindings["app.model"][0] } : {}) });
69
- }
70
- for (const command of input.extraCommands) items.push({ value: `command:${command.name}`, label: `/${command.name}`, description: command.description, section: command.section });
71
- for (const entry of input.models) items.push({ value: `model:${entry.key}`, label: entry.label, description: entry.description, section: "Switch model", keywords: entry.key, disabled: input.busy,
72
- ...(input.busy ? { hint: "after this turn" } : entry.current ? { hint: "current" } : {}) });
73
- return sortSections(items);
74
- }
75
-
76
- const ORDER = ["View", "Model", "Session", "Compose", "Inspect", "Configure", "Commands", "Prompts", "Skills", "Extensions", "Run", "Switch model"];
77
- function sortSections(items: ListItem[]): ListItem[] {
78
- const rank = (section: string | undefined) => { const at = ORDER.indexOf(section ?? ""); return at < 0 ? ORDER.length : at; };
79
- return items.map((item, index) => ({ item, index })).sort((a, b) => rank(a.item.section) - rank(b.item.section) || a.index - b.index).map(entry => entry.item);
80
- }
81
-
82
- /** `/` completion: the same command entries, filtered by prefix, ranked by the list model's fuzzy order. */
83
- export function slashItems(input: PaletteInput, prefix: string): ListItem[] {
84
- const all = paletteItems(input).filter(item => item.value.startsWith("command:"));
85
- const q = prefix.toLowerCase();
86
- return all.filter(item => item.label.slice(1).toLowerCase().startsWith(q)).concat(all.filter(item => !item.label.slice(1).toLowerCase().startsWith(q) && item.label.toLowerCase().includes(q)))
87
- .map(item => ({ ...item, value: `${item.label} ` }));
88
- }
@@ -1,127 +0,0 @@
1
- /**
2
- * Log projection → view items. Pure apart from the renderer callbacks it
3
- * hands to tool cards, which are themselves pure functions of the log.
4
- *
5
- * Inputs are the durable transcript (core/transcript.ts), the UI-only
6
- * notices anchored to it, and the live (uncommitted) stream. The stream is
7
- * never history: once the log has the assistant event, the stream is gone
8
- * and the projected item takes its place (same id scheme, no flash).
9
- */
10
- import type { TranscriptItem } from "../../core/transcript.ts";
11
- import type { ImageView, ViewItem } from "../../tui/ui/items.ts";
12
- import type { ImageRef } from "../../core/images.ts";
13
- import { toolCard, toolDetail, toolPreview, toolSummary } from "../../tui/ui/tool-card.ts";
14
- import type { Style } from "../../tui/style.ts";
15
-
16
- export interface Notice { readonly id: string; readonly after: number; readonly item: ViewItem }
17
-
18
- export interface LiveStream {
19
- readonly thinking: string;
20
- readonly text: string;
21
- readonly tools: readonly { id: string; name: string; arguments: string }[];
22
- readonly output: string;
23
- readonly bash: string | null;
24
- readonly retry?: string;
25
- }
26
-
27
- export interface ProjectOptions {
28
- readonly notices: readonly Notice[];
29
- readonly live?: LiveStream;
30
- /** Extension renderers; return lines to replace the default card, undefined to keep it. */
31
- readonly customTool?: (item: Extract<TranscriptItem, { kind: "tool" }>) => ((width: number) => string[]) | undefined;
32
- readonly customMessage?: (item: Extract<TranscriptItem, { kind: "custom" }>) => ((width: number) => string[]) | undefined;
33
- readonly hiddenThinkingLabel?: string;
34
- readonly style?: () => Style | undefined;
35
- /**
36
- * Views of durable items from earlier paints, by item id. A log item never
37
- * changes without a new revision, so an equal key means an equal view, and
38
- * a long session re-projects only what changed (refactor-tui-chrome-cleanup
39
- * DoD 5). The caller clears it when renderers or labels change.
40
- */
41
- readonly cache?: Map<string, { readonly key: string; readonly view: ViewItem | undefined }>;
42
- }
43
-
44
- /** CutReason → words. Unknown reasons (future log versions) print verbatim. */
45
- const SEGMENT_REASONS: Readonly<Record<string, string>> = {
46
- trigger: "context boundary", hard_limit: "context limit", model_checkpoint: "model switch",
47
- };
48
-
49
- export function projectItems(items: readonly TranscriptItem[], options: ProjectOptions): ViewItem[] {
50
- const out: ViewItem[] = [];
51
- let notice = 0;
52
- const flush = (upTo: number) => { while (notice < options.notices.length && options.notices[notice]!.after <= upTo) out.push(options.notices[notice++]!.item); };
53
- flush(0);
54
- let segment = 1;
55
- items.forEach((item, index) => {
56
- // A segment's label depends on how many came before it, so that count is part of its key.
57
- const key = item.kind === "segment" ? `${item.revision}|${segment}` : item.revision;
58
- const hit = options.cache?.get(item.id);
59
- let view: ViewItem | undefined;
60
- if (hit && hit.key === key) { view = hit.view; if (item.kind === "segment" && item.nextNode === undefined) segment++; }
61
- else { view = projectOne(item, options, () => ++segment); options.cache?.set(item.id, { key, view }); }
62
- if (view) out.push(view);
63
- flush(index + 1);
64
- });
65
- while (notice < options.notices.length) out.push(options.notices[notice++]!.item);
66
- const live = options.live;
67
- if (live) {
68
- if (live.bash !== null) out.push({ kind: "tool", id: "live:bash", rev: `${live.bash.length}`, name: "shell", summary: "running", status: "pending", detail: () => live.bash!.split("\n").slice(-40) });
69
- const logged = new Set(items.flatMap(item => item.kind === "tool" ? [item.callId] : []));
70
- for (const tool of live.tools) {
71
- if (logged.has(tool.id)) continue;
72
- let input: unknown;
73
- try { input = JSON.parse(tool.arguments) as unknown; } catch { input = {}; }
74
- out.push({ kind: "tool", id: `live:tool:${tool.id}`, rev: `${tool.arguments.length}`, name: tool.name, summary: toolSummary(tool.name, input) || "preparing…", status: "pending",
75
- detail: width => toolDetail({ name: tool.name, input, result: null }, width, options.style?.()) });
76
- }
77
- if (live.output) out.push({ kind: "lines", id: "live:output", rev: `${live.output.length}`, render: () => live.output.split("\n").slice(-12) });
78
- if (live.retry) out.push({ kind: "notice", id: "live:retry", rev: live.retry, text: live.retry, tone: "warning" });
79
- if (live.thinking) out.push({ kind: "thinking", id: "live:thinking", rev: `${live.thinking.length}`, text: live.thinking, streaming: true, ...(options.hiddenThinkingLabel !== undefined ? { label: options.hiddenThinkingLabel } : {}) });
80
- if (live.text) out.push({ kind: "assistant", id: "live:text", rev: `${live.text.length}`, text: live.text, streaming: true });
81
- }
82
- return out;
83
- }
84
-
85
- function projectOne(item: TranscriptItem, options: ProjectOptions, nextSegment: () => number): ViewItem | undefined {
86
- const base = { id: item.id, rev: item.revision };
87
- switch (item.kind) {
88
- case "user":
89
- return { ...base, kind: "user", text: item.text, ...(item.images?.length ? { images: item.images.map(imageView) } : {}), ...(item.interrupted ? { interrupted: true } : {}) };
90
- case "assistant": return item.text.trim() ? { ...base, kind: "assistant", text: item.text } : undefined;
91
- case "thinking": return item.text.trim() ? { ...base, kind: "thinking", text: item.text, ...(options.hiddenThinkingLabel !== undefined ? { label: options.hiddenThinkingLabel } : {}) } : undefined;
92
- case "notice": return { ...base, kind: "notice", text: item.text };
93
- case "custom": {
94
- const render = options.customMessage?.(item);
95
- return render ? { ...base, kind: "lines", render } : { ...base, kind: "notice", text: `[${item.customType}] ${item.text}` };
96
- }
97
- case "tool": {
98
- const custom = options.customTool?.(item);
99
- const card = toolCard(item);
100
- return { ...base, kind: "tool", name: item.name, summary: card.summary, status: card.status, ...(card.evidence ? { evidence: card.evidence } : {}),
101
- ...(custom ? {} : { preview: toolPreview(item) }), ...(item.result?.images?.length ? { images: item.result.images.map(imageView) } : {}),
102
- detail: custom ?? (width => toolDetail(item, width, options.style?.())) };
103
- }
104
- case "shell": {
105
- const code = item.exit; // the logged exit code is the evidence (D11)
106
- // The log appends `exit N · status · reason` as the content's last line; the card's evidence already says it.
107
- const output = item.output.replace(/^exit -?\d+[^\n]*\n?/, "").replace(/\n?exit -?\d+ · [^\n]*$/, "").replace(/\s+$/, "");
108
- return { ...base, kind: "tool", name: item.excluded ? "!!" : "!", summary: item.command, status: code === 0 ? "ok" : "error", evidence: `exit ${code}`,
109
- // Shell output keeps its last two lines in view while folded (refactor-tui-chrome-cleanup DoD 2).
110
- preview: output.split("\n").filter(line => line.trim()).slice(-2),
111
- // The exit code is already the card's evidence; the body is the output itself.
112
- detail: () => (output || "(no output)").split("\n").slice(0, 200) };
113
- }
114
- case "segment": {
115
- const node = item.nextNode ?? nextSegment();
116
- return { ...base, kind: "segment", label: `node ${node} · ${SEGMENT_REASONS[item.reason] ?? item.reason}`,
117
- detail: `handoff ${shortRef(item.handoff)}${item.nextModel ? ` → ${item.nextModel}` : ""} · history before this line is carried as structured state, not replayed` };
118
- }
119
- }
120
- }
121
-
122
- function shortRef(ref: string): string { return ref.split("/").pop() ?? ref; }
123
-
124
- /** What the view needs of a logged image: its address, name and pixel size. Never the bytes (D8). */
125
- export function imageView(image: ImageRef): ImageView {
126
- return { sha256: image.sha256, name: image.name, width: image.width, height: image.height, ...(image.original ? { full: image.original.sha256 } : {}) };
127
- }