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,235 @@
1
+ /** @jsxImportSource @opentui/solid */
2
+ import { For, Show, createEffect, createSignal, on, onCleanup, onMount } from "solid-js";
3
+ import { createHash, randomUUID } from "node:crypto";
4
+ import type { ScrollBoxRenderable, SelectRenderable, TextareaRenderable } from "@opentui/core";
5
+ import { usePlugin } from "@opencode/plugin/tui";
6
+ import { clickSelect } from "./select-mouse.js";
7
+ import { dailyQuestsClient, dailySourceClient } from "./client.js";
8
+ import type { DailyEvidence, DailyQuestDetail, DailySnapshot, DailySubmission, DailyRecord } from "../daily-quests-rpc.js";
9
+ import type { DailySourceReport } from "../daily-source-rpc.js";
10
+ import { createDailyQuestsState, dailySubmissionLabel } from "./daily-quests-state.mjs";
11
+ import { handleDailyQuestsEvent } from "./daily-quests-events.mjs";
12
+ import { syncDailyEditor } from "./daily-editor.mjs";
13
+ import { createDailyEditorFocus } from "./daily-editor-focus.mjs";
14
+
15
+ type Request = NonNullable<DailyRecord['request']>;
16
+ type View = {
17
+ snapshot?: DailySnapshot; detail?: DailyQuestDetail; busy: boolean; error: string;
18
+ draft?: { accountKey: string; questId: string; id: string; text: string; provenance: string };
19
+ preview?: Request; phase: "overview" | "edit" | "preview";
20
+ };
21
+
22
+ /** Stable opaque references, not proof of authorship. Never send a local path. */
23
+ function reference(...parts: string[]) {
24
+ const h = createHash("sha256").update(JSON.stringify(parts)).digest("hex");
25
+ return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20, 32)}`;
26
+ }
27
+
28
+ const privacy = "Only the exact preview below is sent to Carthagent. External JEV receives the revision, goal, change, checks, findings and server rubric, not project/outcome/run IDs. No transcript or whole project is uploaded. Review and remove secrets yourself; automatic secret detection is not provided.\n\nBackend raw evidence: pending up to 24 hours, erased on terminal processing (outages can delay cleanup). Metadata: 90 days. Provider policy applies; immediate provider/backup deletion is not promised. Unknown-send recovery stores this exact payload in a private local journal until acceptance is known or for up to 24 hours on the next journal access, not a background deletion timer. You can erase its local payload while retaining the submission ID.\n\nClassifier assessment is not independent verification. No coins, XP, distance, inventory or other rewards. Signing in or running Ship is not consent.";
29
+
30
+ export function DailyQuestsPage(props: { sessionID?: string; questID?: string }) {
31
+ const context = usePlugin();
32
+ const location = () => (props.sessionID ? context.data.session.get(props.sessionID)?.location : undefined) ?? context.location ?? context.data.location.default();
33
+ const [view, setView] = createSignal<View>({ busy: false, error: "", phase: "overview" });
34
+ const [editorFocus, setEditorFocus] = createSignal(true);
35
+ const [replayID, setReplayID] = createSignal<string>();
36
+ const [cancellationNotice, setCancellationNotice] = createSignal("");
37
+ const controller = createDailyQuestsState({ api: dailyQuestsClient(context, location), changed: (next: View) => setView(next) });
38
+ const sourceApi = dailySourceClient(context, location);
39
+ let summary: ScrollBoxRenderable | undefined;
40
+ let editor: TextareaRenderable | undefined;
41
+ let actions: SelectRenderable | undefined;
42
+ const focus = createDailyEditorFocus({
43
+ state: () => ({ phase: view().phase, hasDraft: !!view().draft, busy: view().busy, editorFocus: editorFocus(), mode: context.keymap.mode.current() }),
44
+ editor: () => editor,
45
+ });
46
+ createEffect(() => focus.reconcile());
47
+ const preview = () => { if (view().phase === "edit") syncDailyEditor(editor, controller); controller.preview(); };
48
+ let activeDialog: symbol | undefined;
49
+ const ownDialog = async <T,>(show: () => Promise<T>): Promise<T> => {
50
+ const token = Symbol(); activeDialog = token;
51
+ try { return await show(); } finally { if (activeDialog === token) activeDialog = undefined; }
52
+ };
53
+ const closeOwnDialog = () => {
54
+ if (!activeDialog) return;
55
+ activeDialog = undefined;
56
+ context.ui.dialog.clear();
57
+ };
58
+ const confirm = (title: string, message: string, label: string) => ownDialog(() => context.ui.dialog.confirm({ title, message, label: { confirm: label, cancel: "Cancel" } }));
59
+ context.keymap.layer(() => ({ commands: [
60
+ { bind: "ctrl+u", title: "Scroll daily quest preview up", run: () => { summary?.scrollBy(-1); } },
61
+ { bind: "ctrl+d", title: "Scroll daily quest preview down", run: () => { summary?.scrollBy(1); } },
62
+ { bind: "ctrl+g", title: "Validate daily evidence preview", enabled: () => view().phase === "edit" && !view().busy, run: preview },
63
+ { bind: "tab", title: "Switch evidence editor / actions", enabled: () => view().phase === "edit" && !view().busy, run: () => { setEditorFocus((focused) => !focused); } },
64
+ ] }));
65
+
66
+ const chooseProjection = async (source: DailySourceReport, sessionID: string, live: () => boolean): Promise<DailyEvidence | undefined> => {
67
+ let goal = false, change = false;
68
+ const checks = new Set<string>(), findings = new Set<string>();
69
+ while (live()) {
70
+ const selected = await ownDialog(() => context.ui.dialog.select({
71
+ title: "Select local evidence · nothing included by default",
72
+ options: [
73
+ { title: "Done selecting", value: "done", description: "Open editable JSON; nothing is sent yet" },
74
+ { title: `${goal ? "Remove" : "Include"} recorded goal`, value: "goal", description: "May contain private task text" },
75
+ { title: `${change ? "Remove" : "Include"} recorded change`, value: "change", description: source.change ? "Recorded claim, not independently verified" : "Not recorded; remains empty" },
76
+ ...source.checks.map((c) => ({ title: `${checks.has(c.id) ? "Remove" : "Include"} check ${c.id}`, value: `check:${c.id}`, description: `${c.after} · ${c.after === "passed" ? "fresh" : "not fresh"} · includes output tail, potentially sensitive; before not recorded` })),
77
+ ...source.findings.map((f) => ({ title: `${findings.has(f.id) ? "Remove" : "Include"} finding ${f.id}`, value: `finding:${f.id}`, description: "Includes issue, evidence and resolution text; model/user-authored claim" })),
78
+ ],
79
+ }));
80
+ if (!live() || selected === undefined) return;
81
+ if (selected === "done") break;
82
+ if (selected === "goal") goal = !goal;
83
+ else if (selected === "change") change = !change;
84
+ else {
85
+ const isCheck = selected.startsWith("check:");
86
+ const id = selected.slice(isCheck ? 6 : 8), set = isCheck ? checks : findings;
87
+ if (set.has(id)) set.delete(id); else set.add(id);
88
+ }
89
+ }
90
+ if (!live()) return;
91
+ const session = context.data.session.get(sessionID);
92
+ const project = reference("daily-project-v1", session?.projectID ?? location().directory);
93
+ return {
94
+ projectId: project, outcomeId: reference("daily-outcome-v1", project, source.runId), runId: source.runId,
95
+ revision: source.revision, goal: goal ? source.goal : "", change: change ? source.change : "",
96
+ checks: source.checks.filter((c) => checks.has(c.id)).map(({ id, before, after, revision, result }) => ({ name: id, before, after, revision, result })),
97
+ findings: source.findings.filter((f) => findings.has(f.id)).map(({ issue, evidence, resolution }) => ({ issue, evidence, resolution })),
98
+ };
99
+ };
100
+
101
+ const compose = () => controller.compose(async (_quest: DailyQuestDetail, live: () => boolean, signal: AbortSignal) => {
102
+ const kind = await ownDialog(() => context.ui.dialog.select({ title: "Choose evidence source", options: [
103
+ { title: "Select actual local Ship report", value: "local", description: "Owned session only; choose individual checks/findings, no transcript" },
104
+ { title: "Write a structured manual claim", value: "manual", description: "Blank evidence, not a claim that checks ran; edit every field" },
105
+ ] }));
106
+ if (!kind || !live()) return;
107
+ if (kind === "manual") {
108
+ const project = reference("daily-project-v1", props.sessionID ? context.data.session.get(props.sessionID)?.projectID ?? location().directory : location().directory);
109
+ return { id: randomUUID(), evidence: { projectId: project, outcomeId: randomUUID(), runId: randomUUID(), revision: "", goal: "", change: "", checks: [], findings: [] }, provenance: "Manual user claim. Enter a genuine 40/64-hex revision. Empty evidence does not establish completion. Keep project/outcome/run IDs when revising the same outcome; they do not prove authorship." };
110
+ }
111
+ const sessions = context.data.session.list().filter((s) => s.location.directory === location().directory);
112
+ const sessionID = props.sessionID ?? await ownDialog(() => context.ui.dialog.select({ title: "Choose local Ship session", options: sessions.map((s) => ({ title: s.title ?? s.id, value: s.id, description: s.id })) }));
113
+ if (!sessionID || !live()) return;
114
+ const { source } = await sourceApi.report({ sessionID }, AbortSignal.any([signal, AbortSignal.timeout(15_000)]));
115
+ if (!live()) return;
116
+ if (!source) throw new Error("daily_source_unavailable");
117
+ const evidence = await chooseProjection(source, sessionID, live);
118
+ if (!evidence || !live()) return;
119
+ return { id: randomUUID(), evidence, provenance: `Local Ship session ${sessionID} · run ${source.runId} · plan revision ${source.planRevision} · ${source.freshness}. Source revision is the actual workspace fingerprint, not the plan revision. Earlier failed-before execution is not recorded and is never inferred. Selected text is editable and remains a user claim, not independent verification.${source.truncated ? " Source projection is truncated; inspect and correct missing details before submitting." : ""}` };
120
+ });
121
+
122
+ const submit = () => controller.submit(({ quest, request }: { quest: DailyQuestDetail; request: Request }) => confirm(
123
+ "Explicit consent for this one JEV submission",
124
+ `${quest.definition.title}\nSubmission ${request.id}\nSend only the exact JSON shown in the scrollable preview to Carthagent for external JEV grading? Review/redact before consent. Raw backend evidence: pending up to 24h, erased on terminal processing; metadata 90d. Outages can delay cleanup. Provider/backup deletion is not guaranteed. No rewards or independent verification.\n\nCancel to review the preview and full retention notice again. This authorizes this submission only.`,
125
+ "Consent and send",
126
+ ));
127
+ const retry = (id: string) => controller.retry(id, (record: DailyRecord) => confirm("Retry the SAME consented submission?", `Submission ${record.id}\nReplay only the exact saved ID/body shown in the scrollable recovery preview to Carthagent/external JEV under the same retention notice? Poll first when possible. This cannot edit evidence or create a replacement ID. Cancel keeps it pending.`, "Replay same request"));
128
+ const forget = (id: string) => controller.forget(id, (record: DailyRecord) => confirm("Erase local recovery payload?", `Submission ${record.id}\nErase the selected evidence from the local journal. Keep the stable submission/quest IDs for read-only status checks. Exact replay will no longer be possible. This does not cancel a server job, withdraw evidence already sent, or promise provider/backup deletion.`, "Erase local payload"));
129
+
130
+ const statusText = (submission: DailySubmission) => `${dailySubmissionLabel(submission)}\nSubmission ${submission.id} · provider attempts ${submission.attempts}/4\nCreated ${submission.createdAt}${submission.nextAttemptAt ? ` · next attempt ${submission.nextAttemptAt}` : ""}${submission.finishedAt ? ` · finished ${submission.finishedAt}` : ""}${submission.errorCode ? `\nProcessing code: ${submission.errorCode}` : ""}${submission.model ? `\nModel ${submission.model} · rubric ${submission.rubricVersion}` : ""}${submission.scores ? `\nUncalibrated rubric scores (not confidence): ${Object.entries(submission.scores).map(([id, score]) => `${id}: ${score}`).join(" · ")}` : ""}`;
131
+ const options = () => {
132
+ const state = view(), detail = state.detail;
133
+ const basic = [
134
+ { name: "Refresh daily assignments and recovery journal", description: "Read-only; never uploads or replays evidence", value: "refresh" },
135
+ { name: "Sign in / reconnect", description: "Native Carthagent Cloud device sign-in; not grading consent", value: "signin" },
136
+ { name: "Back to Voyages", description: "Discard unsent editor contents", value: "back" },
137
+ ];
138
+ if (!state.snapshot) return basic;
139
+ return [...basic,
140
+ ...(replayID() ? [
141
+ { name: "Confirm exact saved replay…", description: "Review the scrollable recovery preview first; explicit same-ID replay", value: "confirm-replay" },
142
+ { name: "Close recovery preview", description: "No replay; keep saved payload", value: "close-replay" },
143
+ ] : []),
144
+ ...state.snapshot.slate.items.map((q) => ({ name: `${q.definition.title} · ${q.status}`, description: `${q.definition.kind} · UTC window ends ${q.endsAt}`, value: `quest:${q.id}` })),
145
+ ...(detail?.status === "open" && !state.draft ? [{ name: "Choose evidence for selected quest", description: "Select local report data or write a manual claim; no upload", value: "compose" }] : []),
146
+ ...(state.phase === "edit" ? [{ name: "Validate and preview exact payload", description: "Ctrl+G; canonical UTF-8 limits, no network", value: "preview" }] : []),
147
+ ...(state.phase === "preview" ? [
148
+ { name: "Consent and submit this preview…", description: "Fresh explicit consent dialog; no automatic acceptance or rewards", value: "submit" },
149
+ { name: "Edit / redact evidence again", description: "Invalidates the preview; requires validation and fresh consent", value: "edit" },
150
+ ] : []),
151
+ ...(state.draft ? [{ name: "Discard unsent evidence", description: "Clear local editor; no submission", value: "cancel" }] : []),
152
+ ...(detail?.latestSubmission ? [{ name: "Poll selected quest's latest submission", description: "Read-only authoritative status; no provider dispatch", value: `poll:${detail.latestSubmission.id}` }] : []),
153
+ ...state.snapshot.records.flatMap((r) => [
154
+ { name: `Poll saved ${r.id}`, description: r.submission ? dailySubmissionLabel(r.submission) : "Outcome unknown; stable ID retained, including previous UTC days", value: `poll:${r.id}` },
155
+ ...(r.request ? [
156
+ { name: `Review exact replay ${r.id}`, description: "Saved same ID/body; explicit confirmation, never automatic", value: `retry:${r.id}` },
157
+ { name: `Erase local payload ${r.id}`, description: "Keep ID for polling; cannot replay afterward", value: `forget:${r.id}` },
158
+ ] : []),
159
+ ]),
160
+ ];
161
+ };
162
+ const select = (value?: string) => {
163
+ // A stale native refocus must not turn editor Enter into an action.
164
+ if (!value || !focus.actionsEnabled()) return;
165
+ if (value === "back") { context.ui.router.navigate({ type: "plugin", name: "voyages", data: { ...(props.sessionID ? { sessionID: props.sessionID } : {}) } }); return; }
166
+ if (value === "signin") { controller.invalidate(); closeOwnDialog(); context.keymap.dispatch("provider.connect"); return; }
167
+ if (value === "refresh") { setReplayID(undefined); void controller.refresh(); return; }
168
+ if (value === "compose") { setCancellationNotice(""); setEditorFocus(true); void compose(); return; }
169
+ if (value === "preview") { preview(); return; }
170
+ if (value === "submit") { void submit(); return; }
171
+ if (value === "edit") { setEditorFocus(true); controller.backToEdit(); return; }
172
+ if (value === "cancel") { controller.cancelDraft(); return; }
173
+ if (value === "close-replay") { setReplayID(undefined); return; }
174
+ if (value === "confirm-replay") { const id = replayID(); if (id) { void retry(id).finally(() => setReplayID(undefined)); } return; }
175
+ const colon = value.indexOf(":"), id = value.slice(colon + 1);
176
+ if (value.startsWith("quest:")) void controller.select(id);
177
+ if (value.startsWith("poll:")) void controller.poll(id);
178
+ if (value.startsWith("retry:")) { setCancellationNotice(""); controller.cancelDraft(); setReplayID(id); }
179
+ if (value.startsWith("forget:")) void forget(id);
180
+ };
181
+ onMount(() => {
182
+ let binding = JSON.stringify(location());
183
+ const stop = context.data.listen(({ details }) => {
184
+ const nextBinding = JSON.stringify(location());
185
+ const changedLocation = binding !== nextBinding;
186
+ binding = nextBinding;
187
+ void handleDailyQuestsEvent({
188
+ controller, event: details, location: location(), changedLocation, actions,
189
+ interrupted: !!(view().draft || view().preview || replayID() || activeDialog),
190
+ clearReplay: () => setReplayID(undefined), closeDialog: closeOwnDialog, notice: setCancellationNotice,
191
+ });
192
+ });
193
+ // A navigator link selects only an assignment in the authenticated slate.
194
+ // It never composes evidence or opens a consent dialog.
195
+ void controller.refresh().then(() => { if (props.questID) return controller.select(props.questID); });
196
+ // The router reuses this page when another rail quest is opened.
197
+ createEffect(on(() => props.questID, (questID) => { if (questID) void controller.select(questID); }, { defer: true }));
198
+ onCleanup(() => { focus.dispose(); stop(); actions = undefined; controller.dispose(); closeOwnDialog(); });
199
+ });
200
+ return <box flexDirection="column" flexGrow={1} padding={1}>
201
+ <text flexShrink={0} fg={context.theme.text.action.primary.base}>CARTHAGENT · DAILY CODING QUESTS</text>
202
+ <scrollbox ref={summary} flexBasis={0} flexGrow={1} minHeight={1} contentOptions={{ flexDirection: "column" }}>
203
+ <text flexShrink={0} fg={context.theme.text.muted}>Classifier-assessed submitted evidence, not independent verification. No rewards. Refresh/poll never uploads evidence.</text>
204
+ <Show when={cancellationNotice()}><text flexShrink={0} fg={context.theme.text.feedback.warning.base}>{cancellationNotice()}</text></Show>
205
+ <Show when={view().busy}><text flexShrink={0} fg={context.theme.text.feedback.warning.base}>Awaiting response / explicit selection or consent. Other actions locked.</text></Show>
206
+ <Show when={view().error}><text flexShrink={0} fg={context.theme.text.feedback.error.base}>{view().error}</text></Show>
207
+ <Show when={view().snapshot} fallback={<text flexShrink={0} fg={context.theme.text.muted}>Protected assignments and evidence hidden until successful authentication and refresh.</text>}>{(snapshot) => <box flexDirection="column" flexShrink={0}>
208
+ <text flexShrink={0} fg={context.theme.text.base}>UTC day {snapshot().slate.day} · closes {snapshot().slate.endsAt} · policy {snapshot().slate.policyVersion}</text>
209
+ <text flexShrink={0} fg={context.theme.text.muted}>New submissions close at the server's UTC boundary. Accepted jobs can finish afterward. Select a quest below for its actual criteria.</text>
210
+ <For each={snapshot().records}>{(r) => <text flexShrink={0} fg={context.theme.text.muted}>{r.submission ? statusText(r.submission) : `Outcome unknown / no confirmed acceptance · submission ${r.id} · quest ${r.questId}${r.request ? " · exact replay payload available" : " · payload erased; poll ID only"}`}</text>}</For>
211
+ </box>}</Show>
212
+ <Show when={view().detail}>{(detail) => <box flexDirection="column" flexShrink={0}>
213
+ <text flexShrink={0} fg={context.theme.text.action.primary.base}>{detail().definition.title} · {detail().status}</text>
214
+ <text flexShrink={0} fg={context.theme.text.muted}>Quest {detail().id} · {detail().definition.kind} · ends {detail().endsAt} · rubric {detail().definition.rubricVersion}</text>
215
+ <For each={detail().definition.criteria}>{(c) => <text flexShrink={0} fg={context.theme.text.base}>{c.id}: {c.text}</text>}</For>
216
+ <Show when={detail().latestSubmission}>{(s) => <text flexShrink={0} fg={context.theme.text.base}>{statusText(s())}</text>}</Show>
217
+ </box>}</Show>
218
+ <Show when={view().draft}><text flexShrink={0} fg={context.theme.text.muted}>{view().draft?.provenance}</text></Show>
219
+ <Show when={view().phase === "preview" && view().preview}>
220
+ <text flexShrink={0} fg={context.theme.text.base}>{privacy}</text>
221
+ <text flexShrink={0} fg={context.theme.text.base}>{`Exact request for quest ${view().draft?.questId}:\n${JSON.stringify(view().preview, null, 2)}`}</text>
222
+ </Show>
223
+ <Show when={view().snapshot?.records.find((r) => r.id === replayID() && r.request)}>{(record) => <box flexDirection="column" flexShrink={0}>
224
+ <text flexShrink={0} fg={context.theme.text.base}>{privacy}</text>
225
+ <text flexShrink={0} fg={context.theme.text.base}>{`Exact saved recovery request · quest ${record().questId}:\n${JSON.stringify(record().request, null, 2)}`}</text>
226
+ </box>}</Show>
227
+ </scrollbox>
228
+ <Show when={view().phase === "edit" && view().draft}>
229
+ <text flexShrink={0} fg={context.theme.text.faint}>Ctrl+J newline · Tab actions · Ctrl+G preview, never sends</text>
230
+ <textarea ref={editor} flexBasis={0} flexGrow={1} minHeight={2} initialValue={view().draft?.text ?? ""} focused={focus.editorEnabled()} onContentChange={() => syncDailyEditor(editor, controller)} keyBindings={[{ name: "g", ctrl: true, action: "submit" }]} onSubmit={preview} />
231
+ </Show>
232
+ <text flexShrink={0} fg={context.theme.text.faint}>↑/↓ Enter · PgUp/Dn · Ctrl+U/D details · Cancel: no send</text>
233
+ <select ref={(node) => { actions = node; focus.attachActions(node); }} onMouseUp={(event) => { if (focus.actionsEnabled()) clickSelect(actions, event); }} flexBasis={0} flexGrow={1} flexShrink={0} minHeight={3} showScrollIndicator options={options()} focused={focus.actionsEnabled()} keyBindings={[{ name: "pageup", action: "move-up-fast" }, { name: "pagedown", action: "move-down-fast" }]} onSelect={(_index, option) => select(option?.value)} />
234
+ </box>;
235
+ }
@@ -0,0 +1,42 @@
1
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+
4
+ export type NativeCliConfig = Record<string, unknown> & { theme?: { name?: string } | string };
5
+
6
+ /** Resolve OpenCode's native CLI profile config (not CTG's separate user profile). */
7
+ export function cliConfigPath(env = process.env, home = process.env.HOME ?? "") {
8
+ const configRoot = env.OPENCODE_CONFIG_DIR || join(env.XDG_CONFIG_HOME || join(home, ".config"), "opencode");
9
+ return join(configRoot, "cli.json");
10
+ }
11
+
12
+ /** Native TUI themes are discovered from config/opencode/themes for this profile. */
13
+ export function cliConfigDirectory(env = process.env, home = process.env.HOME ?? "") {
14
+ return env.OPENCODE_CONFIG_DIR || join(env.XDG_CONFIG_HOME || join(home, ".config"), "opencode");
15
+ }
16
+
17
+ export async function readCliConfig(path: string): Promise<NativeCliConfig> {
18
+ try {
19
+ const value: unknown = JSON.parse(await readFile(path, "utf8"));
20
+ return value && typeof value === "object" && !Array.isArray(value) ? value as NativeCliConfig : {};
21
+ } catch (error) {
22
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return {};
23
+ throw error;
24
+ }
25
+ }
26
+
27
+ /** Atomically preserve unrelated native CLI settings while selecting a theme. */
28
+ export async function writeNativeTheme(path: string, name: string) {
29
+ const config = await readCliConfig(path);
30
+ config.theme = typeof config.theme === "object" && config.theme !== null
31
+ ? { ...config.theme, name }
32
+ : { name };
33
+ await mkdir(dirname(path), { recursive: true });
34
+ const temp = `${path}.${process.pid}.${crypto.randomUUID()}.tmp`;
35
+ try {
36
+ await writeFile(temp, `${JSON.stringify(config, null, 2)}\n`, { encoding: "utf8", flag: "wx", mode: 0o600 });
37
+ await rename(temp, path);
38
+ } catch (error) {
39
+ await import("node:fs/promises").then(({ rm }) => rm(temp, { force: true })).catch(() => undefined);
40
+ throw error;
41
+ }
42
+ }
@@ -0,0 +1,32 @@
1
+ type CatalogModel = { providerID: string; modelID: string; id: string; name: string };
2
+
3
+ export type ModelCommandHost = {
4
+ sync(): Promise<unknown>;
5
+ list(): readonly CatalogModel[];
6
+ select(options: Array<{ title: string; value: CatalogModel; description: string }>): Promise<CatalogModel | undefined>;
7
+ switchModel(sessionID: string, model: { providerID: string; id: string }): Promise<unknown>;
8
+ /** Opens the native configured-model picker (the `/models` command) with an optional search. */
9
+ openNativePicker(query: string): void;
10
+ providerName(providerID: string): string;
11
+ toast(message: string, variant: "success" | "warning"): void;
12
+ };
13
+
14
+ /**
15
+ * `/model [name]`. Without a session the choice belongs to the next session's draft, which only the native
16
+ * picker owns, so it is opened directly; inside a session the selection is committed to that session.
17
+ */
18
+ export async function runModelCommand(host: ModelCommandHost, input: { sessionID?: string; arg: string }) {
19
+ if (!input.sessionID) {
20
+ host.openNativePicker(input.arg);
21
+ return;
22
+ }
23
+ await host.sync();
24
+ const query = input.arg.toLowerCase();
25
+ const options = host.list()
26
+ .filter((model) => !query || `${host.providerName(model.providerID)} ${model.providerID}/${model.modelID} ${model.name}`.toLowerCase().includes(query))
27
+ .map((model) => ({ title: `${host.providerName(model.providerID)}/${model.modelID}`, value: model, description: model.name }));
28
+ const selected = await host.select(options);
29
+ if (!selected) return;
30
+ await host.switchModel(input.sessionID, { providerID: selected.providerID, id: selected.id });
31
+ host.toast(`Selected ${host.providerName(selected.providerID)}/${selected.modelID}.`, "success");
32
+ }
@@ -0,0 +1,23 @@
1
+ import type { Context } from "@opencode/plugin/tui/context";
2
+ import type { JSX } from "solid-js";
3
+
4
+ /** Optional host extension: released runtimes retain slash-command navigation. */
5
+ export type NavigationInput = {
6
+ readonly width: number;
7
+ readonly compact: boolean;
8
+ readonly active: boolean;
9
+ readonly focused: boolean;
10
+ readonly sessionID?: string;
11
+ readonly focus: () => void;
12
+ readonly blur: () => void;
13
+ };
14
+
15
+ export type NativeNavigation = {
16
+ register(pane: { name: string; title: string; render(input: NavigationInput): JSX.Element }): () => void;
17
+ select(name: string): boolean;
18
+ current(): string;
19
+ };
20
+
21
+ export function nativeNavigation(context: Pick<Context, "ui">) {
22
+ return (context.ui as Context["ui"] & { navigation?: NativeNavigation }).navigation;
23
+ }
@@ -0,0 +1,33 @@
1
+ export interface NativeReviewOffer {
2
+ sessionID: string;
3
+ runKey: string;
4
+ writes: number;
5
+ start: () => Promise<boolean>;
6
+ }
7
+
8
+ export interface NativeReviewConsumerOptions {
9
+ mode: (sessionID: string) => string;
10
+ diff: (sessionID: string, signal?: AbortSignal) => Promise<unknown>;
11
+ offer: (value: NativeReviewOffer) => Promise<unknown>;
12
+ kickoff: (sessionID: string, signal?: AbortSignal) => Promise<unknown>;
13
+ /** False or a rejection suppresses the offer (e.g. blocked formal delivery). */
14
+ eligible?: (sessionID: string, signal?: AbortSignal) => Promise<boolean>;
15
+ isLoopActive?: (sessionID: string) => boolean;
16
+ setLoopActive?: (sessionID: string, active: boolean) => void;
17
+ signal?: AbortSignal;
18
+ }
19
+
20
+ export function sendNativeReviewKickoff(options: { client: any; sessionID: string; signal?: AbortSignal; prompt?: string }): Promise<void>;
21
+ export function nativeReviewMode(session: any, config: any, explicitMode?: string): string;
22
+ export function createNativeReviewConsumer(options: NativeReviewConsumerOptions): {
23
+ setLoopActive(sessionID: string, active: boolean): void;
24
+ onUserPromptAdmitted(sessionID: string, message?: { type?: string; metadata?: Record<string, unknown> }): void;
25
+ onInboxDelivered(event: any, message?: { type?: string; metadata?: Record<string, unknown> }): void;
26
+ onStarted(event: any): void;
27
+ onSucceeded(event: any): Promise<void>;
28
+ onFailed(event: any): Promise<void>;
29
+ onInterrupted(event: any): Promise<void>;
30
+ enqueueInboxDelivery(event: any, getMessage: (sessionID: string, messageID: string, signal: AbortSignal) => Promise<{ type?: string; metadata?: Record<string, unknown> } | undefined>): Promise<void>;
31
+ dispose(): void;
32
+ };
33
+ export function subscribeNativeReviewEvents(data: any, consumer: ReturnType<typeof createNativeReviewConsumer>, onError?: (error: unknown) => void, getMessage?: (sessionID: string, messageID: string, signal: AbortSignal) => Promise<{ type?: string; metadata?: Record<string, unknown> } | undefined>): Array<() => void>;
@@ -0,0 +1,159 @@
1
+ import { reviewKickoff } from "../../../../lib/review.mjs";
2
+ import { resolveReviewMode } from "../../../../lib/review.mjs";
3
+ import { SESSION_OPTIONS_KEY } from "../../../../lib/opencode/session-options.mjs";
4
+
5
+ /** Send the PR ↔ fresh-review ↔ fixes instruction to the working session. */
6
+ export async function sendNativeReviewKickoff({ client, sessionID, signal, prompt = reviewKickoff() }) {
7
+ await client.session.prompt({ sessionID, text: prompt, metadata: { "ctg.raw": true, "ctg.reviewKickoff": true } }, { signal });
8
+ }
9
+
10
+ /** Explicit launch mode persisted for the session wins over the profile default. */
11
+ export function nativeReviewMode(session, config, explicitMode) {
12
+ return resolveReviewMode(explicitMode ?? session?.metadata?.[SESSION_OPTIONS_KEY]?.review, config);
13
+ }
14
+
15
+ /** Correlate host executions and admit one current-run kickoff per session. */
16
+ /** @param {{ mode: (sessionID: string) => string, diff: (sessionID: string, signal?: AbortSignal) => Promise<unknown>, offer: (value: { sessionID: string; runKey: string; writes: number; start: () => Promise<boolean> }) => Promise<unknown>, eligible?: (sessionID: string, signal?: AbortSignal) => Promise<boolean>, kickoff: (sessionID: string, signal?: AbortSignal) => Promise<unknown>, isLoopActive?: (sessionID: string) => boolean, setLoopActive?: (sessionID: string, active: boolean) => void, signal?: AbortSignal }} options */
17
+ export function createNativeReviewConsumer({ mode, diff, offer, kickoff, eligible = async () => true, isLoopActive = () => false, setLoopActive = () => {}, signal }) {
18
+ const runs = new Map();
19
+ const admitted = new Set();
20
+ const pending = new Set();
21
+ const loopSessions = new Set();
22
+ const admissionTails = new Map();
23
+ const loopGenerations = new Map();
24
+ const admissionAbort = new AbortController();
25
+ let sequence = 0;
26
+ let disposed = false;
27
+
28
+ async function finish(sessionID, succeeded) {
29
+ const run = runs.get(sessionID);
30
+ if (!run || disposed) return;
31
+ // Inbox delivery follows execution.started and precedes the terminal event.
32
+ // Wait for the exact delivered message lookup before deciding whether the
33
+ // restored review loop still suppresses this run.
34
+ const admissionTail = admissionTails.get(sessionID);
35
+ try { await admissionTail; } catch {}
36
+ if (admissionTails.get(sessionID) === admissionTail) admissionTails.delete(sessionID);
37
+ if (disposed || runs.get(sessionID) !== run || run.finished) return;
38
+ run.finished = true;
39
+ if (!succeeded || signal?.aborted || loopSessions.has(sessionID) || isLoopActive(sessionID)) return;
40
+
41
+ let baseline;
42
+ try { baseline = await run.baseline; }
43
+ catch { return; }
44
+ if (disposed || signal?.aborted || runs.get(sessionID) !== run) return;
45
+ let changes;
46
+ try { changes = await diff(sessionID, signal); }
47
+ catch { return; }
48
+ if (disposed || signal?.aborted || runs.get(sessionID) !== run) return;
49
+ if (!Array.isArray(changes) || !Array.isArray(baseline)) return;
50
+ const before = new Map(baseline.map((file) => [file.file, `${file.status}\0${file.patch}`]));
51
+ const writes = changes.filter((file) => file && (file.additions > 0 || file.deletions > 0 || file.status === "added" || file.status === "deleted") && before.get(file.file) !== `${file.status}\0${file.patch}`).length;
52
+ const runKey = `${sessionID}@${run.id}`;
53
+ const currentMode = mode(sessionID);
54
+ if (!(currentMode === "ask" || currentMode === "yes") || writes <= 0 || admitted.has(runKey) || (currentMode === "yes" && loopSessions.has(sessionID))) return;
55
+ // A formally blocked or unfinished delivery is not a reviewable run.
56
+ let reviewable;
57
+ try { reviewable = await eligible(sessionID, signal); }
58
+ catch { return; }
59
+ if (!reviewable || disposed || signal?.aborted || runs.get(sessionID) !== run || admitted.has(runKey)) return;
60
+ admitted.add(runKey);
61
+
62
+ const start = async () => {
63
+ if (disposed || signal?.aborted || runs.get(sessionID) !== run || pending.has(sessionID) || loopSessions.has(sessionID) || isLoopActive(sessionID)) return false;
64
+ pending.add(sessionID);
65
+ loopSessions.add(sessionID);
66
+ setLoopActive(sessionID, true);
67
+ try {
68
+ await kickoff(sessionID, signal);
69
+ return true;
70
+ } catch (error) {
71
+ loopSessions.delete(sessionID);
72
+ setLoopActive(sessionID, false);
73
+ throw error;
74
+ } finally {
75
+ pending.delete(sessionID);
76
+ }
77
+ };
78
+
79
+ if (currentMode === "yes") {
80
+ await start();
81
+ return;
82
+ }
83
+ let offered = false;
84
+ await offer({ sessionID, runKey, writes, start: async () => {
85
+ if (offered) return false;
86
+ offered = true;
87
+ if (runs.get(sessionID) !== run) return false;
88
+ return start();
89
+ } });
90
+ }
91
+
92
+ return {
93
+ onUserPromptAdmitted(sessionID, message, expectedGeneration) {
94
+ if (!sessionID || message?.type !== "user" || message.metadata?.["ctg.reviewKickoff"] === true) return;
95
+ if (expectedGeneration !== undefined && (loopGenerations.get(sessionID) ?? 0) !== expectedGeneration) return;
96
+ loopSessions.delete(sessionID);
97
+ if (isLoopActive(sessionID)) setLoopActive(sessionID, false);
98
+ },
99
+ setLoopActive(sessionID, active) {
100
+ if (!sessionID) return;
101
+ if (active) {
102
+ loopSessions.add(sessionID);
103
+ loopGenerations.set(sessionID, (loopGenerations.get(sessionID) ?? 0) + 1);
104
+ }
105
+ else loopSessions.delete(sessionID);
106
+ setLoopActive(sessionID, active);
107
+ },
108
+ onStarted(event) {
109
+ const sessionID = event?.data?.sessionID;
110
+ if (!sessionID || disposed) return;
111
+ const id = event.id ?? `${event.created ?? Date.now()}-${++sequence}`;
112
+ // A rejection is handled here, not left as an unhandled promise if the run fails.
113
+ const baseline = Promise.resolve().then(() => diff(sessionID, signal)).catch(() => undefined);
114
+ runs.set(sessionID, { id, baseline, finished: false });
115
+ },
116
+ onSucceeded(event) { return finish(event?.data?.sessionID, true); },
117
+ onFailed(event) { return finish(event?.data?.sessionID, false); },
118
+ onInterrupted(event) { return finish(event?.data?.sessionID, false); },
119
+ onInboxDelivered(event, message) {
120
+ const sessionID = event?.data?.sessionID;
121
+ if (!sessionID || disposed) return;
122
+ this.onUserPromptAdmitted(sessionID, message);
123
+ },
124
+ enqueueInboxDelivery(event, getMessage) {
125
+ const sessionID = event?.data?.sessionID;
126
+ const messageID = event?.data?.inboxID;
127
+ if (!sessionID || !messageID || disposed) return Promise.resolve();
128
+ const expectedGeneration = loopGenerations.get(sessionID) ?? 0;
129
+ const previous = admissionTails.get(sessionID) ?? Promise.resolve();
130
+ const current = previous.catch(() => {}).then(async () => {
131
+ if (disposed) return;
132
+ const timeout = AbortSignal.timeout(5_000);
133
+ const lookupSignal = signal ? AbortSignal.any([signal, admissionAbort.signal, timeout]) : AbortSignal.any([admissionAbort.signal, timeout]);
134
+ lookupSignal.throwIfAborted();
135
+ const message = await Promise.race([
136
+ getMessage(sessionID, messageID, lookupSignal),
137
+ new Promise((resolve) => lookupSignal.addEventListener("abort", () => resolve(undefined), { once: true })),
138
+ ]);
139
+ if (!disposed && !lookupSignal.aborted) this.onUserPromptAdmitted(sessionID, message, expectedGeneration);
140
+ });
141
+ admissionTails.set(sessionID, current);
142
+ return current;
143
+ },
144
+ dispose() { disposed = true; admissionAbort.abort(); admissionTails.clear(); runs.clear(); pending.clear(); },
145
+ };
146
+ }
147
+
148
+ /** Wire host admission and execution events; returned disposers belong to plugin setup. */
149
+ export function subscribeNativeReviewEvents(data, consumer, onError = (_error) => {}, getMessage = async () => undefined) {
150
+ return [
151
+ data.on("session.inbox.delivered", (event) => {
152
+ return consumer.enqueueInboxDelivery(event, getMessage).catch(onError);
153
+ }),
154
+ data.on("session.execution.started", (event) => consumer.onStarted(event)),
155
+ data.on("session.execution.succeeded", (event) => consumer.onSucceeded(event).catch(onError)),
156
+ data.on("session.execution.failed", (event) => consumer.onFailed(event).catch(onError)),
157
+ data.on("session.execution.interrupted", (event) => consumer.onInterrupted(event).catch(onError)),
158
+ ];
159
+ }
@@ -0,0 +1,34 @@
1
+ /** Resolve session IDs independently of the host's title-oriented search filter. */
2
+ export async function findNativeResumeSession({ query, directory, client, pageSize = 100 }) {
3
+ const normalized = query.trim().toLowerCase();
4
+ if (!normalized) return undefined;
5
+ const isSessionID = /^ses(?:sion)?[_-]/i.test(query.trim());
6
+
7
+ if (isSessionID) {
8
+ try {
9
+ const exact = await client.session.get({ sessionID: query.trim() });
10
+ if (exact && !exact.parentID && exact.location.directory === directory) return exact;
11
+ } catch { /* non-exact IDs continue as prefix search */ }
12
+ }
13
+
14
+ const loadPages = async (search) => {
15
+ const sessions = [];
16
+ let cursor;
17
+ do {
18
+ const page = await client.session.list({
19
+ limit: pageSize, order: 'desc', directory,
20
+ ...(cursor ? { cursor } : {}), ...(search ? { search } : {}),
21
+ });
22
+ sessions.push(...page.data.filter((session) => !session.parentID));
23
+ cursor = page.cursor.next ?? undefined;
24
+ } while (cursor);
25
+ return sessions;
26
+ };
27
+
28
+ const unfiltered = await loadPages();
29
+ if (isSessionID) {
30
+ const prefix = unfiltered.find((session) => session.id.toLowerCase().startsWith(normalized));
31
+ if (prefix) return prefix;
32
+ }
33
+ return (await loadPages(query)).find((session) => (session.title ?? '').toLowerCase().includes(normalized));
34
+ }
@@ -0,0 +1,17 @@
1
+ import type { MouseEvent, SelectRenderable } from "@opentui/core";
2
+
3
+ // SelectRenderable only handles keys; map a left click to the fully drawn row under the pointer and activate it.
4
+ // Bind it to mouse-up: an action that opens a dialog on mouse-down is closed by that dialog's backdrop on release.
5
+ export function clickSelect(node: SelectRenderable | undefined, event: MouseEvent) {
6
+ if (!node || event.button !== 0) return;
7
+ const { scrollOffset, linesPerItem, maxVisibleItems } = node as unknown as {
8
+ scrollOffset: number;
9
+ linesPerItem: number;
10
+ maxVisibleItems: number;
11
+ };
12
+ const row = Math.floor((event.y - node.screenY) / Math.max(1, linesPerItem));
13
+ const index = scrollOffset + row;
14
+ if (row < 0 || row >= maxVisibleItems || index >= node.options.length) return;
15
+ node.setSelectedIndex(index);
16
+ node.selectCurrent();
17
+ }
@@ -0,0 +1,7 @@
1
+ import type { JsonValue } from "@opencode/client";
2
+ export type AutoCompactionAction = "on" | "off" | "toggle";
3
+ export interface SessionMetadataClient {
4
+ get(input: { sessionID: string }, options: { location: unknown; signal: AbortSignal }): Promise<{ metadata?: Record<string, JsonValue> }>;
5
+ update(input: { sessionID: string; metadata: Record<string, JsonValue> }, options: { location: unknown; signal: AbortSignal }): Promise<unknown>;
6
+ }
7
+ export function setSessionAutoCompaction(client: SessionMetadataClient, sessionID: string, location: unknown, action: AutoCompactionAction): Promise<boolean>;
@@ -0,0 +1,14 @@
1
+ /** Persist the host-owned per-session opt-out without disturbing neighbor metadata. */
2
+ export async function setSessionAutoCompaction(client, sessionID, location, action) {
3
+ if (action !== "toggle" && action !== "on" && action !== "off") throw new TypeError("Usage: /autocompact [on|off].");
4
+ const options = { location, signal: AbortSignal.timeout(15_000) };
5
+ const session = await client.get({ sessionID }, options);
6
+ const metadata = { ...session.metadata };
7
+ const enabled = metadata["ctg.autoCompaction"] !== false;
8
+ const next = action === "on" ? true : action === "off" ? false : !enabled;
9
+ if (next === enabled) return enabled;
10
+ if (next) delete metadata["ctg.autoCompaction"];
11
+ else metadata["ctg.autoCompaction"] = false;
12
+ await client.update({ sessionID, metadata }, { location, signal: AbortSignal.timeout(15_000) });
13
+ return next;
14
+ }
@@ -0,0 +1,3 @@
1
+ export declare function convertLegacyTheme(name: string, palette: Record<string, unknown>, lightPalette?: Record<string, unknown>): Record<string, unknown>;
2
+ export declare function provisionNativeThemes(profileConfigDir: string, names?: readonly string[]): Promise<{ directory: string; installed: string[] }>;
3
+ export declare function provisionDefaultNativeTheme(profileConfigDir: string): Promise<{ directory: string; installed: string[] }>;