carthagent 1.0.7 → 1.1.0-preview.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +140 -83
- package/bin/ctg.mjs +14 -732
- package/docs/glyph-review/host-chrome-glyph-compatibility-v4.md +83 -0
- package/docs/glyph-review/test-isolation-v1.md +26 -0
- package/docs/glyph-review/typecheck-base.log +1 -0
- package/docs/glyph-review/typecheck-v4.log +1 -0
- package/docs/glyph-review/unicode-inventory-v4.csv +402 -0
- package/docs/help-dispatch-review.md +69 -0
- package/docs/legacy-cli.md +150 -0
- package/docs/live-theme-plugin-api.md +18 -0
- package/docs/native-cli.md +102 -0
- package/docs/native-compatibility.md +16 -0
- package/docs/native-daily-quests.md +132 -0
- package/docs/native-glyph-pty.md +59 -0
- package/docs/native-manual-testing.md +336 -0
- package/docs/native-slash-acceptance-handoff.md +49 -0
- package/docs/native-terminal-paired-failure-2026-10-17.md +40 -0
- package/docs/native-terminal-performance.md +310 -0
- package/docs/native-terminal-semantics-evidence/README.md +25 -0
- package/docs/native-terminal-semantics-evidence/controlled-host-model-selftest.json +16 -0
- package/docs/native-terminal-semantics-evidence/current-acceptance-status.json +59 -0
- package/docs/native-terminal-semantics-evidence/final-binary-focused-85bb44a.json +154 -0
- package/docs/native-terminal-semantics-evidence/final-binary-full-incomplete-85bb44a.json +160 -0
- package/docs/native-terminal-semantics-evidence/final-binary-layout-85bb44a.json +149 -0
- package/docs/native-terminal-semantics-evidence/final-binary-layout-interactive-85bb44a.json +159 -0
- package/docs/native-terminal-semantics-evidence/final-binary-model-picker-escape-unchanged-85bb44a.json +44 -0
- package/docs/native-terminal-semantics-evidence/final-binary-model-picker-exact-identity-85bb44a.json +47 -0
- package/docs/native-terminal-semantics-evidence/final-binary-model-picker-unchanged-model-incomplete-85bb44a.json +58 -0
- package/docs/native-terminal-semantics-evidence/final-binary-model-selftest.json +16 -0
- package/docs/native-terminal-semantics-evidence/final-binary-picker-cancel-incomplete-85bb44a.json +62 -0
- package/docs/native-terminal-semantics-evidence/focused-clipboard-title-stale-binary.json +161 -0
- package/docs/native-terminal-semantics-evidence/layout-stale-binary-observation-not-accepted.json +153 -0
- package/docs/native-terminal-semantics-evidence/picker-latest-incomplete-attempt.json +62 -0
- package/docs/native-terminal-semantics-evidence/picker-previous-probe-pass.json +61 -0
- package/docs/native-terminal-semantics.md +89 -0
- package/docs/native-upgrade.md +177 -0
- package/docs/opencode-composed-source-review.md +89 -0
- package/docs/opencode-migration.md +225 -9
- package/docs/opencode-owned-runtime.md +241 -0
- package/docs/opencode-runtime.md +12 -0
- package/docs/opencode-theme-provider-proof/README.md +55 -0
- package/docs/opencode-theme-provider-proof/live-theme-api-base.patch +122 -0
- package/docs/opencode-theme-provider-proof/plugin-theme-provider.patch +107 -0
- package/docs/opencode-theme-provider-proof/session-ui-base.patch +1217 -0
- package/docs/owned-host-controls-proof.md +39 -0
- package/docs/owned-runtime-release-manifest.md +42 -0
- package/docs/owned-visual-clear-proof.md +40 -0
- package/docs/patches/opencode-v2.0.32-glyph-spinner-test-isolation-v1.patch +50 -0
- package/docs/patches/opencode-v2.0.32-glyphs.patch +3670 -0
- package/docs/patches/opencode-v2.0.32-help-dispatch.patch +156 -0
- package/docs/patches/opencode-v2.0.32-live-theme-api.patch +122 -0
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v1.patch +5272 -0
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v2.patch +6052 -0
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v3.patch +8657 -0
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v4.patch +9928 -0
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v5.patch +13965 -0
- package/docs/patches/opencode-v2.0.32-session-ui.patch +5137 -0
- package/docs/release-notes-native-draft.md +73 -0
- package/docs/release-notes-preview.10.md +121 -0
- package/docs/release-notes-preview.11.md +76 -0
- package/docs/release-notes-preview.12.md +55 -0
- package/docs/release-notes-preview.13.md +68 -0
- package/docs/release-notes-preview.9.md +130 -0
- package/docs/source-preview.md +120 -0
- package/evaluate.mjs +28 -1
- package/lib/budget.mjs +1 -1
- package/lib/codegraph.mjs +1 -1
- package/lib/delivery-review.mjs +11 -1
- package/lib/delivery.mjs +3 -2
- package/lib/environment.mjs +8 -0
- package/lib/guidance.mjs +1 -1
- package/lib/opencode/cli-runtime.mjs +1171 -0
- package/lib/opencode/cli.mjs +252 -0
- package/lib/opencode/credentials.mjs +63 -8
- package/lib/opencode/demo.mjs +53 -0
- package/lib/opencode/evaluation-budget-plugin/index.ts +3 -0
- package/lib/opencode/evaluation.mjs +32 -12
- package/lib/opencode/headless.mjs +8 -4
- package/lib/opencode/provider-policy.mjs +23 -0
- package/lib/opencode/review.mjs +37 -16
- package/lib/opencode/runtime-artifact.mjs +335 -0
- package/lib/opencode/runtime-github-transport.mjs +121 -0
- package/lib/opencode/runtime-release.json +36 -0
- package/lib/opencode/runtime.mjs +299 -20
- package/lib/opencode/service.mjs +77 -35
- package/lib/opencode/session-options.mjs +6 -2
- package/lib/opencode/update.mjs +222 -0
- package/lib/opencode/workspace.mjs +1 -1
- package/lib/planning-advice.mjs +2 -2
- package/lib/providers/experiential.mjs +2 -2
- package/lib/tui/framing.mjs +1 -80
- package/lib/update.mjs +10 -1
- package/package.json +33 -7
- package/plugins/carthagent/README.md +23 -0
- package/plugins/carthagent/src/bootstrap.ts +1 -1
- package/plugins/carthagent/src/daily-evidence.mjs +57 -0
- package/plugins/carthagent/src/daily-quests-rpc.ts +50 -0
- package/plugins/carthagent/src/daily-source-rpc.ts +52 -0
- package/plugins/carthagent/src/index.ts +71 -24
- package/plugins/carthagent/src/rpc.ts +51 -6
- package/plugins/carthagent/src/server/codegraph.ts +12 -5
- package/plugins/carthagent/src/server/daily-quests-rpc.ts +277 -0
- package/plugins/carthagent/src/server/daily-source-rpc.ts +211 -0
- package/plugins/carthagent/src/server/delivery.ts +227 -112
- package/plugins/carthagent/src/server/framing.mjs +80 -0
- package/plugins/carthagent/src/server/policy.ts +1 -1
- package/plugins/carthagent/src/server/providers.ts +59 -31
- package/plugins/carthagent/src/server/session-controls.ts +3 -1
- package/plugins/carthagent/src/server/ship-agents.ts +77 -0
- package/plugins/carthagent/src/server/ship-context.mjs +60 -0
- package/plugins/carthagent/src/server/ship-guidance.ts +41 -0
- package/plugins/carthagent/src/server/ship-rpc.ts +69 -0
- package/plugins/carthagent/src/server/voyages-rpc.ts +223 -0
- package/plugins/carthagent/src/testing.ts +5 -0
- package/plugins/carthagent/src/tui.tsx +496 -0
- package/plugins/carthagent/src/ui/client.ts +125 -0
- package/plugins/carthagent/src/ui/commands.ts +70 -0
- package/plugins/carthagent/src/ui/daily-editor-focus.mjs +52 -0
- package/plugins/carthagent/src/ui/daily-editor.mjs +4 -0
- package/plugins/carthagent/src/ui/daily-quests-events.mjs +18 -0
- package/plugins/carthagent/src/ui/daily-quests-state.mjs +218 -0
- package/plugins/carthagent/src/ui/daily-quests.tsx +235 -0
- package/plugins/carthagent/src/ui/host-state.ts +42 -0
- package/plugins/carthagent/src/ui/model-command.ts +32 -0
- package/plugins/carthagent/src/ui/native-navigation.ts +23 -0
- package/plugins/carthagent/src/ui/native-review.d.mts +33 -0
- package/plugins/carthagent/src/ui/native-review.mjs +159 -0
- package/plugins/carthagent/src/ui/resume-session.mjs +34 -0
- package/plugins/carthagent/src/ui/select-mouse.ts +17 -0
- package/plugins/carthagent/src/ui/session-preferences.d.mts +7 -0
- package/plugins/carthagent/src/ui/session-preferences.mjs +14 -0
- package/plugins/carthagent/src/ui/theme-provision.d.mts +3 -0
- package/plugins/carthagent/src/ui/theme-provision.mjs +134 -0
- package/plugins/carthagent/src/ui/voyages-navigation-state.mjs +56 -0
- package/plugins/carthagent/src/ui/voyages-navigation.tsx +131 -0
- package/plugins/carthagent/src/ui/voyages-state.mjs +142 -0
- package/plugins/carthagent/src/ui/voyages.tsx +129 -0
- package/plugins/carthagent/src/ui/workspace-live.ts +61 -0
- package/plugins/carthagent/src/ui/workspace.tsx +379 -0
- package/plugins/carthagent/src/voyages-rpc.ts +60 -0
- package/plugins/carthagent/src/workspace-rpc.ts +36 -0
- package/plugins/carthagent/tests/commands.test.ts +107 -0
- package/plugins/carthagent/tests/session-preferences.test.mjs +41 -0
- package/plugins/carthagent/tests/voyages-navigation.test.tsx +269 -0
- package/plugins/carthagent/themes/carthage.json +67 -0
- package/plugins/carthagent/tui.ts +26 -1
- package/script/ctg/build-owned-runtime.sh +166 -0
- package/tests/budget-engine.test.mjs +4 -3
- package/tests/cli.test.mjs +52 -96
- package/tests/daily-evidence.test.mjs +62 -0
- package/tests/delivery-review.test.mjs +11 -0
- package/tests/delivery.test.mjs +22 -1
- package/tests/fixtures/native-discovery-plugin/server.ts +15 -0
- package/tests/generate-owned-runtime-release.test.mjs +354 -0
- package/tests/installed-runtime-contract.test.mjs +43 -0
- package/tests/native-cli-host-smoke.mjs +457 -0
- package/tests/native-daily-editor-focus.test.mjs +213 -0
- package/tests/native-daily-editor.test.mjs +43 -0
- package/tests/native-daily-quests-layout.test.mjs +53 -0
- package/tests/native-daily-quests-rpc.test.mjs +456 -0
- package/tests/native-daily-quests-ui.test.mjs +778 -0
- package/tests/native-daily-source.test.mjs +451 -0
- package/tests/native-demo-ui.test.mjs +48 -0
- package/tests/native-evaluation-admission.test.mjs +24 -0
- package/tests/native-fixture-browser-cleanup.test.mjs +65 -0
- package/tests/native-fixture-runtime.test.mjs +111 -0
- package/tests/native-fixture-stop-diagnostics.test.mjs +67 -0
- package/tests/native-glyph-pty.test.mjs +107 -0
- package/tests/native-manual-projects.test.mjs +170 -0
- package/tests/native-model-command.test.mjs +67 -0
- package/tests/native-owned-runtime-env.test.mjs +140 -0
- package/tests/native-paired-guards.test.mjs +464 -0
- package/tests/native-provider-policy.test.mjs +31 -0
- package/tests/native-pty-invocation.test.mjs +114 -0
- package/tests/native-pty-runtime.test.mjs +45 -0
- package/tests/native-resume-session.test.mjs +46 -0
- package/tests/native-review-consumer.test.mjs +286 -0
- package/tests/native-runtime-identity.test.mjs +214 -0
- package/tests/native-select-mouse.test.mjs +23 -0
- package/tests/native-ship-parity.test.mjs +526 -0
- package/tests/native-ship-plan.test.mjs +85 -0
- package/tests/native-ship-rpc.test.mjs +71 -0
- package/tests/native-ui-commands.test.mjs +6 -0
- package/tests/native-voyages-layout.test.mjs +28 -0
- package/tests/native-voyages-navigation.test.mjs +169 -0
- package/tests/native-voyages-rpc.test.mjs +395 -0
- package/tests/native-voyages-ui.test.mjs +277 -0
- package/tests/opencode-atomic-write.test.mjs +55 -0
- package/tests/opencode-cli.test.mjs +1312 -0
- package/tests/opencode-codegraph-plugin.test.mjs +47 -0
- package/tests/opencode-delivery.test.mjs +16 -0
- package/tests/opencode-providers.test.mjs +245 -18
- package/tests/opencode-runtime.test.mjs +546 -8
- package/tests/opencode-session-controls.test.mjs +15 -0
- package/tests/opencode-ship-rpc.test.mjs +71 -0
- package/tests/opencode-update.test.mjs +475 -0
- package/tests/opencode-workspace.test.mjs +51 -2
- package/tests/owned-runtime-publish-workflow.test.mjs +477 -0
- package/tests/release-workflow.test.mjs +46 -0
- package/tests/runtime-artifact.test.mjs +269 -0
- package/tests/runtime-development-trust.test.mjs +230 -0
- package/tests/runtime-github-transport.test.mjs +132 -0
- package/tests/runtime-token-env.test.mjs +23 -0
- package/tests/server.test.mjs +3 -1
- package/tests/ship-telemetry.test.mjs +1 -1
- package/tests/source-preview.test.mjs +253 -0
- package/tests/test_native_terminal_probe.py +76 -0
- package/tests/test_native_terminal_semantics.py +232 -0
- package/tests/update.test.mjs +42 -5
- package/tests/verify-draft-runtime-gate.test.mjs +280 -0
- package/tests/verify-owned-runtime-release.test.mjs +371 -0
- package/tests/workspace-live.test.mjs +127 -0
- package/tools/benchmark/diagnose-request.mjs +59 -36
- package/tools/benchmark/eval-common.mjs +3 -3
- package/tools/benchmark/evaluators.test.mjs +4 -4
- package/tools/benchmark/guard.test.mjs +35 -2
- package/tools/benchmark/guard.ts +20 -7
- package/tools/benchmark/planning-startup.test.mjs +30 -34
- package/tools/benchmark/runner.mjs +12 -7
- package/tools/benchmark/runner.test.mjs +23 -7
- package/tools/benchmark/upstream-error.test.mjs +16 -2
- package/tools/generate-owned-runtime-release.mjs +217 -0
- package/tools/installed-runtime-contract.mjs +16 -0
- package/tools/manual-runtime-manifest.mjs +39 -0
- package/tools/native-fixture-browser-cleanup.mjs +59 -0
- package/tools/native-fixture-stop-diagnostics.mjs +43 -0
- package/tools/native-fixture.mjs +199 -40
- package/tools/native-glyph-helpers.py +127 -0
- package/tools/native-glyph-pty.py +144 -0
- package/tools/native-manual-projects.mjs +259 -0
- package/tools/native-manual.mjs +338 -0
- package/tools/native-model-picker-acceptance.py +306 -0
- package/tools/native-owned-runtime-env.sh +47 -0
- package/tools/native-paired-guards.mjs +282 -0
- package/tools/native-paired-manifest.mjs +72 -0
- package/tools/native-paired-orchestration.sh +51 -0
- package/tools/native-plugin-readiness.mjs +33 -0
- package/tools/native-plugin-readiness.test.mjs +53 -0
- package/tools/native-pty-invocation.mjs +26 -0
- package/tools/native-pty-runtime.mjs +83 -0
- package/tools/native-pty-smoke.py +147 -24
- package/tools/native-public-repo-shell-result.mjs +13 -0
- package/tools/native-public-repo-shell-result.test.mjs +28 -0
- package/tools/native-runtime-identity.mjs +201 -0
- package/tools/native-slash-acceptance-env.py +38 -0
- package/tools/native-terminal-semantics.py +876 -0
- package/tools/owned-shell-observer/index.ts +16 -0
- package/tools/owned-shell-observer/package.json +1 -0
- package/tools/owned-visual-clear-env.py +48 -0
- package/tools/owned-visual-clear-model.py +21 -0
- package/tools/pinned-owned-runtime-inputs.mjs +46 -0
- package/tools/prepare-native-manual.mjs +122 -0
- package/tools/run-native-terminal-paired.sh +403 -0
- package/tools/runtime-token-env.mjs +8 -0
- package/tools/seed-native-switch-fixture.mjs +79 -0
- package/tools/test-native-controls.mjs +3 -1
- package/tools/test-native-daily-quests.mjs +65 -0
- package/tools/test-native-default-cli.mjs +106 -0
- package/tools/test-native-delivery-host.mjs +102 -20
- package/tools/test-native-evaluation.mjs +7 -3
- package/tools/test-native-glyph-pty.mjs +94 -0
- package/tools/test-native-host.mjs +87 -63
- package/tools/test-native-outcomes.mjs +3 -1
- package/tools/test-native-package.mjs +206 -38
- package/tools/test-native-performance.mjs +26 -0
- package/tools/test-native-product.mjs +90 -0
- package/tools/test-native-pty.mjs +34 -9
- package/tools/test-native-public-repo.mjs +224 -0
- package/tools/test-native-refresh-reentry.mjs +106 -0
- package/tools/test-native-report.mjs +27 -20
- package/tools/test-native-review.mjs +75 -0
- package/tools/test-native-runtime.mjs +120 -0
- package/tools/test-native-ship-models.mjs +176 -0
- package/tools/test-native-ship-walkthrough.mjs +96 -0
- package/tools/test-native-slash-commands.mjs +125 -0
- package/tools/test-native-slash-commands.py +242 -0
- package/tools/test-native-swe-host.mjs +224 -55
- package/tools/test-native-tasks.mjs +77 -0
- package/tools/test-native-terminal-performance.py +887 -0
- package/tools/test-native-terminal-readiness.sh +85 -0
- package/tools/test-native-terminal-semantics-installed.mjs +38 -0
- package/tools/test-native-visual-clear-installed.mjs +48 -0
- package/tools/test-owned-host-controls.mjs +238 -0
- package/tools/test-owned-opencode-runtime.mjs +35 -0
- package/tools/test-owned-visual-clear-env.py +120 -0
- package/tools/test-owned-visual-clear.py +373 -0
- package/tools/verify-draft-runtime-gate.mjs +138 -0
- package/tools/verify-owned-runtime-release.mjs +114 -0
- package/tools/verify-release-version.mjs +21 -1
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import { setSessionAutoCompaction } from "../src/ui/session-preferences.mjs";
|
|
4
|
+
|
|
5
|
+
test("autocompact persists explicit, idempotent session preferences without replacing neighbor metadata", async () => {
|
|
6
|
+
let current = { "ctg.autoCompaction": false, "ctg.raw": true, "user.flag": "keep" };
|
|
7
|
+
const updates = [];
|
|
8
|
+
const client = {
|
|
9
|
+
async get(input, options) {
|
|
10
|
+
assert.equal(input.sessionID, "s1");
|
|
11
|
+
assert.equal(options.location.directory, "/project");
|
|
12
|
+
assert.ok(options.signal instanceof AbortSignal);
|
|
13
|
+
return { metadata: current };
|
|
14
|
+
},
|
|
15
|
+
async update(input, options) {
|
|
16
|
+
assert.equal(input.sessionID, "s1");
|
|
17
|
+
assert.equal(options.location.directory, "/project");
|
|
18
|
+
current = input.metadata;
|
|
19
|
+
updates.push(input.metadata);
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
const location = { directory: "/project" };
|
|
23
|
+
assert.equal(await setSessionAutoCompaction(client, "s1", location, "off"), false);
|
|
24
|
+
assert.equal(updates.length, 0, "repeating off must not write");
|
|
25
|
+
assert.equal(await setSessionAutoCompaction(client, "s1", location, "on"), true);
|
|
26
|
+
assert.deepEqual(current, { "ctg.raw": true, "user.flag": "keep" });
|
|
27
|
+
assert.equal(await setSessionAutoCompaction(client, "s1", location, "toggle"), false);
|
|
28
|
+
assert.deepEqual(current, { "ctg.autoCompaction": false, "ctg.raw": true, "user.flag": "keep" });
|
|
29
|
+
assert.equal(await setSessionAutoCompaction(client, "s1", location, "toggle"), true);
|
|
30
|
+
assert.equal(updates.length, 3);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("invalid autocompact arguments fail before reading or mutating session state", async () => {
|
|
34
|
+
let calls = 0;
|
|
35
|
+
const client = {
|
|
36
|
+
async get() { calls++; throw new Error("must not read"); },
|
|
37
|
+
async update() { calls++; throw new Error("must not update"); },
|
|
38
|
+
};
|
|
39
|
+
await assert.rejects(setSessionAutoCompaction(client, "s1", { directory: "/project" }, "sometimes"), /Usage: \/autocompact \[on\|off\]/);
|
|
40
|
+
assert.equal(calls, 0);
|
|
41
|
+
});
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/solid */
|
|
2
|
+
import { expect, test } from "bun:test";
|
|
3
|
+
import { testRender } from "@opentui/solid";
|
|
4
|
+
import { ScrollBoxRenderable, SelectRenderable, type Renderable } from "@opentui/core";
|
|
5
|
+
import { createSignal } from "solid-js";
|
|
6
|
+
import { PluginContextProvider } from "@opencode/plugin/tui";
|
|
7
|
+
import type { Context } from "@opencode/plugin/tui/context";
|
|
8
|
+
import { parseThemeDocument, resolveThemeDocument } from "@opencode/theme/tui";
|
|
9
|
+
import theme from "../themes/carthage.json";
|
|
10
|
+
import { Voyages } from "../src/voyages-rpc.js";
|
|
11
|
+
import { VoyagesNavigation } from "../src/ui/voyages-navigation.js";
|
|
12
|
+
|
|
13
|
+
const profile = { profile: { name: "Test captain", shipName: "Test vessel", coins: 10, level: 2, totalXp: 150 } };
|
|
14
|
+
const slate = { slate: { day: "2026-10-30", endsAt: "2026-10-31T00:00:00Z", items: [
|
|
15
|
+
{ id: "quest-one", definition: { title: "Protect a boundary" }, status: "open" },
|
|
16
|
+
{ id: "quest-two", definition: { title: "Review a change" }, status: "assessed_complete" },
|
|
17
|
+
] }, records: [{ request: { evidence: "must not appear in navigation", consent: true } }] };
|
|
18
|
+
|
|
19
|
+
async function fixture({ width = 30, height = 36, authenticated = true } = {}) {
|
|
20
|
+
const [active, setActive] = createSignal(true);
|
|
21
|
+
const [focused, setFocused] = createSignal(false);
|
|
22
|
+
const [compact, setCompact] = createSignal(false);
|
|
23
|
+
const [mode, setMode] = createSignal("base");
|
|
24
|
+
const [sessionID, setSessionID] = createSignal<string | undefined>("coding-session");
|
|
25
|
+
const routes: unknown[] = [], calls: string[] = [], selected: string[] = [];
|
|
26
|
+
const responses: Promise<unknown>[] = [];
|
|
27
|
+
let responseGate: Promise<void> | undefined;
|
|
28
|
+
let listener: ((event: { details: { type: string } }) => void) | undefined;
|
|
29
|
+
let listening = true;
|
|
30
|
+
const context = {
|
|
31
|
+
theme: resolveThemeDocument(parseThemeDocument(theme), "dark"),
|
|
32
|
+
location: { directory: "/test-project" },
|
|
33
|
+
data: {
|
|
34
|
+
session: { get: () => ({ location: { directory: "/test-project" } }) },
|
|
35
|
+
location: { default: () => ({ directory: "/test-project" }) },
|
|
36
|
+
listen: (next: typeof listener) => { listener = next; return () => { listening = false; }; },
|
|
37
|
+
},
|
|
38
|
+
client: { rpc: (definition: unknown) => ({ snapshot: () => {
|
|
39
|
+
calls.push(definition === Voyages ? "profile" : "daily");
|
|
40
|
+
const response = authenticated ? (responseGate ?? Promise.resolve()).then(() => definition === Voyages ? profile : slate) : Promise.reject({ type: "voyagesError", data: { code: "authentication_required" } });
|
|
41
|
+
responses.push(response);
|
|
42
|
+
return response;
|
|
43
|
+
} }) },
|
|
44
|
+
keymap: { mode: { current: mode }, dispatch: (id: string) => { calls.push(id); } },
|
|
45
|
+
ui: {
|
|
46
|
+
router: { navigate: (route: unknown) => routes.push(route) },
|
|
47
|
+
navigation: { current: () => "voyages", select: (name: string) => { selected.push(name); return true; } },
|
|
48
|
+
},
|
|
49
|
+
} as unknown as Context;
|
|
50
|
+
const app = await testRender(() => <PluginContextProvider value={context}>
|
|
51
|
+
<box width={width} height={height} visible={active()}>
|
|
52
|
+
<VoyagesNavigation width={width} active={active()} compact={compact()} focused={focused()} sessionID={sessionID()} focus={() => { if (mode() !== "base") return; setFocused(true); setMode("navigation"); }} blur={() => { setFocused(false); if (mode() === "navigation") setMode("base"); }} />
|
|
53
|
+
</box>
|
|
54
|
+
</PluginContextProvider>, { width, height, kittyKeyboard: true });
|
|
55
|
+
const settle = async () => { await Promise.allSettled(responses); await app.renderOnce(); };
|
|
56
|
+
await settle();
|
|
57
|
+
return {
|
|
58
|
+
app, calls, routes, selected, mode, setMode, setActive, setFocused, setCompact, setSessionID, settle,
|
|
59
|
+
focus: async () => { setFocused(true); setMode("navigation"); await app.renderOnce(); },
|
|
60
|
+
event: (type: string) => listener?.({ details: { type } }),
|
|
61
|
+
delay: (promise: Promise<void> | undefined) => { responseGate = promise; },
|
|
62
|
+
listening: () => listening,
|
|
63
|
+
list: () => app.renderer.currentFocusedRenderable as SelectRenderable,
|
|
64
|
+
scroll: () => {
|
|
65
|
+
const find = (node: Renderable): ScrollBoxRenderable | undefined => node instanceof ScrollBoxRenderable ? node : node.getChildren().map(find).find(Boolean);
|
|
66
|
+
return find(app.renderer.root)!;
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
test.each([30, 18])("real Voyages navigator renders account and assignment summaries at width %i", async (width) => {
|
|
72
|
+
const f = await fixture({ width });
|
|
73
|
+
try {
|
|
74
|
+
const frame = f.app.captureCharFrame();
|
|
75
|
+
expect(frame).toContain("Test captain");
|
|
76
|
+
expect(frame).toContain("10 coins");
|
|
77
|
+
expect(frame).toContain("Daily coding");
|
|
78
|
+
expect(frame).not.toContain("must not appear");
|
|
79
|
+
expect(f.calls).toEqual(["profile", "daily"]);
|
|
80
|
+
expect(f.routes).toHaveLength(0);
|
|
81
|
+
} finally { f.app.renderer.destroy(); }
|
|
82
|
+
expect(f.listening()).toBe(false);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("rail focus owns arrows/Enter; palette or modal mode cannot trigger a route", async () => {
|
|
86
|
+
const f = await fixture();
|
|
87
|
+
try {
|
|
88
|
+
expect(f.app.renderer.currentFocusedRenderable).not.toBeInstanceOf(SelectRenderable);
|
|
89
|
+
await f.focus();
|
|
90
|
+
expect(f.list()).toBeInstanceOf(SelectRenderable);
|
|
91
|
+
f.app.mockInput.pressArrow("down");
|
|
92
|
+
await f.app.renderOnce();
|
|
93
|
+
expect(f.list().getSelectedOption()?.value).toBe("quest:quest-one");
|
|
94
|
+
for (const mode of ["modal", "palette", "base"]) {
|
|
95
|
+
f.setMode(mode);
|
|
96
|
+
await f.app.renderOnce();
|
|
97
|
+
f.app.mockInput.pressEnter();
|
|
98
|
+
await f.app.renderOnce();
|
|
99
|
+
expect(f.routes).toHaveLength(0);
|
|
100
|
+
}
|
|
101
|
+
f.setMode("navigation");
|
|
102
|
+
await f.app.renderOnce();
|
|
103
|
+
f.app.mockInput.pressEnter();
|
|
104
|
+
await f.app.renderOnce();
|
|
105
|
+
expect(f.routes).toEqual([{ type: "plugin", name: "daily-quests", data: { sessionID: "coding-session", questID: "quest-one" } }]);
|
|
106
|
+
expect(f.mode()).toBe("base");
|
|
107
|
+
expect(f.calls).toEqual(["profile", "daily"]);
|
|
108
|
+
} finally { f.app.renderer.destroy(); }
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("Projects/Voyages toggles retain selection and do not route, submit or purchase", async () => {
|
|
112
|
+
const f = await fixture();
|
|
113
|
+
try {
|
|
114
|
+
await f.focus();
|
|
115
|
+
f.app.mockInput.pressArrow("down");
|
|
116
|
+
await f.app.renderOnce();
|
|
117
|
+
expect(f.list().getSelectedOption()?.value).toBe("quest:quest-one");
|
|
118
|
+
f.setActive(false);
|
|
119
|
+
await f.settle();
|
|
120
|
+
expect(f.app.captureCharFrame()).not.toContain("Test captain");
|
|
121
|
+
f.setActive(true);
|
|
122
|
+
await f.settle();
|
|
123
|
+
expect(f.list().getSelectedOption()?.value).toBe("quest:quest-one");
|
|
124
|
+
expect(f.routes).toHaveLength(0);
|
|
125
|
+
expect(f.calls).toEqual(["profile", "daily", "profile", "daily"]);
|
|
126
|
+
} finally { f.app.renderer.destroy(); }
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("compact mode uses the same mounted selection and keyboard behavior", async () => {
|
|
130
|
+
const f = await fixture();
|
|
131
|
+
try {
|
|
132
|
+
await f.focus();
|
|
133
|
+
f.app.mockInput.pressArrow("down");
|
|
134
|
+
await f.app.renderOnce();
|
|
135
|
+
const list = f.list();
|
|
136
|
+
f.setCompact(true);
|
|
137
|
+
f.app.resize(5, 36);
|
|
138
|
+
await f.app.renderOnce();
|
|
139
|
+
expect(f.list()).toBe(list);
|
|
140
|
+
expect(f.list().getSelectedOption()?.value).toBe("quest:quest-one");
|
|
141
|
+
expect(f.app.captureCharFrame()).not.toContain("Test captain");
|
|
142
|
+
f.app.mockInput.pressEnter();
|
|
143
|
+
await f.app.renderOnce();
|
|
144
|
+
expect(f.routes).toEqual([{ type: "plugin", name: "daily-quests", data: { sessionID: "coding-session", questID: "quest-one" } }]);
|
|
145
|
+
} finally { f.app.renderer.destroy(); }
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("switching modes preserves the Voyages scroll position at a short terminal height", async () => {
|
|
149
|
+
const f = await fixture({ height: 12 });
|
|
150
|
+
try {
|
|
151
|
+
const scroll = f.scroll();
|
|
152
|
+
scroll.scrollBy(5);
|
|
153
|
+
await f.app.renderOnce();
|
|
154
|
+
const before = scroll.scrollTop;
|
|
155
|
+
expect(before).toBeGreaterThan(0);
|
|
156
|
+
f.setActive(false);
|
|
157
|
+
await f.settle();
|
|
158
|
+
f.setActive(true);
|
|
159
|
+
await f.settle();
|
|
160
|
+
expect(f.scroll()).toBe(scroll);
|
|
161
|
+
expect(scroll.scrollTop).toBe(before);
|
|
162
|
+
expect(f.routes).toHaveLength(0);
|
|
163
|
+
} finally { f.app.renderer.destroy(); }
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("slow summary reads preserve scrolled position after an intervening loading layout", async () => {
|
|
167
|
+
const f = await fixture({ height: 12 });
|
|
168
|
+
try {
|
|
169
|
+
const scroll = f.scroll();
|
|
170
|
+
scroll.scrollTo(100);
|
|
171
|
+
await f.app.renderOnce();
|
|
172
|
+
const before = scroll.scrollTop;
|
|
173
|
+
expect(before).toBeGreaterThan(0);
|
|
174
|
+
f.setActive(false);
|
|
175
|
+
await f.app.renderOnce();
|
|
176
|
+
const gate = Promise.withResolvers<void>();
|
|
177
|
+
f.delay(gate.promise);
|
|
178
|
+
f.setActive(true);
|
|
179
|
+
await f.app.renderOnce();
|
|
180
|
+
expect(f.app.captureCharFrame()).not.toContain("Test captain");
|
|
181
|
+
expect(f.calls).toEqual(["profile", "daily", "profile", "daily"]);
|
|
182
|
+
gate.resolve();
|
|
183
|
+
await f.settle();
|
|
184
|
+
await f.app.renderOnce();
|
|
185
|
+
expect(scroll.scrollTop).toBe(before);
|
|
186
|
+
} finally { f.app.renderer.destroy(); }
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("short terminal keyboard navigation keeps the highlighted action on screen", async () => {
|
|
190
|
+
const f = await fixture({ height: 12 });
|
|
191
|
+
try {
|
|
192
|
+
await f.focus();
|
|
193
|
+
for (let index = 0; index < 7; index++) {
|
|
194
|
+
f.app.mockInput.pressArrow("down");
|
|
195
|
+
await f.app.renderOnce();
|
|
196
|
+
}
|
|
197
|
+
expect(f.list().getSelectedOption()?.value).toBe("back");
|
|
198
|
+
expect(f.app.captureCharFrame()).toContain("Return to coding");
|
|
199
|
+
expect(f.list().height).toBeLessThanOrEqual(f.scroll().viewport.height);
|
|
200
|
+
expect(f.routes).toHaveLength(0);
|
|
201
|
+
} finally { f.app.renderer.destroy(); }
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("mouse press focuses the rail and release opens the clicked row without acting through a modal", async () => {
|
|
205
|
+
const f = await fixture();
|
|
206
|
+
try {
|
|
207
|
+
const row = f.app.captureCharFrame().split("\n").findIndex((line) => line.includes("Daily coding quests"));
|
|
208
|
+
expect(row).toBeGreaterThanOrEqual(0);
|
|
209
|
+
for (const mode of ["modal", "palette"]) {
|
|
210
|
+
f.setMode(mode);
|
|
211
|
+
await f.app.renderOnce();
|
|
212
|
+
await f.app.mockMouse.click(5, row);
|
|
213
|
+
await f.app.renderOnce();
|
|
214
|
+
expect(f.mode()).toBe(mode);
|
|
215
|
+
expect(f.routes).toHaveLength(0);
|
|
216
|
+
}
|
|
217
|
+
f.setMode("base");
|
|
218
|
+
f.app.renderer.clearSelection();
|
|
219
|
+
await f.app.renderOnce();
|
|
220
|
+
await f.app.mockMouse.pressDown(5, row);
|
|
221
|
+
await f.app.renderOnce();
|
|
222
|
+
expect(f.mode()).toBe("navigation");
|
|
223
|
+
expect(f.app.renderer.currentFocusedRenderable).toBeInstanceOf(SelectRenderable);
|
|
224
|
+
expect(f.routes).toHaveLength(0);
|
|
225
|
+
await f.app.mockMouse.release(5, row);
|
|
226
|
+
await f.app.renderOnce();
|
|
227
|
+
expect(f.routes).toEqual([{ type: "plugin", name: "daily-quests", data: { sessionID: "coding-session" } }]);
|
|
228
|
+
expect(f.mode()).toBe("base");
|
|
229
|
+
const questRow = f.app.captureCharFrame().split("\n").findIndex((line) => line.includes("Review a change"));
|
|
230
|
+
expect(questRow).toBeGreaterThanOrEqual(0);
|
|
231
|
+
await f.app.mockMouse.click(5, questRow);
|
|
232
|
+
await f.app.renderOnce();
|
|
233
|
+
expect(f.routes).toEqual([
|
|
234
|
+
{ type: "plugin", name: "daily-quests", data: { sessionID: "coding-session" } },
|
|
235
|
+
{ type: "plugin", name: "daily-quests", data: { sessionID: "coding-session", questID: "quest-two" } },
|
|
236
|
+
]);
|
|
237
|
+
} finally { f.app.renderer.destroy(); }
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("signed-out account and invalidation hide protected summaries, reset stale selection, and stay read-only", async () => {
|
|
241
|
+
const f = await fixture({ authenticated: false });
|
|
242
|
+
try {
|
|
243
|
+
expect(f.app.captureCharFrame()).toContain("authentication_required");
|
|
244
|
+
expect(f.app.captureCharFrame()).not.toContain("Test captain");
|
|
245
|
+
await f.focus();
|
|
246
|
+
f.app.mockInput.pressArrow("down");
|
|
247
|
+
await f.app.renderOnce();
|
|
248
|
+
f.event("credential.updated");
|
|
249
|
+
await f.settle();
|
|
250
|
+
expect(f.list().getSelectedOption()?.value).toBe("daily");
|
|
251
|
+
expect(f.routes).toHaveLength(0);
|
|
252
|
+
expect(f.calls).toEqual(["profile", "daily", "profile", "daily"]);
|
|
253
|
+
} finally { f.app.renderer.destroy(); }
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test.each(["coding-session", undefined])("return to coding explicitly navigates to the selected session or Home (%s)", async (sessionID) => {
|
|
257
|
+
const f = await fixture();
|
|
258
|
+
try {
|
|
259
|
+
f.setSessionID(sessionID);
|
|
260
|
+
await f.focus();
|
|
261
|
+
const list = f.list();
|
|
262
|
+
const index = list.options.findIndex((option) => option.value === "back");
|
|
263
|
+
list.setSelectedIndex(index);
|
|
264
|
+
f.app.mockInput.pressEnter();
|
|
265
|
+
await f.app.renderOnce();
|
|
266
|
+
expect(f.selected).toEqual(["projects"]);
|
|
267
|
+
expect(f.routes).toEqual([sessionID ? { type: "session", sessionID } : { type: "home" }]);
|
|
268
|
+
} finally { f.app.renderer.destroy(); }
|
|
269
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://opencode.ai/theme.json",
|
|
3
|
+
"base": {
|
|
4
|
+
"categorical": ["accent", "purple", "cyan", "green", "blue"],
|
|
5
|
+
"text": {
|
|
6
|
+
"base": "$hue.neutral.100",
|
|
7
|
+
"muted": "$hue.neutral.300",
|
|
8
|
+
"faint": "$hue.neutral.400",
|
|
9
|
+
"action": {
|
|
10
|
+
"primary": { "base": "$hue.purple.400", "$hovered": "$hue.purple.300", "$pressed": "$hue.purple.500" },
|
|
11
|
+
"secondary": { "base": "$hue.neutral.200", "$hovered": "$hue.yellow.400" },
|
|
12
|
+
"destructive": { "base": "$hue.red.300", "$hovered": "$hue.red.200" }
|
|
13
|
+
},
|
|
14
|
+
"formfield": { "base": "$hue.neutral.100", "$focused": "$hue.yellow.400" },
|
|
15
|
+
"feedback": {
|
|
16
|
+
"info": { "base": "$hue.cyan.300", "muted": "$hue.cyan.500" },
|
|
17
|
+
"success": { "base": "$hue.green.300", "muted": "$hue.green.500" },
|
|
18
|
+
"warning": { "base": "$hue.yellow.300", "muted": "$hue.yellow.500" },
|
|
19
|
+
"error": { "base": "$hue.red.300", "muted": "$hue.red.500" }
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"background": {
|
|
23
|
+
"base": "$hue.neutral.900",
|
|
24
|
+
"raised": { "base": "$hue.neutral.800", "high": "$hue.neutral.700", "max": "$hue.neutral.600" },
|
|
25
|
+
"action": {
|
|
26
|
+
"primary": { "base": "$hue.yellow.400", "$hovered": "$hue.yellow.300", "$pressed": "$hue.yellow.500" },
|
|
27
|
+
"secondary": { "base": "$hue.neutral.800", "$hovered": "$hue.neutral.700" },
|
|
28
|
+
"destructive": { "base": "$hue.red.700", "$hovered": "$hue.red.600" }
|
|
29
|
+
},
|
|
30
|
+
"formfield": { "base": "$hue.neutral.900", "$focused": "$hue.neutral.800" },
|
|
31
|
+
"feedback": {
|
|
32
|
+
"info": { "base": "$hue.blue.800" }, "success": { "base": "$hue.green.800" },
|
|
33
|
+
"warning": { "base": "$hue.orange.800" }, "error": { "base": "$hue.red.800" }
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"border": { "base": "$hue.neutral.600" },
|
|
37
|
+
"scrollbar": { "base": "$hue.neutral.600" },
|
|
38
|
+
"diff": {
|
|
39
|
+
"text": { "added": "$hue.green.300", "removed": "$hue.red.300", "context": "$hue.neutral.300", "hunkHeader": "$hue.cyan.300" },
|
|
40
|
+
"background": { "added": "$hue.green.800", "removed": "$hue.red.800", "context": "$hue.neutral.800" },
|
|
41
|
+
"highlight": { "added": "$hue.green.700", "removed": "$hue.red.700" },
|
|
42
|
+
"lineNumber": { "text": "$hue.neutral.400", "background": { "added": "$hue.green.900", "removed": "$hue.red.900" } }
|
|
43
|
+
},
|
|
44
|
+
"syntax": {
|
|
45
|
+
"comment": "$hue.neutral.400", "keyword": "$hue.purple.300", "function": "$hue.yellow.300", "variable": "$hue.neutral.100",
|
|
46
|
+
"string": "$hue.green.300", "number": "$hue.orange.300", "type": "$hue.cyan.300", "operator": "$hue.purple.300", "punctuation": "$hue.neutral.300"
|
|
47
|
+
},
|
|
48
|
+
"markdown": {
|
|
49
|
+
"text": "$hue.neutral.100", "heading": "$hue.yellow.300", "link": "$hue.cyan.300", "linkText": "$hue.cyan.300", "code": "$hue.green.300",
|
|
50
|
+
"blockQuote": "$hue.neutral.300", "emphasis": "$hue.orange.300", "strong": "$hue.yellow.300", "horizontalRule": "$hue.neutral.500",
|
|
51
|
+
"listItem": "$hue.yellow.400", "listEnumeration": "$hue.yellow.400", "image": "$hue.purple.300", "imageText": "$hue.neutral.300", "codeBlock": "$hue.neutral.100"
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"dark": {
|
|
55
|
+
"hue": {
|
|
56
|
+
"gray": { "100": "#f4eef5", "200": "#d8cbdc", "300": "#c0b3ca", "400": "#9a8ca6", "500": "#705e7a", "600": "#49364f", "700": "#35243d", "800": "#271733", "900": "#160e20" },
|
|
57
|
+
"red": { "100": "#f6d4dd", "200": "#e8a9ba", "300": "#d58ca2", "400": "#b8758d", "500": "#9c5a73", "600": "#713e55", "700": "#512a40", "800": "#351a2d", "900": "#21101d" },
|
|
58
|
+
"orange": { "100": "#ffead3", "200": "#f8d8b3", "300": "#eec28b", "400": "#d5af66", "500": "#ad8b50", "600": "#80673e", "700": "#5d4a32", "800": "#3b2e28", "900": "#241c21" },
|
|
59
|
+
"yellow": { "100": "#fff4d8", "200": "#f5dca9", "300": "#e6c480", "400": "#d5af66", "500": "#b18c4e", "600": "#84683d", "700": "#604a30", "800": "#3e3026", "900": "#251d20" },
|
|
60
|
+
"green": { "100": "#d9f2df", "200": "#b1e2bd", "300": "#83c996", "400": "#62ad78", "500": "#46885e", "600": "#35684a", "700": "#284d39", "800": "#1e382b", "900": "#17251e" },
|
|
61
|
+
"cyan": { "100": "#d5f2f3", "200": "#a9e0e2", "300": "#83c8ce", "400": "#62aab5", "500": "#4a8997", "600": "#396874", "700": "#2c4c58", "800": "#203640", "900": "#17242b" },
|
|
62
|
+
"blue": { "100": "#dce9fb", "200": "#b9d0f0", "300": "#91b2df", "400": "#7193c6", "500": "#5876a4", "600": "#445b7e", "700": "#34445d", "800": "#263342", "900": "#1a222d" },
|
|
63
|
+
"purple": { "100": "#f0e2f5", "200": "#ddc0e7", "300": "#c99bd7", "400": "#ad7abe", "500": "#895f99", "600": "#694878", "700": "#4d365b", "800": "#352440", "900": "#211629" },
|
|
64
|
+
"accent": "$hue.yellow", "interactive": "$hue.yellow", "neutral": "$hue.gray"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -1 +1,26 @@
|
|
|
1
|
-
|
|
1
|
+
import { Plugin } from '@opencode/plugin/tui';
|
|
2
|
+
import diagnostics from './src/bootstrap';
|
|
3
|
+
import workspace from './src/tui';
|
|
4
|
+
|
|
5
|
+
export default Plugin.define({
|
|
6
|
+
id: 'carthagent.tui',
|
|
7
|
+
async setup(context) {
|
|
8
|
+
const cleanups: Plugin.Cleanup[] = [];
|
|
9
|
+
const dispose = async () => {
|
|
10
|
+
const results = await Promise.allSettled(cleanups.splice(0).reverse().map((cleanup) => Promise.resolve().then(cleanup)));
|
|
11
|
+
const failures = results.filter((result): result is PromiseRejectedResult => result.status === 'rejected');
|
|
12
|
+
if (failures.length) throw new AggregateError(failures.map((result) => result.reason), 'Carthagent terminal cleanup failed');
|
|
13
|
+
};
|
|
14
|
+
try {
|
|
15
|
+
for (const definition of [diagnostics, workspace]) {
|
|
16
|
+
const cleanup = await definition.setup(context);
|
|
17
|
+
if (cleanup) cleanups.push(cleanup);
|
|
18
|
+
}
|
|
19
|
+
return dispose;
|
|
20
|
+
} catch (error) {
|
|
21
|
+
try { await dispose(); }
|
|
22
|
+
catch (cleanupError) { throw new AggregateError([error, cleanupError], 'Carthagent terminal setup failed'); }
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
});
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Reproducible native build for CTG's separately named OpenCode v2.0.32 runtime.
|
|
5
|
+
# Run from a clean checkout of the locked source revision with Bun 1.4.2.
|
|
6
|
+
ROOT="$(git rev-parse --show-toplevel)"
|
|
7
|
+
EXPECTED_SOURCE="843a6c514e606b436a7fe713422ace0a93ad3d61"
|
|
8
|
+
EXPECTED_LOCK="9d78379cd264584a495440a217b9d2ecab524c9a56f7f8b4b5bed3926daa330d"
|
|
9
|
+
EXPECTED_PATCH="ec4bb182308ed8b311513aad5c9c7d57dcd0db103f0882f3dc3517d535b4c975"
|
|
10
|
+
TARGET="${1:-opencode-linux-x64}"
|
|
11
|
+
HOST_PLATFORM="$(node -p 'process.platform')"
|
|
12
|
+
HOST_ARCH="$(node -p 'process.arch')"
|
|
13
|
+
|
|
14
|
+
cd "$ROOT"
|
|
15
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
16
|
+
HASH_COMMAND=(sha256sum)
|
|
17
|
+
elif command -v shasum >/dev/null 2>&1; then
|
|
18
|
+
HASH_COMMAND=(shasum -a 256)
|
|
19
|
+
else
|
|
20
|
+
echo "Need sha256sum or shasum to verify pinned inputs" >&2
|
|
21
|
+
exit 1
|
|
22
|
+
fi
|
|
23
|
+
if [[ "$(git rev-parse HEAD)" != "$EXPECTED_SOURCE" ]]; then
|
|
24
|
+
echo "Expected pristine OpenCode source $EXPECTED_SOURCE; got $(git rev-parse HEAD)" >&2
|
|
25
|
+
exit 1
|
|
26
|
+
fi
|
|
27
|
+
# Reject staged edits and every tracked worktree edit other than a bun.lock
|
|
28
|
+
# line-ending conversion before restoring anything. On Windows, validate that
|
|
29
|
+
# normalizing CRLF to LF exactly reproduces the committed blob; this keeps
|
|
30
|
+
# checkout conversion distinct from user edits and fails closed on content
|
|
31
|
+
# changes. The blob itself must still match the pinned lock hash.
|
|
32
|
+
if ! git diff --cached --quiet HEAD --; then
|
|
33
|
+
echo "Refusing to build with staged source changes" >&2
|
|
34
|
+
exit 1
|
|
35
|
+
fi
|
|
36
|
+
expected_inputs=(
|
|
37
|
+
"docs/patches/opencode-v2.0.32-session-ui-combined-v5.patch"
|
|
38
|
+
"script/ctg/build-owned-runtime.sh"
|
|
39
|
+
)
|
|
40
|
+
actual_status="$(git status --porcelain --untracked-files=all)"
|
|
41
|
+
unexpected=""
|
|
42
|
+
while IFS= read -r entry; do
|
|
43
|
+
[[ -z "$entry" ]] && continue
|
|
44
|
+
path="${entry:3}"
|
|
45
|
+
allowed=false
|
|
46
|
+
for input in "${expected_inputs[@]}"; do
|
|
47
|
+
[[ "$path" == "$input" ]] && allowed=true
|
|
48
|
+
done
|
|
49
|
+
if [[ "$allowed" != true && "$path" != "bun.lock" ]]; then
|
|
50
|
+
unexpected+="${entry}"$'\n'
|
|
51
|
+
fi
|
|
52
|
+
done <<< "$actual_status"
|
|
53
|
+
if [[ -n "$unexpected" ]]; then
|
|
54
|
+
echo "Expected pristine tracked source and only the two reviewed CTG build inputs as untracked files" >&2
|
|
55
|
+
git status --short >&2
|
|
56
|
+
exit 1
|
|
57
|
+
fi
|
|
58
|
+
if ! git diff --quiet HEAD -- . ':(exclude)bun.lock'; then
|
|
59
|
+
echo "Refusing to build with modified tracked source files" >&2
|
|
60
|
+
git status --short >&2
|
|
61
|
+
exit 1
|
|
62
|
+
fi
|
|
63
|
+
node - "$EXPECTED_LOCK" <<'NODE'
|
|
64
|
+
const { createHash } = require("node:crypto")
|
|
65
|
+
const { execFileSync } = require("node:child_process")
|
|
66
|
+
const { readFileSync } = require("node:fs")
|
|
67
|
+
const expectedHash = process.argv[2]
|
|
68
|
+
const committed = execFileSync("git", ["show", "HEAD:bun.lock"])
|
|
69
|
+
const current = readFileSync("bun.lock")
|
|
70
|
+
const normalized = Buffer.from(current.toString("utf8").replace(/\r\n/g, "\n"))
|
|
71
|
+
const hash = createHash("sha256").update(committed).digest("hex")
|
|
72
|
+
if (hash !== expectedHash || !normalized.equals(committed)) {
|
|
73
|
+
console.error("Refusing to restore bun.lock: tracked content differs from the pinned blob")
|
|
74
|
+
process.exit(1)
|
|
75
|
+
}
|
|
76
|
+
NODE
|
|
77
|
+
# Only after the fail-closed pristine check, restore Windows checkout line
|
|
78
|
+
# endings with conversion disabled so Bun sees the exact committed bytes.
|
|
79
|
+
git -c core.autocrlf=false checkout-index --force -- bun.lock
|
|
80
|
+
if [[ "$("${HASH_COMMAND[@]}" bun.lock | awk '{print $1}')" != "$EXPECTED_LOCK" ]]; then
|
|
81
|
+
echo "Pinned bun.lock checksum mismatch" >&2
|
|
82
|
+
exit 1
|
|
83
|
+
fi
|
|
84
|
+
if [[ "$(bun --version)" != "1.4.2" ]]; then
|
|
85
|
+
echo "Expected Bun 1.4.2; got $(bun --version)" >&2
|
|
86
|
+
exit 1
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
patch_file="$ROOT/docs/patches/opencode-v2.0.32-session-ui-combined-v5.patch"
|
|
90
|
+
actual_patch="$("${HASH_COMMAND[@]}" "$patch_file" | awk '{print $1}')"
|
|
91
|
+
if [[ "$actual_patch" != "$EXPECTED_PATCH" ]]; then
|
|
92
|
+
echo "Patch checksum mismatch: $actual_patch" >&2
|
|
93
|
+
exit 1
|
|
94
|
+
fi
|
|
95
|
+
git apply --check "$patch_file"
|
|
96
|
+
git apply "$patch_file"
|
|
97
|
+
|
|
98
|
+
echo "source=$(git rev-parse HEAD)"
|
|
99
|
+
echo "patch_sha256=$actual_patch"
|
|
100
|
+
echo "license=$(grep -m1 '"license"' packages/cli/package.json | cut -d '"' -f4)"
|
|
101
|
+
|
|
102
|
+
bun install --frozen-lockfile
|
|
103
|
+
bun run --cwd packages/core typecheck
|
|
104
|
+
bun run --cwd packages/server typecheck
|
|
105
|
+
bun run --cwd packages/plugin typecheck
|
|
106
|
+
bun run --cwd packages/client typecheck
|
|
107
|
+
bun run --cwd packages/tui typecheck
|
|
108
|
+
bun test --cwd packages/core test/session-compaction.test.ts
|
|
109
|
+
bun test --cwd packages/core test/shell-retention.test.ts
|
|
110
|
+
bun test --cwd packages/server test/shell-provenance.test.ts
|
|
111
|
+
bun test --cwd packages/tui test/plugin-source.test.ts
|
|
112
|
+
bun test --cwd packages/tui test/clear-session.test.tsx
|
|
113
|
+
bun test --cwd packages/tui --timeout 30000 test/navigation-rail.test.tsx --rerun-each 10
|
|
114
|
+
bun test --cwd packages/tui --timeout 30000 test/component/seascape-render.test.tsx --rerun-each 10
|
|
115
|
+
bun test --cwd packages/tui --timeout 30000 test/app-lifecycle.test.tsx test/session-panel.test.tsx
|
|
116
|
+
bun test --cwd packages/tui test/util/renderer.test.ts
|
|
117
|
+
bun test --cwd packages/tui test/component/plan-view.test.tsx test/component/transcript-scrollbox.test.ts test/component/seascape.test.ts test/component/session-tabs-rail.test.tsx test/component/session-tabs-marquee.test.ts test/component/session-tabs-mouse.test.tsx test/context/session-tabs-model.test.ts test/ui/layout.test.ts
|
|
118
|
+
bun test --cwd packages/tui --timeout 60000 test/glyphs.test.ts test/plugin-keymap.test.tsx test/plugin-theme.test.tsx test/plugin-theme-provider.test.tsx
|
|
119
|
+
|
|
120
|
+
cd packages/cli
|
|
121
|
+
# A CTG build is intentionally detached from upstream updater channels and
|
|
122
|
+
# receives an explicit non-upstream identity; it must never masquerade as 2.0.32.
|
|
123
|
+
if [[ "$TARGET" == opencode-windows-* ]]; then
|
|
124
|
+
BUILD_OS=win32
|
|
125
|
+
MANIFEST_PLATFORM="windows-${TARGET#opencode-windows-}"
|
|
126
|
+
BINARY=opencode.exe
|
|
127
|
+
elif [[ "$TARGET" == opencode-darwin-* ]]; then
|
|
128
|
+
BUILD_OS=darwin
|
|
129
|
+
MANIFEST_PLATFORM="darwin-${TARGET#opencode-darwin-}"
|
|
130
|
+
BINARY=opencode
|
|
131
|
+
else
|
|
132
|
+
BUILD_OS=linux
|
|
133
|
+
MANIFEST_PLATFORM="linux-${TARGET#opencode-linux-}"
|
|
134
|
+
BINARY=opencode
|
|
135
|
+
fi
|
|
136
|
+
BUILD_ARCH="$(sed -E 's/^opencode-(linux|darwin|windows)-([^-]+).*$/\2/' <<< "$TARGET")"
|
|
137
|
+
if [[ "$BUILD_OS" != "$HOST_PLATFORM" || "$BUILD_ARCH" != "$HOST_ARCH" ]]; then
|
|
138
|
+
echo "Cross-compiling target $TARGET is not supported by this recipe; run on matching native CI (host $HOST_PLATFORM-$HOST_ARCH)" >&2
|
|
139
|
+
exit 1
|
|
140
|
+
fi
|
|
141
|
+
OPENCODE_VERSION=0.0.0-ctg.2.0.32 OPENCODE_CHANNEL=ctg.2.0.32 \
|
|
142
|
+
bun run script/build.ts --single --target="$TARGET" --skip-web-ui --outdir="$ROOT/packages/cli/dist-ctg"
|
|
143
|
+
|
|
144
|
+
cd "$ROOT"
|
|
145
|
+
artifact="${TARGET#opencode-}"
|
|
146
|
+
binary="$ROOT/packages/cli/dist-ctg/cli-$artifact/bin/$BINARY"
|
|
147
|
+
test -x "$binary"
|
|
148
|
+
"$binary" --version
|
|
149
|
+
cp "$ROOT/LICENSE" "$ROOT/packages/cli/dist-ctg/LICENSE"
|
|
150
|
+
relative_binary="cli-$artifact/bin/$BINARY"
|
|
151
|
+
cd "$ROOT/packages/cli/dist-ctg"
|
|
152
|
+
"${HASH_COMMAND[@]}" "$relative_binary" | tee SHA256SUMS
|
|
153
|
+
cat > ctg-runtime.json <<EOF
|
|
154
|
+
{
|
|
155
|
+
"owner": "Carthagent",
|
|
156
|
+
"sourceRepository": "https://github.com/anomalyco/opencode",
|
|
157
|
+
"sourceRevision": "$EXPECTED_SOURCE",
|
|
158
|
+
"lockSha256": "$EXPECTED_LOCK",
|
|
159
|
+
"patchSha256": "$EXPECTED_PATCH",
|
|
160
|
+
"compatibilityVersion": "2.0.32",
|
|
161
|
+
"runtimeVersion": "0.0.0-ctg.2.0.32",
|
|
162
|
+
"channel": "ctg.2.0.32",
|
|
163
|
+
"platform": "$MANIFEST_PLATFORM",
|
|
164
|
+
"webUI": false
|
|
165
|
+
}
|
|
166
|
+
EOF
|
|
@@ -6,8 +6,9 @@ import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'nod
|
|
|
6
6
|
import { tmpdir } from 'node:os';
|
|
7
7
|
import { join } from 'node:path';
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import { buildEngineArgs } from '../lib/engine.mjs';
|
|
9
10
|
|
|
10
|
-
for (const mode of ['tools', 'seconds', 'repairs']) test(`
|
|
11
|
+
for (const mode of ['tools', 'seconds', 'repairs']) test(`legacy engine boundary stops ${mode}, restores budget and accepts explicit reset offline`, { timeout: 120000 }, async t => {
|
|
11
12
|
const cwd = mkdtempSync(join(tmpdir(), 'budget café '));
|
|
12
13
|
t.after(() => rmSync(cwd, { recursive: true, force: true }));
|
|
13
14
|
let calls = 0;
|
|
@@ -36,7 +37,7 @@ for (const mode of ['tools', 'seconds', 'repairs']) test(`actual engine stops ${
|
|
|
36
37
|
writeFileSync(join(agent, 'models.json'), JSON.stringify({ providers: { localtest: { baseUrl: `http://127.0.0.1:${server.address().port}/v1`, api: 'openai-completions', apiKey: 'dummy', models: [{ id: 'fixture', contextWindow: 128000, maxTokens: 1024 }] } } }));
|
|
37
38
|
writeFileSync(join(cwd, 'input.txt'), 'café ✓');
|
|
38
39
|
const session = join(cwd, 'session.jsonl');
|
|
39
|
-
const cli = fileURLToPath(new URL('../
|
|
40
|
+
const cli = fileURLToPath(new URL('../vendor/agent/cli.js', import.meta.url));
|
|
40
41
|
let beforeRestore = -1;
|
|
41
42
|
let forcedKill = false;
|
|
42
43
|
async function run(prompt, secondsLimit = mode === 'seconds' ? '5' : '10000', expectBudgetStop = false) {
|
|
@@ -44,7 +45,7 @@ for (const mode of ['tools', 'seconds', 'repairs']) test(`actual engine stops ${
|
|
|
44
45
|
let priorEntryCount = 0;
|
|
45
46
|
try { priorEntryCount = readFileSync(session, 'utf8').split('\n').filter(Boolean).length; } catch { /* Initial session has not been created yet. */ }
|
|
46
47
|
let budgetStoppedAt = null;
|
|
47
|
-
const child = spawn(process.execPath, [cli, '
|
|
48
|
+
const child = spawn(process.execPath, [cli, '--offline', '-p', ...buildEngineArgs({ isolate: true, provider: 'localtest', model: 'fixture', session, maxTools: mode === 'tools' ? '1' : '0', maxSeconds: secondsLimit, maxRepairs: mode === 'repairs' ? '1' : '0' }), prompt], { cwd, env: { ...process.env, PI_OFFLINE: '1', CARTHAGENT_CODING_AGENT_DIR: agent, PI_CODING_AGENT_DIR: agent } });
|
|
48
49
|
child.stdin.end();
|
|
49
50
|
let output = ''; child.stdout.on('data', d => output += d); child.stderr.on('data', d => output += d);
|
|
50
51
|
let timer;
|