carthagent 1.0.7 → 1.1.0-beta.2

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 (303) hide show
  1. package/README.md +140 -83
  2. package/bin/ctg.mjs +14 -732
  3. package/docs/beta-rewards-e2e-plan.md +48 -0
  4. package/docs/beta-rewards-e2e-results.md +84 -0
  5. package/docs/cloud-onboarding-acceptance.md +65 -0
  6. package/docs/glyph-review/host-chrome-glyph-compatibility-v4.md +83 -0
  7. package/docs/glyph-review/test-isolation-v1.md +26 -0
  8. package/docs/glyph-review/typecheck-base.log +1 -0
  9. package/docs/glyph-review/typecheck-v4.log +1 -0
  10. package/docs/glyph-review/unicode-inventory-v4.csv +402 -0
  11. package/docs/help-dispatch-review.md +69 -0
  12. package/docs/launch-readiness-live-results.md +65 -0
  13. package/docs/legacy-cli.md +150 -0
  14. package/docs/live-theme-plugin-api.md +18 -0
  15. package/docs/native-cli.md +102 -0
  16. package/docs/native-compatibility.md +16 -0
  17. package/docs/native-daily-quests.md +132 -0
  18. package/docs/native-glyph-pty.md +59 -0
  19. package/docs/native-manual-testing.md +336 -0
  20. package/docs/native-slash-acceptance-handoff.md +49 -0
  21. package/docs/native-terminal-paired-failure-2026-10-17.md +40 -0
  22. package/docs/native-terminal-performance.md +310 -0
  23. package/docs/native-terminal-semantics-evidence/README.md +25 -0
  24. package/docs/native-terminal-semantics-evidence/controlled-host-model-selftest.json +16 -0
  25. package/docs/native-terminal-semantics-evidence/current-acceptance-status.json +59 -0
  26. package/docs/native-terminal-semantics-evidence/final-binary-focused-85bb44a.json +154 -0
  27. package/docs/native-terminal-semantics-evidence/final-binary-full-incomplete-85bb44a.json +160 -0
  28. package/docs/native-terminal-semantics-evidence/final-binary-layout-85bb44a.json +149 -0
  29. package/docs/native-terminal-semantics-evidence/final-binary-layout-interactive-85bb44a.json +159 -0
  30. package/docs/native-terminal-semantics-evidence/final-binary-model-picker-escape-unchanged-85bb44a.json +44 -0
  31. package/docs/native-terminal-semantics-evidence/final-binary-model-picker-exact-identity-85bb44a.json +47 -0
  32. package/docs/native-terminal-semantics-evidence/final-binary-model-picker-unchanged-model-incomplete-85bb44a.json +58 -0
  33. package/docs/native-terminal-semantics-evidence/final-binary-model-selftest.json +16 -0
  34. package/docs/native-terminal-semantics-evidence/final-binary-picker-cancel-incomplete-85bb44a.json +62 -0
  35. package/docs/native-terminal-semantics-evidence/focused-clipboard-title-stale-binary.json +161 -0
  36. package/docs/native-terminal-semantics-evidence/layout-stale-binary-observation-not-accepted.json +153 -0
  37. package/docs/native-terminal-semantics-evidence/picker-latest-incomplete-attempt.json +62 -0
  38. package/docs/native-terminal-semantics-evidence/picker-previous-probe-pass.json +61 -0
  39. package/docs/native-terminal-semantics.md +89 -0
  40. package/docs/native-upgrade.md +175 -0
  41. package/docs/opencode-composed-source-review.md +89 -0
  42. package/docs/opencode-migration.md +225 -9
  43. package/docs/opencode-owned-runtime.md +241 -0
  44. package/docs/opencode-runtime.md +12 -0
  45. package/docs/opencode-theme-provider-proof/README.md +55 -0
  46. package/docs/opencode-theme-provider-proof/live-theme-api-base.patch +122 -0
  47. package/docs/opencode-theme-provider-proof/plugin-theme-provider.patch +107 -0
  48. package/docs/opencode-theme-provider-proof/session-ui-base.patch +1217 -0
  49. package/docs/owned-host-controls-proof.md +39 -0
  50. package/docs/owned-runtime-release-manifest.md +42 -0
  51. package/docs/owned-visual-clear-proof.md +40 -0
  52. package/docs/patches/opencode-v2.0.32-glyph-spinner-test-isolation-v1.patch +50 -0
  53. package/docs/patches/opencode-v2.0.32-glyphs.patch +3670 -0
  54. package/docs/patches/opencode-v2.0.32-help-dispatch.patch +156 -0
  55. package/docs/patches/opencode-v2.0.32-live-theme-api.patch +122 -0
  56. package/docs/patches/opencode-v2.0.32-session-ui-combined-v1.patch +5272 -0
  57. package/docs/patches/opencode-v2.0.32-session-ui-combined-v2.patch +6052 -0
  58. package/docs/patches/opencode-v2.0.32-session-ui-combined-v3.patch +8657 -0
  59. package/docs/patches/opencode-v2.0.32-session-ui-combined-v4.patch +9928 -0
  60. package/docs/patches/opencode-v2.0.32-session-ui-combined-v5.patch +13965 -0
  61. package/docs/patches/opencode-v2.0.32-session-ui.patch +5137 -0
  62. package/docs/release-notes-beta.1.md +60 -0
  63. package/docs/release-notes-beta.2.md +60 -0
  64. package/docs/release-notes-native-draft.md +73 -0
  65. package/docs/release-notes-preview.10.md +121 -0
  66. package/docs/release-notes-preview.11.md +76 -0
  67. package/docs/release-notes-preview.12.md +55 -0
  68. package/docs/release-notes-preview.13.md +68 -0
  69. package/docs/release-notes-preview.9.md +130 -0
  70. package/docs/source-preview.md +120 -0
  71. package/evaluate.mjs +28 -1
  72. package/lib/budget.mjs +1 -1
  73. package/lib/cloud/client.mjs +13 -7
  74. package/lib/codegraph.mjs +1 -1
  75. package/lib/delivery-review.mjs +19 -6
  76. package/lib/delivery.mjs +3 -2
  77. package/lib/environment.mjs +8 -0
  78. package/lib/guidance.mjs +1 -1
  79. package/lib/opencode/cli-runtime.mjs +1188 -0
  80. package/lib/opencode/cli.mjs +253 -0
  81. package/lib/opencode/credentials.mjs +63 -8
  82. package/lib/opencode/demo.mjs +53 -0
  83. package/lib/opencode/evaluation-budget-plugin/index.ts +3 -0
  84. package/lib/opencode/evaluation.mjs +32 -12
  85. package/lib/opencode/headless.mjs +22 -5
  86. package/lib/opencode/provider-policy.mjs +23 -0
  87. package/lib/opencode/review.mjs +37 -16
  88. package/lib/opencode/runtime-artifact.mjs +335 -0
  89. package/lib/opencode/runtime-github-transport.mjs +121 -0
  90. package/lib/opencode/runtime-release.json +36 -0
  91. package/lib/opencode/runtime.mjs +299 -20
  92. package/lib/opencode/service.mjs +77 -35
  93. package/lib/opencode/session-options.mjs +6 -2
  94. package/lib/opencode/update.mjs +222 -0
  95. package/lib/opencode/workspace.mjs +1 -1
  96. package/lib/planning-advice.mjs +2 -2
  97. package/lib/providers/experiential.mjs +2 -2
  98. package/lib/providers/names.mjs +1 -1
  99. package/lib/tui/framing.mjs +1 -80
  100. package/lib/tui/models.mjs +1 -1
  101. package/lib/update.mjs +10 -1
  102. package/package.json +33 -7
  103. package/plugins/carthagent/README.md +23 -0
  104. package/plugins/carthagent/src/bootstrap.ts +1 -1
  105. package/plugins/carthagent/src/daily-evidence.mjs +57 -0
  106. package/plugins/carthagent/src/daily-quests-rpc.ts +50 -0
  107. package/plugins/carthagent/src/daily-source-rpc.ts +52 -0
  108. package/plugins/carthagent/src/index.ts +71 -24
  109. package/plugins/carthagent/src/rpc.ts +51 -6
  110. package/plugins/carthagent/src/server/codegraph.ts +12 -5
  111. package/plugins/carthagent/src/server/daily-quests-rpc.ts +277 -0
  112. package/plugins/carthagent/src/server/daily-source-rpc.ts +243 -0
  113. package/plugins/carthagent/src/server/delivery.ts +227 -112
  114. package/plugins/carthagent/src/server/framing.mjs +80 -0
  115. package/plugins/carthagent/src/server/policy.ts +1 -1
  116. package/plugins/carthagent/src/server/providers.ts +65 -36
  117. package/plugins/carthagent/src/server/session-controls.ts +3 -1
  118. package/plugins/carthagent/src/server/ship-agents.ts +77 -0
  119. package/plugins/carthagent/src/server/ship-context.mjs +60 -0
  120. package/plugins/carthagent/src/server/ship-guidance.ts +41 -0
  121. package/plugins/carthagent/src/server/ship-rpc.ts +71 -0
  122. package/plugins/carthagent/src/server/voyages-rpc.ts +280 -0
  123. package/plugins/carthagent/src/testing.ts +5 -0
  124. package/plugins/carthagent/src/tui.tsx +506 -0
  125. package/plugins/carthagent/src/ui/client.ts +127 -0
  126. package/plugins/carthagent/src/ui/commands.ts +70 -0
  127. package/plugins/carthagent/src/ui/daily-editor-focus.mjs +52 -0
  128. package/plugins/carthagent/src/ui/daily-editor.mjs +4 -0
  129. package/plugins/carthagent/src/ui/daily-quests-events.mjs +18 -0
  130. package/plugins/carthagent/src/ui/daily-quests-state.mjs +218 -0
  131. package/plugins/carthagent/src/ui/daily-quests.tsx +235 -0
  132. package/plugins/carthagent/src/ui/host-state.ts +42 -0
  133. package/plugins/carthagent/src/ui/model-command.ts +32 -0
  134. package/plugins/carthagent/src/ui/native-navigation.ts +23 -0
  135. package/plugins/carthagent/src/ui/native-review.d.mts +33 -0
  136. package/plugins/carthagent/src/ui/native-review.mjs +159 -0
  137. package/plugins/carthagent/src/ui/resume-session.mjs +34 -0
  138. package/plugins/carthagent/src/ui/select-mouse.ts +17 -0
  139. package/plugins/carthagent/src/ui/session-preferences.d.mts +7 -0
  140. package/plugins/carthagent/src/ui/session-preferences.mjs +14 -0
  141. package/plugins/carthagent/src/ui/theme-provision.d.mts +3 -0
  142. package/plugins/carthagent/src/ui/theme-provision.mjs +134 -0
  143. package/plugins/carthagent/src/ui/voyages-navigation-state.mjs +56 -0
  144. package/plugins/carthagent/src/ui/voyages-navigation.tsx +131 -0
  145. package/plugins/carthagent/src/ui/voyages-state.mjs +165 -0
  146. package/plugins/carthagent/src/ui/voyages.tsx +131 -0
  147. package/plugins/carthagent/src/ui/workspace-live.ts +61 -0
  148. package/plugins/carthagent/src/ui/workspace.tsx +379 -0
  149. package/plugins/carthagent/src/voyages-rpc.ts +64 -0
  150. package/plugins/carthagent/src/workspace-rpc.ts +36 -0
  151. package/plugins/carthagent/tests/commands.test.ts +107 -0
  152. package/plugins/carthagent/tests/session-preferences.test.mjs +41 -0
  153. package/plugins/carthagent/tests/voyages-navigation.test.tsx +269 -0
  154. package/plugins/carthagent/themes/carthage.json +67 -0
  155. package/plugins/carthagent/tui.ts +26 -1
  156. package/script/ctg/build-owned-runtime.sh +166 -0
  157. package/tests/budget-engine.test.mjs +4 -3
  158. package/tests/cli.test.mjs +52 -96
  159. package/tests/cloud-client.test.mjs +2 -2
  160. package/tests/daily-evidence.test.mjs +62 -0
  161. package/tests/delivery-review.test.mjs +57 -2
  162. package/tests/delivery.test.mjs +22 -1
  163. package/tests/experiential-extension.test.mjs +2 -2
  164. package/tests/experiential.test.mjs +1 -1
  165. package/tests/fixtures/native-discovery-plugin/server.ts +15 -0
  166. package/tests/generate-owned-runtime-release.test.mjs +354 -0
  167. package/tests/installed-runtime-contract.test.mjs +43 -0
  168. package/tests/native-cli-host-smoke.mjs +457 -0
  169. package/tests/native-daily-editor-focus.test.mjs +213 -0
  170. package/tests/native-daily-editor.test.mjs +43 -0
  171. package/tests/native-daily-quests-layout.test.mjs +55 -0
  172. package/tests/native-daily-quests-rpc.test.mjs +456 -0
  173. package/tests/native-daily-quests-ui.test.mjs +778 -0
  174. package/tests/native-daily-source.test.mjs +525 -0
  175. package/tests/native-demo-ui.test.mjs +48 -0
  176. package/tests/native-evaluation-admission.test.mjs +24 -0
  177. package/tests/native-fixture-browser-cleanup.test.mjs +65 -0
  178. package/tests/native-fixture-runtime.test.mjs +111 -0
  179. package/tests/native-fixture-stop-diagnostics.test.mjs +67 -0
  180. package/tests/native-glyph-pty.test.mjs +107 -0
  181. package/tests/native-manual-projects.test.mjs +170 -0
  182. package/tests/native-model-command.test.mjs +67 -0
  183. package/tests/native-owned-runtime-env.test.mjs +140 -0
  184. package/tests/native-paired-guards.test.mjs +464 -0
  185. package/tests/native-provider-policy.test.mjs +33 -0
  186. package/tests/native-pty-invocation.test.mjs +114 -0
  187. package/tests/native-pty-runtime.test.mjs +45 -0
  188. package/tests/native-resume-session.test.mjs +46 -0
  189. package/tests/native-review-consumer.test.mjs +286 -0
  190. package/tests/native-runtime-identity.test.mjs +214 -0
  191. package/tests/native-select-mouse.test.mjs +23 -0
  192. package/tests/native-ship-guidance.test.mjs +16 -0
  193. package/tests/native-ship-parity.test.mjs +526 -0
  194. package/tests/native-ship-plan.test.mjs +85 -0
  195. package/tests/native-ship-rpc.test.mjs +83 -0
  196. package/tests/native-ui-commands.test.mjs +6 -0
  197. package/tests/native-voyages-layout.test.mjs +28 -0
  198. package/tests/native-voyages-navigation.test.mjs +169 -0
  199. package/tests/native-voyages-rpc.test.mjs +397 -0
  200. package/tests/native-voyages-ui.test.mjs +307 -0
  201. package/tests/opencode-atomic-write.test.mjs +55 -0
  202. package/tests/opencode-cli.test.mjs +1312 -0
  203. package/tests/opencode-codegraph-plugin.test.mjs +47 -0
  204. package/tests/opencode-delivery.test.mjs +16 -0
  205. package/tests/opencode-providers.test.mjs +248 -20
  206. package/tests/opencode-runtime.test.mjs +597 -8
  207. package/tests/opencode-session-controls.test.mjs +15 -0
  208. package/tests/opencode-ship-rpc.test.mjs +71 -0
  209. package/tests/opencode-update.test.mjs +475 -0
  210. package/tests/opencode-workspace.test.mjs +51 -2
  211. package/tests/owned-runtime-publish-workflow.test.mjs +477 -0
  212. package/tests/release-workflow.test.mjs +46 -0
  213. package/tests/runtime-artifact.test.mjs +269 -0
  214. package/tests/runtime-development-trust.test.mjs +231 -0
  215. package/tests/runtime-github-transport.test.mjs +132 -0
  216. package/tests/runtime-token-env.test.mjs +23 -0
  217. package/tests/server.test.mjs +3 -1
  218. package/tests/ship-telemetry.test.mjs +1 -1
  219. package/tests/source-preview.test.mjs +253 -0
  220. package/tests/test_native_terminal_probe.py +76 -0
  221. package/tests/test_native_terminal_semantics.py +232 -0
  222. package/tests/tui.test.mjs +2 -2
  223. package/tests/update.test.mjs +42 -5
  224. package/tests/verify-draft-runtime-gate.test.mjs +280 -0
  225. package/tests/verify-owned-runtime-release.test.mjs +371 -0
  226. package/tests/workspace-live.test.mjs +127 -0
  227. package/tools/benchmark/diagnose-request.mjs +59 -36
  228. package/tools/benchmark/eval-common.mjs +3 -3
  229. package/tools/benchmark/evaluators.test.mjs +4 -4
  230. package/tools/benchmark/guard.test.mjs +35 -2
  231. package/tools/benchmark/guard.ts +20 -7
  232. package/tools/benchmark/planning-startup.test.mjs +30 -34
  233. package/tools/benchmark/runner.mjs +12 -7
  234. package/tools/benchmark/runner.test.mjs +23 -7
  235. package/tools/benchmark/upstream-error.test.mjs +16 -2
  236. package/tools/generate-owned-runtime-release.mjs +217 -0
  237. package/tools/installed-runtime-contract.mjs +16 -0
  238. package/tools/manual-runtime-manifest.mjs +39 -0
  239. package/tools/native-fixture-browser-cleanup.mjs +59 -0
  240. package/tools/native-fixture-stop-diagnostics.mjs +45 -0
  241. package/tools/native-fixture.mjs +199 -40
  242. package/tools/native-glyph-helpers.py +127 -0
  243. package/tools/native-glyph-pty.py +144 -0
  244. package/tools/native-manual-projects.mjs +259 -0
  245. package/tools/native-manual.mjs +338 -0
  246. package/tools/native-model-picker-acceptance.py +306 -0
  247. package/tools/native-owned-runtime-env.sh +47 -0
  248. package/tools/native-paired-guards.mjs +282 -0
  249. package/tools/native-paired-manifest.mjs +72 -0
  250. package/tools/native-paired-orchestration.sh +51 -0
  251. package/tools/native-plugin-readiness.mjs +33 -0
  252. package/tools/native-plugin-readiness.test.mjs +53 -0
  253. package/tools/native-pty-invocation.mjs +26 -0
  254. package/tools/native-pty-runtime.mjs +83 -0
  255. package/tools/native-pty-smoke.py +147 -24
  256. package/tools/native-public-repo-shell-result.mjs +13 -0
  257. package/tools/native-public-repo-shell-result.test.mjs +28 -0
  258. package/tools/native-runtime-identity.mjs +201 -0
  259. package/tools/native-slash-acceptance-env.py +38 -0
  260. package/tools/native-terminal-semantics.py +882 -0
  261. package/tools/owned-shell-observer/index.ts +16 -0
  262. package/tools/owned-shell-observer/package.json +1 -0
  263. package/tools/owned-visual-clear-env.py +48 -0
  264. package/tools/owned-visual-clear-model.py +21 -0
  265. package/tools/pinned-owned-runtime-inputs.mjs +46 -0
  266. package/tools/prepare-native-manual.mjs +122 -0
  267. package/tools/run-native-terminal-paired.sh +403 -0
  268. package/tools/runtime-token-env.mjs +8 -0
  269. package/tools/seed-native-switch-fixture.mjs +79 -0
  270. package/tools/test-native-controls.mjs +3 -1
  271. package/tools/test-native-daily-quests.mjs +65 -0
  272. package/tools/test-native-default-cli.mjs +106 -0
  273. package/tools/test-native-delivery-host.mjs +102 -20
  274. package/tools/test-native-evaluation.mjs +7 -3
  275. package/tools/test-native-glyph-pty.mjs +94 -0
  276. package/tools/test-native-host.mjs +91 -65
  277. package/tools/test-native-outcomes.mjs +3 -1
  278. package/tools/test-native-package.mjs +206 -38
  279. package/tools/test-native-performance.mjs +26 -0
  280. package/tools/test-native-product.mjs +90 -0
  281. package/tools/test-native-pty.mjs +34 -9
  282. package/tools/test-native-public-repo.mjs +224 -0
  283. package/tools/test-native-refresh-reentry.mjs +106 -0
  284. package/tools/test-native-report.mjs +27 -20
  285. package/tools/test-native-review.mjs +75 -0
  286. package/tools/test-native-runtime.mjs +120 -0
  287. package/tools/test-native-ship-models.mjs +176 -0
  288. package/tools/test-native-ship-walkthrough.mjs +96 -0
  289. package/tools/test-native-slash-commands.mjs +125 -0
  290. package/tools/test-native-slash-commands.py +242 -0
  291. package/tools/test-native-swe-host.mjs +224 -55
  292. package/tools/test-native-tasks.mjs +77 -0
  293. package/tools/test-native-terminal-performance.py +887 -0
  294. package/tools/test-native-terminal-readiness.sh +85 -0
  295. package/tools/test-native-terminal-semantics-installed.mjs +38 -0
  296. package/tools/test-native-visual-clear-installed.mjs +48 -0
  297. package/tools/test-owned-host-controls.mjs +238 -0
  298. package/tools/test-owned-opencode-runtime.mjs +35 -0
  299. package/tools/test-owned-visual-clear-env.py +120 -0
  300. package/tools/test-owned-visual-clear.py +373 -0
  301. package/tools/verify-draft-runtime-gate.mjs +138 -0
  302. package/tools/verify-owned-runtime-release.mjs +114 -0
  303. package/tools/verify-release-version.mjs +21 -1
@@ -0,0 +1,107 @@
1
+ import { strict as assert } from "node:assert";
2
+ import { test } from "node:test";
3
+ import { COMMANDS, matchCommand, NATIVE_COMMAND_COLLISIONS, resolveCommand } from "../src/ui/commands.js";
4
+ import { readFile } from "node:fs/promises";
5
+ import { parseThemeDocument } from "@opencode/theme/tui";
6
+ import { cliConfigDirectory, cliConfigPath, readCliConfig, writeNativeTheme } from "../src/ui/host-state.js";
7
+ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
8
+ import { tmpdir } from "node:os";
9
+ import { join } from "node:path";
10
+ import { provisionNativeThemes } from "../src/ui/theme-provision.mjs";
11
+
12
+ test("Carthagent command manifest resolves visible CTG commands", () => {
13
+ assert.equal(resolveCommand("/help")?.id, "help");
14
+ assert.equal(resolveCommand("/ctg-help")?.id, "help");
15
+ assert.deepEqual(COMMANDS.find((item) => item.id === "help")?.aliases, ["ctg-help"]);
16
+ assert.equal(COMMANDS.find((item) => item.id === "help")?.submitSlash, true);
17
+ assert.equal(resolveCommand("/review")?.id, "review");
18
+ assert.equal(resolveCommand("/review-mode")?.id, "review");
19
+ for (const name of ["workflow", "architecture", "evidence"]) assert.equal(resolveCommand(name)?.hidden, undefined);
20
+ for (const name of ["model", "compact", "autocompact", "abort", "clear"]) assert.equal(resolveCommand(name)?.id, name);
21
+ assert.deepEqual(matchCommand("/arch").map((item) => item.name), ["architecture", "archive"]);
22
+ assert.deepEqual(matchCommand("/rev status"), []);
23
+ for (const name of ["login", "account", "usage", "billing", "logout-cloud", "archive", "demo", "voyages", "quests"]) {
24
+ assert.ok(resolveCommand(`/${name}`), `/${name} must resolve to a registered handler`);
25
+ }
26
+ assert.equal(resolveCommand("/cloud")?.id, "account");
27
+ assert.equal(resolveCommand("/auth")?.id, "login");
28
+ assert.match(resolveCommand("/voyages")!.description, /daily quests/);
29
+ assert.match(resolveCommand("/quests")!.description, /separate explicit consent/);
30
+ });
31
+
32
+ test("manifest prevents collisions with native OpenCode slash commands", () => {
33
+ for (const name of NATIVE_COMMAND_COLLISIONS) assert.equal(COMMANDS.some((item) => item.name === name || item.aliases?.includes(name)), false, name);
34
+ assert.equal(NATIVE_COMMAND_COLLISIONS.includes("help" as typeof NATIVE_COMMAND_COLLISIONS[number]), false);
35
+ assert.deepEqual(COMMANDS.filter((item) => ["model", "compact", "abort", "resume"].includes(item.name)).map((item) => item.name).sort(), ["abort", "compact", "model", "resume"]);
36
+ });
37
+
38
+ test("theme provisioning installs the startup default through its launcher-facing export", async () => {
39
+ const { provisionDefaultNativeTheme } = await import("../src/ui/theme-provision.mjs");
40
+ const directory = await mkdtemp(join(tmpdir(), "ctg-default-theme-"));
41
+ try {
42
+ const result = await provisionDefaultNativeTheme(directory);
43
+ assert.ok(result.installed.includes("carthage"));
44
+ const theme = parseThemeDocument(JSON.parse(await readFile(join(result.directory, "carthage.json"), "utf8")), "carthage");
45
+ assert.equal(theme.dark.hue.yellow[400], "#d5af66");
46
+ } finally { await rm(directory, { recursive: true, force: true }); }
47
+ });
48
+
49
+ test("startup theme provisioning covers compatibility themes and preserves host/user themes", async () => {
50
+ const { provisionDefaultNativeTheme } = await import("../src/ui/theme-provision.mjs");
51
+ const directory = await mkdtemp(join(tmpdir(), "ctg-startup-themes-"));
52
+ try {
53
+ await mkdir(join(directory, "themes"), { recursive: true });
54
+ await writeFile(join(directory, "themes", "forest.json"), '{"userOwned":true}');
55
+ const result = await provisionDefaultNativeTheme(directory);
56
+ assert.ok(result.installed.includes("carthage"));
57
+ assert.ok(result.installed.includes("forest") === false);
58
+ assert.ok(result.conflicts.includes("forest"));
59
+ assert.ok(result.installed.includes("opencode") === false);
60
+ assert.ok(result.installed.includes("tokyonight") === false);
61
+ assert.deepEqual(JSON.parse(await readFile(join(directory, "themes", "forest.json"), "utf8")), { userOwned: true });
62
+ } finally { await rm(directory, { recursive: true, force: true }); }
63
+ });
64
+
65
+ test("Carthage theme validates against the OpenCode V2 theme schema and resolver", async () => {
66
+ const raw = JSON.parse(await readFile(new URL("../themes/carthage.json", import.meta.url), "utf8"));
67
+ const resolved = parseThemeDocument(raw, "carthage");
68
+ assert.ok(resolved.dark?.hue.yellow);
69
+ assert.equal(resolved.base.categorical?.length, 5);
70
+ });
71
+
72
+ test("native CLI theme write preserves unrelated cli.json settings", async () => {
73
+ const directory = await mkdtemp(join(tmpdir(), "ctg-cli-config-"));
74
+ const path = join(directory, "cli.json");
75
+ try {
76
+ await writeFile(path, JSON.stringify({ theme: { name: "opencode", mode: "dark" }, session: { thinking: "show" }, custom: true }));
77
+ await writeNativeTheme(path, "carthage");
78
+ const config = await readCliConfig(path);
79
+ assert.deepEqual(config.theme, { name: "carthage", mode: "dark" });
80
+ assert.deepEqual(config.session, { thinking: "show" });
81
+ assert.equal(config.custom, true);
82
+ assert.equal(cliConfigPath({ XDG_CONFIG_HOME: "/tmp/profile" }, "/home/test"), join("/tmp/profile", "opencode", "cli.json"));
83
+ assert.equal(cliConfigDirectory({ OPENCODE_CONFIG_DIR: "/tmp/opencode-profile" }), "/tmp/opencode-profile");
84
+ assert.equal(cliConfigDirectory({ XDG_CONFIG_HOME: "/tmp/profile" }, "/home/test"), join("/tmp/profile", "opencode"));
85
+ } finally { await rm(directory, { recursive: true, force: true }); }
86
+ });
87
+
88
+ test("native theme provisioning converts legacy palettes inside the selected OpenCode profile", async () => {
89
+ const directory = await mkdtemp(join(tmpdir(), "ctg-native-themes-"));
90
+ const themeDirectory = join(directory, "themes");
91
+ try {
92
+ await mkdir(themeDirectory, { recursive: true });
93
+ await writeFile(join(themeDirectory, "user-theme.json"), '{"custom":true}');
94
+ const result = await provisionNativeThemes(directory, ["carthage", "nebula"]);
95
+ assert.deepEqual(result.installed.sort(), ["carthage", "nebula"]);
96
+ assert.deepEqual(JSON.parse(await readFile(join(themeDirectory, "user-theme.json"), "utf8")), { custom: true });
97
+ const carthage = parseThemeDocument(JSON.parse(await readFile(join(themeDirectory, "carthage.json"), "utf8")), "carthage");
98
+ const nebula = parseThemeDocument(JSON.parse(await readFile(join(themeDirectory, "nebula.json"), "utf8")), "nebula");
99
+ assert.equal(carthage.dark.hue.yellow[400], "#d5af66");
100
+ assert.equal(carthage.dark.hue.yellow[300], "#e6c480");
101
+ assert.ok(nebula.dark.hue.blue[400]);
102
+ const again = await provisionNativeThemes(directory, ["carthage"]);
103
+ assert.deepEqual(again.installed, []);
104
+ await writeFile(join(themeDirectory, "forest.json"), '{"userOwned":true}');
105
+ await assert.rejects(provisionNativeThemes(directory, ["forest"]), /Refusing to replace/);
106
+ } finally { await rm(directory, { recursive: true, force: true }); }
107
+ });
@@ -0,0 +1,41 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import { setSessionAutoCompaction } from "../src/ui/session-preferences.mjs";
4
+
5
+ test("autocompact persists explicit, idempotent session preferences without replacing neighbor metadata", async () => {
6
+ let current = { "ctg.autoCompaction": false, "ctg.raw": true, "user.flag": "keep" };
7
+ const updates = [];
8
+ const client = {
9
+ async get(input, options) {
10
+ assert.equal(input.sessionID, "s1");
11
+ assert.equal(options.location.directory, "/project");
12
+ assert.ok(options.signal instanceof AbortSignal);
13
+ return { metadata: current };
14
+ },
15
+ async update(input, options) {
16
+ assert.equal(input.sessionID, "s1");
17
+ assert.equal(options.location.directory, "/project");
18
+ current = input.metadata;
19
+ updates.push(input.metadata);
20
+ },
21
+ };
22
+ const location = { directory: "/project" };
23
+ assert.equal(await setSessionAutoCompaction(client, "s1", location, "off"), false);
24
+ assert.equal(updates.length, 0, "repeating off must not write");
25
+ assert.equal(await setSessionAutoCompaction(client, "s1", location, "on"), true);
26
+ assert.deepEqual(current, { "ctg.raw": true, "user.flag": "keep" });
27
+ assert.equal(await setSessionAutoCompaction(client, "s1", location, "toggle"), false);
28
+ assert.deepEqual(current, { "ctg.autoCompaction": false, "ctg.raw": true, "user.flag": "keep" });
29
+ assert.equal(await setSessionAutoCompaction(client, "s1", location, "toggle"), true);
30
+ assert.equal(updates.length, 3);
31
+ });
32
+
33
+ test("invalid autocompact arguments fail before reading or mutating session state", async () => {
34
+ let calls = 0;
35
+ const client = {
36
+ async get() { calls++; throw new Error("must not read"); },
37
+ async update() { calls++; throw new Error("must not update"); },
38
+ };
39
+ await assert.rejects(setSessionAutoCompaction(client, "s1", { directory: "/project" }, "sometimes"), /Usage: \/autocompact \[on\|off\]/);
40
+ assert.equal(calls, 0);
41
+ });
@@ -0,0 +1,269 @@
1
+ /** @jsxImportSource @opentui/solid */
2
+ import { expect, test } from "bun:test";
3
+ import { testRender } from "@opentui/solid";
4
+ import { ScrollBoxRenderable, SelectRenderable, type Renderable } from "@opentui/core";
5
+ import { createSignal } from "solid-js";
6
+ import { PluginContextProvider } from "@opencode/plugin/tui";
7
+ import type { Context } from "@opencode/plugin/tui/context";
8
+ import { parseThemeDocument, resolveThemeDocument } from "@opencode/theme/tui";
9
+ import theme from "../themes/carthage.json";
10
+ import { Voyages } from "../src/voyages-rpc.js";
11
+ import { VoyagesNavigation } from "../src/ui/voyages-navigation.js";
12
+
13
+ const profile = { profile: { name: "Test captain", shipName: "Test vessel", coins: 10, level: 2, totalXp: 150 } };
14
+ const slate = { slate: { day: "2026-10-30", endsAt: "2026-10-31T00:00:00Z", items: [
15
+ { id: "quest-one", definition: { title: "Protect a boundary" }, status: "open" },
16
+ { id: "quest-two", definition: { title: "Review a change" }, status: "assessed_complete" },
17
+ ] }, records: [{ request: { evidence: "must not appear in navigation", consent: true } }] };
18
+
19
+ async function fixture({ width = 30, height = 36, authenticated = true } = {}) {
20
+ const [active, setActive] = createSignal(true);
21
+ const [focused, setFocused] = createSignal(false);
22
+ const [compact, setCompact] = createSignal(false);
23
+ const [mode, setMode] = createSignal("base");
24
+ const [sessionID, setSessionID] = createSignal<string | undefined>("coding-session");
25
+ const routes: unknown[] = [], calls: string[] = [], selected: string[] = [];
26
+ const responses: Promise<unknown>[] = [];
27
+ let responseGate: Promise<void> | undefined;
28
+ let listener: ((event: { details: { type: string } }) => void) | undefined;
29
+ let listening = true;
30
+ const context = {
31
+ theme: resolveThemeDocument(parseThemeDocument(theme), "dark"),
32
+ location: { directory: "/test-project" },
33
+ data: {
34
+ session: { get: () => ({ location: { directory: "/test-project" } }) },
35
+ location: { default: () => ({ directory: "/test-project" }) },
36
+ listen: (next: typeof listener) => { listener = next; return () => { listening = false; }; },
37
+ },
38
+ client: { rpc: (definition: unknown) => ({ snapshot: () => {
39
+ calls.push(definition === Voyages ? "profile" : "daily");
40
+ const response = authenticated ? (responseGate ?? Promise.resolve()).then(() => definition === Voyages ? profile : slate) : Promise.reject({ type: "voyagesError", data: { code: "authentication_required" } });
41
+ responses.push(response);
42
+ return response;
43
+ } }) },
44
+ keymap: { mode: { current: mode }, dispatch: (id: string) => { calls.push(id); } },
45
+ ui: {
46
+ router: { navigate: (route: unknown) => routes.push(route) },
47
+ navigation: { current: () => "voyages", select: (name: string) => { selected.push(name); return true; } },
48
+ },
49
+ } as unknown as Context;
50
+ const app = await testRender(() => <PluginContextProvider value={context}>
51
+ <box width={width} height={height} visible={active()}>
52
+ <VoyagesNavigation width={width} active={active()} compact={compact()} focused={focused()} sessionID={sessionID()} focus={() => { if (mode() !== "base") return; setFocused(true); setMode("navigation"); }} blur={() => { setFocused(false); if (mode() === "navigation") setMode("base"); }} />
53
+ </box>
54
+ </PluginContextProvider>, { width, height, kittyKeyboard: true });
55
+ const settle = async () => { await Promise.allSettled(responses); await app.renderOnce(); };
56
+ await settle();
57
+ return {
58
+ app, calls, routes, selected, mode, setMode, setActive, setFocused, setCompact, setSessionID, settle,
59
+ focus: async () => { setFocused(true); setMode("navigation"); await app.renderOnce(); },
60
+ event: (type: string) => listener?.({ details: { type } }),
61
+ delay: (promise: Promise<void> | undefined) => { responseGate = promise; },
62
+ listening: () => listening,
63
+ list: () => app.renderer.currentFocusedRenderable as SelectRenderable,
64
+ scroll: () => {
65
+ const find = (node: Renderable): ScrollBoxRenderable | undefined => node instanceof ScrollBoxRenderable ? node : node.getChildren().map(find).find(Boolean);
66
+ return find(app.renderer.root)!;
67
+ },
68
+ };
69
+ }
70
+
71
+ test.each([30, 18])("real Voyages navigator renders account and assignment summaries at width %i", async (width) => {
72
+ const f = await fixture({ width });
73
+ try {
74
+ const frame = f.app.captureCharFrame();
75
+ expect(frame).toContain("Test captain");
76
+ expect(frame).toContain("10 coins");
77
+ expect(frame).toContain("Daily coding");
78
+ expect(frame).not.toContain("must not appear");
79
+ expect(f.calls).toEqual(["profile", "daily"]);
80
+ expect(f.routes).toHaveLength(0);
81
+ } finally { f.app.renderer.destroy(); }
82
+ expect(f.listening()).toBe(false);
83
+ });
84
+
85
+ test("rail focus owns arrows/Enter; palette or modal mode cannot trigger a route", async () => {
86
+ const f = await fixture();
87
+ try {
88
+ expect(f.app.renderer.currentFocusedRenderable).not.toBeInstanceOf(SelectRenderable);
89
+ await f.focus();
90
+ expect(f.list()).toBeInstanceOf(SelectRenderable);
91
+ f.app.mockInput.pressArrow("down");
92
+ await f.app.renderOnce();
93
+ expect(f.list().getSelectedOption()?.value).toBe("quest:quest-one");
94
+ for (const mode of ["modal", "palette", "base"]) {
95
+ f.setMode(mode);
96
+ await f.app.renderOnce();
97
+ f.app.mockInput.pressEnter();
98
+ await f.app.renderOnce();
99
+ expect(f.routes).toHaveLength(0);
100
+ }
101
+ f.setMode("navigation");
102
+ await f.app.renderOnce();
103
+ f.app.mockInput.pressEnter();
104
+ await f.app.renderOnce();
105
+ expect(f.routes).toEqual([{ type: "plugin", name: "daily-quests", data: { sessionID: "coding-session", questID: "quest-one" } }]);
106
+ expect(f.mode()).toBe("base");
107
+ expect(f.calls).toEqual(["profile", "daily"]);
108
+ } finally { f.app.renderer.destroy(); }
109
+ });
110
+
111
+ test("Projects/Voyages toggles retain selection and do not route, submit or purchase", async () => {
112
+ const f = await fixture();
113
+ try {
114
+ await f.focus();
115
+ f.app.mockInput.pressArrow("down");
116
+ await f.app.renderOnce();
117
+ expect(f.list().getSelectedOption()?.value).toBe("quest:quest-one");
118
+ f.setActive(false);
119
+ await f.settle();
120
+ expect(f.app.captureCharFrame()).not.toContain("Test captain");
121
+ f.setActive(true);
122
+ await f.settle();
123
+ expect(f.list().getSelectedOption()?.value).toBe("quest:quest-one");
124
+ expect(f.routes).toHaveLength(0);
125
+ expect(f.calls).toEqual(["profile", "daily", "profile", "daily"]);
126
+ } finally { f.app.renderer.destroy(); }
127
+ });
128
+
129
+ test("compact mode uses the same mounted selection and keyboard behavior", async () => {
130
+ const f = await fixture();
131
+ try {
132
+ await f.focus();
133
+ f.app.mockInput.pressArrow("down");
134
+ await f.app.renderOnce();
135
+ const list = f.list();
136
+ f.setCompact(true);
137
+ f.app.resize(5, 36);
138
+ await f.app.renderOnce();
139
+ expect(f.list()).toBe(list);
140
+ expect(f.list().getSelectedOption()?.value).toBe("quest:quest-one");
141
+ expect(f.app.captureCharFrame()).not.toContain("Test captain");
142
+ f.app.mockInput.pressEnter();
143
+ await f.app.renderOnce();
144
+ expect(f.routes).toEqual([{ type: "plugin", name: "daily-quests", data: { sessionID: "coding-session", questID: "quest-one" } }]);
145
+ } finally { f.app.renderer.destroy(); }
146
+ });
147
+
148
+ test("switching modes preserves the Voyages scroll position at a short terminal height", async () => {
149
+ const f = await fixture({ height: 12 });
150
+ try {
151
+ const scroll = f.scroll();
152
+ scroll.scrollBy(5);
153
+ await f.app.renderOnce();
154
+ const before = scroll.scrollTop;
155
+ expect(before).toBeGreaterThan(0);
156
+ f.setActive(false);
157
+ await f.settle();
158
+ f.setActive(true);
159
+ await f.settle();
160
+ expect(f.scroll()).toBe(scroll);
161
+ expect(scroll.scrollTop).toBe(before);
162
+ expect(f.routes).toHaveLength(0);
163
+ } finally { f.app.renderer.destroy(); }
164
+ });
165
+
166
+ test("slow summary reads preserve scrolled position after an intervening loading layout", async () => {
167
+ const f = await fixture({ height: 12 });
168
+ try {
169
+ const scroll = f.scroll();
170
+ scroll.scrollTo(100);
171
+ await f.app.renderOnce();
172
+ const before = scroll.scrollTop;
173
+ expect(before).toBeGreaterThan(0);
174
+ f.setActive(false);
175
+ await f.app.renderOnce();
176
+ const gate = Promise.withResolvers<void>();
177
+ f.delay(gate.promise);
178
+ f.setActive(true);
179
+ await f.app.renderOnce();
180
+ expect(f.app.captureCharFrame()).not.toContain("Test captain");
181
+ expect(f.calls).toEqual(["profile", "daily", "profile", "daily"]);
182
+ gate.resolve();
183
+ await f.settle();
184
+ await f.app.renderOnce();
185
+ expect(scroll.scrollTop).toBe(before);
186
+ } finally { f.app.renderer.destroy(); }
187
+ });
188
+
189
+ test("short terminal keyboard navigation keeps the highlighted action on screen", async () => {
190
+ const f = await fixture({ height: 12 });
191
+ try {
192
+ await f.focus();
193
+ for (let index = 0; index < 7; index++) {
194
+ f.app.mockInput.pressArrow("down");
195
+ await f.app.renderOnce();
196
+ }
197
+ expect(f.list().getSelectedOption()?.value).toBe("back");
198
+ expect(f.app.captureCharFrame()).toContain("Return to coding");
199
+ expect(f.list().height).toBeLessThanOrEqual(f.scroll().viewport.height);
200
+ expect(f.routes).toHaveLength(0);
201
+ } finally { f.app.renderer.destroy(); }
202
+ });
203
+
204
+ test("mouse press focuses the rail and release opens the clicked row without acting through a modal", async () => {
205
+ const f = await fixture();
206
+ try {
207
+ const row = f.app.captureCharFrame().split("\n").findIndex((line) => line.includes("Daily coding quests"));
208
+ expect(row).toBeGreaterThanOrEqual(0);
209
+ for (const mode of ["modal", "palette"]) {
210
+ f.setMode(mode);
211
+ await f.app.renderOnce();
212
+ await f.app.mockMouse.click(5, row);
213
+ await f.app.renderOnce();
214
+ expect(f.mode()).toBe(mode);
215
+ expect(f.routes).toHaveLength(0);
216
+ }
217
+ f.setMode("base");
218
+ f.app.renderer.clearSelection();
219
+ await f.app.renderOnce();
220
+ await f.app.mockMouse.pressDown(5, row);
221
+ await f.app.renderOnce();
222
+ expect(f.mode()).toBe("navigation");
223
+ expect(f.app.renderer.currentFocusedRenderable).toBeInstanceOf(SelectRenderable);
224
+ expect(f.routes).toHaveLength(0);
225
+ await f.app.mockMouse.release(5, row);
226
+ await f.app.renderOnce();
227
+ expect(f.routes).toEqual([{ type: "plugin", name: "daily-quests", data: { sessionID: "coding-session" } }]);
228
+ expect(f.mode()).toBe("base");
229
+ const questRow = f.app.captureCharFrame().split("\n").findIndex((line) => line.includes("Review a change"));
230
+ expect(questRow).toBeGreaterThanOrEqual(0);
231
+ await f.app.mockMouse.click(5, questRow);
232
+ await f.app.renderOnce();
233
+ expect(f.routes).toEqual([
234
+ { type: "plugin", name: "daily-quests", data: { sessionID: "coding-session" } },
235
+ { type: "plugin", name: "daily-quests", data: { sessionID: "coding-session", questID: "quest-two" } },
236
+ ]);
237
+ } finally { f.app.renderer.destroy(); }
238
+ });
239
+
240
+ test("signed-out account and invalidation hide protected summaries, reset stale selection, and stay read-only", async () => {
241
+ const f = await fixture({ authenticated: false });
242
+ try {
243
+ expect(f.app.captureCharFrame()).toContain("authentication_required");
244
+ expect(f.app.captureCharFrame()).not.toContain("Test captain");
245
+ await f.focus();
246
+ f.app.mockInput.pressArrow("down");
247
+ await f.app.renderOnce();
248
+ f.event("credential.updated");
249
+ await f.settle();
250
+ expect(f.list().getSelectedOption()?.value).toBe("daily");
251
+ expect(f.routes).toHaveLength(0);
252
+ expect(f.calls).toEqual(["profile", "daily", "profile", "daily"]);
253
+ } finally { f.app.renderer.destroy(); }
254
+ });
255
+
256
+ test.each(["coding-session", undefined])("return to coding explicitly navigates to the selected session or Home (%s)", async (sessionID) => {
257
+ const f = await fixture();
258
+ try {
259
+ f.setSessionID(sessionID);
260
+ await f.focus();
261
+ const list = f.list();
262
+ const index = list.options.findIndex((option) => option.value === "back");
263
+ list.setSelectedIndex(index);
264
+ f.app.mockInput.pressEnter();
265
+ await f.app.renderOnce();
266
+ expect(f.selected).toEqual(["projects"]);
267
+ expect(f.routes).toEqual([sessionID ? { type: "session", sessionID } : { type: "home" }]);
268
+ } finally { f.app.renderer.destroy(); }
269
+ });
@@ -0,0 +1,67 @@
1
+ {
2
+ "$schema": "https://opencode.ai/theme.json",
3
+ "base": {
4
+ "categorical": ["accent", "purple", "cyan", "green", "blue"],
5
+ "text": {
6
+ "base": "$hue.neutral.100",
7
+ "muted": "$hue.neutral.300",
8
+ "faint": "$hue.neutral.400",
9
+ "action": {
10
+ "primary": { "base": "$hue.purple.400", "$hovered": "$hue.purple.300", "$pressed": "$hue.purple.500" },
11
+ "secondary": { "base": "$hue.neutral.200", "$hovered": "$hue.yellow.400" },
12
+ "destructive": { "base": "$hue.red.300", "$hovered": "$hue.red.200" }
13
+ },
14
+ "formfield": { "base": "$hue.neutral.100", "$focused": "$hue.yellow.400" },
15
+ "feedback": {
16
+ "info": { "base": "$hue.cyan.300", "muted": "$hue.cyan.500" },
17
+ "success": { "base": "$hue.green.300", "muted": "$hue.green.500" },
18
+ "warning": { "base": "$hue.yellow.300", "muted": "$hue.yellow.500" },
19
+ "error": { "base": "$hue.red.300", "muted": "$hue.red.500" }
20
+ }
21
+ },
22
+ "background": {
23
+ "base": "$hue.neutral.900",
24
+ "raised": { "base": "$hue.neutral.800", "high": "$hue.neutral.700", "max": "$hue.neutral.600" },
25
+ "action": {
26
+ "primary": { "base": "$hue.yellow.400", "$hovered": "$hue.yellow.300", "$pressed": "$hue.yellow.500" },
27
+ "secondary": { "base": "$hue.neutral.800", "$hovered": "$hue.neutral.700" },
28
+ "destructive": { "base": "$hue.red.700", "$hovered": "$hue.red.600" }
29
+ },
30
+ "formfield": { "base": "$hue.neutral.900", "$focused": "$hue.neutral.800" },
31
+ "feedback": {
32
+ "info": { "base": "$hue.blue.800" }, "success": { "base": "$hue.green.800" },
33
+ "warning": { "base": "$hue.orange.800" }, "error": { "base": "$hue.red.800" }
34
+ }
35
+ },
36
+ "border": { "base": "$hue.neutral.600" },
37
+ "scrollbar": { "base": "$hue.neutral.600" },
38
+ "diff": {
39
+ "text": { "added": "$hue.green.300", "removed": "$hue.red.300", "context": "$hue.neutral.300", "hunkHeader": "$hue.cyan.300" },
40
+ "background": { "added": "$hue.green.800", "removed": "$hue.red.800", "context": "$hue.neutral.800" },
41
+ "highlight": { "added": "$hue.green.700", "removed": "$hue.red.700" },
42
+ "lineNumber": { "text": "$hue.neutral.400", "background": { "added": "$hue.green.900", "removed": "$hue.red.900" } }
43
+ },
44
+ "syntax": {
45
+ "comment": "$hue.neutral.400", "keyword": "$hue.purple.300", "function": "$hue.yellow.300", "variable": "$hue.neutral.100",
46
+ "string": "$hue.green.300", "number": "$hue.orange.300", "type": "$hue.cyan.300", "operator": "$hue.purple.300", "punctuation": "$hue.neutral.300"
47
+ },
48
+ "markdown": {
49
+ "text": "$hue.neutral.100", "heading": "$hue.yellow.300", "link": "$hue.cyan.300", "linkText": "$hue.cyan.300", "code": "$hue.green.300",
50
+ "blockQuote": "$hue.neutral.300", "emphasis": "$hue.orange.300", "strong": "$hue.yellow.300", "horizontalRule": "$hue.neutral.500",
51
+ "listItem": "$hue.yellow.400", "listEnumeration": "$hue.yellow.400", "image": "$hue.purple.300", "imageText": "$hue.neutral.300", "codeBlock": "$hue.neutral.100"
52
+ }
53
+ },
54
+ "dark": {
55
+ "hue": {
56
+ "gray": { "100": "#f4eef5", "200": "#d8cbdc", "300": "#c0b3ca", "400": "#9a8ca6", "500": "#705e7a", "600": "#49364f", "700": "#35243d", "800": "#271733", "900": "#160e20" },
57
+ "red": { "100": "#f6d4dd", "200": "#e8a9ba", "300": "#d58ca2", "400": "#b8758d", "500": "#9c5a73", "600": "#713e55", "700": "#512a40", "800": "#351a2d", "900": "#21101d" },
58
+ "orange": { "100": "#ffead3", "200": "#f8d8b3", "300": "#eec28b", "400": "#d5af66", "500": "#ad8b50", "600": "#80673e", "700": "#5d4a32", "800": "#3b2e28", "900": "#241c21" },
59
+ "yellow": { "100": "#fff4d8", "200": "#f5dca9", "300": "#e6c480", "400": "#d5af66", "500": "#b18c4e", "600": "#84683d", "700": "#604a30", "800": "#3e3026", "900": "#251d20" },
60
+ "green": { "100": "#d9f2df", "200": "#b1e2bd", "300": "#83c996", "400": "#62ad78", "500": "#46885e", "600": "#35684a", "700": "#284d39", "800": "#1e382b", "900": "#17251e" },
61
+ "cyan": { "100": "#d5f2f3", "200": "#a9e0e2", "300": "#83c8ce", "400": "#62aab5", "500": "#4a8997", "600": "#396874", "700": "#2c4c58", "800": "#203640", "900": "#17242b" },
62
+ "blue": { "100": "#dce9fb", "200": "#b9d0f0", "300": "#91b2df", "400": "#7193c6", "500": "#5876a4", "600": "#445b7e", "700": "#34445d", "800": "#263342", "900": "#1a222d" },
63
+ "purple": { "100": "#f0e2f5", "200": "#ddc0e7", "300": "#c99bd7", "400": "#ad7abe", "500": "#895f99", "600": "#694878", "700": "#4d365b", "800": "#352440", "900": "#211629" },
64
+ "accent": "$hue.yellow", "interactive": "$hue.yellow", "neutral": "$hue.gray"
65
+ }
66
+ }
67
+ }
@@ -1 +1,26 @@
1
- export { default } from './src/bootstrap';
1
+ import { Plugin } from '@opencode/plugin/tui';
2
+ import diagnostics from './src/bootstrap';
3
+ import workspace from './src/tui';
4
+
5
+ export default Plugin.define({
6
+ id: 'carthagent.tui',
7
+ async setup(context) {
8
+ const cleanups: Plugin.Cleanup[] = [];
9
+ const dispose = async () => {
10
+ const results = await Promise.allSettled(cleanups.splice(0).reverse().map((cleanup) => Promise.resolve().then(cleanup)));
11
+ const failures = results.filter((result): result is PromiseRejectedResult => result.status === 'rejected');
12
+ if (failures.length) throw new AggregateError(failures.map((result) => result.reason), 'Carthagent terminal cleanup failed');
13
+ };
14
+ try {
15
+ for (const definition of [diagnostics, workspace]) {
16
+ const cleanup = await definition.setup(context);
17
+ if (cleanup) cleanups.push(cleanup);
18
+ }
19
+ return dispose;
20
+ } catch (error) {
21
+ try { await dispose(); }
22
+ catch (cleanupError) { throw new AggregateError([error, cleanupError], 'Carthagent terminal setup failed'); }
23
+ throw error;
24
+ }
25
+ },
26
+ });