carthagent 1.1.0-preview.12 → 1.1.0-preview.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +69 -13
  2. package/docs/native-manual-testing.md +27 -5
  3. package/docs/opencode-owned-runtime.md +3 -3
  4. package/docs/patches/opencode-v2.0.32-session-ui-combined-v5.patch +13965 -0
  5. package/docs/release-notes-preview.13.md +68 -0
  6. package/docs/source-preview.md +120 -0
  7. package/lib/delivery-review.mjs +11 -1
  8. package/lib/opencode/runtime-release.json +12 -12
  9. package/lib/opencode/runtime.mjs +5 -4
  10. package/lib/providers/experiential.mjs +2 -2
  11. package/package.json +1 -1
  12. package/plugins/carthagent/src/daily-source-rpc.ts +2 -2
  13. package/plugins/carthagent/src/server/daily-source-rpc.ts +18 -4
  14. package/plugins/carthagent/src/server/delivery.ts +10 -6
  15. package/plugins/carthagent/src/server/providers.ts +1 -1
  16. package/plugins/carthagent/src/server/ship-guidance.ts +4 -2
  17. package/plugins/carthagent/src/tui.tsx +17 -13
  18. package/plugins/carthagent/src/ui/daily-quests.tsx +5 -2
  19. package/plugins/carthagent/src/ui/model-command.ts +32 -0
  20. package/plugins/carthagent/src/ui/native-review.d.mts +2 -0
  21. package/plugins/carthagent/src/ui/native-review.mjs +7 -2
  22. package/plugins/carthagent/src/ui/select-mouse.ts +17 -0
  23. package/plugins/carthagent/src/ui/voyages-navigation.tsx +11 -2
  24. package/plugins/carthagent/src/ui/voyages.tsx +4 -2
  25. package/plugins/carthagent/tests/voyages-navigation.test.tsx +20 -9
  26. package/script/ctg/build-owned-runtime.sh +3 -3
  27. package/tests/delivery-review.test.mjs +11 -0
  28. package/tests/native-daily-source.test.mjs +27 -2
  29. package/tests/native-manual-projects.test.mjs +3 -3
  30. package/tests/native-model-command.test.mjs +67 -0
  31. package/tests/native-review-consumer.test.mjs +19 -0
  32. package/tests/native-select-mouse.test.mjs +23 -0
  33. package/tests/native-ship-plan.test.mjs +13 -3
  34. package/tests/opencode-delivery.test.mjs +16 -0
  35. package/tests/owned-runtime-publish-workflow.test.mjs +6 -6
  36. package/tests/runtime-development-trust.test.mjs +5 -5
  37. package/tests/server.test.mjs +3 -1
  38. package/tests/ship-telemetry.test.mjs +1 -1
  39. package/tests/source-preview.test.mjs +253 -0
  40. package/tests/update.test.mjs +6 -5
  41. package/tools/benchmark/planning-startup.test.mjs +1 -1
  42. package/tools/test-native-delivery-host.mjs +2 -2
  43. package/tools/test-native-host.mjs +4 -2
  44. package/tools/test-native-public-repo.mjs +1 -1
  45. package/tools/test-native-ship-walkthrough.mjs +1 -1
  46. package/tools/test-native-swe-host.mjs +4 -4
@@ -0,0 +1,68 @@
1
+ # 1.1.0-preview.13 experimental release
2
+
3
+ Experimental preview. Install it explicitly with the experimental channel;
4
+ stable 1.0.7 is unchanged and published preview.12 remains immutable.
5
+
6
+ ## What changes
7
+
8
+ **Directory-first Projects rail.** The native left rail is organized around
9
+ folders: a current-folder card with Browse, New session and model controls,
10
+ distinct folder and group headings, and an active-session marker. A
11
+ folder-browser dialog adds Back, Forward, Up, Home, direct paths and
12
+ filtering, with async cancellation, exact path handling and preserved drafts.
13
+
14
+ **Shared folder/model setup for new sessions.** Global and per-folder New
15
+ session entries route through one setup flow. The session is created only on
16
+ the first send, keeping the chosen folder, agent, model and applicable
17
+ variant; unavailable selections are blocked instead of silently substituted.
18
+ Pre-session `/model` is supported.
19
+
20
+ **Isolated source-preview launcher.** `script/ctg/source-preview.sh` runs the
21
+ checked-in development patch against its own profile for verification without
22
+ touching the published runtime or its caches. The combined-v5 patch is a
23
+ replacement patch: it contains all of combined-v4 plus these sources and
24
+ their tests, and applies directly to the pinned upstream.
25
+
26
+ The upstream revision, lockfile and detached runtime identity are unchanged
27
+ from preview.12; the runtime is rebuilt because each runtime manifest is
28
+ bound to its exact package version.
29
+
30
+ **Ship and Voyages follow-up fixes.** Beam replay requests include the required
31
+ nonempty reasoning placeholder when earlier assistant messages have no recorded
32
+ reasoning. The delivery plan entry point stays visible in the code-mode catalog,
33
+ and final review accepts single-assertion aliases and assertion arrays. Voyages
34
+ and quest lists activate on mouse release so newly opened dialogs remain open;
35
+ rail clicks select the intended quest and respect modal focus.
36
+ Delivery tool guidance explicitly identifies JSON-text responses and requires
37
+ relative file paths for promised outputs, avoiding prose-as-filename contracts.
38
+
39
+ ## Provenance
40
+
41
+ | Input | Exact value |
42
+ | --- | --- |
43
+ | Package | `1.1.0-preview.13` |
44
+ | Prebuild source | `e5de273811a11c2f8678b54605c0068ab8cfcf38` |
45
+ | Prebuild workflow run | [41380550073](https://github.com/B-icy/Carthagent/actions/runs/41380550073) |
46
+ | Full manifest SHA-256 | `5083dbe10b6f4be2550ab0abb04c9299197dacabe4b9d3556bd3b03fc9f6151a` |
47
+ | Upstream revision | `843a6c514e606b436a7fe713422ace0a93ad3d61` |
48
+ | CTG patch (combined-v5) SHA-256 | `ec4bb182308ed8b311513aad5c9c7d57dcd0db103f0882f3dc3517d535b4c975` |
49
+ | Lock SHA-256 | `9d78379cd264584a495440a217b9d2ecab524c9a56f7f8b4b5bed3926daa330d` |
50
+ | Runtime / compatibility | `0.0.0-ctg.2.0.32` / `2.0.32` |
51
+
52
+ ## Trying it
53
+
54
+ ```sh
55
+ ctg update --channel experimental --check
56
+ ctg update --channel experimental
57
+ ctg --version # 1.1.0-preview.13
58
+ ```
59
+
60
+ Or install the exact version from npm or the exact tag:
61
+
62
+ ```sh
63
+ npm install -g carthagent@1.1.0-preview.13
64
+ npm install -g --install-links=true github:B-icy/Carthagent#v1.1.0-preview.13
65
+ ```
66
+
67
+ `ctg update --channel stable` returns to stable 1.0.7 without `--force`. See
68
+ the [upgrade guide](native-upgrade.md) for profile preservation.
@@ -0,0 +1,120 @@
1
+ # Projects sidebar source preview
2
+
3
+ This preview runs the redesigned Projects sidebar from OpenCode source plus the
4
+ Carthagent plugin in this checkout. It is a development preview. Nothing here
5
+ builds, publishes, installs, or changes the published `ctg` runtime.
6
+
7
+ ## What is pinned where
8
+
9
+ | Purpose | Patch | Recipe | Hash reference |
10
+ | --- | --- | --- | --- |
11
+ | Published runtime (preview.13) | `docs/patches/opencode-v2.0.32-session-ui-combined-v5.patch` | `script/ctg/build-owned-runtime.sh`, publish workflow | `CTG_RUNTIME_PATCH_SHA256`, `lib/opencode/runtime-release.json` |
12
+ | Source preview (development) | `docs/patches/opencode-v2.0.32-session-ui-combined-v5.patch` | `script/ctg/prepare-development-runtime.sh` | `CTG_DEVELOPMENT_RUNTIME_PATCH_SHA256` |
13
+
14
+ Both patches apply to clean upstream `https://github.com/anomalyco/opencode` at
15
+ `843a6c514e606b436a7fe713422ace0a93ad3d61`. v5 is a replacement patch: it
16
+ contains everything in v4 plus the sidebar, folder browser and new-session
17
+ setup sources and their tests under `packages/tui/test/sidebar`. Do not apply
18
+ v4 first.
19
+
20
+ ## Setup
21
+
22
+ 1. `npm ci` in this checkout (the plugin runs from `plugins/carthagent`).
23
+ 2. Clone upstream into a new directory, check out the pinned commit, and run
24
+ `script/ctg/prepare-development-runtime.sh <checkout>`. The script refuses a
25
+ different commit, a dirty checkout or a patch with the wrong SHA-256, then
26
+ runs `git apply --check` and `git apply`. It does not install or compile.
27
+ 3. Run `bun install` with Bun 1.4.2 in the prepared checkout.
28
+
29
+ ## Launch
30
+
31
+ ```sh
32
+ script/ctg/source-preview.sh --source DIR --profile DIR --bun PATH [--workspace DIR] [-- ARGS...]
33
+ ```
34
+
35
+ - All three of `--source`, `--profile` and `--bun` are required. Relative
36
+ paths resolve against your current directory, so you can launch from
37
+ anywhere. Paths with spaces work when quoted.
38
+ - The launcher checks that `packages/cli/src/index.ts` and `node_modules`
39
+ exist and that `bun --version` matches the checkout's `packageManager` pin.
40
+ It fails before touching the profile when a prerequisite is missing.
41
+ - It runs Bun with `--no-env-file` from `<source>/packages/cli` (required for
42
+ JSX resolution) under `env -i`. Only these variables reach OpenCode: `HOME`,
43
+ `PATH` (Bun's directory, Node's directory if found, `/usr/bin`, `/bin`),
44
+ `TERM`, `COLORTERM`, `TMPDIR`, the four `XDG_*` directories,
45
+ `OPENCODE_CONFIG_DIR`, `OPENCODE_TEST_HOME`,
46
+ `OPENCODE_DISABLE_PROJECT_CONFIG=1`, `OPENCODE_DISABLE_AUTOUPDATE=1` and
47
+ `BUN_RUNTIME_TRANSPILER_CACHE_PATH=0`. Provider API-key variables are never
48
+ passed through.
49
+ - First launch creates the profile (0700) with `config/opencode/opencode.json`
50
+ (the plugin path), `cli.json` (Carthage theme) and `themes/carthage.json`
51
+ (all 0600). Each file is created only if absent; existing configuration,
52
+ sessions and authentication are left untouched. If an existing
53
+ `opencode.json` does not list the plugin, the launcher prints a warning and
54
+ still leaves the file alone.
55
+ - `--standalone` is always used, so the CLI starts its own private server on
56
+ a loopback ephemeral port (`--port 0`) that exits with the TUI. `--server`
57
+ is rejected. Without `--`, the workspace (default: current directory) is
58
+ opened. With `--`, the remaining arguments run another CLI command in the
59
+ same profile, for example `-- auth login`.
60
+
61
+ ## Use
62
+
63
+ - **Folder card** (top of Projects): current folder, abbreviated path (click
64
+ for the full path), Browse, New session and the model action. Narrow layouts
65
+ show compact `⌕` search, `▤` Browse and `◇` model controls.
66
+ - **Browse** (`/browse`, palette “Browse folders”): Filter and Path inputs
67
+ (Tab switches), Up/Down select, Enter opens, Alt+Left/Right/Up for
68
+ Back/Forward/Up, Alt+H for Home, Ctrl+S confirms, Escape cancels.
69
+ - **New session** (`Ctrl+X N`, `/new`, palette “New session” or “New session
70
+ in folder…”, folder card, compact `+`, folder right-click menu): choose the
71
+ folder, then the model. Global entries start at the folder a plain new
72
+ session would use; project entries start at that project's folder. Nothing
73
+ is created until the first message is sent; cancelling leaves the current
74
+ session, draft and model unchanged. The new session keeps the agent you were
75
+ using and, when you keep the same model, its variant (including the default).
76
+ If the chosen agent or model is not available at the folder when you send,
77
+ the send is blocked with your text kept and no session created; pick another
78
+ agent (`Shift+Tab`) or model (`/model`, `Ctrl+X M`) and send again. There is
79
+ no automatic fallback to another model. `/model` also works before a session
80
+ exists. The model step inside setup has no integration shortcut; connect
81
+ integrations from the standalone model picker (`Ctrl+X M`).
82
+ - **Exit**: `/exit` or palette “Exit the app”. The private server exits with
83
+ the TUI.
84
+ - **Resume**: relaunch with the same `--profile` and select the session in
85
+ Projects. Automatic resume is not part of this preview.
86
+
87
+ ## Tests
88
+
89
+ Run serially. Close the preview first.
90
+
91
+ ```sh
92
+ # Product (from this checkout)
93
+ node --test --test-concurrency=1 tests/source-preview.test.mjs tests/native-ship-plan.test.mjs tests/native-model-command.test.mjs
94
+ npx eslint . && npm run typecheck
95
+
96
+ # Native (from <source>/packages/tui, using the same isolated variables as the launcher)
97
+ bun --no-env-file test --timeout 30000 test/navigation-rail.test.tsx \
98
+ test/component/session-tabs-rail.test.tsx test/context/session-tabs-model.test.ts \
99
+ test/ui/layout.test.ts test/sidebar
100
+ ```
101
+
102
+ `tests/source-preview.test.mjs` uses a Bun test double and synthetic profiles,
103
+ so it proves argument handling, isolation and config preservation, not a real
104
+ launch. It runs the launcher with `/bin/sh` on Linux and macOS and is skipped
105
+ on native Windows, where the POSIX launcher is not supported.
106
+
107
+ ## Limits
108
+
109
+ - Source preview only. No native binary, package or release includes these
110
+ changes, and the published runtime remains v4.
111
+ - The launcher is a POSIX `sh` script for Linux and macOS (GNU and BSD
112
+ utilities). Native Windows is not supported.
113
+ - Real launches were verified on Linux with Bun 1.4.2. No cross-platform
114
+ launch matrix was run.
115
+ - The native type checker (`tsgo`) and native build are not part of this
116
+ workflow; Bun tests compile the changed modules but are not a full static
117
+ type check.
118
+ - Some native tests outside the focused list fail in the same way on the
119
+ pre-change sources (for example missing `ToastProvider` in
120
+ `session-tabs-status` harnesses).
@@ -41,13 +41,23 @@ export async function captureDeliveryReview(state, cwd, directory, signal) {
41
41
  return { ...capture, diff: text.slice(0, 12000), truncated: text.length > 12000, unresolvedFindings: (state.reviewFindings || []).filter(f => !f.resolution), next: 'Read the full capture path if truncated; new text source is included. Inspect binary assets separately. Probe critical component connections and user outcomes, not only individual functions or artifact existence. Record coverage, findings and limitations; earlier findings require explicit resolutions with evidence, never silently move them to limitations.' };
42
42
  }
43
43
 
44
+ /** Models commonly send `assertion` or a string array; both carry the same claim. */
45
+ function coverageAssertions(item) {
46
+ const value = item.assertions ?? item.assertion;
47
+ if (Array.isArray(value)) return value.filter(v => typeof v === 'string' && v.trim()).join('; ');
48
+ return typeof value === 'string' ? value : '';
49
+ }
50
+
44
51
  export function recordDeliveryReview(state, cwd, params) {
45
52
  const capture = state.reviewCapture;
46
53
  const hash = fingerprint(cwd, ['.']);
47
54
  if (!capture || capture.id !== params.captureId || capture.runId !== state.runId || capture.revision !== state.revision || capture.fingerprint !== hash) throw Error('Review capture missing or stale. Use delivery_review action=inspect after final source edits.');
48
55
  if (pendingRequirements(state, hash).length) throw Error('Run fresh checks for every requirement before recording review.');
49
56
  const nonempty = value => typeof value === 'string' && value.trim().length > 0;
50
- if (!Array.isArray(params.coverage) || (state.plan.acceptance || []).some(a => !params.coverage.some(c => c.requirement === a.requirement && nonempty(c.assertions)))) throw Error('Review coverage must explain concrete assertions for every exact acceptance requirement, including required validators.');
57
+ if (!Array.isArray(params.coverage) || params.coverage.some(c => !c || typeof c !== 'object')) throw Error('coverage must be an array of {requirement, assertions, status?} objects.');
58
+ params = { ...params, coverage: params.coverage.map(c => ({ ...c, assertions: coverageAssertions(c) })) };
59
+ const uncovered = (state.plan.acceptance || []).filter(a => !params.coverage.some(c => c.requirement === a.requirement && nonempty(c.assertions))).map(a => a.requirement);
60
+ if (uncovered.length) throw Error(`Review coverage must explain concrete assertions for every exact acceptance requirement, including required validators. Missing: ${JSON.stringify(uncovered.slice(0, 10))}${uncovered.length > 10 ? ` (+${uncovered.length - 10} more)` : ''}. Each item: {requirement: <exact acceptance text>, assertions: "<what the checks assert>", status?: "demonstrated"|"unverified"}.`);
51
61
  if (params.coverage.some(c => c.status !== undefined && !['demonstrated', 'unverified'].includes(c.status))) throw Error('Coverage status must be demonstrated or unverified.');
52
62
  if (!Array.isArray(params.probes) || !params.probes.length || params.probes.some(p => !nonempty(p))) throw Error('Record boundary/metamorphic probes and observed results, not just a generic review claim.');
53
63
  if (!Array.isArray(params.findings) || params.findings.some(p => !nonempty(p))) throw Error('findings must be an array; unresolved findings require repairs or a blocked handoff.');
@@ -1,36 +1,36 @@
1
1
  {
2
2
  "version": 1,
3
- "releaseVersion": "1.1.0-preview.12",
3
+ "releaseVersion": "1.1.0-preview.13",
4
4
  "compatibilityVersion": "2.0.32",
5
5
  "runtimeVersion": "0.0.0-ctg.2.0.32",
6
6
  "sourceRevision": "843a6c514e606b436a7fe713422ace0a93ad3d61",
7
- "patchSha256": "1f2309bb3a31dfb8e8a0827a6a1144a1f76a005abff58f6f48857639ec02478a",
7
+ "patchSha256": "ec4bb182308ed8b311513aad5c9c7d57dcd0db103f0882f3dc3517d535b4c975",
8
8
  "lockSha256": "9d78379cd264584a495440a217b9d2ecab524c9a56f7f8b4b5bed3926daa330d",
9
9
  "assets": {
10
10
  "linux-x64": {
11
11
  "name": "opencode-linux-x64.tar.gz",
12
- "archiveSha256": "d4c202e0825fc77321ccd23d91a7baede4951a4eb4cee28f258c171e72f03852",
13
- "executableSha256": "24bcfff03d32db163e0d40c4d0b212eede300a71a3dc26aa23810b9088d52280"
12
+ "archiveSha256": "d7c1869ace6dde4672f8ade4a8ba7826b15769a04b943571122d9541e52b62a7",
13
+ "executableSha256": "8a02fe556eb64def1507ff34a2cdd012dee636c0a6678f14d1fe57a020efa620"
14
14
  },
15
15
  "linux-arm64": {
16
16
  "name": "opencode-linux-arm64.tar.gz",
17
- "archiveSha256": "c3a1e68a6fd7db5909d2d38639c5b1e8df7a5de64fc40f6cc758211a280ea058",
18
- "executableSha256": "b73708103d62de570e629a0bf9c2bce9b1a4db0a87a79b1c86c2d2ebe998db7b"
17
+ "archiveSha256": "7e5f7ea55e54a752d2923296f6917f7b48a175e5ae39a8caa540d844d89e6d8b",
18
+ "executableSha256": "eefc048de0b3932148050844b0d574c112bf13aeae001c767fac463dbbd1292e"
19
19
  },
20
20
  "darwin-x64": {
21
21
  "name": "opencode-darwin-x64.tar.gz",
22
- "archiveSha256": "d733d269bd06a652c858b62b24e5b78a629400fc687d7dc71a999bb03bfab1d5",
23
- "executableSha256": "f2bb0e772744bc04b96368d32cc35582a6a1bca0be795897704c9395e5059f37"
22
+ "archiveSha256": "84be12c83c8cd9ef1028467fb53e94016e2b514013df9f62347a451f3c84044c",
23
+ "executableSha256": "4de7f310debebc00d955d8b4c349749b15bac69ef6998a051144d4e86b79b1a4"
24
24
  },
25
25
  "darwin-arm64": {
26
26
  "name": "opencode-darwin-arm64.tar.gz",
27
- "archiveSha256": "e0a0ac4c7f5b4d14eab8844428d717a9a596c61694b318d0e065080de5a42fda",
28
- "executableSha256": "9bf83ef1fd9e1af8028a23941a05195f8256847deae271c2b016014d5363605b"
27
+ "archiveSha256": "8e9be7bda9df7a1fe84f1344738435f748ee56598c4635c2be8fdfef1433414a",
28
+ "executableSha256": "0963189d87daa88616980f72f3470b69902ac2b2b3b62cadb9e0fa436245b960"
29
29
  },
30
30
  "windows-x64": {
31
31
  "name": "opencode-windows-x64.tar.gz",
32
- "archiveSha256": "0318a74ba0b67ee5119b4289c0b78124c620676c8770e48c24d347513e96e2aa",
33
- "executableSha256": "b057ce293492cff6dabf3939d12072798f53d8b4cf7ab944fd97ef0bb0879bb9"
32
+ "archiveSha256": "5ca306190c354b5e6f690103da8a9db9e90bcf5463214f50cd1dfafd10109d4c",
33
+ "executableSha256": "4249c083b86b9bc034550954c2bc08ffdf9c578cf63c90a0adfabd103cac1594"
34
34
  }
35
35
  }
36
36
  }
@@ -13,11 +13,12 @@ export const OPENCODE_VERSION = '2.0.32';
13
13
  export const CTG_RUNTIME_VERSION = '0.0.0-ctg.2.0.32';
14
14
  export const CTG_RUNTIME_SOURCE = '843a6c514e606b436a7fe713422ace0a93ad3d61';
15
15
  export const CTG_RUNTIME_LOCK_SHA256 = '9d78379cd264584a495440a217b9d2ecab524c9a56f7f8b4b5bed3926daa330d';
16
- export const CTG_RUNTIME_PATCH_SHA256 = '1f2309bb3a31dfb8e8a0827a6a1144a1f76a005abff58f6f48857639ec02478a';
16
+ export const CTG_RUNTIME_PATCH_SHA256 = 'ec4bb182308ed8b311513aad5c9c7d57dcd0db103f0882f3dc3517d535b4c975';
17
17
  // Update only this development pin after reviewing a new checked-in patch and
18
- // its build recipe. The published release pin above and release manifest stay
19
- // immutable until a separately verified runtime release is promoted.
20
- export const CTG_DEVELOPMENT_RUNTIME_PATCH_SHA256 = '1f2309bb3a31dfb8e8a0827a6a1144a1f76a005abff58f6f48857639ec02478a';
18
+ // script/ctg/prepare-development-runtime.sh. The published release pin above,
19
+ // its build recipe, and the release manifest stay immutable until a separately
20
+ // verified runtime release is promoted.
21
+ export const CTG_DEVELOPMENT_RUNTIME_PATCH_SHA256 = 'ec4bb182308ed8b311513aad5c9c7d57dcd0db103f0882f3dc3517d535b4c975';
21
22
  const DEFAULT_ROOT = path.join(homedir(), '.carthagent', 'opencode');
22
23
  const require = createRequire(import.meta.url);
23
24
  const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
@@ -64,11 +64,11 @@ const EXPERIENTIAL_MODEL_PROFILES = Object.freeze({
64
64
  'glm-5.3-flash': { name: 'GLM 5.3 Flash', reasoning: true, thinkingLevelMap: TIERED_REASONING_LEVELS, contextWindow: 1_048_576, maxTokens: 32_768, input: ['text', 'image'], compat: { supportsReasoningEffort: true, thinkingFormat: 'zai' } },
65
65
  'glm-5.3': { name: 'GLM 5.3', reasoning: true, thinkingLevelMap: TIERED_REASONING_LEVELS, contextWindow: 1_048_576, maxTokens: 32_768, input: ['text'], compat: { supportsReasoningEffort: true, thinkingFormat: 'zai' } },
66
66
  'kimi-k3': { name: 'Kimi K3', reasoning: true, thinkingLevelMap: TIERED_REASONING_LEVELS, contextWindow: 1_048_576, maxTokens: 32_768, input: ['text'], compat: { supportsDeveloperRole: false, supportsReasoningEffort: true, thinkingFormat: 'deepseek' } },
67
- 'beam-1.0-501b-a23b': { name: 'Beam 1.0 501B A23B', reasoning: true, thinkingLevelMap: BEAM_REASONING_LEVELS, contextWindow: 1_048_576, maxTokens: 32_768, input: ['text'], compat: { supportsReasoningEffort: true } },
67
+ 'beam-1.0-501b-a23b': { name: 'Beam 1.0 501B A23B', reasoning: true, thinkingLevelMap: BEAM_REASONING_LEVELS, contextWindow: 1_048_576, maxTokens: 32_768, input: ['text'], compat: { supportsReasoningEffort: true, reasoningField: 'reasoning_content', requireReasoning: true, reasoningPlaceholder: ' ' } },
68
68
  'mercury-3': { name: 'Mercury 3', reasoning: true, thinkingLevelMap: MERCURY3_REASONING_LEVELS, contextWindow: 1_048_576, maxTokens: 131_072, input: ['text'], compat: { supportsReasoningEffort: true } },
69
69
  });
70
70
 
71
- const MODEL_METADATA_FIELDS = ['reasoning', 'thinkingLevelMap', 'contextWindow', 'maxTokens', 'input', 'output', 'compat'];
71
+ const MODEL_METADATA_FIELDS = ['reasoning', 'thinkingLevelMap', 'contextWindow', 'maxTokens', 'input', 'output', 'compat', 'cost'];
72
72
 
73
73
  export const EXPERIENTIAL_PROVIDER_ID = PROVIDER_ID;
74
74
  export const EXPERIENTIAL_PROVIDER_NAME = CARTHAGENT_SHIP_PROVIDER_NAME;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "carthagent",
3
- "version": "1.1.0-preview.12",
3
+ "version": "1.1.0-preview.13",
4
4
  "workspaces": [
5
5
  "plugins/carthagent"
6
6
  ],
@@ -8,7 +8,7 @@ export interface DailySourceReport {
8
8
  freshness: 'current' | 'stale';
9
9
  goal: string;
10
10
  change: string;
11
- checks: Array<{ id: string; before: 'not_run'; after: 'passed' | 'failed' | 'not_run'; revision: string; result: string }>;
11
+ checks: Array<{ id: string; before: 'not_run' | 'failed'; after: 'passed' | 'failed' | 'not_run'; revision: string; result: string }>;
12
12
  /** Model-authored review/resolution claims, not independent verification. */
13
13
  findings: Array<{ id: string; issue: string; evidence: string; resolution: string }>;
14
14
  /** At least one field or item was omitted; the UI must warn before selection. */
@@ -20,7 +20,7 @@ const fingerprint = { type: 'string', minLength: 64, maxLength: 64 } as const;
20
20
  const goal = { type: 'string', maxLength: 1024 } as const;
21
21
  const check = {
22
22
  type: 'object', properties: {
23
- id, before: { type: 'string', enum: ['not_run'] },
23
+ id, before: { type: 'string', enum: ['not_run', 'failed'] },
24
24
  after: { type: 'string', enum: ['passed', 'failed', 'not_run'] },
25
25
  revision: fingerprint, result: goal,
26
26
  }, required: ['id', 'before', 'after', 'revision', 'result'], additionalProperties: false,
@@ -57,6 +57,16 @@ function localJson(path: string, max = MAX_FILE): { value: unknown; raw: string
57
57
  } catch (error) { if (missing(error)) return null; throw invalid(); }
58
58
  }
59
59
 
60
+ /** Receipts already hold an output tail; keep its end within the byte budget. */
61
+ function tail(value: unknown, limit: number, cut: () => void) {
62
+ if (typeof value !== 'string' || Buffer.byteLength(value, 'utf8') <= limit) return value;
63
+ cut();
64
+ const bytes = Buffer.from(value, 'utf8');
65
+ const kept = bytes.subarray(bytes.length - (limit - 4)).toString('utf8').replace(/^\uFFFD+/, '');
66
+ const line = kept.indexOf('\n');
67
+ return `…\n${line >= 0 && line < kept.length - 1 ? kept.slice(line + 1) : kept}`;
68
+ }
69
+
60
70
  function validateState(value: unknown, directory: string, runId: string) {
61
71
  if (!record(value) || value.runId !== runId || !Number.isSafeInteger(value.revision) || (value.revision as number) < 1 || !record(value.plan) || !Array.isArray(value.plan.checks) || value.plan.checks.length > 20000) throw invalid();
62
72
  if (value.status !== undefined && !['implementing', 'verifying', 'verified', 'blocked'].includes(value.status as string)) throw invalid();
@@ -76,6 +86,7 @@ function validateState(value: unknown, directory: string, runId: string) {
76
86
  }
77
87
  if (value.reviewEvidence !== undefined && !record(value.reviewEvidence)) throw invalid();
78
88
  if (value.handoff !== undefined && !record(value.handoff)) throw invalid();
89
+ if (value.baselines !== undefined && !record(value.baselines)) throw invalid();
79
90
  if (value.reviewFindings !== undefined && (!Array.isArray(value.reviewFindings) || value.reviewFindings.length > 20000)) throw invalid();
80
91
  const findings = (value.reviewFindings ?? []) as RecordValue[];
81
92
  const findingIDs = new Set<string>();
@@ -129,9 +140,9 @@ function sourceFor(directory: string, sessionID: string): DailySourceReport | nu
129
140
  const result: DailySourceReport = {
130
141
  runId: parts[2], revision: current, planRevision: state.revision as number,
131
142
  freshness: stale ? 'stale' : 'current', goal: text((state.plan as RecordValue).goal, 1024),
132
- // Native delivery_finish stores review/launch/limitations, not a change summary.
133
- // Do not manufacture a change claim from those fields or from the goal.
134
- change: '',
143
+ // Only delivery_finish's explicit change summary for this snapshot; never
144
+ // derived from review/launch/limitations or the goal.
145
+ change: record(state.handoff) && state.handoff.fingerprint === current ? text(state.handoff.change, 3072) : '',
135
146
  checks: checks.slice(0, 12).map(check => {
136
147
  const receipt = evidence[check.id];
137
148
  const fresh = Boolean(isFresh(check));
@@ -139,7 +150,10 @@ function sourceFor(directory: string, sessionID: string): DailySourceReport | nu
139
150
  // Omit unbound failure output rather than making old/scope-error text look
140
151
  // like an execution result for the current revision in the outbound DTO.
141
152
  const output = receipt?.passed === false && !failed ? undefined : receipt?.outputTail;
142
- return { id: check.id, before: 'not_run', after: fresh ? 'passed' : failed ? 'failed' : 'not_run', revision: current, result: text(output, 1024) };
153
+ // A first-in-run execution that failed on different source is a recorded before-failure.
154
+ const baseline = record(state.baselines) && record(state.baselines[check.id]) ? state.baselines[check.id] as RecordValue : undefined;
155
+ const before = baseline?.passed === false && baseline.checkDigest === checkDigest(check) && hash(baseline.fingerprint) && baseline.fingerprint !== current ? 'failed' as const : 'not_run' as const;
156
+ return { id: check.id, before, after: fresh ? 'passed' : failed ? 'failed' : 'not_run', revision: current, result: text(tail(output, 1024, () => { truncated = true; }), 1024) };
143
157
  }),
144
158
  findings: findings.slice(0, 5).map(finding => {
145
159
  const resolution = record(finding.resolution) ? finding.resolution : {};
@@ -43,7 +43,7 @@ const short = S()
43
43
  const steps = A({ anyOf: [short, O({ id: { type: "string", pattern: "^[a-z][a-z0-9_-]{0,39}$" }, title: short, kind: { type: "string", enum: ["work", "regression"] }, dependsOn: A(short, 40), checks: A(short, 24) }, ["id", "title"])] }, 40)
44
44
  const checks = A(O({ id: { type: "string", pattern: "^[a-z][a-z0-9_-]{0,39}$" }, kind: { type: "string", enum: ["test", "runtime", "static"] }, argv: A(short, 80), timeoutSeconds: { type: "integer", minimum: 1, maximum: 300 } }), 24)
45
45
  const acceptance = A(O({ requirement: short, checks: A(short, 24) }), 40)
46
- const planSchema = O({ goal: short, assumptions: A(short), artifacts: A(short, 40), outputs: A(short, 40), steps, verification: { type: "string", enum: ["required", "advisory", "none"] }, acceptance, checks, design: { type: "object", additionalProperties: true }, workflow: { type: "object", additionalProperties: true }, outcomes: A({ type: "object", additionalProperties: true }, 32) }, ["goal", "assumptions", "artifacts", "steps", "acceptance", "checks"])
46
+ const planSchema = O({ goal: short, assumptions: A(short), artifacts: A(short, 40), outputs: A(S("Relative file path that must exist at completion, e.g. invoice.mjs. Not a prose deliverable or command result; put test commands in checks. Omit outputs when no files are promised."), 40), steps, verification: { type: "string", enum: ["required", "advisory", "none"] }, acceptance, checks, design: { type: "object", additionalProperties: true }, workflow: { type: "object", additionalProperties: true }, outcomes: A({ type: "object", additionalProperties: true }, 32) }, ["goal", "assumptions", "artifacts", "steps", "acceptance", "checks"])
47
47
 
48
48
  function sessionKey(sessionID: string) { return `session/${sessionID}` }
49
49
 
@@ -149,8 +149,9 @@ export async function registerDelivery(ctx: Context, options: DeliveryOptions =
149
149
  } finally { release() }
150
150
  })
151
151
  }
152
+ // The code-mode catalog drops the longest signatures first; pin the plan entry point so it is always listed.
152
153
  const add = (editor: Parameters<Parameters<Context["tool"]["transform"]>[0]>[0], name: string, description: string, input: Schema, execute: (input: DeliveryInput, toolContext: RegisteredToolContext) => Promise<{ content: string }>) => editor.add({
153
- name, description, input: input as Tool.ValueSchema<DeliveryInput>, execute: async (rawValue, tool) => {
154
+ name, description: `${description} Returns text; in code mode use JSON.parse(result) for JSON responses before reading fields. Plain-text messages are not JSON.`, input: input as Tool.ValueSchema<DeliveryInput>, execute: async (rawValue, tool) => {
154
155
  const value = rawValue as DeliveryInput
155
156
  const { active } = await scope(tool.sessionID)
156
157
  if (!active || !isShipAgent(tool.agent)) throw Error("Carthagent delivery tools are disabled for this session; select Ship mode to use them.")
@@ -161,12 +162,12 @@ export async function registerDelivery(ctx: Context, options: DeliveryOptions =
161
162
  return { ...result, metadata: { deliveryPlan: planSnapshot(data.state) } }
162
163
  }
163
164
  catch (error) { throw new NativeToolError({ message: error instanceof Error ? error.message : String(error) }) }
164
- }, options: { namespace: "carthagent", codemode: true },
165
+ }, options: { namespace: "carthagent", codemode: true, ...(name === "delivery_plan" ? { pinned: true } : {}) },
165
166
  })
166
167
 
167
168
  registrations.push(await ctx.tool.transform(editor => {
168
169
  editor.namespace?.({ name: "carthagent", description: "Evidence-driven CTG delivery workflow tools" })
169
- add(editor, "delivery_plan", "Create the durable acceptance contract and plan.d2. Informational asks may use verification none/advisory; product changes require fresh checks.", planSchema, async (input, tool) => operate(tool.sessionID, async ({ cwd, data, options: opts }) => {
170
+ add(editor, "delivery_plan", "Create the durable acceptance contract and plan.d2. Outputs are relative file paths, not prose or test results. Informational asks may use verification none/advisory; product changes require fresh checks.", planSchema, async (input, tool) => operate(tool.sessionID, async ({ cwd, data, options: opts }) => {
170
171
  if (data.configError) throw Error(data.configError)
171
172
  const plan = validatePlan(bindRequiredChecks({ ...input, verification: input.verification ?? "required" }, data.required), cwd)
172
173
  validateRevision(data.state, plan)
@@ -277,6 +278,9 @@ export async function registerDelivery(ctx: Context, options: DeliveryOptions =
277
278
  }
278
279
  const passed = result.code === 0 && !result.timedOut && !result.cancelled && !result.outputLimit && before === after
279
280
  data.state.evidence[check.id] = { passed, ...identity, executedRevision: data.state.revision, fingerprint: after, code: result.code, timedOut: result.timedOut, cancelled: result.cancelled, logPath: result.logPath, durationMs: result.durationMs, changedDuringCheck: before !== after, outputTail: result.output.slice(-1200) }
281
+ // The first execution in a run is its baseline; later receipts replace evidence only.
282
+ const baselines = (data.state.baselines ??= {}) as Record<string, unknown>
283
+ if (!baselines[check.id]) baselines[check.id] = { passed, code: result.code, checkDigest: identity.checkDigest, fingerprint: after, at: new Date().toISOString() }
280
284
  results.push({ id: check.id, advisory: verification !== "required", ...data.state.evidence[check.id], outputTail: result.output })
281
285
  await persist(cwd, tool.sessionID, data)
282
286
  }
@@ -285,12 +289,12 @@ export async function registerDelivery(ctx: Context, options: DeliveryOptions =
285
289
  const hash = fingerprint(cwd, ["."]); const issues = completionIssues(data.state, cwd, hash)
286
290
  return text({ results, pendingChecks: pendingChecks(data.state, hash), completionIssues: issues, readyToFinish: Object.values(issues).every(items => !items.length), next: "Complete pending steps, review the evidence, then call delivery_finish." })
287
291
  }))
288
- add(editor, "delivery_review", "Inspect final diff or record assertion coverage, boundary probes and findings. Required for structured plans; assertions are model-authored.", O({ action: { type: "string", enum: ["inspect", "record"] }, captureId: short, coverage: A({ type: "object", additionalProperties: true }, 64), resolutions: A({ type: "object", additionalProperties: true }, 40), outcomes: A({ type: "object", additionalProperties: true }, 32), probes: A(short, 40), findings: A(short, 40), limitations: A(short) }, ["action"]), async (input, tool) => operate(tool.sessionID, async ({ cwd, data }) => {
292
+ add(editor, "delivery_review", "Inspect final diff or record assertion coverage, boundary probes and findings. Required for structured plans; assertions are model-authored.", O({ action: { type: "string", enum: ["inspect", "record"] }, captureId: short, coverage: A({ type: "object", properties: { requirement: { type: "string", description: "Exact acceptance requirement text" }, assertions: { anyOf: [{ type: "string" }, A({ type: "string" }, 32)], description: "Concrete assertions the linked checks make" }, assertion: { type: "string", description: "Alias for a single assertion" }, status: { type: "string", enum: ["demonstrated", "unverified"] } }, required: ["requirement"], additionalProperties: true }, 64), resolutions: A({ type: "object", additionalProperties: true }, 40), outcomes: A({ type: "object", additionalProperties: true }, 32), probes: A(short, 40), findings: A(short, 40), limitations: A(short) }, ["action"]), async (input, tool) => operate(tool.sessionID, async ({ cwd, data }) => {
289
293
  if (!data.state) throw Error("Call delivery_plan first")
290
294
  const result = input.action === "inspect" ? await captureDeliveryReview(data.state, cwd, runDirectory(cwd, tool.sessionID, data.state.runId), tool.signal) : input.action === "record" ? recordDeliveryReview(data.state, cwd, input) : (() => { throw Error("Review action must be inspect or record") })()
291
295
  await persist(cwd, tool.sessionID, data); return text(result)
292
296
  }))
293
- add(editor, "delivery_finish", "Finish delivery. Verified requires every required validator, fresh check, output, step, requirement and review gate; blocked requires specific limitations.", O({ status: { type: "string", enum: ["verified", "blocked"] }, review: short, launch: short, limitations: A(short) }), async (input, tool) => operate(tool.sessionID, async ({ cwd, data }) => {
297
+ add(editor, "delivery_finish", "Finish delivery. Verified requires every required validator, fresh check, output, step, requirement and review gate; blocked requires specific limitations.", O({ status: { type: "string", enum: ["verified", "blocked"] }, change: { ...short, description: "One-paragraph summary of what changed and why" }, review: short, launch: short, limitations: A(short) }), async (input, tool) => operate(tool.sessionID, async ({ cwd, data }) => {
294
298
  if (!data.state) throw Error("Call delivery_plan first")
295
299
  if (data.configError) throw Error(data.configError)
296
300
  const verification = verificationMode(data.state.plan); let hash: string | null = null
@@ -98,7 +98,7 @@ function modelInfo(providerID: string, profile: CatalogModel, defaultBaseURL: st
98
98
 
99
99
  function mapCompatibility(compat: Record<string, unknown>): NonNullable<Model.Info['compatibility']> {
100
100
  const keys = [
101
- 'reasoningField', 'requireReasoning', 'maxTokensField', 'requireFinishReason',
101
+ 'reasoningField', 'requireReasoning', 'reasoningPlaceholder', 'maxTokensField', 'requireFinishReason',
102
102
  'requireAssistantAfterTool', 'supportsPromptCacheKey', 'cacheFormat',
103
103
  'supportsThinkingBlockBinding', 'supportsEffortUpdates',
104
104
  ] as const;
@@ -8,6 +8,8 @@ import { unframe } from "./framing.mjs"
8
8
 
9
9
  export const STANDARD_GUIDANCE = "Standard delivery: inspect relevant files (glob/read/code_nav); create delivery_plan with complete requirements, artifact roots, a few steps and real checks. A valid required plan enables implementation immediately: design components/scenario ceremony is optional. Revisions merge design/workflow fields, replace supplied arrays, preserve obligations and require fresh final evidence, not repeated approval. Mark implementation steps done independently of integrated check execution. Use delivery_check all, final delivery_review inspect/record, then delivery_finish with honest limitations. No fabricated evidence, scope reduction or unrequested dependency changes. Informational plans do not authorize implementation. For bounded environment versions use delivery_probe kind=node-version or npm-version; inspect other APIs through read."
10
10
 
11
+ export const TOOL_SURFACE_GUIDANCE = "File edits and commands use the direct read/write/edit/bash tools. A code-mode search() catalog lists only namespaced tools.* helpers, so their absence there does not mean they are unavailable. If a tool is locked, its error names the required delivery step; take that step instead of searching for alternatives."
12
+
11
13
  const browserCheck = fileURLToPath(new URL("../../../../tools/browser-check.mjs", import.meta.url))
12
14
  const browserGuidance = existsSync(browserCheck)
13
15
  ? `For web/browser tasks, a self-contained browser check is available: ${JSON.stringify(browserCheck)}. Declare it as a runtime check, e.g. ["node",${JSON.stringify(browserCheck)},"--page","index.html","--assert","#app","--assert-count","#items:3","--click","#btn","--then-text","#out:done","--console-clean","--screenshot","artifacts/ui.png"]. It serves the workspace over HTTP, runs jsdom assertions with real inline-script execution (clicks, text, selectors, console-error detection), and captures a real screenshot with the bundled lightweight headless browser (auto-downloaded on first use).`
@@ -24,8 +26,8 @@ export function shipGuidance({ state, workflow, prompt = "", profiles = [], disc
24
26
  const task = originalTask(prompt)
25
27
  const mode = state && !["verified", "blocked"].includes(state.status) ? state.workflowMode : workflow
26
28
  const parts = mode === "standard"
27
- ? ["Preserve every user requirement and promised output.", STANDARD_GUIDANCE]
28
- : [DELIVERY_INVARIANTS, deliveryPhaseGuidance(state)]
29
+ ? ["Preserve every user requirement and promised output.", STANDARD_GUIDANCE, TOOL_SURFACE_GUIDANCE]
30
+ : [DELIVERY_INVARIANTS, deliveryPhaseGuidance(state), TOOL_SURFACE_GUIDANCE]
29
31
  if (/\b(test|typescript|route|backend|api)\b/i.test(task)) parts.push(TEST_INFRASTRUCTURE_GUIDANCE)
30
32
  if (profiles.some(profile => /web|browser/.test(profile.id)) || /\b(browser|web|frontend|screenshot)\b/i.test(task)) parts.push(browserGuidance)
31
33
  if (imageInput === false) parts.push("This session's model cannot process images: do not attempt to view, read, or reason over image files or screenshots — visual review is unavailable. Verify visual behavior through DOM structure, computed styles, and console evidence instead; screenshots remain useful as evidence artifacts for the user even though you cannot inspect them.")
@@ -17,6 +17,7 @@ import { VoyagesNavigation } from "./ui/voyages-navigation.js";
17
17
  import { nativeNavigation } from "./ui/native-navigation.js";
18
18
  import { provisionNativeThemes } from "./ui/theme-provision.mjs";
19
19
  import { setSessionAutoCompaction } from "./ui/session-preferences.mjs";
20
+ import { runModelCommand } from "./ui/model-command.js";
20
21
  import { createNativeReviewConsumer, nativeReviewMode, sendNativeReviewKickoff, subscribeNativeReviewEvents } from "./ui/native-review.mjs";
21
22
 
22
23
  const THEMES = ["carthage", "tokyonight", "nebula", "ember", "forest", "mono", "obsidian", "midnight", "solarized-dark", "nord", "contrast-dark", "okabe-dark", "paper", "daylight", "solarized-light", "contrast-light", "okabe-light", "solarized", "okabe", "contrast", "system"];
@@ -45,8 +46,13 @@ export default Plugin.define({
45
46
  else reviewLoops.delete(sessionID);
46
47
  void updateReviewLoopState((state) => { state.sessions = [...reviewLoops]; });
47
48
  },
49
+ // No formal delivery (unavailable) or a verified one; blocked/unfinished runs are not offered.
50
+ eligible: async (sessionID: string, signal?: AbortSignal) => {
51
+ const delivery = await workspaceClient(context, context.data.session.get(sessionID)?.location).delivery({ sessionID }, signal ? AbortSignal.any([signal, AbortSignal.timeout(15_000)]) : AbortSignal.timeout(15_000));
52
+ return delivery.status === "unavailable" || delivery.status === "verified";
53
+ },
48
54
  offer: async ({ sessionID, writes, start }: { sessionID: string; writes: number; start: () => Promise<boolean> }) => {
49
- const accepted = await context.ui.dialog.confirm({ title: `Review ${context.data.session.get(sessionID)?.title ?? sessionID}?`, message: `This successful run changed ${writes} file${writes === 1 ? "" : "s"}. Start the PR → fresh-context review → fixes workflow?`, label: { confirm: "Start review", cancel: "Not now" } });
55
+ const accepted = await context.ui.dialog.confirm({ title: `Review ${context.data.session.get(sessionID)?.title ?? sessionID}?`, message: `This run changed ${writes} file${writes === 1 ? "" : "s"}. Start the PR → fresh-context review → fixes workflow?`, label: { confirm: "Start review", cancel: "Not now" } });
50
56
  if (accepted) await start();
51
57
  },
52
58
  kickoff: async (reviewSessionID: string, signal?: AbortSignal) => sendNativeReviewKickoff({ client: context.client, sessionID: reviewSessionID, signal }),
@@ -175,19 +181,17 @@ export default Plugin.define({
175
181
  }
176
182
  return;
177
183
  }
178
- case "model": {
179
- await context.data.location.model.sync(location);
180
- const models = context.data.location.model.list(location) ?? [];
181
- const query = arg.toLowerCase();
182
- const options = models.filter((model) => !query || `${nativeProviderName(model.providerID)} ${model.providerID}/${model.modelID} ${model.name}`.toLowerCase().includes(query))
183
- .map((model) => ({ title: `${nativeProviderName(model.providerID)}/${model.modelID}`, value: model, description: model.name }));
184
- const selected = await context.ui.dialog.select({ title: "Select provider model", placeholder: "Search provider/model…", options });
185
- if (selected && sessionID) {
186
- await context.client.session.switchModel({ sessionID, model: { providerID: selected.providerID, id: selected.id } });
187
- toast(`Selected ${nativeProviderName(selected.providerID)}/${selected.modelID}.`, "success");
188
- } else if (selected) toast("Open or create a session before selecting its model.", "warning");
184
+ case "model":
185
+ await runModelCommand({
186
+ sync: () => context.data.location.model.sync(location),
187
+ list: () => context.data.location.model.list(location) ?? [],
188
+ select: (options) => context.ui.dialog.select({ title: "Select provider model", placeholder: "Search provider/model…", options }),
189
+ switchModel: (target, model) => context.client.session.switchModel({ sessionID: target, model }),
190
+ openNativePicker: (query) => context.keymap.dispatch("model.list", query),
191
+ providerName: nativeProviderName,
192
+ toast,
193
+ }, { sessionID, arg });
189
194
  return;
190
- }
191
195
  case "thinking": {
192
196
  if (!selectedModel) { toast("Choose a model first.", "warning"); return; }
193
197
  const variants = context.ui.model.variant.list();
@@ -1,8 +1,9 @@
1
1
  /** @jsxImportSource @opentui/solid */
2
- import { For, Show, createEffect, createSignal, onCleanup, onMount } from "solid-js";
2
+ import { For, Show, createEffect, createSignal, on, onCleanup, onMount } from "solid-js";
3
3
  import { createHash, randomUUID } from "node:crypto";
4
4
  import type { ScrollBoxRenderable, SelectRenderable, TextareaRenderable } from "@opentui/core";
5
5
  import { usePlugin } from "@opencode/plugin/tui";
6
+ import { clickSelect } from "./select-mouse.js";
6
7
  import { dailyQuestsClient, dailySourceClient } from "./client.js";
7
8
  import type { DailyEvidence, DailyQuestDetail, DailySnapshot, DailySubmission, DailyRecord } from "../daily-quests-rpc.js";
8
9
  import type { DailySourceReport } from "../daily-source-rpc.js";
@@ -192,6 +193,8 @@ export function DailyQuestsPage(props: { sessionID?: string; questID?: string })
192
193
  // A navigator link selects only an assignment in the authenticated slate.
193
194
  // It never composes evidence or opens a consent dialog.
194
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 }));
195
198
  onCleanup(() => { focus.dispose(); stop(); actions = undefined; controller.dispose(); closeOwnDialog(); });
196
199
  });
197
200
  return <box flexDirection="column" flexGrow={1} padding={1}>
@@ -227,6 +230,6 @@ export function DailyQuestsPage(props: { sessionID?: string; questID?: string })
227
230
  <textarea ref={editor} flexBasis={0} flexGrow={1} minHeight={2} initialValue={view().draft?.text ?? ""} focused={focus.editorEnabled()} onContentChange={() => syncDailyEditor(editor, controller)} keyBindings={[{ name: "p", ctrl: true, action: "submit" }]} onSubmit={preview} />
228
231
  </Show>
229
232
  <text flexShrink={0} fg={context.theme.text.faint}>↑/↓ Enter · PgUp/Dn · Ctrl+U/D details · Cancel: no send</text>
230
- <select ref={(node) => { actions = node; focus.attachActions(node); }} 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)} />
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)} />
231
234
  </box>;
232
235
  }
@@ -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
+ }
@@ -10,6 +10,8 @@ export interface NativeReviewConsumerOptions {
10
10
  diff: (sessionID: string, signal?: AbortSignal) => Promise<unknown>;
11
11
  offer: (value: NativeReviewOffer) => Promise<unknown>;
12
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>;
13
15
  isLoopActive?: (sessionID: string) => boolean;
14
16
  setLoopActive?: (sessionID: string, active: boolean) => void;
15
17
  signal?: AbortSignal;