@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,456 +0,0 @@
1
- /**
2
- * Processes that outlive one tool call: servers, REPLs, long builds, prompts.
3
- *
4
- * `exec_command` starts one and waits up to a yield time; if it is still
5
- * running, the model gets a session id and polls or types into it with
6
- * `write_stdin`. The shape and names follow Codex's unified exec
7
- * (codex-rs `unified_exec/`, Apache-2.0; design only), which is what GPT-family
8
- * models are trained against, and Warp's Terminal-Bench write-up found that
9
- * control over interactive programs was worth more than anything else.
10
- *
11
- * Every call still ends in one tool_result, so the log records each
12
- * observation in order (log-as-truth). What the log cannot hold is the
13
- * process itself: sessions belong to this OS process and die with it, and a
14
- * restarted xeno recovers observations from the log without reattaching a PID.
15
- *
16
- * Output is spooled to a file as it arrives. Each call archives exactly the
17
- * bytes since the previous call as an observation blob (D8), without keeping
18
- * cumulative output strings. Capture errors are explicit partial observations.
19
- */
20
- import { open, mkdir, rm, type FileHandle } from "node:fs/promises";
21
- import { createReadStream } from "node:fs";
22
- import { join } from "node:path";
23
- import { randomUUID } from "node:crypto";
24
- import { commandArgv, commandEnvironment, COMMAND_TERMINATION_GRACE_MS } from "../core/command-contract.ts";
25
- import { archiveObservation, type ArchivedObservation } from "../store/observations.ts";
26
- import { configDir } from "../store/config.ts";
27
- import { xenoBinDir } from "../store/tool-binaries.ts";
28
- import { ToolFault, NOT_STARTED } from "../core/tool-failure.ts";
29
- import { processReceipt, retainProcessReceipts, type ProcessReceipt, type ProcessSource } from "../core/process-receipt.ts";
30
- import type { CommandArtifactObserver } from "./command-artifacts.ts";
31
- import type { CommandArtifactReceipt } from "../core/command-artifacts.ts";
32
-
33
- /** Codex's default: long enough for most commands to finish in one call. */
34
- export const DEFAULT_YIELD_MS = 10_000;
35
- /** Bounds on a foreground poll or input send. Owned observation waits have no polling deadline. */
36
- export const MIN_YIELD_MS = 100;
37
- export const MAX_YIELD_MS = 30_000;
38
-
39
- /**
40
- * The wait actually taken, clamped like Codex's `clamp_yield_time` (S.6
41
- * item 5): an out-of-range value still runs the call, and `note` tells the
42
- * model what it waited instead of what it asked for.
43
- */
44
- export function yieldWait(value: unknown): { ms: number; note: string } {
45
- const requested = typeof value === "number" && Number.isFinite(value) ? value : DEFAULT_YIELD_MS;
46
- const ms = Math.min(MAX_YIELD_MS, Math.max(MIN_YIELD_MS, requested));
47
- return { ms, note: ms === requested ? "" : ` · yield_time_ms ${requested} clamped to ${ms}` };
48
- }
49
- /**
50
- * Bounds on one owned empty wait's window, copied from Codex's empty
51
- * `write_stdin` (MIN_EMPTY_YIELD_TIME_MS, DEFAULT_MAX_BACKGROUND_TERMINAL_TIMEOUT_MS),
52
- * not measured. The floor is what stops a chatty process from waking the
53
- * model every line: below it, a 100 ms request brings the per-line wakeups back.
54
- */
55
- export const MIN_EMPTY_YIELD_MS = 5_000;
56
- export const MAX_EMPTY_YIELD_MS = 300_000;
57
- export function emptyYieldWait(value: unknown): { ms: number; note: string } {
58
- const requested = typeof value === "number" && Number.isFinite(value) ? value : DEFAULT_YIELD_MS;
59
- const ms = Math.min(MAX_EMPTY_YIELD_MS, Math.max(MIN_EMPTY_YIELD_MS, requested));
60
- return { ms, note: ms === requested ? "" : ` · yield_time_ms ${requested} clamped to ${ms}` };
61
- }
62
- /**
63
- * Concurrent sessions. An assumption, not a measurement: comfortably above a
64
- * server + watcher + REPL, low enough that a runaway loop of starts is refused
65
- * well before it exhausts the machine.
66
- */
67
- export const MAX_SESSIONS = 16;
68
- /** Assumed drain bound for descendants which escaped the owned group; never claim EOF on expiry. */
69
- const EXIT_DRAIN_MS = 1_000;
70
- /** Disposable acceleration only. Original log receipts outlive these assumed memory bounds. */
71
- export const CLOSED_RECEIPT_LIMIT = 64;
72
- export const CLOSED_RECEIPT_AGE_MS = 10 * 60_000;
73
-
74
- type TakenOutput = {
75
- stored: ArchivedObservation; receipt: ProcessReceipt; wallMs: number;
76
- exit: number | null; stopped?: "timeout" | "cancelled";
77
- /**
78
- * Input that arrived after the process exited and was not written:
79
- * "control" for interrupt/EOF, whose intent the exit already satisfied;
80
- * "refused" for real input, which the caller must report as not sent.
81
- */
82
- unsent?: "control" | "refused";
83
- };
84
- type ClosedSession = { owner?: string; id: number; endedAt: number; receipt: ProcessReceipt };
85
-
86
- interface Session {
87
- owner?: string;
88
- id: number;
89
- command: string;
90
- cwd: string;
91
- tty: boolean;
92
- source: ProcessSource;
93
- artifactObserver?: CommandArtifactObserver;
94
- artifacts?: Promise<CommandArtifactReceipt>;
95
- started: number;
96
- pid: number;
97
- spool: FileHandle;
98
- spoolPath: string;
99
- queue: Promise<unknown>;
100
- operations: Promise<unknown>;
101
- final?: TakenOutput;
102
- captureError?: string;
103
- written: number;
104
- read: number;
105
- exit: number | null;
106
- stopped?: "timeout" | "cancelled";
107
- escalation?: ReturnType<typeof setTimeout>;
108
- timeoutMs: number;
109
- exited: Promise<void>;
110
- drained: Promise<void>;
111
- finishing?: Promise<void>;
112
- closeCapture(): Promise<void>;
113
- alive(): boolean;
114
- write(chars: string): void;
115
- kill(signal: NodeJS.Signals): void;
116
- timer: ReturnType<typeof setTimeout>;
117
- }
118
-
119
- const sessions = new Map<number, Session>();
120
- const closedSessions = new Map<number, ClosedSession>();
121
- // O.14: a restarted process must not accidentally reuse an old poll capability.
122
- let nextId = Number.parseInt(randomUUID().replaceAll("-", "").slice(0, 12), 16);
123
- let hooked = false;
124
-
125
- function killGroup(pid: number, signal: NodeJS.Signals): void {
126
- try { process.kill(-pid, signal); } catch (error) {
127
- if ((error as { code?: string }).code !== "ESRCH") {
128
- try { process.kill(pid, signal); } catch { /* already gone */ }
129
- }
130
- }
131
- }
132
-
133
- /** Every session dies with xeno: a server nobody can poll is a leak, not a feature. */
134
- export function killAllExecSessions(): void {
135
- for (const session of sessions.values()) {
136
- clearTimeout(session.timer);
137
- clearTimeout(session.escalation);
138
- if (session.exit === null) session.kill("SIGKILL");
139
- void session.spool.close().catch(() => {});
140
- void rm(session.spoolPath, { force: true });
141
- }
142
- sessions.clear();
143
- closedSessions.clear();
144
- }
145
-
146
- export function runningSessions(owner?: string): { id: number; command: string; exit: number | null }[] {
147
- return [...sessions.values()].filter(session => session.owner === owner).map(({ id, command, exit }) => ({ id, command, exit }));
148
- }
149
-
150
- function pruneClosed(now: number): void {
151
- const retained = retainProcessReceipts([...closedSessions.values()], now, CLOSED_RECEIPT_AGE_MS, CLOSED_RECEIPT_LIMIT);
152
- closedSessions.clear();
153
- for (const item of retained) closedSessions.set(item.id, item);
154
- }
155
-
156
- export function closedSession(id: number, owner?: string): ClosedSession | undefined {
157
- pruneClosed(performance.now());
158
- const item = closedSessions.get(id);
159
- return item?.owner === owner ? item : undefined;
160
- }
161
-
162
- function append(session: Session, chunk: Uint8Array): void {
163
- if (chunk.length === 0) return;
164
- const bytes = chunk.slice();
165
- session.queue = session.queue.then(async () => {
166
- let offset = 0;
167
- while (offset < bytes.length) {
168
- const { bytesWritten } = await session.spool.write(bytes, offset, bytes.length - offset, session.written);
169
- if (bytesWritten < 1) throw new Error("process output spool made no write progress");
170
- offset += bytesWritten; session.written += bytesWritten;
171
- }
172
- }).catch(error => {
173
- session.captureError = `process output capture failed: ${String(error)}`;
174
- stopSession(session, "cancelled");
175
- });
176
- }
177
-
178
- export async function startSession(input: {
179
- command: string; cwd: string; tty: boolean; timeoutMs: number; spoolDir: string; source: ProcessSource; owner?: string;
180
- artifactObserver?: CommandArtifactObserver;
181
- /** Internal literal launch capability; model exec_command remains a Bash contract. */
182
- argv?: readonly string[];
183
- }): Promise<Session | { error: string }> {
184
- if (sessions.size >= MAX_SESSIONS) {
185
- const list = runningSessions(input.owner).map((s) => s.id).join(", ");
186
- return { error: `refused: managed process capacity ${MAX_SESSIONS} reached; your handles: ${list || "none"}; finish or cancel an owned process before starting another` };
187
- }
188
- if (!hooked) { hooked = true; process.once("exit", killAllExecSessions); }
189
- await mkdir(input.spoolDir, { recursive: true });
190
- const spoolPath = join(input.spoolDir, `.${randomUUID()}.session`);
191
- const spool = await open(spoolPath, "w+", 0o600);
192
- const env = commandEnvironment(process.env, xenoBinDir(configDir()));
193
- let resolveExit!: () => void;
194
- const exited = new Promise<void>((resolve) => { resolveExit = resolve; });
195
- const session = {
196
- id: nextId++, ...(input.owner === undefined ? {} : { owner: input.owner }), command: input.command, cwd: input.cwd, tty: input.tty, source: input.source, started: performance.now(),
197
- spool, spoolPath, queue: Promise.resolve(), operations: Promise.resolve(), written: 0, read: 0,
198
- exit: null, timeoutMs: input.timeoutMs, exited, drained: Promise.resolve(),
199
- ...(input.artifactObserver ? { artifactObserver: input.artifactObserver } : {}),
200
- } as Omit<Session, "pid" | "write" | "kill" | "timer"> as Session;
201
-
202
- try { if (input.tty) {
203
- // A PTY: the line discipline turns ^C into SIGINT for the foreground job,
204
- // which is what an interactive program expects. Bun ships the PTY.
205
- let drained!: () => void;
206
- session.drained = new Promise<void>(resolve => { drained = resolve; });
207
- const proc = Bun.spawn(input.argv ? [...input.argv] : commandArgv(input.command), {
208
- cwd: input.cwd, env,
209
- terminal: { cols: 120, rows: 40, data: (_terminal, data) => append(session, data),
210
- // This callback closes the stream; only proc.exited supplies the command exit.
211
- exit: (_terminal, code) => { if (code !== 0) session.captureError = "PTY output stream closed with a read error"; drained(); } },
212
- });
213
- session.pid = proc.pid;
214
- session.alive = () => proc.exitCode === null;
215
- session.write = (chars) => { proc.terminal?.write(chars); };
216
- session.kill = (signal) => killGroup(proc.pid, signal);
217
- session.closeCapture = async () => { proc.terminal?.close(); drained(); };
218
- void proc.exited.then((code) => { session.exit = code; resolveExit(); void drainExitedSession(session); });
219
- } else {
220
- const proc = Bun.spawn(input.argv ? [...input.argv] : commandArgv(input.command), {
221
- cwd: input.cwd, env, stdin: "pipe", stdout: "pipe", stderr: "pipe", detached: true,
222
- });
223
- session.pid = proc.pid;
224
- session.alive = () => proc.exitCode === null;
225
- // Without a terminal there is no line discipline, so the two control
226
- // characters a model reaches for are mapped by hand: ^C interrupts the
227
- // group, ^D closes stdin.
228
- session.write = (chars) => {
229
- for (const part of chars.split(/(\u0003|\u0004)/)) {
230
- if (part === "\u0003") killGroup(proc.pid, "SIGINT");
231
- else if (part === "\u0004") void proc.stdin.end();
232
- else if (part.length > 0) { proc.stdin.write(part); void proc.stdin.flush(); }
233
- }
234
- };
235
- session.kill = (signal) => killGroup(proc.pid, signal);
236
- const readers: ReadableStreamDefaultReader<Uint8Array>[] = [];
237
- const pump = async (stream: ReadableStream<Uint8Array>) => {
238
- const reader = stream.getReader();
239
- readers.push(reader);
240
- try { for (;;) { const { done, value } = await reader.read(); if (done) return; append(session, value); await session.queue; } }
241
- catch (error) { session.captureError = `process output stream failed: ${String(error)}`; }
242
- finally { reader.releaseLock(); }
243
- };
244
- session.closeCapture = async () => { await Promise.allSettled(readers.map(reader => reader.cancel())); };
245
- session.drained = Promise.all([pump(proc.stdout), pump(proc.stderr)]).then(() => {});
246
- void proc.exited.then((code) => { session.exit = code; resolveExit(); void drainExitedSession(session); });
247
- } } catch (error) {
248
- await spool.close(); await rm(spoolPath, { force: true });
249
- const os = (error as { code?: string }).code;
250
- throw new ToolFault(os === "ENOENT" || os === "ENOTDIR" ? "environment_missing" : os === "EACCES" || os === "EPERM" ? "permission_denied" : "execution_failed", String(error), NOT_STARTED);
251
- }
252
- session.timer = setTimeout(() => stopSession(session, "timeout"), input.timeoutMs);
253
- sessions.set(session.id, session);
254
- return session;
255
- }
256
-
257
- export function findSession(id: number, owner?: string): Session | undefined {
258
- const session = sessions.get(id);
259
- return session?.owner === owner ? session : undefined;
260
- }
261
-
262
- function stopSession(session: Session, reason: "timeout" | "cancelled"): void {
263
- if (session.stopped !== undefined || session.exit !== null) return;
264
- session.stopped = reason;
265
- clearTimeout(session.timer);
266
- session.kill("SIGTERM");
267
- session.escalation = setTimeout(() => session.kill("SIGKILL"), COMMAND_TERMINATION_GRACE_MS);
268
- }
269
-
270
- /** A managed group is owned for its foreground command lifetime, never silently detached. */
271
- function drainExitedSession(session: Session): Promise<void> {
272
- // Once only: a later artifact retry must not signal a potentially reused PID/group.
273
- session.finishing ??= (async () => {
274
- try { await finishCapture(session); } catch (error) { session.captureError = `output drain failed: ${String(error)}`; }
275
- // The exit owns this observation even if no model polls until files change again.
276
- if (session.artifactObserver && session.exit !== null) {
277
- session.artifacts = session.artifactObserver.finish({ exit: session.exit,
278
- ...(session.stopped ? { stopped: session.stopped } : {}), capture: session.captureError ? "partial" : "complete" });
279
- await session.artifacts;
280
- }
281
- })();
282
- return session.finishing;
283
- }
284
- async function finishCapture(session: Session): Promise<void> {
285
- session.kill("SIGKILL");
286
- clearTimeout(session.escalation);
287
- clearTimeout(session.timer);
288
- let timer: ReturnType<typeof setTimeout> | undefined;
289
- const deadline = new Promise<false>(resolve => { timer = setTimeout(() => resolve(false), EXIT_DRAIN_MS); });
290
- const complete = await Promise.race([session.drained.then(() => true), deadline]);
291
- clearTimeout(timer);
292
- if (!complete) {
293
- session.captureError = "output drain deadline reached; a descendant may have escaped the owned group";
294
- await session.closeCapture();
295
- }
296
- await session.queue;
297
- }
298
-
299
- /** Wait for exit, the yield deadline or cancellation, whichever is first. */
300
- async function waitSession(session: Session, yieldMs: number, signal?: AbortSignal): Promise<void> {
301
- let timer: ReturnType<typeof setTimeout> | undefined;
302
- let onAbort: (() => void) | undefined;
303
- const deadline = new Promise<void>((resolve) => { timer = setTimeout(resolve, yieldMs); });
304
- const aborted = new Promise<void>((resolve) => {
305
- if (signal === undefined) return;
306
- onAbort = resolve;
307
- if (signal.aborted) resolve();
308
- else signal.addEventListener("abort", onAbort, { once: true });
309
- });
310
- try { await Promise.race([session.exited, deadline, aborted]); }
311
- finally { clearTimeout(timer); if (onAbort) signal?.removeEventListener("abort", onAbort); }
312
- if (signal?.aborted) stopSession(session, "cancelled");
313
- if (session.stopped !== undefined) await session.exited;
314
- if (session.exit !== null) await drainExitedSession(session);
315
- await session.queue;
316
- }
317
-
318
- export interface ObservationWait {
319
- /** Steering releases this observation wait without cancelling the owned command. */
320
- wake?: AbortSignal;
321
- onWaiting?: (active: boolean) => void;
322
- /** One window; output is delivered at a window's end, never as it arrives. */
323
- windowMs: number;
324
- }
325
-
326
- /**
327
- * OMP's job owner pattern: no paid model turn merely to observe elapsed time.
328
- * Exit, timeout, cancellation and steering end the wait at once. Output does
329
- * not: it is collected until the window ends, and a window with nothing new
330
- * rolls into the next. Waking per byte made a process that prints a warning
331
- * every few seconds cost one model request per line (session mv4wyiyg-lebu,
332
- * fix-owned-wait-coalesce-output); Codex collects to the deadline the same way.
333
- */
334
- async function waitForObservation(session: Session, signal: AbortSignal | undefined, wait: ObservationWait): Promise<void> {
335
- await session.queue;
336
- let interrupt!: () => void;
337
- const interrupted = new Promise<void>(resolve => { interrupt = resolve; });
338
- signal?.addEventListener("abort", interrupt, { once: true });
339
- wait.wake?.addEventListener("abort", interrupt, { once: true });
340
- const ended = () => session.exit !== null || signal?.aborted === true || wait.wake?.aborted === true;
341
- const pending = !ended();
342
- let timer: ReturnType<typeof setTimeout> | undefined;
343
- try {
344
- if (pending) {
345
- wait.onWaiting?.(true);
346
- while (!ended()) {
347
- const deadline = new Promise<void>(resolve => { timer = setTimeout(resolve, wait.windowMs); });
348
- await Promise.race([session.exited, interrupted, deadline]);
349
- clearTimeout(timer);
350
- await session.queue;
351
- if (session.written > session.read) break;
352
- }
353
- }
354
- } finally {
355
- clearTimeout(timer);
356
- signal?.removeEventListener("abort", interrupt);
357
- wait.wake?.removeEventListener("abort", interrupt);
358
- if (pending) wait.onWaiting?.(false);
359
- }
360
- if (signal?.aborted) stopSession(session, "cancelled");
361
- if (session.stopped !== undefined) await session.exited;
362
- if (session.exit !== null) await drainExitedSession(session);
363
- await session.queue;
364
- }
365
-
366
- /**
367
- * Bytes printed since the previous call, archived in full. A session whose
368
- * exit has now been reported is closed.
369
- */
370
- async function takeOutput(session: Session, outputStore: string, previewBytes: number): Promise<TakenOutput> {
371
- if (session.exit !== null) await drainExitedSession(session);
372
- await session.queue;
373
- if (session.exit !== null) await drainExitedSession(session);
374
- const from = session.read, to = session.written;
375
- // Snapshot exit before async archival: a later exit belongs to the next observation.
376
- const exit = session.exit;
377
- const path = session.spoolPath;
378
- async function* range(start: number): AsyncGenerator<Uint8Array> {
379
- if (to <= start) return;
380
- for await (const chunk of createReadStream(path, { start, end: to - 1 })) yield chunk as Uint8Array;
381
- }
382
- const stored = await archiveObservation(outputStore, range(from), previewBytes);
383
- const full = exit === null ? undefined : from === 0 ? stored : await archiveObservation(outputStore, range(0), previewBytes);
384
- const artifacts = exit === null ? session.artifactObserver?.pending : await session.artifacts;
385
- // Neither the cursor nor the handle advances until every referenced artifact exists.
386
- session.read = to;
387
- const taken: TakenOutput = { stored, exit, ...(session.stopped ? { stopped: session.stopped } : {}), wallMs: performance.now() - session.started,
388
- receipt: processReceipt({ session: session.id, source: session.source, exit, output: stored.ref, start: from, end: to,
389
- ...(session.stopped ? { stopped: session.stopped } : {}), ...(full ? { fullOutput: full.ref } : {}),
390
- ...(artifacts ? { artifacts } : {}),
391
- ...(session.captureError ? { capture: "partial" } : exit !== null ? { capture: "complete" } : {}) }) };
392
- if (exit !== null) {
393
- clearTimeout(session.timer);
394
- clearTimeout(session.escalation);
395
- sessions.delete(session.id);
396
- session.final = taken;
397
- closedSessions.set(session.id, { ...(session.owner === undefined ? {} : { owner: session.owner }), id: session.id,
398
- endedAt: performance.now(), receipt: taken.receipt });
399
- pruneClosed(performance.now());
400
- await session.closeCapture();
401
- await session.spool.close().catch(() => {});
402
- await rm(path, { force: true });
403
- }
404
- return taken;
405
- }
406
-
407
- /**
408
- * Input made only of Ctrl-C / Ctrl-D. Sent to a process that has already
409
- * exited, it asks for something the exit already did (stop, end of input), so
410
- * it is answered as a poll rather than refused -- otherwise the exit and the
411
- * unread tail wait for a second call the model may never make (ticket session
412
- * mv4ugd29, write-stdin-exited card).
413
- */
414
- export function isControlInput(chars: string): boolean {
415
- return chars.length > 0 && /^[\u0003\u0004]+$/.test(chars);
416
- }
417
-
418
- /** One lease covers stdin, waiting, archival and cursor commit; cancellation can interrupt a queued wait. */
419
- export async function pollSession(session: Session, outputStore: string, previewBytes: number, yieldMs: number,
420
- chars = "", signal?: AbortSignal, wait?: ObservationWait): Promise<TakenOutput> {
421
- const cancel = () => stopSession(session, "cancelled");
422
- if (signal?.aborted) cancel(); else signal?.addEventListener("abort", cancel, { once: true });
423
- const result = session.operations.then(async () => {
424
- if (session.final) {
425
- if (chars.length > 0 && !isControlInput(chars)) throw new ToolFault("not_found", "process is closed; input was not sent", NOT_STARTED);
426
- return { ...session.final, receipt: { ...session.final.receipt, replayed: true } };
427
- }
428
- // An exit not yet reported is still delivered with any late input: the
429
- // input is never written, but the exit and unread output are not withheld.
430
- let unsent: TakenOutput["unsent"];
431
- if (chars.length > 0) {
432
- if (session.exit !== null) unsent = isControlInput(chars) ? "control" : "refused";
433
- else session.write(chars);
434
- }
435
- if (chars.length === 0 && wait) await waitForObservation(session, signal, wait);
436
- else await waitSession(session, yieldMs, signal);
437
- const taken = await takeOutput(session, outputStore, previewBytes);
438
- return unsent === undefined ? taken : { ...taken, unsent };
439
- });
440
- session.operations = result.catch(() => {});
441
- try { return await result; }
442
- finally { signal?.removeEventListener("abort", cancel); }
443
- }
444
-
445
- /** Stop only this owner, drain its actual exit and preserve every unread byte (D8). */
446
- export async function disposeExecSessions(owner: string, outputStore: string, previewBytes: number) {
447
- const owned = [...sessions.values()].filter(session => session.owner === owner);
448
- const results = await Promise.all(owned.map(async session => {
449
- stopSession(session, "cancelled");
450
- const output = await pollSession(session, outputStore, previewBytes, 0);
451
- if (output.exit === null) throw new Error("disposed process has no exit observation");
452
- if (output.receipt.replayed) return null;
453
- return { session: session.id, command: session.command, exit: output.exit, output: output.stored.ref, receipt: output.receipt };
454
- }));
455
- return results.filter(item => item !== null);
456
- }
@@ -1,40 +0,0 @@
1
- /** O.1: the creator owns cancellation and waits for cleanup before admitting another run. */
2
- export class ExecutionControl {
3
- private active: { controller: AbortController; done: Promise<void>; finish: () => void } | null = null;
4
-
5
- begin(parent?: AbortSignal): { signal: AbortSignal; finish: () => void } {
6
- if (this.active !== null) throw new Error("this session already has an active execution");
7
- const controller = new AbortController();
8
- let finish!: () => void;
9
- const done = new Promise<void>(resolve => { finish = resolve; });
10
- const owned = { controller, done, finish };
11
- this.active = owned;
12
- const abort = () => controller.abort();
13
- parent?.addEventListener("abort", abort, { once: true });
14
- if (parent?.aborted) abort();
15
- return {
16
- signal: controller.signal,
17
- finish: () => {
18
- parent?.removeEventListener("abort", abort);
19
- if (this.active === owned) this.active = null;
20
- finish();
21
- },
22
- };
23
- }
24
-
25
- cancel(): void { this.active?.controller.abort(); }
26
- whenIdle(): Promise<void> { return this.active?.done ?? Promise.resolve(); }
27
- get running(): boolean { return this.active !== null; }
28
- get signal(): AbortSignal | undefined { return this.active?.controller.signal; }
29
- }
30
-
31
- /** Only for admission waits: an unstarted action has no effect to drain. */
32
- export async function waitForAdmission<T>(pending: Promise<T>, signal?: AbortSignal): Promise<T | undefined> {
33
- if (signal === undefined) return pending;
34
- if (signal.aborted) return undefined;
35
- let abort!: () => void;
36
- const cancelled = new Promise<undefined>(resolve => { abort = () => resolve(undefined); });
37
- signal.addEventListener("abort", abort, { once: true });
38
- try { return await Promise.race([pending, cancelled]); }
39
- finally { signal.removeEventListener("abort", abort); }
40
- }
@@ -1,37 +0,0 @@
1
- /** Reload local dependency graphs without clearing unrelated process modules. */
2
- import { dirname, isAbsolute, join } from "node:path";
3
- import { pathToFileURL } from "node:url";
4
- import { mkdtemp, rm } from "node:fs/promises";
5
- import { tmpdir } from "node:os";
6
-
7
- export async function loadExtensionFactory(path: string, generation: number): Promise<unknown> {
8
- // Pi uses a fresh jiti loader with moduleCache:false. Bun can rebuild the
9
- // local graph itself, so this adds no production loader dependency (D13).
10
- const result = await Bun.build({ entrypoints: [path], target: "bun", format: "esm",
11
- plugins: [{ name: "extension-local-graph", setup(build) {
12
- build.onResolve({ filter: /^[^./]/ }, args => {
13
- if (isAbsolute(args.path) || args.path.startsWith("node:") || args.path.startsWith("bun:")) return undefined;
14
- // A package belongs to the extension's installation. Never auto-install
15
- // dependencies or resolve them against the current workspace.
16
- return { path: Bun.resolveSync(args.path, dirname(args.importer || path)), external: true };
17
- });
18
- build.onLoad({ filter: /\.[cm]?[jt]sx?$/ }, async args => {
19
- const url = pathToFileURL(args.path).href;
20
- const transpiler = new Bun.Transpiler({ loader: /tsx$/.test(args.path) ? "tsx" : /jsx$/.test(args.path) ? "jsx" : "ts",
21
- define: { "import.meta.url": JSON.stringify(url), "import.meta.dir": JSON.stringify(dirname(args.path)),
22
- "import.meta.dirname": JSON.stringify(dirname(args.path)), "import.meta.path": JSON.stringify(args.path),
23
- "import.meta.filename": JSON.stringify(args.path), "import.meta.main": "false" } });
24
- return { contents: transpiler.transformSync(await Bun.file(args.path).text()), loader: "js" };
25
- });
26
- } }] });
27
- if (!result.success || !result.outputs[0]) throw new Error(result.logs.map(log => log.message).join("\n") || "Extension build failed");
28
- const code = await result.outputs[0].text() + `\n//# sourceURL=${pathToFileURL(path).href}?generation=${generation}\n`;
29
- // A fresh file gives Bun an ordinary module identity without its data-URL
30
- // path-length limit. The original import.meta locations were frozen above.
31
- const staging = await mkdtemp(join(tmpdir(), "xeno-extension-module-"));
32
- try {
33
- const entry = join(staging, "extension.mjs"); await Bun.write(entry, code);
34
- const module: { default?: unknown } = await import(pathToFileURL(entry).href);
35
- return module.default;
36
- } finally { await rm(staging, { recursive: true, force: true }); }
37
- }
@@ -1,47 +0,0 @@
1
- /**
2
- * What an extension may do to the interactive UI (D20). The runtime never
3
- * constructs a terminal; the app supplies an implementation.
4
- *
5
- * Deliberately small. Extensions describe content (`render(width): string[]`
6
- * ANSI lines) and ask for decisions (select / confirm / input); they do not
7
- * own focus, overlays or the composer. That keeps a misbehaving extension
8
- * from wedging input, and keeps the UI framework replaceable without
9
- * breaking extensions again.
10
- */
11
- import type { Theme } from "../core/theme.ts";
12
-
13
- /** A block of ANSI lines that may depend on width. */
14
- export interface ExtensionComponent { render(width: number): string[]; dispose?(): void }
15
-
16
- export interface ExtensionTheme extends Theme {
17
- fg(color: string, text: string): string; bg(color: string, text: string): string;
18
- bold(text: string): string; italic(text: string): string; underline(text: string): string; dim(text: string): string; inverse(text: string): string; strikethrough(text: string): string;
19
- }
20
-
21
- export interface DialogOptions { signal?: AbortSignal; timeout?: number }
22
-
23
- export interface ExtensionUI {
24
- select(title: string, options: string[], opts?: DialogOptions): Promise<string | undefined>;
25
- confirm(title: string, message: string, opts?: DialogOptions): Promise<boolean>;
26
- input(title: string, placeholder?: string, opts?: DialogOptions): Promise<string | undefined>;
27
- /** Read-only block in a dialog; resolves when the user closes it. */
28
- show(title: string, component: ExtensionComponent): Promise<void>;
29
- notify(message: string, type?: "info" | "warning" | "error"): void;
30
- setStatus(key: string, text: string | undefined): void;
31
- /** Lines shown above the composer, keyed so an extension can replace or clear its own. */
32
- setWidget(key: string, lines: string[] | undefined): void;
33
- setWorkingMessage(message?: string): void;
34
- setHiddenThinkingLabel(label?: string): void;
35
- setTitle(title: string): void;
36
- getEditorText(): string; setEditorText(text: string): void; pasteToEditor(text: string): void;
37
- readonly theme: ExtensionTheme;
38
- getToolsExpanded(): boolean; setToolsExpanded(expanded: boolean): void;
39
- }
40
-
41
- /** Headless extensions can notify; interactive requests fail explicitly. */
42
- export function headlessUI(notify: ExtensionUI["notify"]): ExtensionUI {
43
- return new Proxy({ notify }, { get(target, key) {
44
- if (key === "notify") return target.notify;
45
- return () => { throw new Error("Extension UI requires an interactive terminal; check ctx.hasUI"); };
46
- } }) as ExtensionUI;
47
- }