carthagent 1.0.7 → 1.1.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +140 -83
- package/bin/ctg.mjs +14 -732
- package/docs/beta-rewards-e2e-plan.md +48 -0
- package/docs/beta-rewards-e2e-results.md +84 -0
- package/docs/cloud-onboarding-acceptance.md +65 -0
- 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/launch-readiness-live-results.md +65 -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 +175 -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-beta.1.md +60 -0
- package/docs/release-notes-beta.2.md +60 -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/cloud/client.mjs +13 -7
- package/lib/codegraph.mjs +1 -1
- package/lib/delivery-review.mjs +19 -6
- package/lib/delivery.mjs +3 -2
- package/lib/environment.mjs +8 -0
- package/lib/guidance.mjs +1 -1
- package/lib/opencode/cli-runtime.mjs +1188 -0
- package/lib/opencode/cli.mjs +253 -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 +22 -5
- 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/providers/names.mjs +1 -1
- package/lib/tui/framing.mjs +1 -80
- package/lib/tui/models.mjs +1 -1
- 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 +243 -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 +65 -36
- 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 +71 -0
- package/plugins/carthagent/src/server/voyages-rpc.ts +280 -0
- package/plugins/carthagent/src/testing.ts +5 -0
- package/plugins/carthagent/src/tui.tsx +506 -0
- package/plugins/carthagent/src/ui/client.ts +127 -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 +165 -0
- package/plugins/carthagent/src/ui/voyages.tsx +131 -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 +64 -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/cloud-client.test.mjs +2 -2
- package/tests/daily-evidence.test.mjs +62 -0
- package/tests/delivery-review.test.mjs +57 -2
- package/tests/delivery.test.mjs +22 -1
- package/tests/experiential-extension.test.mjs +2 -2
- package/tests/experiential.test.mjs +1 -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 +55 -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 +525 -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 +33 -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-guidance.test.mjs +16 -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 +83 -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 +397 -0
- package/tests/native-voyages-ui.test.mjs +307 -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 +248 -20
- package/tests/opencode-runtime.test.mjs +597 -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 +231 -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/tui.test.mjs +2 -2
- 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 +45 -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 +882 -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 +91 -65
- 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,107 @@
|
|
|
1
|
+
import { strict as assert } from "node:assert";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import { COMMANDS, matchCommand, NATIVE_COMMAND_COLLISIONS, resolveCommand } from "../src/ui/commands.js";
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
5
|
+
import { parseThemeDocument } from "@opencode/theme/tui";
|
|
6
|
+
import { cliConfigDirectory, cliConfigPath, readCliConfig, writeNativeTheme } from "../src/ui/host-state.js";
|
|
7
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { provisionNativeThemes } from "../src/ui/theme-provision.mjs";
|
|
11
|
+
|
|
12
|
+
test("Carthagent command manifest resolves visible CTG commands", () => {
|
|
13
|
+
assert.equal(resolveCommand("/help")?.id, "help");
|
|
14
|
+
assert.equal(resolveCommand("/ctg-help")?.id, "help");
|
|
15
|
+
assert.deepEqual(COMMANDS.find((item) => item.id === "help")?.aliases, ["ctg-help"]);
|
|
16
|
+
assert.equal(COMMANDS.find((item) => item.id === "help")?.submitSlash, true);
|
|
17
|
+
assert.equal(resolveCommand("/review")?.id, "review");
|
|
18
|
+
assert.equal(resolveCommand("/review-mode")?.id, "review");
|
|
19
|
+
for (const name of ["workflow", "architecture", "evidence"]) assert.equal(resolveCommand(name)?.hidden, undefined);
|
|
20
|
+
for (const name of ["model", "compact", "autocompact", "abort", "clear"]) assert.equal(resolveCommand(name)?.id, name);
|
|
21
|
+
assert.deepEqual(matchCommand("/arch").map((item) => item.name), ["architecture", "archive"]);
|
|
22
|
+
assert.deepEqual(matchCommand("/rev status"), []);
|
|
23
|
+
for (const name of ["login", "account", "usage", "billing", "logout-cloud", "archive", "demo", "voyages", "quests"]) {
|
|
24
|
+
assert.ok(resolveCommand(`/${name}`), `/${name} must resolve to a registered handler`);
|
|
25
|
+
}
|
|
26
|
+
assert.equal(resolveCommand("/cloud")?.id, "account");
|
|
27
|
+
assert.equal(resolveCommand("/auth")?.id, "login");
|
|
28
|
+
assert.match(resolveCommand("/voyages")!.description, /daily quests/);
|
|
29
|
+
assert.match(resolveCommand("/quests")!.description, /separate explicit consent/);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("manifest prevents collisions with native OpenCode slash commands", () => {
|
|
33
|
+
for (const name of NATIVE_COMMAND_COLLISIONS) assert.equal(COMMANDS.some((item) => item.name === name || item.aliases?.includes(name)), false, name);
|
|
34
|
+
assert.equal(NATIVE_COMMAND_COLLISIONS.includes("help" as typeof NATIVE_COMMAND_COLLISIONS[number]), false);
|
|
35
|
+
assert.deepEqual(COMMANDS.filter((item) => ["model", "compact", "abort", "resume"].includes(item.name)).map((item) => item.name).sort(), ["abort", "compact", "model", "resume"]);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("theme provisioning installs the startup default through its launcher-facing export", async () => {
|
|
39
|
+
const { provisionDefaultNativeTheme } = await import("../src/ui/theme-provision.mjs");
|
|
40
|
+
const directory = await mkdtemp(join(tmpdir(), "ctg-default-theme-"));
|
|
41
|
+
try {
|
|
42
|
+
const result = await provisionDefaultNativeTheme(directory);
|
|
43
|
+
assert.ok(result.installed.includes("carthage"));
|
|
44
|
+
const theme = parseThemeDocument(JSON.parse(await readFile(join(result.directory, "carthage.json"), "utf8")), "carthage");
|
|
45
|
+
assert.equal(theme.dark.hue.yellow[400], "#d5af66");
|
|
46
|
+
} finally { await rm(directory, { recursive: true, force: true }); }
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("startup theme provisioning covers compatibility themes and preserves host/user themes", async () => {
|
|
50
|
+
const { provisionDefaultNativeTheme } = await import("../src/ui/theme-provision.mjs");
|
|
51
|
+
const directory = await mkdtemp(join(tmpdir(), "ctg-startup-themes-"));
|
|
52
|
+
try {
|
|
53
|
+
await mkdir(join(directory, "themes"), { recursive: true });
|
|
54
|
+
await writeFile(join(directory, "themes", "forest.json"), '{"userOwned":true}');
|
|
55
|
+
const result = await provisionDefaultNativeTheme(directory);
|
|
56
|
+
assert.ok(result.installed.includes("carthage"));
|
|
57
|
+
assert.ok(result.installed.includes("forest") === false);
|
|
58
|
+
assert.ok(result.conflicts.includes("forest"));
|
|
59
|
+
assert.ok(result.installed.includes("opencode") === false);
|
|
60
|
+
assert.ok(result.installed.includes("tokyonight") === false);
|
|
61
|
+
assert.deepEqual(JSON.parse(await readFile(join(directory, "themes", "forest.json"), "utf8")), { userOwned: true });
|
|
62
|
+
} finally { await rm(directory, { recursive: true, force: true }); }
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("Carthage theme validates against the OpenCode V2 theme schema and resolver", async () => {
|
|
66
|
+
const raw = JSON.parse(await readFile(new URL("../themes/carthage.json", import.meta.url), "utf8"));
|
|
67
|
+
const resolved = parseThemeDocument(raw, "carthage");
|
|
68
|
+
assert.ok(resolved.dark?.hue.yellow);
|
|
69
|
+
assert.equal(resolved.base.categorical?.length, 5);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("native CLI theme write preserves unrelated cli.json settings", async () => {
|
|
73
|
+
const directory = await mkdtemp(join(tmpdir(), "ctg-cli-config-"));
|
|
74
|
+
const path = join(directory, "cli.json");
|
|
75
|
+
try {
|
|
76
|
+
await writeFile(path, JSON.stringify({ theme: { name: "opencode", mode: "dark" }, session: { thinking: "show" }, custom: true }));
|
|
77
|
+
await writeNativeTheme(path, "carthage");
|
|
78
|
+
const config = await readCliConfig(path);
|
|
79
|
+
assert.deepEqual(config.theme, { name: "carthage", mode: "dark" });
|
|
80
|
+
assert.deepEqual(config.session, { thinking: "show" });
|
|
81
|
+
assert.equal(config.custom, true);
|
|
82
|
+
assert.equal(cliConfigPath({ XDG_CONFIG_HOME: "/tmp/profile" }, "/home/test"), join("/tmp/profile", "opencode", "cli.json"));
|
|
83
|
+
assert.equal(cliConfigDirectory({ OPENCODE_CONFIG_DIR: "/tmp/opencode-profile" }), "/tmp/opencode-profile");
|
|
84
|
+
assert.equal(cliConfigDirectory({ XDG_CONFIG_HOME: "/tmp/profile" }, "/home/test"), join("/tmp/profile", "opencode"));
|
|
85
|
+
} finally { await rm(directory, { recursive: true, force: true }); }
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("native theme provisioning converts legacy palettes inside the selected OpenCode profile", async () => {
|
|
89
|
+
const directory = await mkdtemp(join(tmpdir(), "ctg-native-themes-"));
|
|
90
|
+
const themeDirectory = join(directory, "themes");
|
|
91
|
+
try {
|
|
92
|
+
await mkdir(themeDirectory, { recursive: true });
|
|
93
|
+
await writeFile(join(themeDirectory, "user-theme.json"), '{"custom":true}');
|
|
94
|
+
const result = await provisionNativeThemes(directory, ["carthage", "nebula"]);
|
|
95
|
+
assert.deepEqual(result.installed.sort(), ["carthage", "nebula"]);
|
|
96
|
+
assert.deepEqual(JSON.parse(await readFile(join(themeDirectory, "user-theme.json"), "utf8")), { custom: true });
|
|
97
|
+
const carthage = parseThemeDocument(JSON.parse(await readFile(join(themeDirectory, "carthage.json"), "utf8")), "carthage");
|
|
98
|
+
const nebula = parseThemeDocument(JSON.parse(await readFile(join(themeDirectory, "nebula.json"), "utf8")), "nebula");
|
|
99
|
+
assert.equal(carthage.dark.hue.yellow[400], "#d5af66");
|
|
100
|
+
assert.equal(carthage.dark.hue.yellow[300], "#e6c480");
|
|
101
|
+
assert.ok(nebula.dark.hue.blue[400]);
|
|
102
|
+
const again = await provisionNativeThemes(directory, ["carthage"]);
|
|
103
|
+
assert.deepEqual(again.installed, []);
|
|
104
|
+
await writeFile(join(themeDirectory, "forest.json"), '{"userOwned":true}');
|
|
105
|
+
await assert.rejects(provisionNativeThemes(directory, ["forest"]), /Refusing to replace/);
|
|
106
|
+
} finally { await rm(directory, { recursive: true, force: true }); }
|
|
107
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import { setSessionAutoCompaction } from "../src/ui/session-preferences.mjs";
|
|
4
|
+
|
|
5
|
+
test("autocompact persists explicit, idempotent session preferences without replacing neighbor metadata", async () => {
|
|
6
|
+
let current = { "ctg.autoCompaction": false, "ctg.raw": true, "user.flag": "keep" };
|
|
7
|
+
const updates = [];
|
|
8
|
+
const client = {
|
|
9
|
+
async get(input, options) {
|
|
10
|
+
assert.equal(input.sessionID, "s1");
|
|
11
|
+
assert.equal(options.location.directory, "/project");
|
|
12
|
+
assert.ok(options.signal instanceof AbortSignal);
|
|
13
|
+
return { metadata: current };
|
|
14
|
+
},
|
|
15
|
+
async update(input, options) {
|
|
16
|
+
assert.equal(input.sessionID, "s1");
|
|
17
|
+
assert.equal(options.location.directory, "/project");
|
|
18
|
+
current = input.metadata;
|
|
19
|
+
updates.push(input.metadata);
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
const location = { directory: "/project" };
|
|
23
|
+
assert.equal(await setSessionAutoCompaction(client, "s1", location, "off"), false);
|
|
24
|
+
assert.equal(updates.length, 0, "repeating off must not write");
|
|
25
|
+
assert.equal(await setSessionAutoCompaction(client, "s1", location, "on"), true);
|
|
26
|
+
assert.deepEqual(current, { "ctg.raw": true, "user.flag": "keep" });
|
|
27
|
+
assert.equal(await setSessionAutoCompaction(client, "s1", location, "toggle"), false);
|
|
28
|
+
assert.deepEqual(current, { "ctg.autoCompaction": false, "ctg.raw": true, "user.flag": "keep" });
|
|
29
|
+
assert.equal(await setSessionAutoCompaction(client, "s1", location, "toggle"), true);
|
|
30
|
+
assert.equal(updates.length, 3);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("invalid autocompact arguments fail before reading or mutating session state", async () => {
|
|
34
|
+
let calls = 0;
|
|
35
|
+
const client = {
|
|
36
|
+
async get() { calls++; throw new Error("must not read"); },
|
|
37
|
+
async update() { calls++; throw new Error("must not update"); },
|
|
38
|
+
};
|
|
39
|
+
await assert.rejects(setSessionAutoCompaction(client, "s1", { directory: "/project" }, "sometimes"), /Usage: \/autocompact \[on\|off\]/);
|
|
40
|
+
assert.equal(calls, 0);
|
|
41
|
+
});
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/solid */
|
|
2
|
+
import { expect, test } from "bun:test";
|
|
3
|
+
import { testRender } from "@opentui/solid";
|
|
4
|
+
import { ScrollBoxRenderable, SelectRenderable, type Renderable } from "@opentui/core";
|
|
5
|
+
import { createSignal } from "solid-js";
|
|
6
|
+
import { PluginContextProvider } from "@opencode/plugin/tui";
|
|
7
|
+
import type { Context } from "@opencode/plugin/tui/context";
|
|
8
|
+
import { parseThemeDocument, resolveThemeDocument } from "@opencode/theme/tui";
|
|
9
|
+
import theme from "../themes/carthage.json";
|
|
10
|
+
import { Voyages } from "../src/voyages-rpc.js";
|
|
11
|
+
import { VoyagesNavigation } from "../src/ui/voyages-navigation.js";
|
|
12
|
+
|
|
13
|
+
const profile = { profile: { name: "Test captain", shipName: "Test vessel", coins: 10, level: 2, totalXp: 150 } };
|
|
14
|
+
const slate = { slate: { day: "2026-10-30", endsAt: "2026-10-31T00:00:00Z", items: [
|
|
15
|
+
{ id: "quest-one", definition: { title: "Protect a boundary" }, status: "open" },
|
|
16
|
+
{ id: "quest-two", definition: { title: "Review a change" }, status: "assessed_complete" },
|
|
17
|
+
] }, records: [{ request: { evidence: "must not appear in navigation", consent: true } }] };
|
|
18
|
+
|
|
19
|
+
async function fixture({ width = 30, height = 36, authenticated = true } = {}) {
|
|
20
|
+
const [active, setActive] = createSignal(true);
|
|
21
|
+
const [focused, setFocused] = createSignal(false);
|
|
22
|
+
const [compact, setCompact] = createSignal(false);
|
|
23
|
+
const [mode, setMode] = createSignal("base");
|
|
24
|
+
const [sessionID, setSessionID] = createSignal<string | undefined>("coding-session");
|
|
25
|
+
const routes: unknown[] = [], calls: string[] = [], selected: string[] = [];
|
|
26
|
+
const responses: Promise<unknown>[] = [];
|
|
27
|
+
let responseGate: Promise<void> | undefined;
|
|
28
|
+
let listener: ((event: { details: { type: string } }) => void) | undefined;
|
|
29
|
+
let listening = true;
|
|
30
|
+
const context = {
|
|
31
|
+
theme: resolveThemeDocument(parseThemeDocument(theme), "dark"),
|
|
32
|
+
location: { directory: "/test-project" },
|
|
33
|
+
data: {
|
|
34
|
+
session: { get: () => ({ location: { directory: "/test-project" } }) },
|
|
35
|
+
location: { default: () => ({ directory: "/test-project" }) },
|
|
36
|
+
listen: (next: typeof listener) => { listener = next; return () => { listening = false; }; },
|
|
37
|
+
},
|
|
38
|
+
client: { rpc: (definition: unknown) => ({ snapshot: () => {
|
|
39
|
+
calls.push(definition === Voyages ? "profile" : "daily");
|
|
40
|
+
const response = authenticated ? (responseGate ?? Promise.resolve()).then(() => definition === Voyages ? profile : slate) : Promise.reject({ type: "voyagesError", data: { code: "authentication_required" } });
|
|
41
|
+
responses.push(response);
|
|
42
|
+
return response;
|
|
43
|
+
} }) },
|
|
44
|
+
keymap: { mode: { current: mode }, dispatch: (id: string) => { calls.push(id); } },
|
|
45
|
+
ui: {
|
|
46
|
+
router: { navigate: (route: unknown) => routes.push(route) },
|
|
47
|
+
navigation: { current: () => "voyages", select: (name: string) => { selected.push(name); return true; } },
|
|
48
|
+
},
|
|
49
|
+
} as unknown as Context;
|
|
50
|
+
const app = await testRender(() => <PluginContextProvider value={context}>
|
|
51
|
+
<box width={width} height={height} visible={active()}>
|
|
52
|
+
<VoyagesNavigation width={width} active={active()} compact={compact()} focused={focused()} sessionID={sessionID()} focus={() => { if (mode() !== "base") return; setFocused(true); setMode("navigation"); }} blur={() => { setFocused(false); if (mode() === "navigation") setMode("base"); }} />
|
|
53
|
+
</box>
|
|
54
|
+
</PluginContextProvider>, { width, height, kittyKeyboard: true });
|
|
55
|
+
const settle = async () => { await Promise.allSettled(responses); await app.renderOnce(); };
|
|
56
|
+
await settle();
|
|
57
|
+
return {
|
|
58
|
+
app, calls, routes, selected, mode, setMode, setActive, setFocused, setCompact, setSessionID, settle,
|
|
59
|
+
focus: async () => { setFocused(true); setMode("navigation"); await app.renderOnce(); },
|
|
60
|
+
event: (type: string) => listener?.({ details: { type } }),
|
|
61
|
+
delay: (promise: Promise<void> | undefined) => { responseGate = promise; },
|
|
62
|
+
listening: () => listening,
|
|
63
|
+
list: () => app.renderer.currentFocusedRenderable as SelectRenderable,
|
|
64
|
+
scroll: () => {
|
|
65
|
+
const find = (node: Renderable): ScrollBoxRenderable | undefined => node instanceof ScrollBoxRenderable ? node : node.getChildren().map(find).find(Boolean);
|
|
66
|
+
return find(app.renderer.root)!;
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
test.each([30, 18])("real Voyages navigator renders account and assignment summaries at width %i", async (width) => {
|
|
72
|
+
const f = await fixture({ width });
|
|
73
|
+
try {
|
|
74
|
+
const frame = f.app.captureCharFrame();
|
|
75
|
+
expect(frame).toContain("Test captain");
|
|
76
|
+
expect(frame).toContain("10 coins");
|
|
77
|
+
expect(frame).toContain("Daily coding");
|
|
78
|
+
expect(frame).not.toContain("must not appear");
|
|
79
|
+
expect(f.calls).toEqual(["profile", "daily"]);
|
|
80
|
+
expect(f.routes).toHaveLength(0);
|
|
81
|
+
} finally { f.app.renderer.destroy(); }
|
|
82
|
+
expect(f.listening()).toBe(false);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("rail focus owns arrows/Enter; palette or modal mode cannot trigger a route", async () => {
|
|
86
|
+
const f = await fixture();
|
|
87
|
+
try {
|
|
88
|
+
expect(f.app.renderer.currentFocusedRenderable).not.toBeInstanceOf(SelectRenderable);
|
|
89
|
+
await f.focus();
|
|
90
|
+
expect(f.list()).toBeInstanceOf(SelectRenderable);
|
|
91
|
+
f.app.mockInput.pressArrow("down");
|
|
92
|
+
await f.app.renderOnce();
|
|
93
|
+
expect(f.list().getSelectedOption()?.value).toBe("quest:quest-one");
|
|
94
|
+
for (const mode of ["modal", "palette", "base"]) {
|
|
95
|
+
f.setMode(mode);
|
|
96
|
+
await f.app.renderOnce();
|
|
97
|
+
f.app.mockInput.pressEnter();
|
|
98
|
+
await f.app.renderOnce();
|
|
99
|
+
expect(f.routes).toHaveLength(0);
|
|
100
|
+
}
|
|
101
|
+
f.setMode("navigation");
|
|
102
|
+
await f.app.renderOnce();
|
|
103
|
+
f.app.mockInput.pressEnter();
|
|
104
|
+
await f.app.renderOnce();
|
|
105
|
+
expect(f.routes).toEqual([{ type: "plugin", name: "daily-quests", data: { sessionID: "coding-session", questID: "quest-one" } }]);
|
|
106
|
+
expect(f.mode()).toBe("base");
|
|
107
|
+
expect(f.calls).toEqual(["profile", "daily"]);
|
|
108
|
+
} finally { f.app.renderer.destroy(); }
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("Projects/Voyages toggles retain selection and do not route, submit or purchase", async () => {
|
|
112
|
+
const f = await fixture();
|
|
113
|
+
try {
|
|
114
|
+
await f.focus();
|
|
115
|
+
f.app.mockInput.pressArrow("down");
|
|
116
|
+
await f.app.renderOnce();
|
|
117
|
+
expect(f.list().getSelectedOption()?.value).toBe("quest:quest-one");
|
|
118
|
+
f.setActive(false);
|
|
119
|
+
await f.settle();
|
|
120
|
+
expect(f.app.captureCharFrame()).not.toContain("Test captain");
|
|
121
|
+
f.setActive(true);
|
|
122
|
+
await f.settle();
|
|
123
|
+
expect(f.list().getSelectedOption()?.value).toBe("quest:quest-one");
|
|
124
|
+
expect(f.routes).toHaveLength(0);
|
|
125
|
+
expect(f.calls).toEqual(["profile", "daily", "profile", "daily"]);
|
|
126
|
+
} finally { f.app.renderer.destroy(); }
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("compact mode uses the same mounted selection and keyboard behavior", async () => {
|
|
130
|
+
const f = await fixture();
|
|
131
|
+
try {
|
|
132
|
+
await f.focus();
|
|
133
|
+
f.app.mockInput.pressArrow("down");
|
|
134
|
+
await f.app.renderOnce();
|
|
135
|
+
const list = f.list();
|
|
136
|
+
f.setCompact(true);
|
|
137
|
+
f.app.resize(5, 36);
|
|
138
|
+
await f.app.renderOnce();
|
|
139
|
+
expect(f.list()).toBe(list);
|
|
140
|
+
expect(f.list().getSelectedOption()?.value).toBe("quest:quest-one");
|
|
141
|
+
expect(f.app.captureCharFrame()).not.toContain("Test captain");
|
|
142
|
+
f.app.mockInput.pressEnter();
|
|
143
|
+
await f.app.renderOnce();
|
|
144
|
+
expect(f.routes).toEqual([{ type: "plugin", name: "daily-quests", data: { sessionID: "coding-session", questID: "quest-one" } }]);
|
|
145
|
+
} finally { f.app.renderer.destroy(); }
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("switching modes preserves the Voyages scroll position at a short terminal height", async () => {
|
|
149
|
+
const f = await fixture({ height: 12 });
|
|
150
|
+
try {
|
|
151
|
+
const scroll = f.scroll();
|
|
152
|
+
scroll.scrollBy(5);
|
|
153
|
+
await f.app.renderOnce();
|
|
154
|
+
const before = scroll.scrollTop;
|
|
155
|
+
expect(before).toBeGreaterThan(0);
|
|
156
|
+
f.setActive(false);
|
|
157
|
+
await f.settle();
|
|
158
|
+
f.setActive(true);
|
|
159
|
+
await f.settle();
|
|
160
|
+
expect(f.scroll()).toBe(scroll);
|
|
161
|
+
expect(scroll.scrollTop).toBe(before);
|
|
162
|
+
expect(f.routes).toHaveLength(0);
|
|
163
|
+
} finally { f.app.renderer.destroy(); }
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("slow summary reads preserve scrolled position after an intervening loading layout", async () => {
|
|
167
|
+
const f = await fixture({ height: 12 });
|
|
168
|
+
try {
|
|
169
|
+
const scroll = f.scroll();
|
|
170
|
+
scroll.scrollTo(100);
|
|
171
|
+
await f.app.renderOnce();
|
|
172
|
+
const before = scroll.scrollTop;
|
|
173
|
+
expect(before).toBeGreaterThan(0);
|
|
174
|
+
f.setActive(false);
|
|
175
|
+
await f.app.renderOnce();
|
|
176
|
+
const gate = Promise.withResolvers<void>();
|
|
177
|
+
f.delay(gate.promise);
|
|
178
|
+
f.setActive(true);
|
|
179
|
+
await f.app.renderOnce();
|
|
180
|
+
expect(f.app.captureCharFrame()).not.toContain("Test captain");
|
|
181
|
+
expect(f.calls).toEqual(["profile", "daily", "profile", "daily"]);
|
|
182
|
+
gate.resolve();
|
|
183
|
+
await f.settle();
|
|
184
|
+
await f.app.renderOnce();
|
|
185
|
+
expect(scroll.scrollTop).toBe(before);
|
|
186
|
+
} finally { f.app.renderer.destroy(); }
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("short terminal keyboard navigation keeps the highlighted action on screen", async () => {
|
|
190
|
+
const f = await fixture({ height: 12 });
|
|
191
|
+
try {
|
|
192
|
+
await f.focus();
|
|
193
|
+
for (let index = 0; index < 7; index++) {
|
|
194
|
+
f.app.mockInput.pressArrow("down");
|
|
195
|
+
await f.app.renderOnce();
|
|
196
|
+
}
|
|
197
|
+
expect(f.list().getSelectedOption()?.value).toBe("back");
|
|
198
|
+
expect(f.app.captureCharFrame()).toContain("Return to coding");
|
|
199
|
+
expect(f.list().height).toBeLessThanOrEqual(f.scroll().viewport.height);
|
|
200
|
+
expect(f.routes).toHaveLength(0);
|
|
201
|
+
} finally { f.app.renderer.destroy(); }
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("mouse press focuses the rail and release opens the clicked row without acting through a modal", async () => {
|
|
205
|
+
const f = await fixture();
|
|
206
|
+
try {
|
|
207
|
+
const row = f.app.captureCharFrame().split("\n").findIndex((line) => line.includes("Daily coding quests"));
|
|
208
|
+
expect(row).toBeGreaterThanOrEqual(0);
|
|
209
|
+
for (const mode of ["modal", "palette"]) {
|
|
210
|
+
f.setMode(mode);
|
|
211
|
+
await f.app.renderOnce();
|
|
212
|
+
await f.app.mockMouse.click(5, row);
|
|
213
|
+
await f.app.renderOnce();
|
|
214
|
+
expect(f.mode()).toBe(mode);
|
|
215
|
+
expect(f.routes).toHaveLength(0);
|
|
216
|
+
}
|
|
217
|
+
f.setMode("base");
|
|
218
|
+
f.app.renderer.clearSelection();
|
|
219
|
+
await f.app.renderOnce();
|
|
220
|
+
await f.app.mockMouse.pressDown(5, row);
|
|
221
|
+
await f.app.renderOnce();
|
|
222
|
+
expect(f.mode()).toBe("navigation");
|
|
223
|
+
expect(f.app.renderer.currentFocusedRenderable).toBeInstanceOf(SelectRenderable);
|
|
224
|
+
expect(f.routes).toHaveLength(0);
|
|
225
|
+
await f.app.mockMouse.release(5, row);
|
|
226
|
+
await f.app.renderOnce();
|
|
227
|
+
expect(f.routes).toEqual([{ type: "plugin", name: "daily-quests", data: { sessionID: "coding-session" } }]);
|
|
228
|
+
expect(f.mode()).toBe("base");
|
|
229
|
+
const questRow = f.app.captureCharFrame().split("\n").findIndex((line) => line.includes("Review a change"));
|
|
230
|
+
expect(questRow).toBeGreaterThanOrEqual(0);
|
|
231
|
+
await f.app.mockMouse.click(5, questRow);
|
|
232
|
+
await f.app.renderOnce();
|
|
233
|
+
expect(f.routes).toEqual([
|
|
234
|
+
{ type: "plugin", name: "daily-quests", data: { sessionID: "coding-session" } },
|
|
235
|
+
{ type: "plugin", name: "daily-quests", data: { sessionID: "coding-session", questID: "quest-two" } },
|
|
236
|
+
]);
|
|
237
|
+
} finally { f.app.renderer.destroy(); }
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("signed-out account and invalidation hide protected summaries, reset stale selection, and stay read-only", async () => {
|
|
241
|
+
const f = await fixture({ authenticated: false });
|
|
242
|
+
try {
|
|
243
|
+
expect(f.app.captureCharFrame()).toContain("authentication_required");
|
|
244
|
+
expect(f.app.captureCharFrame()).not.toContain("Test captain");
|
|
245
|
+
await f.focus();
|
|
246
|
+
f.app.mockInput.pressArrow("down");
|
|
247
|
+
await f.app.renderOnce();
|
|
248
|
+
f.event("credential.updated");
|
|
249
|
+
await f.settle();
|
|
250
|
+
expect(f.list().getSelectedOption()?.value).toBe("daily");
|
|
251
|
+
expect(f.routes).toHaveLength(0);
|
|
252
|
+
expect(f.calls).toEqual(["profile", "daily", "profile", "daily"]);
|
|
253
|
+
} finally { f.app.renderer.destroy(); }
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test.each(["coding-session", undefined])("return to coding explicitly navigates to the selected session or Home (%s)", async (sessionID) => {
|
|
257
|
+
const f = await fixture();
|
|
258
|
+
try {
|
|
259
|
+
f.setSessionID(sessionID);
|
|
260
|
+
await f.focus();
|
|
261
|
+
const list = f.list();
|
|
262
|
+
const index = list.options.findIndex((option) => option.value === "back");
|
|
263
|
+
list.setSelectedIndex(index);
|
|
264
|
+
f.app.mockInput.pressEnter();
|
|
265
|
+
await f.app.renderOnce();
|
|
266
|
+
expect(f.selected).toEqual(["projects"]);
|
|
267
|
+
expect(f.routes).toEqual([sessionID ? { type: "session", sessionID } : { type: "home" }]);
|
|
268
|
+
} finally { f.app.renderer.destroy(); }
|
|
269
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://opencode.ai/theme.json",
|
|
3
|
+
"base": {
|
|
4
|
+
"categorical": ["accent", "purple", "cyan", "green", "blue"],
|
|
5
|
+
"text": {
|
|
6
|
+
"base": "$hue.neutral.100",
|
|
7
|
+
"muted": "$hue.neutral.300",
|
|
8
|
+
"faint": "$hue.neutral.400",
|
|
9
|
+
"action": {
|
|
10
|
+
"primary": { "base": "$hue.purple.400", "$hovered": "$hue.purple.300", "$pressed": "$hue.purple.500" },
|
|
11
|
+
"secondary": { "base": "$hue.neutral.200", "$hovered": "$hue.yellow.400" },
|
|
12
|
+
"destructive": { "base": "$hue.red.300", "$hovered": "$hue.red.200" }
|
|
13
|
+
},
|
|
14
|
+
"formfield": { "base": "$hue.neutral.100", "$focused": "$hue.yellow.400" },
|
|
15
|
+
"feedback": {
|
|
16
|
+
"info": { "base": "$hue.cyan.300", "muted": "$hue.cyan.500" },
|
|
17
|
+
"success": { "base": "$hue.green.300", "muted": "$hue.green.500" },
|
|
18
|
+
"warning": { "base": "$hue.yellow.300", "muted": "$hue.yellow.500" },
|
|
19
|
+
"error": { "base": "$hue.red.300", "muted": "$hue.red.500" }
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"background": {
|
|
23
|
+
"base": "$hue.neutral.900",
|
|
24
|
+
"raised": { "base": "$hue.neutral.800", "high": "$hue.neutral.700", "max": "$hue.neutral.600" },
|
|
25
|
+
"action": {
|
|
26
|
+
"primary": { "base": "$hue.yellow.400", "$hovered": "$hue.yellow.300", "$pressed": "$hue.yellow.500" },
|
|
27
|
+
"secondary": { "base": "$hue.neutral.800", "$hovered": "$hue.neutral.700" },
|
|
28
|
+
"destructive": { "base": "$hue.red.700", "$hovered": "$hue.red.600" }
|
|
29
|
+
},
|
|
30
|
+
"formfield": { "base": "$hue.neutral.900", "$focused": "$hue.neutral.800" },
|
|
31
|
+
"feedback": {
|
|
32
|
+
"info": { "base": "$hue.blue.800" }, "success": { "base": "$hue.green.800" },
|
|
33
|
+
"warning": { "base": "$hue.orange.800" }, "error": { "base": "$hue.red.800" }
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"border": { "base": "$hue.neutral.600" },
|
|
37
|
+
"scrollbar": { "base": "$hue.neutral.600" },
|
|
38
|
+
"diff": {
|
|
39
|
+
"text": { "added": "$hue.green.300", "removed": "$hue.red.300", "context": "$hue.neutral.300", "hunkHeader": "$hue.cyan.300" },
|
|
40
|
+
"background": { "added": "$hue.green.800", "removed": "$hue.red.800", "context": "$hue.neutral.800" },
|
|
41
|
+
"highlight": { "added": "$hue.green.700", "removed": "$hue.red.700" },
|
|
42
|
+
"lineNumber": { "text": "$hue.neutral.400", "background": { "added": "$hue.green.900", "removed": "$hue.red.900" } }
|
|
43
|
+
},
|
|
44
|
+
"syntax": {
|
|
45
|
+
"comment": "$hue.neutral.400", "keyword": "$hue.purple.300", "function": "$hue.yellow.300", "variable": "$hue.neutral.100",
|
|
46
|
+
"string": "$hue.green.300", "number": "$hue.orange.300", "type": "$hue.cyan.300", "operator": "$hue.purple.300", "punctuation": "$hue.neutral.300"
|
|
47
|
+
},
|
|
48
|
+
"markdown": {
|
|
49
|
+
"text": "$hue.neutral.100", "heading": "$hue.yellow.300", "link": "$hue.cyan.300", "linkText": "$hue.cyan.300", "code": "$hue.green.300",
|
|
50
|
+
"blockQuote": "$hue.neutral.300", "emphasis": "$hue.orange.300", "strong": "$hue.yellow.300", "horizontalRule": "$hue.neutral.500",
|
|
51
|
+
"listItem": "$hue.yellow.400", "listEnumeration": "$hue.yellow.400", "image": "$hue.purple.300", "imageText": "$hue.neutral.300", "codeBlock": "$hue.neutral.100"
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"dark": {
|
|
55
|
+
"hue": {
|
|
56
|
+
"gray": { "100": "#f4eef5", "200": "#d8cbdc", "300": "#c0b3ca", "400": "#9a8ca6", "500": "#705e7a", "600": "#49364f", "700": "#35243d", "800": "#271733", "900": "#160e20" },
|
|
57
|
+
"red": { "100": "#f6d4dd", "200": "#e8a9ba", "300": "#d58ca2", "400": "#b8758d", "500": "#9c5a73", "600": "#713e55", "700": "#512a40", "800": "#351a2d", "900": "#21101d" },
|
|
58
|
+
"orange": { "100": "#ffead3", "200": "#f8d8b3", "300": "#eec28b", "400": "#d5af66", "500": "#ad8b50", "600": "#80673e", "700": "#5d4a32", "800": "#3b2e28", "900": "#241c21" },
|
|
59
|
+
"yellow": { "100": "#fff4d8", "200": "#f5dca9", "300": "#e6c480", "400": "#d5af66", "500": "#b18c4e", "600": "#84683d", "700": "#604a30", "800": "#3e3026", "900": "#251d20" },
|
|
60
|
+
"green": { "100": "#d9f2df", "200": "#b1e2bd", "300": "#83c996", "400": "#62ad78", "500": "#46885e", "600": "#35684a", "700": "#284d39", "800": "#1e382b", "900": "#17251e" },
|
|
61
|
+
"cyan": { "100": "#d5f2f3", "200": "#a9e0e2", "300": "#83c8ce", "400": "#62aab5", "500": "#4a8997", "600": "#396874", "700": "#2c4c58", "800": "#203640", "900": "#17242b" },
|
|
62
|
+
"blue": { "100": "#dce9fb", "200": "#b9d0f0", "300": "#91b2df", "400": "#7193c6", "500": "#5876a4", "600": "#445b7e", "700": "#34445d", "800": "#263342", "900": "#1a222d" },
|
|
63
|
+
"purple": { "100": "#f0e2f5", "200": "#ddc0e7", "300": "#c99bd7", "400": "#ad7abe", "500": "#895f99", "600": "#694878", "700": "#4d365b", "800": "#352440", "900": "#211629" },
|
|
64
|
+
"accent": "$hue.yellow", "interactive": "$hue.yellow", "neutral": "$hue.gray"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -1 +1,26 @@
|
|
|
1
|
-
|
|
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
|
+
});
|