carthagent 1.0.7 → 1.1.0-preview.13

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 (289) hide show
  1. package/README.md +140 -83
  2. package/bin/ctg.mjs +14 -732
  3. package/docs/glyph-review/host-chrome-glyph-compatibility-v4.md +83 -0
  4. package/docs/glyph-review/test-isolation-v1.md +26 -0
  5. package/docs/glyph-review/typecheck-base.log +1 -0
  6. package/docs/glyph-review/typecheck-v4.log +1 -0
  7. package/docs/glyph-review/unicode-inventory-v4.csv +402 -0
  8. package/docs/help-dispatch-review.md +69 -0
  9. package/docs/legacy-cli.md +150 -0
  10. package/docs/live-theme-plugin-api.md +18 -0
  11. package/docs/native-cli.md +102 -0
  12. package/docs/native-compatibility.md +16 -0
  13. package/docs/native-daily-quests.md +132 -0
  14. package/docs/native-glyph-pty.md +59 -0
  15. package/docs/native-manual-testing.md +336 -0
  16. package/docs/native-slash-acceptance-handoff.md +49 -0
  17. package/docs/native-terminal-paired-failure-2026-10-17.md +40 -0
  18. package/docs/native-terminal-performance.md +310 -0
  19. package/docs/native-terminal-semantics-evidence/README.md +25 -0
  20. package/docs/native-terminal-semantics-evidence/controlled-host-model-selftest.json +16 -0
  21. package/docs/native-terminal-semantics-evidence/current-acceptance-status.json +59 -0
  22. package/docs/native-terminal-semantics-evidence/final-binary-focused-85bb44a.json +154 -0
  23. package/docs/native-terminal-semantics-evidence/final-binary-full-incomplete-85bb44a.json +160 -0
  24. package/docs/native-terminal-semantics-evidence/final-binary-layout-85bb44a.json +149 -0
  25. package/docs/native-terminal-semantics-evidence/final-binary-layout-interactive-85bb44a.json +159 -0
  26. package/docs/native-terminal-semantics-evidence/final-binary-model-picker-escape-unchanged-85bb44a.json +44 -0
  27. package/docs/native-terminal-semantics-evidence/final-binary-model-picker-exact-identity-85bb44a.json +47 -0
  28. package/docs/native-terminal-semantics-evidence/final-binary-model-picker-unchanged-model-incomplete-85bb44a.json +58 -0
  29. package/docs/native-terminal-semantics-evidence/final-binary-model-selftest.json +16 -0
  30. package/docs/native-terminal-semantics-evidence/final-binary-picker-cancel-incomplete-85bb44a.json +62 -0
  31. package/docs/native-terminal-semantics-evidence/focused-clipboard-title-stale-binary.json +161 -0
  32. package/docs/native-terminal-semantics-evidence/layout-stale-binary-observation-not-accepted.json +153 -0
  33. package/docs/native-terminal-semantics-evidence/picker-latest-incomplete-attempt.json +62 -0
  34. package/docs/native-terminal-semantics-evidence/picker-previous-probe-pass.json +61 -0
  35. package/docs/native-terminal-semantics.md +89 -0
  36. package/docs/native-upgrade.md +177 -0
  37. package/docs/opencode-composed-source-review.md +89 -0
  38. package/docs/opencode-migration.md +225 -9
  39. package/docs/opencode-owned-runtime.md +241 -0
  40. package/docs/opencode-runtime.md +12 -0
  41. package/docs/opencode-theme-provider-proof/README.md +55 -0
  42. package/docs/opencode-theme-provider-proof/live-theme-api-base.patch +122 -0
  43. package/docs/opencode-theme-provider-proof/plugin-theme-provider.patch +107 -0
  44. package/docs/opencode-theme-provider-proof/session-ui-base.patch +1217 -0
  45. package/docs/owned-host-controls-proof.md +39 -0
  46. package/docs/owned-runtime-release-manifest.md +42 -0
  47. package/docs/owned-visual-clear-proof.md +40 -0
  48. package/docs/patches/opencode-v2.0.32-glyph-spinner-test-isolation-v1.patch +50 -0
  49. package/docs/patches/opencode-v2.0.32-glyphs.patch +3670 -0
  50. package/docs/patches/opencode-v2.0.32-help-dispatch.patch +156 -0
  51. package/docs/patches/opencode-v2.0.32-live-theme-api.patch +122 -0
  52. package/docs/patches/opencode-v2.0.32-session-ui-combined-v1.patch +5272 -0
  53. package/docs/patches/opencode-v2.0.32-session-ui-combined-v2.patch +6052 -0
  54. package/docs/patches/opencode-v2.0.32-session-ui-combined-v3.patch +8657 -0
  55. package/docs/patches/opencode-v2.0.32-session-ui-combined-v4.patch +9928 -0
  56. package/docs/patches/opencode-v2.0.32-session-ui-combined-v5.patch +13965 -0
  57. package/docs/patches/opencode-v2.0.32-session-ui.patch +5137 -0
  58. package/docs/release-notes-native-draft.md +73 -0
  59. package/docs/release-notes-preview.10.md +121 -0
  60. package/docs/release-notes-preview.11.md +76 -0
  61. package/docs/release-notes-preview.12.md +55 -0
  62. package/docs/release-notes-preview.13.md +68 -0
  63. package/docs/release-notes-preview.9.md +130 -0
  64. package/docs/source-preview.md +120 -0
  65. package/evaluate.mjs +28 -1
  66. package/lib/budget.mjs +1 -1
  67. package/lib/codegraph.mjs +1 -1
  68. package/lib/delivery-review.mjs +11 -1
  69. package/lib/delivery.mjs +3 -2
  70. package/lib/environment.mjs +8 -0
  71. package/lib/guidance.mjs +1 -1
  72. package/lib/opencode/cli-runtime.mjs +1171 -0
  73. package/lib/opencode/cli.mjs +252 -0
  74. package/lib/opencode/credentials.mjs +63 -8
  75. package/lib/opencode/demo.mjs +53 -0
  76. package/lib/opencode/evaluation-budget-plugin/index.ts +3 -0
  77. package/lib/opencode/evaluation.mjs +32 -12
  78. package/lib/opencode/headless.mjs +8 -4
  79. package/lib/opencode/provider-policy.mjs +23 -0
  80. package/lib/opencode/review.mjs +37 -16
  81. package/lib/opencode/runtime-artifact.mjs +335 -0
  82. package/lib/opencode/runtime-github-transport.mjs +121 -0
  83. package/lib/opencode/runtime-release.json +36 -0
  84. package/lib/opencode/runtime.mjs +299 -20
  85. package/lib/opencode/service.mjs +77 -35
  86. package/lib/opencode/session-options.mjs +6 -2
  87. package/lib/opencode/update.mjs +222 -0
  88. package/lib/opencode/workspace.mjs +1 -1
  89. package/lib/planning-advice.mjs +2 -2
  90. package/lib/providers/experiential.mjs +2 -2
  91. package/lib/tui/framing.mjs +1 -80
  92. package/lib/update.mjs +10 -1
  93. package/package.json +33 -7
  94. package/plugins/carthagent/README.md +23 -0
  95. package/plugins/carthagent/src/bootstrap.ts +1 -1
  96. package/plugins/carthagent/src/daily-evidence.mjs +57 -0
  97. package/plugins/carthagent/src/daily-quests-rpc.ts +50 -0
  98. package/plugins/carthagent/src/daily-source-rpc.ts +52 -0
  99. package/plugins/carthagent/src/index.ts +71 -24
  100. package/plugins/carthagent/src/rpc.ts +51 -6
  101. package/plugins/carthagent/src/server/codegraph.ts +12 -5
  102. package/plugins/carthagent/src/server/daily-quests-rpc.ts +277 -0
  103. package/plugins/carthagent/src/server/daily-source-rpc.ts +211 -0
  104. package/plugins/carthagent/src/server/delivery.ts +227 -112
  105. package/plugins/carthagent/src/server/framing.mjs +80 -0
  106. package/plugins/carthagent/src/server/policy.ts +1 -1
  107. package/plugins/carthagent/src/server/providers.ts +59 -31
  108. package/plugins/carthagent/src/server/session-controls.ts +3 -1
  109. package/plugins/carthagent/src/server/ship-agents.ts +77 -0
  110. package/plugins/carthagent/src/server/ship-context.mjs +60 -0
  111. package/plugins/carthagent/src/server/ship-guidance.ts +41 -0
  112. package/plugins/carthagent/src/server/ship-rpc.ts +69 -0
  113. package/plugins/carthagent/src/server/voyages-rpc.ts +223 -0
  114. package/plugins/carthagent/src/testing.ts +5 -0
  115. package/plugins/carthagent/src/tui.tsx +496 -0
  116. package/plugins/carthagent/src/ui/client.ts +125 -0
  117. package/plugins/carthagent/src/ui/commands.ts +70 -0
  118. package/plugins/carthagent/src/ui/daily-editor-focus.mjs +52 -0
  119. package/plugins/carthagent/src/ui/daily-editor.mjs +4 -0
  120. package/plugins/carthagent/src/ui/daily-quests-events.mjs +18 -0
  121. package/plugins/carthagent/src/ui/daily-quests-state.mjs +218 -0
  122. package/plugins/carthagent/src/ui/daily-quests.tsx +235 -0
  123. package/plugins/carthagent/src/ui/host-state.ts +42 -0
  124. package/plugins/carthagent/src/ui/model-command.ts +32 -0
  125. package/plugins/carthagent/src/ui/native-navigation.ts +23 -0
  126. package/plugins/carthagent/src/ui/native-review.d.mts +33 -0
  127. package/plugins/carthagent/src/ui/native-review.mjs +159 -0
  128. package/plugins/carthagent/src/ui/resume-session.mjs +34 -0
  129. package/plugins/carthagent/src/ui/select-mouse.ts +17 -0
  130. package/plugins/carthagent/src/ui/session-preferences.d.mts +7 -0
  131. package/plugins/carthagent/src/ui/session-preferences.mjs +14 -0
  132. package/plugins/carthagent/src/ui/theme-provision.d.mts +3 -0
  133. package/plugins/carthagent/src/ui/theme-provision.mjs +134 -0
  134. package/plugins/carthagent/src/ui/voyages-navigation-state.mjs +56 -0
  135. package/plugins/carthagent/src/ui/voyages-navigation.tsx +131 -0
  136. package/plugins/carthagent/src/ui/voyages-state.mjs +142 -0
  137. package/plugins/carthagent/src/ui/voyages.tsx +129 -0
  138. package/plugins/carthagent/src/ui/workspace-live.ts +61 -0
  139. package/plugins/carthagent/src/ui/workspace.tsx +379 -0
  140. package/plugins/carthagent/src/voyages-rpc.ts +60 -0
  141. package/plugins/carthagent/src/workspace-rpc.ts +36 -0
  142. package/plugins/carthagent/tests/commands.test.ts +107 -0
  143. package/plugins/carthagent/tests/session-preferences.test.mjs +41 -0
  144. package/plugins/carthagent/tests/voyages-navigation.test.tsx +269 -0
  145. package/plugins/carthagent/themes/carthage.json +67 -0
  146. package/plugins/carthagent/tui.ts +26 -1
  147. package/script/ctg/build-owned-runtime.sh +166 -0
  148. package/tests/budget-engine.test.mjs +4 -3
  149. package/tests/cli.test.mjs +52 -96
  150. package/tests/daily-evidence.test.mjs +62 -0
  151. package/tests/delivery-review.test.mjs +11 -0
  152. package/tests/delivery.test.mjs +22 -1
  153. package/tests/fixtures/native-discovery-plugin/server.ts +15 -0
  154. package/tests/generate-owned-runtime-release.test.mjs +354 -0
  155. package/tests/installed-runtime-contract.test.mjs +43 -0
  156. package/tests/native-cli-host-smoke.mjs +457 -0
  157. package/tests/native-daily-editor-focus.test.mjs +213 -0
  158. package/tests/native-daily-editor.test.mjs +43 -0
  159. package/tests/native-daily-quests-layout.test.mjs +53 -0
  160. package/tests/native-daily-quests-rpc.test.mjs +456 -0
  161. package/tests/native-daily-quests-ui.test.mjs +778 -0
  162. package/tests/native-daily-source.test.mjs +451 -0
  163. package/tests/native-demo-ui.test.mjs +48 -0
  164. package/tests/native-evaluation-admission.test.mjs +24 -0
  165. package/tests/native-fixture-browser-cleanup.test.mjs +65 -0
  166. package/tests/native-fixture-runtime.test.mjs +111 -0
  167. package/tests/native-fixture-stop-diagnostics.test.mjs +67 -0
  168. package/tests/native-glyph-pty.test.mjs +107 -0
  169. package/tests/native-manual-projects.test.mjs +170 -0
  170. package/tests/native-model-command.test.mjs +67 -0
  171. package/tests/native-owned-runtime-env.test.mjs +140 -0
  172. package/tests/native-paired-guards.test.mjs +464 -0
  173. package/tests/native-provider-policy.test.mjs +31 -0
  174. package/tests/native-pty-invocation.test.mjs +114 -0
  175. package/tests/native-pty-runtime.test.mjs +45 -0
  176. package/tests/native-resume-session.test.mjs +46 -0
  177. package/tests/native-review-consumer.test.mjs +286 -0
  178. package/tests/native-runtime-identity.test.mjs +214 -0
  179. package/tests/native-select-mouse.test.mjs +23 -0
  180. package/tests/native-ship-parity.test.mjs +526 -0
  181. package/tests/native-ship-plan.test.mjs +85 -0
  182. package/tests/native-ship-rpc.test.mjs +71 -0
  183. package/tests/native-ui-commands.test.mjs +6 -0
  184. package/tests/native-voyages-layout.test.mjs +28 -0
  185. package/tests/native-voyages-navigation.test.mjs +169 -0
  186. package/tests/native-voyages-rpc.test.mjs +395 -0
  187. package/tests/native-voyages-ui.test.mjs +277 -0
  188. package/tests/opencode-atomic-write.test.mjs +55 -0
  189. package/tests/opencode-cli.test.mjs +1312 -0
  190. package/tests/opencode-codegraph-plugin.test.mjs +47 -0
  191. package/tests/opencode-delivery.test.mjs +16 -0
  192. package/tests/opencode-providers.test.mjs +245 -18
  193. package/tests/opencode-runtime.test.mjs +546 -8
  194. package/tests/opencode-session-controls.test.mjs +15 -0
  195. package/tests/opencode-ship-rpc.test.mjs +71 -0
  196. package/tests/opencode-update.test.mjs +475 -0
  197. package/tests/opencode-workspace.test.mjs +51 -2
  198. package/tests/owned-runtime-publish-workflow.test.mjs +477 -0
  199. package/tests/release-workflow.test.mjs +46 -0
  200. package/tests/runtime-artifact.test.mjs +269 -0
  201. package/tests/runtime-development-trust.test.mjs +230 -0
  202. package/tests/runtime-github-transport.test.mjs +132 -0
  203. package/tests/runtime-token-env.test.mjs +23 -0
  204. package/tests/server.test.mjs +3 -1
  205. package/tests/ship-telemetry.test.mjs +1 -1
  206. package/tests/source-preview.test.mjs +253 -0
  207. package/tests/test_native_terminal_probe.py +76 -0
  208. package/tests/test_native_terminal_semantics.py +232 -0
  209. package/tests/update.test.mjs +42 -5
  210. package/tests/verify-draft-runtime-gate.test.mjs +280 -0
  211. package/tests/verify-owned-runtime-release.test.mjs +371 -0
  212. package/tests/workspace-live.test.mjs +127 -0
  213. package/tools/benchmark/diagnose-request.mjs +59 -36
  214. package/tools/benchmark/eval-common.mjs +3 -3
  215. package/tools/benchmark/evaluators.test.mjs +4 -4
  216. package/tools/benchmark/guard.test.mjs +35 -2
  217. package/tools/benchmark/guard.ts +20 -7
  218. package/tools/benchmark/planning-startup.test.mjs +30 -34
  219. package/tools/benchmark/runner.mjs +12 -7
  220. package/tools/benchmark/runner.test.mjs +23 -7
  221. package/tools/benchmark/upstream-error.test.mjs +16 -2
  222. package/tools/generate-owned-runtime-release.mjs +217 -0
  223. package/tools/installed-runtime-contract.mjs +16 -0
  224. package/tools/manual-runtime-manifest.mjs +39 -0
  225. package/tools/native-fixture-browser-cleanup.mjs +59 -0
  226. package/tools/native-fixture-stop-diagnostics.mjs +43 -0
  227. package/tools/native-fixture.mjs +199 -40
  228. package/tools/native-glyph-helpers.py +127 -0
  229. package/tools/native-glyph-pty.py +144 -0
  230. package/tools/native-manual-projects.mjs +259 -0
  231. package/tools/native-manual.mjs +338 -0
  232. package/tools/native-model-picker-acceptance.py +306 -0
  233. package/tools/native-owned-runtime-env.sh +47 -0
  234. package/tools/native-paired-guards.mjs +282 -0
  235. package/tools/native-paired-manifest.mjs +72 -0
  236. package/tools/native-paired-orchestration.sh +51 -0
  237. package/tools/native-plugin-readiness.mjs +33 -0
  238. package/tools/native-plugin-readiness.test.mjs +53 -0
  239. package/tools/native-pty-invocation.mjs +26 -0
  240. package/tools/native-pty-runtime.mjs +83 -0
  241. package/tools/native-pty-smoke.py +147 -24
  242. package/tools/native-public-repo-shell-result.mjs +13 -0
  243. package/tools/native-public-repo-shell-result.test.mjs +28 -0
  244. package/tools/native-runtime-identity.mjs +201 -0
  245. package/tools/native-slash-acceptance-env.py +38 -0
  246. package/tools/native-terminal-semantics.py +876 -0
  247. package/tools/owned-shell-observer/index.ts +16 -0
  248. package/tools/owned-shell-observer/package.json +1 -0
  249. package/tools/owned-visual-clear-env.py +48 -0
  250. package/tools/owned-visual-clear-model.py +21 -0
  251. package/tools/pinned-owned-runtime-inputs.mjs +46 -0
  252. package/tools/prepare-native-manual.mjs +122 -0
  253. package/tools/run-native-terminal-paired.sh +403 -0
  254. package/tools/runtime-token-env.mjs +8 -0
  255. package/tools/seed-native-switch-fixture.mjs +79 -0
  256. package/tools/test-native-controls.mjs +3 -1
  257. package/tools/test-native-daily-quests.mjs +65 -0
  258. package/tools/test-native-default-cli.mjs +106 -0
  259. package/tools/test-native-delivery-host.mjs +102 -20
  260. package/tools/test-native-evaluation.mjs +7 -3
  261. package/tools/test-native-glyph-pty.mjs +94 -0
  262. package/tools/test-native-host.mjs +87 -63
  263. package/tools/test-native-outcomes.mjs +3 -1
  264. package/tools/test-native-package.mjs +206 -38
  265. package/tools/test-native-performance.mjs +26 -0
  266. package/tools/test-native-product.mjs +90 -0
  267. package/tools/test-native-pty.mjs +34 -9
  268. package/tools/test-native-public-repo.mjs +224 -0
  269. package/tools/test-native-refresh-reentry.mjs +106 -0
  270. package/tools/test-native-report.mjs +27 -20
  271. package/tools/test-native-review.mjs +75 -0
  272. package/tools/test-native-runtime.mjs +120 -0
  273. package/tools/test-native-ship-models.mjs +176 -0
  274. package/tools/test-native-ship-walkthrough.mjs +96 -0
  275. package/tools/test-native-slash-commands.mjs +125 -0
  276. package/tools/test-native-slash-commands.py +242 -0
  277. package/tools/test-native-swe-host.mjs +224 -55
  278. package/tools/test-native-tasks.mjs +77 -0
  279. package/tools/test-native-terminal-performance.py +887 -0
  280. package/tools/test-native-terminal-readiness.sh +85 -0
  281. package/tools/test-native-terminal-semantics-installed.mjs +38 -0
  282. package/tools/test-native-visual-clear-installed.mjs +48 -0
  283. package/tools/test-owned-host-controls.mjs +238 -0
  284. package/tools/test-owned-opencode-runtime.mjs +35 -0
  285. package/tools/test-owned-visual-clear-env.py +120 -0
  286. package/tools/test-owned-visual-clear.py +373 -0
  287. package/tools/verify-draft-runtime-gate.mjs +138 -0
  288. package/tools/verify-owned-runtime-release.mjs +114 -0
  289. package/tools/verify-release-version.mjs +21 -1
@@ -0,0 +1,156 @@
1
+ diff --git a/packages/tui/src/component/prompt/autocomplete.tsx b/packages/tui/src/component/prompt/autocomplete.tsx
2
+ index c669491..2c0c63a 100644
3
+ --- a/packages/tui/src/component/prompt/autocomplete.tsx
4
+ +++ b/packages/tui/src/component/prompt/autocomplete.tsx
5
+ @@ -472,15 +472,28 @@ export function Autocomplete(props: {
6
+ }
7
+
8
+ const commands = createMemo((): AutocompleteOption[] => {
9
+ - const results: AutocompleteOption[] = keymapCommands().flatMap((command) => {
10
+ + const registered = keymapCommands()
11
+ + let help: KeymapCommand | undefined
12
+ + const results: AutocompleteOption[] = registered.flatMap((command) => {
13
+ const slash = command.slash
14
+ if (!slash) return []
15
+ - return [slash.name, ...(slash.aliases ?? [])].map((name) => ({
16
+ + const names = [slash.name, ...(slash.aliases ?? [])]
17
+ + if (names.includes("help") && command.id !== "help.show") help = command
18
+ + return names.filter((name) => name !== "help").map((name) => ({
19
+ display: `/${name}`,
20
+ description: command.description ?? command.title,
21
+ onSelect: slash.arguments ? () => insertSlash(name) : command.run,
22
+ }))
23
+ })
24
+ + const helpCommand = help ?? registered.find((command) => command.id === "help.show")
25
+ + if (helpCommand?.slash) {
26
+ + const slash = helpCommand.slash
27
+ + results.push({
28
+ + display: "/help",
29
+ + description: helpCommand.description ?? helpCommand.title,
30
+ + onSelect: slash.arguments ? () => insertSlash("help") : helpCommand.run,
31
+ + })
32
+ + }
33
+ const commandNames = new Set<string>()
34
+
35
+ for (const serverCommand of data.location.command.list(location.current) ?? []) {
36
+ diff --git a/packages/tui/src/component/prompt/index.tsx b/packages/tui/src/component/prompt/index.tsx
37
+ index a9f699c..63fe438 100644
38
+ --- a/packages/tui/src/component/prompt/index.tsx
39
+ +++ b/packages/tui/src/component/prompt/index.tsx
40
+ @@ -173,11 +173,15 @@ function formatEditorContext(selection: EditorSelection) {
41
+ function argumentSlash(input: string, commands: readonly KeymapCommand[]) {
42
+ const head = parseSlashHead(input, /\s/)
43
+ if (!head) return
44
+ - const command = commands.find(
45
+ + const matches = commands.filter(
46
+ (command) =>
47
+ command.slash?.arguments &&
48
+ (command.slash.name === head.name || command.slash.aliases?.includes(head.name) === true),
49
+ )
50
+ + // The built-in help action is a fallback; let an extension own the natural
51
+ + // `/help` spelling regardless of which layer happened to register first.
52
+ + const command =
53
+ + head.name === "help" ? (matches.findLast((item) => item.id !== "help.show") ?? matches[0]) : matches[0]
54
+ if (!command) return
55
+ return { command, input: head.arguments }
56
+ }
57
+ diff --git a/packages/tui/test/plugin-keymap.test.tsx b/packages/tui/test/plugin-keymap.test.tsx
58
+ index 0790e9a..23f502c 100644
59
+ --- a/packages/tui/test/plugin-keymap.test.tsx
60
+ +++ b/packages/tui/test/plugin-keymap.test.tsx
61
+ @@ -59,6 +59,38 @@ test("setup layers before and after await reach keyboard, palette, and slash acr
62
+ await setup.waitForFrame((frame) => frame.includes("Sync keymap ran 1"))
63
+ })
64
+
65
+ +test("a plugin /help overrides native /help for natural autocomplete selection and restores it on disable", async () => {
66
+ + await using plugin = await copyHelpPlugin()
67
+ + const requests: URL[] = []
68
+ + await using setup = await createAppFixture({
69
+ + config: { animations: false, plugins: [plugin.directory] },
70
+ + fetch(url) {
71
+ + requests.push(url)
72
+ + },
73
+ + })
74
+ + await setup.ready
75
+ + await setup.waitForFrame((frame) => frame.includes("Help override ready"))
76
+ +
77
+ + await setup.mockInput.typeText("/help")
78
+ + await setup.waitForFrame((frame) => frame.includes("/help"))
79
+ + setup.mockInput.pressEnter()
80
+ + await setup.waitForFrame((frame) => frame.includes("Plugin help handler 2"))
81
+ + expect(requests.some((url) => url.pathname === "/session")).toBe(false)
82
+ +
83
+ + setup.mockInput.pressEscape()
84
+ + await openPlugin(setup, "help-override")
85
+ + await setup.waitForFrame((frame) => frame.includes("disable"))
86
+ + setup.mockInput.pressEnter()
87
+ + await setup.waitForFrame((frame) => frame.includes("inactive"))
88
+ + setup.mockInput.pressEscape()
89
+ +
90
+ + await setup.mockInput.typeText("/help")
91
+ + await setup.waitForFrame((frame) => frame.includes("/help"))
92
+ + setup.mockInput.pressEnter()
93
+ + await setup.waitForFrame((frame) => frame.includes("Press ctrl+p to see all available actions"))
94
+ + expect(requests.some((url) => url.pathname === "/session")).toBe(false)
95
+ +})
96
+ +
97
+ test("a component layer ends when its component unmounts while the plugin stays active", async () => {
98
+ await using plugin = await copyPlugin()
99
+ await using setup = await createAppFixture({ config: { animations: false, plugins: [plugin.directory] } })
100
+ @@ -138,6 +170,41 @@ async function copyPlugin() {
101
+ return { directory, entry, [Symbol.asyncDispose]: root[Symbol.asyncDispose] }
102
+ }
103
+
104
+ +async function copyHelpPlugin() {
105
+ + const root = await tmpdir()
106
+ + const directory = path.join(root.path, "help-override")
107
+ + const entry = path.join(directory, "tui.tsx")
108
+ + await mkdir(directory)
109
+ + await symlink(path.join(import.meta.dir, "../node_modules"), path.join(directory, "node_modules"))
110
+ + await Bun.write(entry, `
111
+ + import { Plugin } from "@opencode/plugin/tui"
112
+ + export default Plugin.define({
113
+ + id: "fixture.help-override",
114
+ + setup(context) {
115
+ + context.keymap.layer(() => ({
116
+ + mode: "global",
117
+ + commands: [
118
+ + {
119
+ + id: "fixture.help-override.first",
120
+ + title: "First plugin help",
121
+ + slash: { name: "help" },
122
+ + run: () => context.ui.dialog.show(() => <text>Plugin help handler 1</text>),
123
+ + },
124
+ + {
125
+ + id: "fixture.help-override.second",
126
+ + title: "Second plugin help",
127
+ + slash: { name: "help" },
128
+ + run: () => context.ui.dialog.show(() => <text>Plugin help handler 2</text>),
129
+ + },
130
+ + ],
131
+ + }))
132
+ + context.ui.toast.show({ message: "Help override ready" })
133
+ + },
134
+ + })
135
+ + `)
136
+ + return { directory, [Symbol.asyncDispose]: root[Symbol.asyncDispose] }
137
+ +}
138
+ +
139
+ async function search(setup: Fixture, query: string) {
140
+ setup.mockInput.pressKey("p", { ctrl: true })
141
+ await setup.waitForFrame((frame) => frame.includes("Commands"))
142
+ @@ -145,11 +212,11 @@ async function search(setup: Fixture, query: string) {
143
+ await setup.mockInput.typeText(query)
144
+ }
145
+
146
+ -async function openPlugin(setup: Fixture) {
147
+ +async function openPlugin(setup: Fixture, query = "setup-keymap") {
148
+ await setup.mockInput.typeText("/plugins")
149
+ await setup.waitForFrame((frame) => frame.includes("/plugins"))
150
+ setup.mockInput.pressEnter()
151
+ - await setup.waitForFrame((frame) => frame.includes("fixture.setup-keymap"))
152
+ + await setup.waitForFrame((frame) => frame.includes(query === "help-override" ? "fixture.help-override" : "fixture.setup-keymap"))
153
+ await setup.waitFor(() => setup.renderer.currentFocusedEditor instanceof InputRenderable)
154
+ - await setup.mockInput.typeText("setup-keymap")
155
+ + await setup.mockInput.typeText(query)
156
+ }
@@ -0,0 +1,122 @@
1
+ diff --git a/packages/plugin/src/tui/context.ts b/packages/plugin/src/tui/context.ts
2
+ index fe641b3..21ec351 100644
3
+ --- a/packages/plugin/src/tui/context.ts
4
+ +++ b/packages/plugin/src/tui/context.ts
5
+ @@ -490,6 +490,13 @@ export interface Keymap {
6
+ export interface UI {
7
+ readonly dialog: Dialog
8
+ readonly toast: Toast
9
+ + /** Live host theme controls. Theme files must already be available to the host. */
10
+ + readonly theme: {
11
+ + /** Returns the selected theme name and active color mode. */
12
+ + current(): { readonly name: string; readonly mode: "dark" | "light" }
13
+ + /** Switches the live theme and optionally locks its color mode; returns false if either is unavailable. */
14
+ + set(selection: { readonly name: string; readonly mode?: "dark" | "light" }): boolean
15
+ + }
16
+ readonly format: {
17
+ path(value: string): string
18
+ }
19
+ diff --git a/packages/tui/src/context/theme.tsx b/packages/tui/src/context/theme.tsx
20
+ index ed568e6..2d7d363 100644
21
+ --- a/packages/tui/src/context/theme.tsx
22
+ +++ b/packages/tui/src/context/theme.tsx
23
+ @@ -94,6 +94,7 @@ type Themes = {
24
+ mode: Accessor<"dark" | "light">
25
+ modes: Accessor<readonly ("dark" | "light")[]>
26
+ supports(mode: "dark" | "light"): boolean
27
+ + supportsThemeMode(theme: string, mode: "dark" | "light"): boolean
28
+ locked: Accessor<boolean>
29
+ lock(): void
30
+ unlock(): void
31
+ @@ -329,6 +330,10 @@ const themeContext = createSimpleContext({
32
+ mode,
33
+ modes,
34
+ supports: (requested) => modes().includes(requested),
35
+ + supportsThemeMode: (theme, requested) => {
36
+ + const source = available()[theme]
37
+ + return source !== undefined && themeModes(parseTheme(source, theme)).includes(requested)
38
+ + },
39
+ locked: () => store.lock !== undefined,
40
+ lock: () => pin(mode()),
41
+ unlock: free,
42
+ diff --git a/packages/tui/src/plugin/api.tsx b/packages/tui/src/plugin/api.tsx
43
+ index 35a4eac..4150ba9 100644
44
+ --- a/packages/tui/src/plugin/api.tsx
45
+ +++ b/packages/tui/src/plugin/api.tsx
46
+ @@ -204,6 +204,16 @@ export function createPluginContext(input: {
47
+ ui: {
48
+ dialog: dialogApi,
49
+ toast: toastApi,
50
+ + theme: {
51
+ + current: () => ({ name: host.themes.selected, mode: host.themes.mode() }),
52
+ + set(selection) {
53
+ + if (!selection || typeof selection.name !== "string" || !host.themes.has(selection.name)) return false
54
+ + if (selection.mode !== undefined && !host.themes.supportsThemeMode(selection.name, selection.mode)) return false
55
+ + if (!host.themes.set(selection.name)) return false
56
+ + if (selection.mode !== undefined) host.themes.setMode(selection.mode)
57
+ + return true
58
+ + },
59
+ + },
60
+ format: {
61
+ path: (value) => abbreviateHome(value, host.paths.home),
62
+ },
63
+ diff --git a/packages/tui/test/plugin-theme.test.tsx b/packages/tui/test/plugin-theme.test.tsx
64
+ new file mode 100644
65
+ index 0000000..9a46b4d
66
+ --- /dev/null
67
+ +++ b/packages/tui/test/plugin-theme.test.tsx
68
+ @@ -0,0 +1,54 @@
69
+ +import { expect, test } from "bun:test"
70
+ +import { createPluginContext, type Registry, type usePluginHost } from "../src/plugin/api"
71
+ +
72
+ +function setup() {
73
+ + let selected = "opencode"
74
+ + let mode: "dark" | "light" = "dark"
75
+ + const available = new Set(["opencode", "custom"])
76
+ + const host = {
77
+ + app: {},
78
+ + client: {},
79
+ + keymap: {},
80
+ + shortcuts: {},
81
+ + keymapState: {},
82
+ + sessionTabs: {},
83
+ + themes: {
84
+ + get selected() {
85
+ + return selected
86
+ + },
87
+ + mode: () => mode,
88
+ + has: (name: string) => available.has(name),
89
+ + supportsThemeMode: (name: string, requested: "dark" | "light") => requested === "dark" || name === "custom",
90
+ + set(name: string) {
91
+ + if (!available.has(name)) return false
92
+ + selected = name
93
+ + return true
94
+ + },
95
+ + setMode(requested: "dark" | "light") {
96
+ + mode = requested
97
+ + return true
98
+ + },
99
+ + },
100
+ + } as unknown as ReturnType<typeof usePluginHost>
101
+ + const registry: Registry = { has: () => false, set() {}, remove() {}, active: () => true }
102
+ + const theme = createPluginContext({ host, id: "test", options: undefined, owned: [], registry }).ui.theme
103
+ + return { theme, current: () => ({ selected, mode }) }
104
+ +}
105
+ +
106
+ +test("plugin theme setter applies available theme and mode live through the host service", () => {
107
+ + const harness = setup()
108
+ + expect(harness.theme.current()).toEqual({ name: "opencode", mode: "dark" })
109
+ + expect(harness.theme.set({ name: "custom", mode: "light" })).toBe(true)
110
+ + expect(harness.theme.current()).toEqual({ name: "custom", mode: "light" })
111
+ + expect(harness.current()).toEqual({ selected: "custom", mode: "light" })
112
+ +})
113
+ +
114
+ +test("plugin theme setter rejects unknown themes and unsupported modes without partial changes", () => {
115
+ + const harness = setup()
116
+ + expect(harness.theme.set({ name: "missing", mode: "dark" })).toBe(false)
117
+ + expect(harness.theme.set({ name: "custom", mode: "light" })).toBe(true)
118
+ + expect(harness.theme.set({ name: "opencode", mode: "light" })).toBe(false)
119
+ + expect(harness.theme.current()).toEqual({ name: "custom", mode: "light" })
120
+ + expect(harness.theme.set({ name: "opencode" })).toBe(true)
121
+ + expect(harness.theme.current()).toEqual({ name: "opencode", mode: "light" })
122
+ +})