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
@@ -5,8 +5,20 @@ CTG runs the package-owned native OpenCode v2.0.32 CLI from the pinned
5
5
  user installation. An explicitly supplied `CTG_OPENCODE_BIN` is accepted only
6
6
  when its version is exactly 2.0.32.
7
7
 
8
+ Runtime identity records the concrete executable path and SHA-256, including
9
+ when an explicit bare command is resolved through the supplied `PATH`. A
10
+ healthy process registered to a different runtime remains untouched and blocks
11
+ selection changes. A dead registration, including a legacy record without an
12
+ identity, is removed only after its PID is confirmed dead, so it cannot
13
+ permanently prevent startup.
14
+
8
15
  ## Isolation and persistence
9
16
 
17
+ An additional separately distributed pinned source variant is documented in
18
+ [`opencode-owned-runtime.md`](./opencode-owned-runtime.md). It is opt-in via
19
+ `CTG_OWNED_OPENCODE_RUNTIME`; unset behavior remains the package-owned
20
+ `@opencode/cli@2.0.32` runtime described here.
21
+
10
22
  The default CTG profile is `~/.carthagent/opencode`. `CTG_OPENCODE_HOME` (or a
11
23
  `root` argument in the runtime API) can select another profile. CTG applies
12
24
  private `XDG_CONFIG_HOME`, `XDG_DATA_HOME`, `XDG_STATE_HOME`, and
@@ -0,0 +1,55 @@
1
+ # Live plugin theme provider proof
2
+
3
+ This focused integration test closes a boundary left open by
4
+ `plugin-theme.test.tsx` in the approved live-theme API patch: those unit tests
5
+ mock `host.themes`, while this proof mounts the real Solid `ThemeProvider` and
6
+ `ConfigProvider` and passes the actual provider service to `createPluginContext`.
7
+
8
+ ## Reproduction
9
+
10
+ Start from upstream OpenCode
11
+ `843a6c514e606b436a7fe713422ace0a93ad3d61` (v2.0.32), apply
12
+ `session-ui-base.patch` followed by `live-theme-api-base.patch`, then apply
13
+ `plugin-theme-provider.patch`. Then run:
14
+
15
+ ```sh
16
+ bun --cwd packages/tui test test/plugin-theme-provider.test.tsx
17
+ ```
18
+
19
+ The test provides dual-mode and dark-only custom theme documents in memory via
20
+ the real provider theme source. It verifies the plugin API reports the selected
21
+ theme and resolved mode, rendered provider tokens change reactively without a
22
+ provider remount, and successful changes update the settings object through the
23
+ injected in-memory `ConfigProvider` service. This proves the config update call
24
+ and in-process state, not filesystem persistence or survival across a restart.
25
+ Unknown theme names and unsupported explicit modes return `false` without
26
+ changing the selection, mode, tokens, or settings object. Omitting a mode for a
27
+ dark-only theme selects the actual dark fallback and updates the selected name
28
+ in the in-memory config service.
29
+
30
+ No external provider or network is used; both the theme documents and config
31
+ service are test-local. No host API changes beyond the approved patch are made.
32
+
33
+ ## Validation
34
+
35
+ Run against the exact upstream base plus the two approved patch files, with Bun
36
+ 1.4.2:
37
+
38
+ All five consecutive reruns passed (each **1 pass, 0 fail, 18 expectations**).
39
+
40
+ The test-only source patch is `plugin-theme-provider.patch`.
41
+
42
+ ## Independently verified patch digests
43
+
44
+ SHA-256 digests for the exact patch inputs used in the independent review:
45
+
46
+ | Patch | SHA-256 |
47
+ | --- | --- |
48
+ | `session-ui-base.patch` (base patch) | `36379e0bf4ec0f63929c12b5ac96dfc73df60b124cef9f4adeb36d86287bc92f` |
49
+ | `live-theme-api-base.patch` | `0bcb1462688c00a09564e9874c3e01429baf92fe004bfc6400704fb24133c123` |
50
+ | `plugin-theme-provider.patch` (test-only patch) | `1afe7901f43261ed1ffa324b1dcf3a317404ecd2175956a1513ca29139427714` |
51
+
52
+ `bun typecheck` from `packages/tui` also passed. The isolated source checkout's
53
+ Git common directory was removed by an environment restart, but its source tree
54
+ and test remain at `/tmp/opencode/ctg-live-theme-proof-clean` for evidence. The
55
+ test was rerun after a fresh frozen-lockfile install and passed.
@@ -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
+ +})
@@ -0,0 +1,107 @@
1
+ --- a/packages/tui/test/plugin-theme-provider.test.tsx
2
+ +++ b/packages/tui/test/plugin-theme-provider.test.tsx
3
+ @@ -0,0 +1,104 @@
4
+ +/** @jsxImportSource @opentui/solid */
5
+ +import { testRender } from "@opentui/solid"
6
+ +import { expect, test } from "bun:test"
7
+ +import { ConfigProvider, type Info } from "../src/config"
8
+ +import { ThemeProvider, useThemes } from "../src/context/theme"
9
+ +import { createPluginContext, type Registry, type usePluginHost } from "../src/plugin/api"
10
+ +import opencode from "../src/theme/assets/v2/opencode.json" with { type: "json" }
11
+ +import { emptyThemeSource, tmpdir } from "./fixture/fixture"
12
+ +import { TestTuiContexts } from "./fixture/tui-environment"
13
+ +import { createTuiResolvedConfig } from "./fixture/tui-runtime"
14
+ +
15
+ +const custom = {
16
+ + ...opencode,
17
+ + base: {
18
+ + ...opencode.base,
19
+ + background: { ...opencode.base.background, base: "#aabbcc" },
20
+ + text: { ...opencode.base.text, base: "#123456" },
21
+ + },
22
+ + dark: { hue: opencode.dark.hue },
23
+ + light: { hue: opencode.dark.hue },
24
+ +}
25
+ +const darkOnly = {
26
+ + ...custom,
27
+ + base: {
28
+ + ...custom.base,
29
+ + background: { ...custom.base.background, base: "#334455" },
30
+ + },
31
+ + light: undefined,
32
+ +}
33
+ +
34
+ +test("plugin theme API reactively updates real ThemeProvider palette and persists selection", async () => {
35
+ + await using temporary = await tmpdir()
36
+ + const settings: Info = {}
37
+ + let themes!: ReturnType<typeof useThemes>
38
+ + let plugin!: ReturnType<typeof createPluginContext>
39
+ + const app = await testRender(() => (
40
+ + <TestTuiContexts paths={{ state: temporary.path }}>
41
+ + <ConfigProvider
42
+ + config={createTuiResolvedConfig(settings)}
43
+ + service={{
44
+ + get: async () => settings,
45
+ + update: async (update) => {
46
+ + update(settings)
47
+ + return settings
48
+ + },
49
+ + }}
50
+ + >
51
+ + <ThemeProvider
52
+ + mode="dark"
53
+ + source={{
54
+ + ...emptyThemeSource,
55
+ + discover: async () => ({ "fixture-live": custom, "fixture-dark-only": darkOnly }),
56
+ + }}
57
+ + >
58
+ + <Probe />
59
+ + </ThemeProvider>
60
+ + </ConfigProvider>
61
+ + </TestTuiContexts>
62
+ + ))
63
+ +
64
+ + function Probe() {
65
+ + themes = useThemes()
66
+ + const host = {
67
+ + app: {}, client: {}, keymap: {}, shortcuts: {}, keymapState: {}, sessionTabs: {},
68
+ + themes,
69
+ + } as unknown as ReturnType<typeof usePluginHost>
70
+ + const registry: Registry = { has: () => false, set() {}, remove() {}, active: () => true }
71
+ + plugin = createPluginContext({ host, id: "fixture", options: undefined, owned: [], registry })
72
+ + return <text>{`${plugin.ui.theme.current().name} ${plugin.ui.theme.current().mode} ${plugin.theme.background.base} ${plugin.theme.text.base}`}</text>
73
+ + }
74
+ +
75
+ + try {
76
+ + app.renderer.start()
77
+ + await app.waitForFrame(() => themes?.ready)
78
+ + await app.waitForFrame((frame) => frame.includes("opencode dark"))
79
+ + expect(plugin.ui.theme.current()).toEqual({ name: "opencode", mode: "dark" })
80
+ +
81
+ + expect(plugin.ui.theme.set({ name: "fixture-live", mode: "light" })).toBe(true)
82
+ + await app.waitForFrame((frame) => frame.includes("fixture-live light"))
83
+ + expect(plugin.ui.theme.current()).toEqual({ name: "fixture-live", mode: "light" })
84
+ + expect(settings.theme).toEqual({ name: "fixture-live", mode: "light" })
85
+ + expect(plugin.theme.background.base.toInts()).toEqual([170, 187, 204, 255])
86
+ + expect(plugin.theme.text.base.toInts()).toEqual([18, 52, 86, 255])
87
+ +
88
+ + const before = structuredClone(settings.theme)
89
+ + expect(plugin.ui.theme.set({ name: "missing-theme", mode: "dark" })).toBe(false)
90
+ + expect(plugin.ui.theme.set({ name: "fixture-dark-only", mode: "light" })).toBe(false)
91
+ + expect(plugin.ui.theme.current()).toEqual({ name: "fixture-live", mode: "light" })
92
+ + expect(settings.theme).toEqual(before)
93
+ + expect(plugin.ui.theme.set({ name: "fixture-dark-only" })).toBe(true)
94
+ + await app.waitForFrame((frame) => frame.includes("fixture-dark-only dark"))
95
+ + expect(plugin.theme.background.base.toInts()).toEqual([51, 68, 85, 255])
96
+ + expect(plugin.ui.theme.current()).toEqual({ name: "fixture-dark-only", mode: "dark" })
97
+ + expect(settings.theme).toEqual({ name: "fixture-dark-only", mode: "light" })
98
+ + const afterFallback = structuredClone(settings.theme)
99
+ + expect(plugin.ui.theme.set({ name: "fixture-live", mode: "invalid" as "dark" })).toBe(false)
100
+ + await Bun.sleep(20)
101
+ + expect(plugin.ui.theme.current()).toEqual({ name: "fixture-dark-only", mode: "dark" })
102
+ + expect(settings.theme).toEqual(afterFallback)
103
+ + expect(before).toEqual({ name: "fixture-live", mode: "light" })
104
+ + } finally {
105
+ + app.renderer.destroy()
106
+ + }
107
+ +})