@visual-z/xeno 0.8.4 → 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 (350) hide show
  1. package/CHANGELOG.md +8 -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,346 +0,0 @@
1
- /**
2
- * Custom endpoints and connected-provider management, reached from `/provider`.
3
- *
4
- * Flow (magpie's add form + OpenCode's /connect, see
5
- * docs/tasks/feat-provider-connect.md): choose a preset or a custom endpoint
6
- * → base URL and id (custom only) → API key (masked; `$VAR` keeps it in the
7
- * environment) → list the provider's own models with that key → check the
8
- * ones to enable → save → offer to switch.
9
- *
10
- * Every write goes through the files the rest of xeno already reads:
11
- * `auth.json` for keys and `models.json` `providers.<id>` for endpoints and
12
- * enabled models. After saving, the catalog is reloaded through the same
13
- * entry `/reload` uses, so a connected provider behaves exactly like a
14
- * hand-configured one.
15
- */
16
- import { modelKey } from "../core/session-position.ts";
17
- import { loadCatalog } from "../runtime/catalog.ts";
18
- import type { ModelSpec } from "../runtime/model.ts";
19
- import { discoverModels, discoveryFamily, likelyChatModel, normalizeBaseUrl, type DiscoveredModel } from "../runtime/model-discovery.ts";
20
- import { SOURCE_PROVIDERS } from "../runtime/model-data.ts";
21
- import { applyConnect, applyDisconnect, catalogTwin } from "../runtime/provider-connect.ts";
22
- import { CUSTOM_PRESETS, presetById, slugProviderId, suggestProviderId, validProviderId, type ProviderPreset } from "../runtime/provider-presets.ts";
23
- import { providerAccess } from "../runtime/provider-access.ts";
24
- import { writeAuthFile } from "../store/auth-store.ts";
25
- import { authPath } from "../store/config.ts";
26
- import { modelsPath, readModelSources, readModelsText, writeModelsFile } from "../store/models.ts";
27
- import { publishModelCatalog } from "./model-catalog.ts";
28
- import type { App } from "./startup.ts";
29
- import type { DialogHost } from "../tui/ui/dialog.ts";
30
- import type { ListItem } from "../tui/ui/list-model.ts";
31
- type SelectItem = ListItem;
32
-
33
- /** Lists at or below this size are preselected whole; larger ones keep only known models. magpie uses 24. */
34
- const PRESELECT_ALL = 24;
35
-
36
- export interface ConnectHost {
37
- app: App;
38
- wizard: Pick<DialogHost, "push" | "reset" | "pop" | "top" | "depth">;
39
- /** Unmount the wizard. */
40
- close(): void;
41
- switchTo(spec: ModelSpec): Promise<void>;
42
- fetch?: (input: string, init?: RequestInit) => Promise<Response>;
43
- }
44
-
45
- interface Draft {
46
- preset: ProviderPreset;
47
- id: string;
48
- name: string;
49
- baseUrl: string;
50
- key: string;
51
- /** `$VAR` / `{env:VAR}` / `!cmd`: stays in config, never stored. */
52
- reference?: string;
53
- previous?: readonly string[];
54
- }
55
-
56
- export function providerReady(app: App, provider: string): { ready: number; source?: string } {
57
- const models = app.catalog.filter(model => model.provider === provider);
58
- const resolved = models.map(model => app.credentialOf(model)).find(got => got.ok);
59
- return { ready: resolved ? models.filter(model => app.credentialOf(model).ok).length : 0, ...(resolved?.ok ? { source: resolved.source } : {}) };
60
- }
61
-
62
- function providerName(app: App, id: string): string {
63
- return SOURCE_PROVIDERS[id]?.name ?? (app.catalog.find(model => model.provider === id) ? connectedName(id) : id);
64
- }
65
-
66
- function connectedName(id: string): string {
67
- return CUSTOM_PRESETS.find(preset => preset.id === id)?.label ?? id;
68
- }
69
-
70
- /**
71
- * Entry points from `/provider`. API-key and subscription sign-in stay on the
72
- * login picker itself (their masked editor and OAuth flows are fixed by
73
- * tests); the wizard owns what login never did: custom endpoints and
74
- * per-provider model choice.
75
- */
76
- export function startConnect(host: ConnectHost, entry: "custom" | "manage"): void {
77
- const { app, wizard } = host;
78
- if (entry === "custom") {
79
- wizard.push({
80
- kind: "list", keep: true,
81
- title: "Which protocol does the endpoint speak?",
82
- items: CUSTOM_PRESETS.map(preset => ({ value: preset.id, label: preset.label, description: preset.hint ?? "" })),
83
- onPick: item => { const preset = presetById(item.value); if (preset) beginPreset(host, preset); },
84
- });
85
- return;
86
- }
87
- const connected = [...new Set(app.catalog.map(model => model.provider))].filter(id => providerReady(app, id).ready > 0).sort();
88
- if (connected.length === 0) {
89
- wizard.push({ kind: "message", title: "No provider is connected yet", lines: ["Choose a subscription, an API-key provider or a custom endpoint from /provider first."], onDone: () => host.close() });
90
- return;
91
- }
92
- wizard.push({
93
- kind: "list", keep: true,
94
- title: `${connected.length} connected · choose one to manage`,
95
- items: connected.map(id => {
96
- const state = providerReady(app, id);
97
- return { value: id, label: providerName(app, id), description: `${state.ready} model${state.ready === 1 ? "" : "s"} · ${state.source ?? "configured"}` };
98
- }),
99
- onPick: item => manage(host, item.value),
100
- });
101
- }
102
-
103
- /** A provider configured by hand (or by an earlier /provider) that is not a preset. */
104
- function customFromCatalog(app: App, id: string): ProviderPreset | undefined {
105
- const model = app.catalog.find(candidate => candidate.provider === id);
106
- if (!model) return undefined;
107
- return { id, label: id, api: model.api, baseUrl: model.baseUrl ?? "", catalog: false, custom: true };
108
- }
109
-
110
- function manage(host: ConnectHost, id: string): void {
111
- const { app, wizard } = host;
112
- const preset = presetById(id) ?? customFromCatalog(app, id);
113
- if (!preset) return;
114
- const enabled = app.catalog.filter(model => model.provider === id).map(model => model.id);
115
- wizard.push({
116
- kind: "list", keep: true,
117
- title: providerName(app, id),
118
- items: [
119
- { value: "models", label: "Choose models", description: "list the provider's models again and pick which appear in /model" },
120
- { value: "key", label: "Replace API key", description: app.auth[id] ? `stored in ${authPath()}` : "currently from the environment or config" },
121
- { value: "remove", label: "Disconnect", description: "remove the stored key and this provider's endpoint config" },
122
- ],
123
- onPick: async item => {
124
- const sample = app.catalog.find(model => model.provider === id);
125
- const baseUrl = sample?.baseUrl ?? preset.baseUrl;
126
- const got = sample ? app.credentialOf(sample) : undefined;
127
- const draft: Draft = { preset, id, name: preset.custom ? id : preset.label, baseUrl, key: got?.ok ? got.key : "", previous: enabled };
128
- if (item.value === "models") { await fetchModels(host, draft); return; }
129
- if (item.value === "key") { askKey(host, draft); return; }
130
- await disconnect(host, id);
131
- },
132
- });
133
- }
134
-
135
- function beginPreset(host: ConnectHost, preset: ProviderPreset): void {
136
- const { app, wizard } = host;
137
- const draft: Draft = { preset, id: preset.id, name: preset.label, baseUrl: preset.baseUrl, key: "" };
138
- if (!preset.custom) { askKey(host, draft); return; }
139
- wizard.push({
140
- kind: "form", keep: true,
141
- title: `${preset.label} · base URL`,
142
- label: "URL",
143
- value: preset.baseUrl,
144
- hint: preset.api === "anthropic-messages" ? "the root, e.g. https://gateway.example.com/anthropic · Enter continue · Esc back"
145
- : preset.api === "google-generative-ai" ? "e.g. https://generativelanguage.googleapis.com/v1beta · Enter continue · Esc back"
146
- : "usually ends in /v1 · a pasted …/chat/completions is trimmed · Enter continue · Esc back",
147
- validate: value => /^https?:\/\/[^\s/]+/.test(value) ? undefined : "Enter an http(s) URL",
148
- onSubmit: value => {
149
- draft.baseUrl = normalizeBaseUrl(value);
150
- const taken = new Set(app.catalog.map(model => model.provider).filter(id => !(id in app.auth) && SOURCE_PROVIDERS[id] !== undefined));
151
- const suggested = preset.keyless ? preset.id : suggestProviderId(draft.baseUrl, taken);
152
- wizard.push({
153
- kind: "form", keep: true,
154
- title: `${preset.label} · name`,
155
- label: "Provider ID",
156
- value: suggested,
157
- hint: "shown in /model as <id>/<model>; lowercase a-z, digits, - and _",
158
- validate: id => !validProviderId(id) ? `Use lowercase a-z, digits, - and _ (no spaces, capitals or non-Latin letters); try "${slugProviderId(id) || suggested}"`
159
- : SOURCE_PROVIDERS[id] !== undefined ? `"${id}" is a built-in provider; pick another id or choose it from the list` : undefined,
160
- onSubmit: id => {
161
- draft.id = id;
162
- draft.name = id;
163
- if (preset.keyless) { void fetchModels(host, draft); return; }
164
- askKey(host, draft);
165
- },
166
- });
167
- },
168
- });
169
- }
170
-
171
- function askKey(host: ConnectHost, draft: Draft): void {
172
- const { app, wizard } = host;
173
- const sample = app.catalog.find(model => model.provider === draft.id);
174
- const existing = sample ? app.credentialOf(sample) : undefined;
175
- const reuse = existing?.ok ? existing.source : undefined;
176
- wizard.push({
177
- kind: "form", keep: true,
178
- title: `${draft.name} · API key`,
179
- label: "Key",
180
- secret: true,
181
- hint: [reuse ? `empty keeps the current key (${reuse})` : draft.preset.keyUrl ? `get one at ${draft.preset.keyUrl}` : "paste the key",
182
- "$VAR reads the environment", "Enter continue · Esc back"].join(" · "),
183
- validate: value => value.length === 0 && !reuse ? "Paste a key, or $VAR to read one from the environment" : undefined,
184
- onSubmit: async value => {
185
- if (value.length === 0 && existing?.ok) draft.key = existing.key;
186
- else if (/^(\$|\{env:|!)/.test(value)) {
187
- draft.reference = value;
188
- const name = /^\$\{?([A-Za-z_][A-Za-z0-9_]*)\}?$/.exec(value)?.[1] ?? /^\{env:([A-Za-z_][A-Za-z0-9_]*)\}$/.exec(value)?.[1];
189
- draft.key = name ? process.env[name] ?? "" : "";
190
- } else draft.key = value;
191
- await fetchModels(host, draft);
192
- },
193
- });
194
- }
195
-
196
- async function fetchModels(host: ConnectHost, draft: Draft): Promise<void> {
197
- const { app, wizard } = host;
198
- const family = discoveryFamily(draft.preset.api);
199
- const known = app.catalog.filter(model => model.provider === draft.id);
200
- const builtin = loadCatalog({ userText: null }).models.filter(model => model.provider === draft.id);
201
- const catalogModels = [...new Map([...builtin, ...known].map(model => [model.id, model])).values()];
202
- if (!family) { chooseModels(host, draft, catalogModels.map(toDiscovered), undefined); return; }
203
- const abort = new AbortController();
204
- wizard.push({ kind: "busy", title: "models", message: `Listing models from ${draft.baseUrl} …`, onCancel: () => abort.abort() });
205
- const headers = Object.fromEntries(Object.entries(providerAccess(known[0] ?? ({} as ModelSpec)).headers ?? {}).filter((entry): entry is [string, string] => typeof entry[1] === "string" && !entry[1].includes("$")));
206
- const result = await discoverModels({ family, baseUrl: draft.baseUrl, key: draft.key, headers, fetch: host.fetch ?? ((input, init) => globalThis.fetch(input, init)), signal: abort.signal });
207
- if (abort.signal.aborted || wizard.top?.kind !== "busy") return;
208
- if (result.ok) {
209
- const live = result.models;
210
- // Catalog metadata wins for display; the live list decides availability.
211
- const merged = live.map(model => {
212
- const twin = catalogModels.find(candidate => candidate.id === model.id);
213
- return twin ? { ...toDiscovered(twin), ...model, name: model.name ?? twin.name } : model;
214
- });
215
- chooseModels(host, draft, merged, `${live.length} models from ${result.url}`, true);
216
- return;
217
- }
218
- const options: SelectItem[] = [
219
- { value: "retry", label: "Retry", description: result.reason },
220
- { value: "key", label: "Change the API key" },
221
- ...(draft.preset.custom ? [{ value: "url", label: "Change the base URL", description: draft.baseUrl }] : []),
222
- ...(catalogModels.length ? [{ value: "catalog", label: `Use the ${catalogModels.length} built-in models`, description: "skip the live list" }] : []),
223
- { value: "manual", label: "Type model IDs", description: "for endpoints without a /models listing" },
224
- ];
225
- wizard.push({
226
- kind: "list", keep: true,
227
- title: `Could not list models: ${result.reason}`,
228
- items: options,
229
- onPick: async item => {
230
- if (item.value === "retry") { wizard.pop(); await fetchModels(host, draft); }
231
- else if (item.value === "key") { wizard.pop(); wizard.pop(); askKey(host, draft); }
232
- else if (item.value === "url") { while (wizard.depth > 1 && wizard.top?.title !== `${draft.preset.label} · base URL`) wizard.pop(); }
233
- else if (item.value === "catalog") chooseModels(host, draft, catalogModels.map(toDiscovered), "built-in catalog");
234
- else wizard.push({
235
- kind: "form", keep: true, title: "Model IDs", label: "IDs", hint: "comma or space separated, exactly as the provider names them · Enter save",
236
- validate: value => value.split(/[\s,]+/).filter(Boolean).length ? undefined : "Enter at least one model id",
237
- onSubmit: async value => { await save(host, draft, value.split(/[\s,]+/).filter(Boolean).map(id => ({ id }))); },
238
- });
239
- },
240
- }, true);
241
- }
242
-
243
- function toDiscovered(model: ModelSpec): DiscoveredModel {
244
- return { id: model.id, ...(model.name !== model.id ? { name: model.name } : {}),
245
- ...(model.contextWindow ? { contextWindow: model.contextWindow } : {}), ...(model.maxTokens ? { maxTokens: model.maxTokens } : {}) };
246
- }
247
-
248
- function chooseModels(host: ConnectHost, draft: Draft, models: readonly DiscoveredModel[], source: string | undefined, replace = false): void {
249
- const { app, wizard } = host;
250
- const chat = models.filter(model => likelyChatModel(model.id));
251
- const known = new Set(app.catalog.filter(model => model.provider === draft.id).map(model => model.id));
252
- const preselect = (model: DiscoveredModel): boolean => {
253
- if (draft.previous) return draft.previous.includes(model.id);
254
- if (!likelyChatModel(model.id)) return false;
255
- if (chat.length <= PRESELECT_ALL) return true;
256
- return known.has(model.id) || catalogTwin(model.id, app.catalog) !== undefined && draft.preset.custom === false;
257
- };
258
- const ordered = [...chat, ...models.filter(model => !likelyChatModel(model.id))];
259
- const items: ListItem[] = ordered.map(model => ({
260
- value: model.id,
261
- label: model.id,
262
- description: [model.name && model.name !== model.id ? model.name : "", model.contextWindow ? `${Math.round(model.contextWindow / 1000)}k ctx` : "", likelyChatModel(model.id) ? "" : "not a chat model?"].filter(Boolean).join(" · "),
263
- }));
264
- wizard.push({
265
- kind: "list", multi: true, keep: true,
266
- title: `${draft.name} · models to enable${source ? ` (${source})` : ""}`,
267
- items, checked: ordered.filter(preselect).map(model => model.id),
268
- hint: "space toggle · ctrl+a all · enter save · esc back",
269
- onSubmitChecked: async values => {
270
- if (!values.length) { fail(host, "Select at least one model (space toggles, ctrl+a selects all)"); return; }
271
- await save(host, draft, ordered.filter(model => values.includes(model.id)));
272
- },
273
- }, replace);
274
- }
275
-
276
- async function save(host: ConnectHost, draft: Draft, chosen: readonly DiscoveredModel[]): Promise<void> {
277
- const { app, wizard } = host;
278
- const read = await readModelsText();
279
- let json: unknown = {};
280
- if (read.error) { fail(host, `${read.error}; nothing was changed`); return; }
281
- if (read.text !== null && read.text.trim().length) {
282
- try { json = JSON.parse(read.text); }
283
- catch { fail(host, `${modelsPath()} is not valid JSON; fix it first — nothing was changed`); return; }
284
- }
285
- const catalogDefault = SOURCE_PROVIDERS[draft.id]?.baseUrl;
286
- const keyless = draft.preset.keyless === true;
287
- const next = applyConnect(json, {
288
- id: draft.id, name: draft.name, api: draft.preset.api, baseUrl: draft.baseUrl, catalog: !draft.preset.custom,
289
- ...(catalogDefault ? { catalogBaseUrl: catalogDefault } : {}), keyless,
290
- ...(draft.reference ? { keyReference: draft.reference } : {}), chosen,
291
- }, loadCatalog({ userText: null }).models);
292
- if (!keyless && !draft.reference && draft.key && (app.auth[draft.id]?.type !== "api_key" || app.auth[draft.id]?.type === "api_key" && (app.auth[draft.id] as { key: string }).key !== draft.key)) {
293
- const auth = { ...app.auth, [draft.id]: { type: "api_key" as const, key: draft.key } };
294
- await writeAuthFile(auth);
295
- app.auth[draft.id] = auth[draft.id];
296
- }
297
- await writeModelsFile(next);
298
- const loaded = loadCatalog(await readModelSources());
299
- await publishModelCatalog(app, loaded.models);
300
- const ready = app.catalog.filter(model => model.provider === draft.id && app.credentialOf(model).ok);
301
- const warnings = loaded.warnings.filter(warning => warning.includes(draft.id));
302
- if (ready.length === 0) {
303
- fail(host, warnings[0] ?? "saved, but no model of this provider resolves a credential yet — check the key");
304
- return;
305
- }
306
- app.io.say(app.io.palette.green(` connected ${draft.name}`) + app.io.palette.dim(` ${ready.length} model${ready.length === 1 ? "" : "s"} · ${modelsPath()}`));
307
- wizard.reset({
308
- kind: "list",
309
- title: `Connected ${draft.name} · ${ready.length} model${ready.length === 1 ? "" : "s"} ready. Switch now?`,
310
- items: [
311
- ...ready.map(model => ({ value: modelKey(model), label: model.id, description: model.name !== model.id ? model.name : "" })),
312
- { value: "stay", label: "Keep the current model", description: modelKey(app.providerConfig.spec) },
313
- ],
314
- onPick: async item => {
315
- host.close();
316
- const spec = app.catalog.find(model => modelKey(model) === item.value);
317
- if (spec) await host.switchTo(spec);
318
- },
319
- });
320
- }
321
-
322
- async function disconnect(host: ConnectHost, id: string): Promise<void> {
323
- const { app } = host;
324
- const read = await readModelsText();
325
- if (read.text !== null && read.text.trim().length) {
326
- let json: unknown;
327
- try { json = JSON.parse(read.text); } catch { fail(host, `${modelsPath()} is not valid JSON; nothing was changed`); return; }
328
- await writeModelsFile(applyDisconnect(json, id));
329
- }
330
- if (app.auth[id] !== undefined) {
331
- const auth = { ...app.auth };
332
- delete auth[id];
333
- await writeAuthFile(auth);
334
- delete app.auth[id];
335
- }
336
- const loaded = loadCatalog(await readModelSources());
337
- if (app.providerConfig.spec.provider === id) app.io.say(app.io.palette.yellow(" the current model belonged to this provider; choose another with /model"));
338
- await publishModelCatalog(app, loaded.models);
339
- const left = providerReady(app, id).ready;
340
- app.io.say(app.io.palette.green(` disconnected ${id}`) + (left ? app.io.palette.dim(` (${left} models still resolve a key from the environment)`) : ""));
341
- host.close();
342
- }
343
-
344
- function fail(host: ConnectHost, reason: string): void {
345
- host.wizard.push({ kind: "message", title: "Not saved", lines: [reason] });
346
- }
@@ -1,27 +0,0 @@
1
- /** Existing app dialogs own focus and cancellation; question tools receive only validated values. */
2
- import type { QuestionRequest, QuestionReply, QuestionAnswer } from "../core/question.ts";
3
- import type { ExtensionUI } from "../runtime/extension-ui.ts";
4
-
5
- export async function presentQuestions(ui: ExtensionUI, request: QuestionRequest, signal: AbortSignal): Promise<QuestionReply | undefined> {
6
- const answers: QuestionAnswer[] = [];
7
- for (const question of request.questions) {
8
- const selected = new Set<string>(); let text: string | undefined;
9
- if (!question.options.length) {
10
- text = await ui.input(`${question.header}: ${question.question}`, "Your answer", { signal });
11
- if (!text?.trim() || signal.aborted) return undefined;
12
- } else for (;;) {
13
- const labels = question.options.map(option => `${selected.has(option.id) ? "✓ " : ""}[${option.id}] ${option.label}${option.description ? " — " + option.description : ""}`);
14
- const custom = "Enter a custom answer", submit = "Submit selected answers";
15
- const choice = await ui.select(`${question.header}: ${question.question}`, [...labels, ...(question.custom ? [custom] : []), ...(question.multiple ? [submit] : [])], { signal });
16
- if (choice === undefined || signal.aborted) return undefined;
17
- if (choice === custom && question.custom) { text = await ui.input(question.question, "Your answer", { signal }); if (!text?.trim() || signal.aborted) return undefined; break; }
18
- if (choice === submit && question.multiple) { if (selected.size) break; ui.notify("Select at least one answer.", "warning"); continue; }
19
- const index = labels.indexOf(choice); if (index < 0) return undefined;
20
- const id = question.options[index]!.id;
21
- if (!question.multiple) { selected.add(id); break; }
22
- if (selected.has(id)) selected.delete(id); else selected.add(id);
23
- }
24
- answers.push({ id: question.id, choices: [...selected], ...(text === undefined ? {} : { text }) });
25
- }
26
- return { request_id: request.request_id, turn_id: request.turn_id, answers };
27
- }
@@ -1,181 +0,0 @@
1
- /**
2
- * Loop events become lines. The shell prints them; this file does not
3
- * touch a terminal, so "what a tool call looks like" can be asserted
4
- * without starting one.
5
- *
6
- * Colour is a parameter, not a global. Piped and CI runs have no tty,
7
- * and a CSI 2026 (or even a dim code) in `-p` output is a parse error
8
- * waiting for a script.
9
- */
10
-
11
- import { assertNever } from "../core/exhaustive.ts";
12
- import type { LoopEvent } from "../runtime/loop.ts";
13
- import { ANSI as ANSI_STYLE, IDENTITY, type Style } from "../tui/style.ts";
14
- import { toolCard, toolDetail } from "../tui/ui/tool-card.ts";
15
-
16
- export interface Palette {
17
- dim: (s: string) => string;
18
- bold: (s: string) => string;
19
- red: (s: string) => string;
20
- green: (s: string) => string;
21
- yellow: (s: string) => string;
22
- cyan: (s: string) => string;
23
- }
24
-
25
- const identity = (s: string): string => s;
26
-
27
- const ANSI: Palette = {
28
- dim: (s) => `\x1b[2m${s}\x1b[0m`,
29
- bold: (s) => `\x1b[1m${s}\x1b[0m`,
30
- red: (s) => `\x1b[31m${s}\x1b[0m`,
31
- green: (s) => `\x1b[32m${s}\x1b[0m`,
32
- yellow: (s) => `\x1b[33m${s}\x1b[0m`,
33
- cyan: (s) => `\x1b[36m${s}\x1b[0m`,
34
- };
35
-
36
- const PLAIN: Palette = {
37
- dim: identity,
38
- bold: identity,
39
- red: identity,
40
- green: identity,
41
- yellow: identity,
42
- cyan: identity,
43
- };
44
-
45
- export function createPalette(color: boolean): Palette {
46
- return color ? ANSI : PLAIN;
47
- }
48
-
49
- /**
50
- * Colour is a tty AND not NO_COLOR. A theme that still emits ESC in a pipe
51
- * or under NO_COLOR is the same class of bug as createPalette(true) on a
52
- * redirected stdout.
53
- */
54
- export function colorEnabled(): boolean {
55
- const noColor = process.env["NO_COLOR"];
56
- if (noColor !== undefined && noColor !== "") return false;
57
- return stdoutIsTty();
58
- }
59
-
60
- /** True only for a real tty. `undefined` (piped, CI) must not count. */
61
- export function stdoutIsTty(): boolean {
62
- return process.stdout.isTTY === true;
63
- }
64
-
65
- export function stdinIsTty(): boolean {
66
- return process.stdin.isTTY === true;
67
- }
68
-
69
- /**
70
- * One readable line from whatever was thrown.
71
- *
72
- * Never a stack. A stack is for whoever wrote the harness; the person running
73
- * it needs the status and the provider's own sentence, which is the part that
74
- * says whether this is their key, their quota, or the provider being down.
75
- */
76
- export function describeError(error: unknown): string {
77
- if (error instanceof Error) {
78
- const status = (error as { status?: unknown }).status;
79
- const prefix = typeof status === "number" ? `HTTP ${status}: ` : "";
80
- return error.message.startsWith("HTTP ") ? error.message : prefix + error.message;
81
- }
82
- return String(error);
83
- }
84
-
85
- export function indent(text: string, pad: string): string {
86
- return text.split("\n").map((l) => pad + l).join("\n");
87
- }
88
-
89
- export function wrap(text: string, width: number): string {
90
- const out: string[] = [];
91
- let line = "";
92
- for (const word of text.split(/\s+/)) {
93
- if (line && line.length + word.length + 1 > width) { out.push(line); line = word; }
94
- else line = line ? `${line} ${word}` : word;
95
- }
96
- if (line) out.push(line);
97
- return out.join("\n");
98
- }
99
-
100
- export function firstLine(text: string, width: number): string {
101
- const line = text.split("\n")[0] ?? "";
102
- return line.length > width ? line.slice(0, width - 1) + "…" : line;
103
- }
104
-
105
- /**
106
- * Zero or more lines that the shell will print, matching the previous
107
- * inline switch one-for-one -- including the leading newline on
108
- * stop_and_ask, which is a blank line in the transcript.
109
- *
110
- * Kinds the old CLI ignored stay silent so this extraction cannot change
111
- * what a piped run prints.
112
- */
113
- function markdownStyle(C: Palette, style?: Style): Style {
114
- if (style !== undefined) return style;
115
- // Palette has no colour flag; identity wrappers are how -p and pipes
116
- // already opt out of CSI. A sentinel that round-trips is that same gate.
117
- return C.bold("\0") === "\0" ? IDENTITY : ANSI_STYLE;
118
- }
119
-
120
- export interface FormatFlags {
121
- readonly toolsExpanded?: boolean;
122
- readonly thinkingExpanded?: boolean;
123
- }
124
-
125
- export function formatEvent(
126
- e: LoopEvent,
127
- C: Palette,
128
- width = 80,
129
- flags: FormatFlags = {},
130
- style?: Style,
131
- ): string[] {
132
- const md = markdownStyle(C, style);
133
- switch (e.kind) {
134
- case "thinking": {
135
- // Print mode sends this to stderr, so 2>&1 can still read the reasoning in full.
136
- const text = (e.text ?? "").trim();
137
- void flags;
138
- return text ? text.split("\n").map(line => C.dim(` ${line}`)) : [];
139
- }
140
- case "assistant_text":
141
- // Markdown stays source text in a pipe: scripts read it, and terminals render the TUI.
142
- return (e.text ?? "").split("\n");
143
- case "tool_call": {
144
- const tool = e.tool ?? { name: "", input: {} };
145
- const card = toolCard({ name: tool.name, input: tool.input, result: null });
146
- return [C.cyan(` → ${tool.name}${card.summary ? ` ${card.summary}` : ""}`)];
147
- }
148
- case "tool_result": {
149
- const tool = e.tool ?? { name: "", input: {} };
150
- const call = { name: tool.name, input: tool.input, result: { text: e.text ?? "", ok: e.ok !== false } };
151
- const card = toolCard(call);
152
- const head = ` ${e.ok === false ? "err" : "ok "}${card.evidence ? ` ${card.evidence}` : ""}`;
153
- const body = flags.toolsExpanded === true ? toolDetail({ ...call, input: tool.name === "bash" ? {} : tool.input }, width - 6, md).map(line => ` ${line}`) : [];
154
- return [e.ok === false ? C.red(head) : C.dim(head), ...body];
155
- }
156
- case "usage": {
157
- const u = e.usage!;
158
- const line =
159
- ` ${u.input_tokens} new ${u.cache_read_input_tokens} cached ` +
160
- `${u.cache_creation_input_tokens} written ${u.output_tokens} out ${u.ms}ms`;
161
- return [e.ok ? C.dim(line) : C.yellow(line + ` ⚠ ${e.prefix}`)];
162
- }
163
- case "cut":
164
- return [C.yellow(` ── node ${e.node} closed (${e.reason}); handoff written`)];
165
- case "stop_and_ask":
166
- return [C.red(`\n stopping: ${e.reason}`)];
167
- case "done":
168
- if (e.reason && e.reason !== "end_turn") return [C.dim(` [${e.reason}]`)];
169
- return [];
170
- case "turn_start":
171
- case "branch_start":
172
- case "branch_done":
173
- case "text_delta":
174
- case "thinking_delta":
175
- case "discard":
176
- case "progress":
177
- return [];
178
- default:
179
- return assertNever(e.kind, "formatEvent");
180
- }
181
- }
@@ -1,66 +0,0 @@
1
- import { loadSessionResources, sessionToolCatalog, processPermissionRules, type App, type Args } from "./startup.ts";
2
- import { loadCatalog, type createProvider } from "../runtime/catalog.ts";
3
- import { readModelSources } from "../store/models.ts";
4
- import { stageModelCatalog, applyModelCatalog } from "./model-catalog.ts";
5
- import { sessionControl } from "../runtime/session-control.ts";
6
-
7
- export interface ResourceReloadOptions {
8
- readonly ownerLocked?: boolean;
9
- readonly args?: Args;
10
- readonly providerFactory?: typeof createProvider;
11
- }
12
-
13
- /** Parsing and discovery never retire the currently published runtime. */
14
- export async function reloadAppResources(app: App, options: ResourceReloadOptions = {}): Promise<boolean> {
15
- if (!options.ownerLocked) return sessionControl(app.session).withIdleChange(() => reloadAppResources(app, { ...options, ownerLocked: true }));
16
- const args = structuredClone(options.args ?? app.args);
17
- const resources = await loadSessionResources(args, app.workspace, app.io, app.projectTrusted, app.foundation?.questionSlot);
18
- let published = false;
19
- try {
20
- const catalog = loadCatalog(await readModelSources());
21
- for (const warning of catalog.warnings) app.io.say(app.io.palette.yellow(warning));
22
- const profileModel = args.role?.profile.model;
23
- const stage = stageModelCatalog(app, catalog.models, {
24
- ...(profileModel ? { model: args.model, provider: args.provider, ...(args.thinking ? { thinking: args.thinking } : {}), requireCredential: true } : {}),
25
- ...(options.providerFactory ? { providerFactory: options.providerFactory } : {}),
26
- });
27
- const tools = sessionToolCatalog(args, resources.extensions, app.session.config.frozen.tools);
28
- const hostRules = app.session.config.tools.appOwnsPermissions === true ? undefined : app.session.config.tools.hostPermissionRules ?? app.session.config.tools.rules ?? [];
29
- const rules = processPermissionRules(args, resources.foundation, hostRules);
30
- const previousMode = app.session.config.tools.workflowMode, previousProfile = app.session.config.agentProfile, previousScope = app.session.config.workflowToolProfile;
31
- const nextProfile = args.agent ? { name: args.agent, workflowExplicit: args.cliFlags?.includes("--workflow") === true } : undefined;
32
- const nextScope = args.toolProfile === "foundation" ? "foundation" as const : undefined;
33
- const metadataChanged = previousMode !== (args.workflow ?? "build") || JSON.stringify(previousProfile) !== JSON.stringify(nextProfile) || previousScope !== nextScope;
34
- // Pairing is checked before retiring any existing integration generation.
35
- app.session.assertResourceIdle();
36
- if (app.extensions && !await app.extensions.releaseIntegrations()) { app.extensions.restoreIntegrations(); throw Error("An integration still owns unfinished work or unconfirmed cleanup. Wait for it to settle before reloading; no replacement was activated."); }
37
- app.session.config.tools.workflowMode = args.workflow ?? "build";
38
- if (nextProfile) app.session.config.agentProfile = nextProfile; else delete app.session.config.agentProfile;
39
- if (nextScope) app.session.config.workflowToolProfile = nextScope; else delete app.session.config.workflowToolProfile;
40
- let cut: boolean;
41
- try { cut = await app.session.reloadResources({ system: resources.system, tools: tools.schemas }, [...resources.slash.skills.values()], stage.next, metadataChanged); }
42
- catch (error) {
43
- if (previousMode === undefined) delete app.session.config.tools.workflowMode; else app.session.config.tools.workflowMode = previousMode;
44
- if (previousProfile === undefined) delete app.session.config.agentProfile; else app.session.config.agentProfile = previousProfile;
45
- if (previousScope === undefined) delete app.session.config.workflowToolProfile; else app.session.config.workflowToolProfile = previousScope;
46
- app.extensions?.restoreIntegrations();
47
- throw error;
48
- }
49
- const oldExtensions = app.extensions, oldFoundation = app.foundation;
50
- app.args = args; app.extensions = resources.extensions; app.foundation = resources.foundation;
51
- app.session.config.tools.questionSlot = resources.foundation.questionSlot;
52
- app.session.config.tools.instructionCheck = (ctx, name, input) => resources.foundation.instructions(ctx, name, input, app.args.noContextFiles);
53
- app.session.config.tools.extensions = resources.extensions.definitions(); app.session.config.tools.deferredTools = tools.deferred;
54
- app.session.config.tools.extensionHooks = resources.extensions.toolHooks(); app.session.config.tools.rules = rules;
55
- if (hostRules) app.session.config.tools.hostPermissionRules = structuredClone([...hostRules]);
56
- app.session.config.maxTurns = args.maxTurns;
57
- if (args.timeBudgetMs === undefined) delete app.session.config.timeBudgetMs; else app.session.config.timeBudgetMs = args.timeBudgetMs;
58
- app.loadedContext = resources.loadedContext; app.slash = resources.slash; app.ui = resources.ui;
59
- app.resources = resources.index; app.projectTrusted = resources.trusted; app.system = app.session.config.frozen.system;
60
- published = true; applyModelCatalog(app, stage);
61
- // The new owner is live. Cleanup errors cannot roll it back to an already retired generation.
62
- try { if (oldExtensions) await oldExtensions.dispose(); else await oldFoundation?.dispose(); }
63
- catch { app.io.err("Previous resources could not finish cleanup; the new checkpoint remains active."); }
64
- return cut;
65
- } finally { if (!published) await resources.extensions.dispose().catch(() => {}); }
66
- }