carthagent 1.0.7 → 1.1.0-beta.1
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/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-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 +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 +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 +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/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 +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 +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 +280 -0
- package/plugins/carthagent/src/testing.ts +5 -0
- package/plugins/carthagent/src/tui.tsx +496 -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/daily-evidence.test.mjs +62 -0
- package/tests/delivery-review.test.mjs +57 -2
- 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 +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 +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-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 +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 +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 +245 -18
- 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/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,42 @@
|
|
|
1
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
|
|
4
|
+
export type NativeCliConfig = Record<string, unknown> & { theme?: { name?: string } | string };
|
|
5
|
+
|
|
6
|
+
/** Resolve OpenCode's native CLI profile config (not CTG's separate user profile). */
|
|
7
|
+
export function cliConfigPath(env = process.env, home = process.env.HOME ?? "") {
|
|
8
|
+
const configRoot = env.OPENCODE_CONFIG_DIR || join(env.XDG_CONFIG_HOME || join(home, ".config"), "opencode");
|
|
9
|
+
return join(configRoot, "cli.json");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Native TUI themes are discovered from config/opencode/themes for this profile. */
|
|
13
|
+
export function cliConfigDirectory(env = process.env, home = process.env.HOME ?? "") {
|
|
14
|
+
return env.OPENCODE_CONFIG_DIR || join(env.XDG_CONFIG_HOME || join(home, ".config"), "opencode");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function readCliConfig(path: string): Promise<NativeCliConfig> {
|
|
18
|
+
try {
|
|
19
|
+
const value: unknown = JSON.parse(await readFile(path, "utf8"));
|
|
20
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value as NativeCliConfig : {};
|
|
21
|
+
} catch (error) {
|
|
22
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return {};
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Atomically preserve unrelated native CLI settings while selecting a theme. */
|
|
28
|
+
export async function writeNativeTheme(path: string, name: string) {
|
|
29
|
+
const config = await readCliConfig(path);
|
|
30
|
+
config.theme = typeof config.theme === "object" && config.theme !== null
|
|
31
|
+
? { ...config.theme, name }
|
|
32
|
+
: { name };
|
|
33
|
+
await mkdir(dirname(path), { recursive: true });
|
|
34
|
+
const temp = `${path}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
35
|
+
try {
|
|
36
|
+
await writeFile(temp, `${JSON.stringify(config, null, 2)}\n`, { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
37
|
+
await rename(temp, path);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
await import("node:fs/promises").then(({ rm }) => rm(temp, { force: true })).catch(() => undefined);
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
type CatalogModel = { providerID: string; modelID: string; id: string; name: string };
|
|
2
|
+
|
|
3
|
+
export type ModelCommandHost = {
|
|
4
|
+
sync(): Promise<unknown>;
|
|
5
|
+
list(): readonly CatalogModel[];
|
|
6
|
+
select(options: Array<{ title: string; value: CatalogModel; description: string }>): Promise<CatalogModel | undefined>;
|
|
7
|
+
switchModel(sessionID: string, model: { providerID: string; id: string }): Promise<unknown>;
|
|
8
|
+
/** Opens the native configured-model picker (the `/models` command) with an optional search. */
|
|
9
|
+
openNativePicker(query: string): void;
|
|
10
|
+
providerName(providerID: string): string;
|
|
11
|
+
toast(message: string, variant: "success" | "warning"): void;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* `/model [name]`. Without a session the choice belongs to the next session's draft, which only the native
|
|
16
|
+
* picker owns, so it is opened directly; inside a session the selection is committed to that session.
|
|
17
|
+
*/
|
|
18
|
+
export async function runModelCommand(host: ModelCommandHost, input: { sessionID?: string; arg: string }) {
|
|
19
|
+
if (!input.sessionID) {
|
|
20
|
+
host.openNativePicker(input.arg);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
await host.sync();
|
|
24
|
+
const query = input.arg.toLowerCase();
|
|
25
|
+
const options = host.list()
|
|
26
|
+
.filter((model) => !query || `${host.providerName(model.providerID)} ${model.providerID}/${model.modelID} ${model.name}`.toLowerCase().includes(query))
|
|
27
|
+
.map((model) => ({ title: `${host.providerName(model.providerID)}/${model.modelID}`, value: model, description: model.name }));
|
|
28
|
+
const selected = await host.select(options);
|
|
29
|
+
if (!selected) return;
|
|
30
|
+
await host.switchModel(input.sessionID, { providerID: selected.providerID, id: selected.id });
|
|
31
|
+
host.toast(`Selected ${host.providerName(selected.providerID)}/${selected.modelID}.`, "success");
|
|
32
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Context } from "@opencode/plugin/tui/context";
|
|
2
|
+
import type { JSX } from "solid-js";
|
|
3
|
+
|
|
4
|
+
/** Optional host extension: released runtimes retain slash-command navigation. */
|
|
5
|
+
export type NavigationInput = {
|
|
6
|
+
readonly width: number;
|
|
7
|
+
readonly compact: boolean;
|
|
8
|
+
readonly active: boolean;
|
|
9
|
+
readonly focused: boolean;
|
|
10
|
+
readonly sessionID?: string;
|
|
11
|
+
readonly focus: () => void;
|
|
12
|
+
readonly blur: () => void;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type NativeNavigation = {
|
|
16
|
+
register(pane: { name: string; title: string; render(input: NavigationInput): JSX.Element }): () => void;
|
|
17
|
+
select(name: string): boolean;
|
|
18
|
+
current(): string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function nativeNavigation(context: Pick<Context, "ui">) {
|
|
22
|
+
return (context.ui as Context["ui"] & { navigation?: NativeNavigation }).navigation;
|
|
23
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface NativeReviewOffer {
|
|
2
|
+
sessionID: string;
|
|
3
|
+
runKey: string;
|
|
4
|
+
writes: number;
|
|
5
|
+
start: () => Promise<boolean>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface NativeReviewConsumerOptions {
|
|
9
|
+
mode: (sessionID: string) => string;
|
|
10
|
+
diff: (sessionID: string, signal?: AbortSignal) => Promise<unknown>;
|
|
11
|
+
offer: (value: NativeReviewOffer) => Promise<unknown>;
|
|
12
|
+
kickoff: (sessionID: string, signal?: AbortSignal) => Promise<unknown>;
|
|
13
|
+
/** False or a rejection suppresses the offer (e.g. blocked formal delivery). */
|
|
14
|
+
eligible?: (sessionID: string, signal?: AbortSignal) => Promise<boolean>;
|
|
15
|
+
isLoopActive?: (sessionID: string) => boolean;
|
|
16
|
+
setLoopActive?: (sessionID: string, active: boolean) => void;
|
|
17
|
+
signal?: AbortSignal;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function sendNativeReviewKickoff(options: { client: any; sessionID: string; signal?: AbortSignal; prompt?: string }): Promise<void>;
|
|
21
|
+
export function nativeReviewMode(session: any, config: any, explicitMode?: string): string;
|
|
22
|
+
export function createNativeReviewConsumer(options: NativeReviewConsumerOptions): {
|
|
23
|
+
setLoopActive(sessionID: string, active: boolean): void;
|
|
24
|
+
onUserPromptAdmitted(sessionID: string, message?: { type?: string; metadata?: Record<string, unknown> }): void;
|
|
25
|
+
onInboxDelivered(event: any, message?: { type?: string; metadata?: Record<string, unknown> }): void;
|
|
26
|
+
onStarted(event: any): void;
|
|
27
|
+
onSucceeded(event: any): Promise<void>;
|
|
28
|
+
onFailed(event: any): Promise<void>;
|
|
29
|
+
onInterrupted(event: any): Promise<void>;
|
|
30
|
+
enqueueInboxDelivery(event: any, getMessage: (sessionID: string, messageID: string, signal: AbortSignal) => Promise<{ type?: string; metadata?: Record<string, unknown> } | undefined>): Promise<void>;
|
|
31
|
+
dispose(): void;
|
|
32
|
+
};
|
|
33
|
+
export function subscribeNativeReviewEvents(data: any, consumer: ReturnType<typeof createNativeReviewConsumer>, onError?: (error: unknown) => void, getMessage?: (sessionID: string, messageID: string, signal: AbortSignal) => Promise<{ type?: string; metadata?: Record<string, unknown> } | undefined>): Array<() => void>;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { reviewKickoff } from "../../../../lib/review.mjs";
|
|
2
|
+
import { resolveReviewMode } from "../../../../lib/review.mjs";
|
|
3
|
+
import { SESSION_OPTIONS_KEY } from "../../../../lib/opencode/session-options.mjs";
|
|
4
|
+
|
|
5
|
+
/** Send the PR ↔ fresh-review ↔ fixes instruction to the working session. */
|
|
6
|
+
export async function sendNativeReviewKickoff({ client, sessionID, signal, prompt = reviewKickoff() }) {
|
|
7
|
+
await client.session.prompt({ sessionID, text: prompt, metadata: { "ctg.raw": true, "ctg.reviewKickoff": true } }, { signal });
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Explicit launch mode persisted for the session wins over the profile default. */
|
|
11
|
+
export function nativeReviewMode(session, config, explicitMode) {
|
|
12
|
+
return resolveReviewMode(explicitMode ?? session?.metadata?.[SESSION_OPTIONS_KEY]?.review, config);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Correlate host executions and admit one current-run kickoff per session. */
|
|
16
|
+
/** @param {{ mode: (sessionID: string) => string, diff: (sessionID: string, signal?: AbortSignal) => Promise<unknown>, offer: (value: { sessionID: string; runKey: string; writes: number; start: () => Promise<boolean> }) => Promise<unknown>, eligible?: (sessionID: string, signal?: AbortSignal) => Promise<boolean>, kickoff: (sessionID: string, signal?: AbortSignal) => Promise<unknown>, isLoopActive?: (sessionID: string) => boolean, setLoopActive?: (sessionID: string, active: boolean) => void, signal?: AbortSignal }} options */
|
|
17
|
+
export function createNativeReviewConsumer({ mode, diff, offer, kickoff, eligible = async () => true, isLoopActive = () => false, setLoopActive = () => {}, signal }) {
|
|
18
|
+
const runs = new Map();
|
|
19
|
+
const admitted = new Set();
|
|
20
|
+
const pending = new Set();
|
|
21
|
+
const loopSessions = new Set();
|
|
22
|
+
const admissionTails = new Map();
|
|
23
|
+
const loopGenerations = new Map();
|
|
24
|
+
const admissionAbort = new AbortController();
|
|
25
|
+
let sequence = 0;
|
|
26
|
+
let disposed = false;
|
|
27
|
+
|
|
28
|
+
async function finish(sessionID, succeeded) {
|
|
29
|
+
const run = runs.get(sessionID);
|
|
30
|
+
if (!run || disposed) return;
|
|
31
|
+
// Inbox delivery follows execution.started and precedes the terminal event.
|
|
32
|
+
// Wait for the exact delivered message lookup before deciding whether the
|
|
33
|
+
// restored review loop still suppresses this run.
|
|
34
|
+
const admissionTail = admissionTails.get(sessionID);
|
|
35
|
+
try { await admissionTail; } catch {}
|
|
36
|
+
if (admissionTails.get(sessionID) === admissionTail) admissionTails.delete(sessionID);
|
|
37
|
+
if (disposed || runs.get(sessionID) !== run || run.finished) return;
|
|
38
|
+
run.finished = true;
|
|
39
|
+
if (!succeeded || signal?.aborted || loopSessions.has(sessionID) || isLoopActive(sessionID)) return;
|
|
40
|
+
|
|
41
|
+
let baseline;
|
|
42
|
+
try { baseline = await run.baseline; }
|
|
43
|
+
catch { return; }
|
|
44
|
+
if (disposed || signal?.aborted || runs.get(sessionID) !== run) return;
|
|
45
|
+
let changes;
|
|
46
|
+
try { changes = await diff(sessionID, signal); }
|
|
47
|
+
catch { return; }
|
|
48
|
+
if (disposed || signal?.aborted || runs.get(sessionID) !== run) return;
|
|
49
|
+
if (!Array.isArray(changes) || !Array.isArray(baseline)) return;
|
|
50
|
+
const before = new Map(baseline.map((file) => [file.file, `${file.status}\0${file.patch}`]));
|
|
51
|
+
const writes = changes.filter((file) => file && (file.additions > 0 || file.deletions > 0 || file.status === "added" || file.status === "deleted") && before.get(file.file) !== `${file.status}\0${file.patch}`).length;
|
|
52
|
+
const runKey = `${sessionID}@${run.id}`;
|
|
53
|
+
const currentMode = mode(sessionID);
|
|
54
|
+
if (!(currentMode === "ask" || currentMode === "yes") || writes <= 0 || admitted.has(runKey) || (currentMode === "yes" && loopSessions.has(sessionID))) return;
|
|
55
|
+
// A formally blocked or unfinished delivery is not a reviewable run.
|
|
56
|
+
let reviewable;
|
|
57
|
+
try { reviewable = await eligible(sessionID, signal); }
|
|
58
|
+
catch { return; }
|
|
59
|
+
if (!reviewable || disposed || signal?.aborted || runs.get(sessionID) !== run || admitted.has(runKey)) return;
|
|
60
|
+
admitted.add(runKey);
|
|
61
|
+
|
|
62
|
+
const start = async () => {
|
|
63
|
+
if (disposed || signal?.aborted || runs.get(sessionID) !== run || pending.has(sessionID) || loopSessions.has(sessionID) || isLoopActive(sessionID)) return false;
|
|
64
|
+
pending.add(sessionID);
|
|
65
|
+
loopSessions.add(sessionID);
|
|
66
|
+
setLoopActive(sessionID, true);
|
|
67
|
+
try {
|
|
68
|
+
await kickoff(sessionID, signal);
|
|
69
|
+
return true;
|
|
70
|
+
} catch (error) {
|
|
71
|
+
loopSessions.delete(sessionID);
|
|
72
|
+
setLoopActive(sessionID, false);
|
|
73
|
+
throw error;
|
|
74
|
+
} finally {
|
|
75
|
+
pending.delete(sessionID);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
if (currentMode === "yes") {
|
|
80
|
+
await start();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
let offered = false;
|
|
84
|
+
await offer({ sessionID, runKey, writes, start: async () => {
|
|
85
|
+
if (offered) return false;
|
|
86
|
+
offered = true;
|
|
87
|
+
if (runs.get(sessionID) !== run) return false;
|
|
88
|
+
return start();
|
|
89
|
+
} });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
onUserPromptAdmitted(sessionID, message, expectedGeneration) {
|
|
94
|
+
if (!sessionID || message?.type !== "user" || message.metadata?.["ctg.reviewKickoff"] === true) return;
|
|
95
|
+
if (expectedGeneration !== undefined && (loopGenerations.get(sessionID) ?? 0) !== expectedGeneration) return;
|
|
96
|
+
loopSessions.delete(sessionID);
|
|
97
|
+
if (isLoopActive(sessionID)) setLoopActive(sessionID, false);
|
|
98
|
+
},
|
|
99
|
+
setLoopActive(sessionID, active) {
|
|
100
|
+
if (!sessionID) return;
|
|
101
|
+
if (active) {
|
|
102
|
+
loopSessions.add(sessionID);
|
|
103
|
+
loopGenerations.set(sessionID, (loopGenerations.get(sessionID) ?? 0) + 1);
|
|
104
|
+
}
|
|
105
|
+
else loopSessions.delete(sessionID);
|
|
106
|
+
setLoopActive(sessionID, active);
|
|
107
|
+
},
|
|
108
|
+
onStarted(event) {
|
|
109
|
+
const sessionID = event?.data?.sessionID;
|
|
110
|
+
if (!sessionID || disposed) return;
|
|
111
|
+
const id = event.id ?? `${event.created ?? Date.now()}-${++sequence}`;
|
|
112
|
+
// A rejection is handled here, not left as an unhandled promise if the run fails.
|
|
113
|
+
const baseline = Promise.resolve().then(() => diff(sessionID, signal)).catch(() => undefined);
|
|
114
|
+
runs.set(sessionID, { id, baseline, finished: false });
|
|
115
|
+
},
|
|
116
|
+
onSucceeded(event) { return finish(event?.data?.sessionID, true); },
|
|
117
|
+
onFailed(event) { return finish(event?.data?.sessionID, false); },
|
|
118
|
+
onInterrupted(event) { return finish(event?.data?.sessionID, false); },
|
|
119
|
+
onInboxDelivered(event, message) {
|
|
120
|
+
const sessionID = event?.data?.sessionID;
|
|
121
|
+
if (!sessionID || disposed) return;
|
|
122
|
+
this.onUserPromptAdmitted(sessionID, message);
|
|
123
|
+
},
|
|
124
|
+
enqueueInboxDelivery(event, getMessage) {
|
|
125
|
+
const sessionID = event?.data?.sessionID;
|
|
126
|
+
const messageID = event?.data?.inboxID;
|
|
127
|
+
if (!sessionID || !messageID || disposed) return Promise.resolve();
|
|
128
|
+
const expectedGeneration = loopGenerations.get(sessionID) ?? 0;
|
|
129
|
+
const previous = admissionTails.get(sessionID) ?? Promise.resolve();
|
|
130
|
+
const current = previous.catch(() => {}).then(async () => {
|
|
131
|
+
if (disposed) return;
|
|
132
|
+
const timeout = AbortSignal.timeout(5_000);
|
|
133
|
+
const lookupSignal = signal ? AbortSignal.any([signal, admissionAbort.signal, timeout]) : AbortSignal.any([admissionAbort.signal, timeout]);
|
|
134
|
+
lookupSignal.throwIfAborted();
|
|
135
|
+
const message = await Promise.race([
|
|
136
|
+
getMessage(sessionID, messageID, lookupSignal),
|
|
137
|
+
new Promise((resolve) => lookupSignal.addEventListener("abort", () => resolve(undefined), { once: true })),
|
|
138
|
+
]);
|
|
139
|
+
if (!disposed && !lookupSignal.aborted) this.onUserPromptAdmitted(sessionID, message, expectedGeneration);
|
|
140
|
+
});
|
|
141
|
+
admissionTails.set(sessionID, current);
|
|
142
|
+
return current;
|
|
143
|
+
},
|
|
144
|
+
dispose() { disposed = true; admissionAbort.abort(); admissionTails.clear(); runs.clear(); pending.clear(); },
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Wire host admission and execution events; returned disposers belong to plugin setup. */
|
|
149
|
+
export function subscribeNativeReviewEvents(data, consumer, onError = (_error) => {}, getMessage = async () => undefined) {
|
|
150
|
+
return [
|
|
151
|
+
data.on("session.inbox.delivered", (event) => {
|
|
152
|
+
return consumer.enqueueInboxDelivery(event, getMessage).catch(onError);
|
|
153
|
+
}),
|
|
154
|
+
data.on("session.execution.started", (event) => consumer.onStarted(event)),
|
|
155
|
+
data.on("session.execution.succeeded", (event) => consumer.onSucceeded(event).catch(onError)),
|
|
156
|
+
data.on("session.execution.failed", (event) => consumer.onFailed(event).catch(onError)),
|
|
157
|
+
data.on("session.execution.interrupted", (event) => consumer.onInterrupted(event).catch(onError)),
|
|
158
|
+
];
|
|
159
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Resolve session IDs independently of the host's title-oriented search filter. */
|
|
2
|
+
export async function findNativeResumeSession({ query, directory, client, pageSize = 100 }) {
|
|
3
|
+
const normalized = query.trim().toLowerCase();
|
|
4
|
+
if (!normalized) return undefined;
|
|
5
|
+
const isSessionID = /^ses(?:sion)?[_-]/i.test(query.trim());
|
|
6
|
+
|
|
7
|
+
if (isSessionID) {
|
|
8
|
+
try {
|
|
9
|
+
const exact = await client.session.get({ sessionID: query.trim() });
|
|
10
|
+
if (exact && !exact.parentID && exact.location.directory === directory) return exact;
|
|
11
|
+
} catch { /* non-exact IDs continue as prefix search */ }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const loadPages = async (search) => {
|
|
15
|
+
const sessions = [];
|
|
16
|
+
let cursor;
|
|
17
|
+
do {
|
|
18
|
+
const page = await client.session.list({
|
|
19
|
+
limit: pageSize, order: 'desc', directory,
|
|
20
|
+
...(cursor ? { cursor } : {}), ...(search ? { search } : {}),
|
|
21
|
+
});
|
|
22
|
+
sessions.push(...page.data.filter((session) => !session.parentID));
|
|
23
|
+
cursor = page.cursor.next ?? undefined;
|
|
24
|
+
} while (cursor);
|
|
25
|
+
return sessions;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const unfiltered = await loadPages();
|
|
29
|
+
if (isSessionID) {
|
|
30
|
+
const prefix = unfiltered.find((session) => session.id.toLowerCase().startsWith(normalized));
|
|
31
|
+
if (prefix) return prefix;
|
|
32
|
+
}
|
|
33
|
+
return (await loadPages(query)).find((session) => (session.title ?? '').toLowerCase().includes(normalized));
|
|
34
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { MouseEvent, SelectRenderable } from "@opentui/core";
|
|
2
|
+
|
|
3
|
+
// SelectRenderable only handles keys; map a left click to the fully drawn row under the pointer and activate it.
|
|
4
|
+
// Bind it to mouse-up: an action that opens a dialog on mouse-down is closed by that dialog's backdrop on release.
|
|
5
|
+
export function clickSelect(node: SelectRenderable | undefined, event: MouseEvent) {
|
|
6
|
+
if (!node || event.button !== 0) return;
|
|
7
|
+
const { scrollOffset, linesPerItem, maxVisibleItems } = node as unknown as {
|
|
8
|
+
scrollOffset: number;
|
|
9
|
+
linesPerItem: number;
|
|
10
|
+
maxVisibleItems: number;
|
|
11
|
+
};
|
|
12
|
+
const row = Math.floor((event.y - node.screenY) / Math.max(1, linesPerItem));
|
|
13
|
+
const index = scrollOffset + row;
|
|
14
|
+
if (row < 0 || row >= maxVisibleItems || index >= node.options.length) return;
|
|
15
|
+
node.setSelectedIndex(index);
|
|
16
|
+
node.selectCurrent();
|
|
17
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { JsonValue } from "@opencode/client";
|
|
2
|
+
export type AutoCompactionAction = "on" | "off" | "toggle";
|
|
3
|
+
export interface SessionMetadataClient {
|
|
4
|
+
get(input: { sessionID: string }, options: { location: unknown; signal: AbortSignal }): Promise<{ metadata?: Record<string, JsonValue> }>;
|
|
5
|
+
update(input: { sessionID: string; metadata: Record<string, JsonValue> }, options: { location: unknown; signal: AbortSignal }): Promise<unknown>;
|
|
6
|
+
}
|
|
7
|
+
export function setSessionAutoCompaction(client: SessionMetadataClient, sessionID: string, location: unknown, action: AutoCompactionAction): Promise<boolean>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Persist the host-owned per-session opt-out without disturbing neighbor metadata. */
|
|
2
|
+
export async function setSessionAutoCompaction(client, sessionID, location, action) {
|
|
3
|
+
if (action !== "toggle" && action !== "on" && action !== "off") throw new TypeError("Usage: /autocompact [on|off].");
|
|
4
|
+
const options = { location, signal: AbortSignal.timeout(15_000) };
|
|
5
|
+
const session = await client.get({ sessionID }, options);
|
|
6
|
+
const metadata = { ...session.metadata };
|
|
7
|
+
const enabled = metadata["ctg.autoCompaction"] !== false;
|
|
8
|
+
const next = action === "on" ? true : action === "off" ? false : !enabled;
|
|
9
|
+
if (next === enabled) return enabled;
|
|
10
|
+
if (next) delete metadata["ctg.autoCompaction"];
|
|
11
|
+
else metadata["ctg.autoCompaction"] = false;
|
|
12
|
+
await client.update({ sessionID, metadata }, { location, signal: AbortSignal.timeout(15_000) });
|
|
13
|
+
return next;
|
|
14
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare function convertLegacyTheme(name: string, palette: Record<string, unknown>, lightPalette?: Record<string, unknown>): Record<string, unknown>;
|
|
2
|
+
export declare function provisionNativeThemes(profileConfigDir: string, names?: readonly string[]): Promise<{ directory: string; installed: string[] }>;
|
|
3
|
+
export declare function provisionDefaultNativeTheme(profileConfigDir: string): Promise<{ directory: string; installed: string[] }>;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { copyFile, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { flattenTheme, THEMES } from "../../../../lib/tui/theme.mjs";
|
|
5
|
+
|
|
6
|
+
const PACKAGE_ROOT = fileURLToPath(new URL("../..", import.meta.url));
|
|
7
|
+
const COLORS = ["red", "orange", "yellow", "green", "cyan", "blue", "purple", "gray"];
|
|
8
|
+
const STEPS = [100, 200, 300, 400, 500, 600, 700, 800, 900];
|
|
9
|
+
|
|
10
|
+
function mixHex(color, target, amount) {
|
|
11
|
+
const expand = (value) => {
|
|
12
|
+
const hex = value.slice(1);
|
|
13
|
+
const full = hex.length === 3 ? [...hex].map((digit) => digit + digit).join("") : hex;
|
|
14
|
+
return full.match(/[\da-f]{2}/gi)?.map((part) => Number.parseInt(part, 16)) ?? [128, 128, 128];
|
|
15
|
+
};
|
|
16
|
+
const source = expand(color);
|
|
17
|
+
const destination = expand(target);
|
|
18
|
+
return `#${source.map((channel, index) => Math.round(channel * (1 - amount) + destination[index] * amount).toString(16).padStart(2, "0")).join("")}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function buildPalette(palette) {
|
|
22
|
+
const fallback = "#808080";
|
|
23
|
+
const semantic = { red: palette.error, orange: palette.warning, yellow: palette.warning, green: palette.success, cyan: palette.info, blue: palette.info, purple: palette.primary ?? palette.secondary, gray: palette.textMuted };
|
|
24
|
+
const darkMode = Number.parseInt(palette.background?.slice(1, 3) ?? "80", 16) < 128;
|
|
25
|
+
const hue = Object.fromEntries(COLORS.map((color) => {
|
|
26
|
+
const source = typeof semantic[color] === "string" && /^#[\da-f]{6}$/i.test(semantic[color]) ? semantic[color] : fallback;
|
|
27
|
+
const scale = Object.fromEntries(STEPS.map((step) => {
|
|
28
|
+
const progress = (step - 100) / 800;
|
|
29
|
+
const towardLight = darkMode ? progress < 0.5 : progress >= 0.5;
|
|
30
|
+
const distance = Math.abs(progress - 0.5) / 0.5;
|
|
31
|
+
const amount = 0.78 * distance;
|
|
32
|
+
return [step, mixHex(source, towardLight ? "#ffffff" : "#000000", amount)];
|
|
33
|
+
}));
|
|
34
|
+
return [color, scale];
|
|
35
|
+
}));
|
|
36
|
+
hue.accent = "$hue.purple";
|
|
37
|
+
hue.interactive = "$hue.purple";
|
|
38
|
+
hue.neutral = "$hue.gray";
|
|
39
|
+
const token = (value) => {
|
|
40
|
+
if (typeof value !== "string" || !/^#[\da-f]{3,8}$/i.test(value)) return value;
|
|
41
|
+
return value;
|
|
42
|
+
};
|
|
43
|
+
const state = (base) => ({ base: token(base), $hovered: token(base), $pressed: token(base), $focused: token(base), $selected: token(base), $disabled: token(base) });
|
|
44
|
+
const feedback = (base) => ({ base: token(base), muted: token(base) });
|
|
45
|
+
return {
|
|
46
|
+
hue,
|
|
47
|
+
text: {
|
|
48
|
+
base: token(palette.text), muted: token(palette.textMuted), faint: token(palette.textMuted),
|
|
49
|
+
action: { primary: state(palette.primary), secondary: state(palette.secondary), destructive: state(palette.error) },
|
|
50
|
+
formfield: state(palette.text),
|
|
51
|
+
feedback: { info: feedback(palette.info), success: feedback(palette.success), warning: feedback(palette.warning), error: feedback(palette.error) },
|
|
52
|
+
},
|
|
53
|
+
background: {
|
|
54
|
+
base: token(palette.background),
|
|
55
|
+
raised: { base: token(palette.backgroundPanel), high: token(palette.backgroundElement), max: token(palette.border) },
|
|
56
|
+
action: { primary: state(palette.accent), secondary: state(palette.backgroundElement), destructive: state(palette.error) },
|
|
57
|
+
formfield: state(palette.background),
|
|
58
|
+
feedback: { info: { base: token(palette.info) }, success: { base: token(palette.success) }, warning: { base: token(palette.warning) }, error: { base: token(palette.error) } },
|
|
59
|
+
},
|
|
60
|
+
border: { base: token(palette.border) }, scrollbar: { base: token(palette.borderActive ?? palette.accent) },
|
|
61
|
+
diff: {
|
|
62
|
+
text: { added: token(palette.diffAdded), removed: token(palette.diffRemoved), context: token(palette.diffContext), hunkHeader: token(palette.diffHunkHeader) },
|
|
63
|
+
background: { added: token(palette.diffAddedBg), removed: token(palette.diffRemovedBg), context: token(palette.diffContextBg) },
|
|
64
|
+
highlight: { added: token(palette.diffHighlightAdded), removed: token(palette.diffHighlightRemoved) },
|
|
65
|
+
lineNumber: { text: token(palette.diffLineNumber), background: { added: token(palette.diffAddedLineNumberBg), removed: token(palette.diffRemovedLineNumberBg) } },
|
|
66
|
+
},
|
|
67
|
+
syntax: { comment: token(palette.syntaxComment), keyword: token(palette.syntaxKeyword), function: token(palette.syntaxFunction), variable: token(palette.syntaxVariable), string: token(palette.syntaxString), number: token(palette.syntaxNumber), type: token(palette.syntaxType), operator: token(palette.syntaxOperator), punctuation: token(palette.syntaxPunctuation) },
|
|
68
|
+
markdown: { text: token(palette.markdownText), heading: token(palette.markdownHeading), link: token(palette.markdownLink), linkText: token(palette.markdownLinkText), code: token(palette.markdownCode), blockQuote: token(palette.markdownBlockQuote), emphasis: token(palette.markdownEmph), strong: token(palette.markdownStrong), horizontalRule: token(palette.markdownHorizontalRule), listItem: token(palette.markdownListItem), listEnumeration: token(palette.markdownListEnumeration), image: token(palette.markdownImage), imageText: token(palette.markdownImageText), codeBlock: token(palette.markdownCodeBlock) },
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Convert a legacy flat semantic palette into OpenCode's native theme JSON. */
|
|
73
|
+
export function convertLegacyTheme(name, palette, lightPalette) {
|
|
74
|
+
const dark = buildPalette(palette);
|
|
75
|
+
const light = lightPalette ? buildPalette(lightPalette) : undefined;
|
|
76
|
+
const { hue: darkHue, ...base } = dark;
|
|
77
|
+
const lightHue = light?.hue ?? (name === "paper" || name.includes("light") ? darkHue : undefined);
|
|
78
|
+
return {
|
|
79
|
+
$schema: "https://opencode.ai/theme.json",
|
|
80
|
+
base: { ...base, categorical: ["accent", "purple", "cyan", "green", "blue"] },
|
|
81
|
+
dark: { hue: darkHue },
|
|
82
|
+
...(lightHue ? { light: { hue: lightHue } } : {}),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Copy selected Carthagent compatibility themes into one OpenCode profile. */
|
|
87
|
+
export async function provisionNativeThemes(profileConfigDir, names = Object.keys(THEMES)) {
|
|
88
|
+
const destination = join(profileConfigDir, "themes");
|
|
89
|
+
await mkdir(destination, { recursive: true });
|
|
90
|
+
const installed = [];
|
|
91
|
+
for (const name of names) {
|
|
92
|
+
const source = THEMES[name];
|
|
93
|
+
if (!source) throw new Error(`Unknown Carthagent theme: ${name}`);
|
|
94
|
+
const path = join(destination, `${name}.json`);
|
|
95
|
+
let exists = false;
|
|
96
|
+
try {
|
|
97
|
+
const existing = JSON.parse(await readFile(path, "utf8"));
|
|
98
|
+
exists = true;
|
|
99
|
+
if (existing.$schema === "https://opencode.ai/theme.json" && existing.dark) continue;
|
|
100
|
+
} catch (error) {
|
|
101
|
+
if (error.code !== "ENOENT") throw error;
|
|
102
|
+
}
|
|
103
|
+
if (exists) throw new Error(`Refusing to replace a non-native theme at ${path}`);
|
|
104
|
+
const document = name === "carthage"
|
|
105
|
+
? JSON.parse(await readFile(join(PACKAGE_ROOT, "themes", "carthage.json"), "utf8"))
|
|
106
|
+
: convertLegacyTheme(name, flattenTheme(source, "dark"), ["solarized", "okabe", "contrast", "system"].includes(name) ? flattenTheme(source, "light") : undefined);
|
|
107
|
+
const temp = `${path}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
108
|
+
try {
|
|
109
|
+
await writeFile(temp, `${JSON.stringify(document, null, 2)}\n`, { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
110
|
+
await copyFile(temp, path, 1);
|
|
111
|
+
await rm(temp, { force: true });
|
|
112
|
+
} catch (error) {
|
|
113
|
+
await import("node:fs/promises").then(({ rm }) => rm(temp, { force: true })).catch(() => undefined);
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
installed.push(name);
|
|
117
|
+
}
|
|
118
|
+
return { directory: destination, installed };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Launcher hook: install Carthagent's default theme before OpenCode scans themes. */
|
|
122
|
+
export async function provisionDefaultNativeTheme(profileConfigDir) {
|
|
123
|
+
const compatibilityNames = Object.keys(THEMES).filter((name) => !["opencode", "tokyonight"].includes(name));
|
|
124
|
+
const installed = [];
|
|
125
|
+
const conflicts = [];
|
|
126
|
+
for (const name of compatibilityNames) {
|
|
127
|
+
try { installed.push(...(await provisionNativeThemes(profileConfigDir, [name])).installed); }
|
|
128
|
+
catch (error) {
|
|
129
|
+
if (!error.message.startsWith("Refusing to replace a non-native theme at ")) throw error;
|
|
130
|
+
conflicts.push(name);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return { directory: join(profileConfigDir, "themes"), installed, conflicts };
|
|
134
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { voyagesErrorCode } from './voyages-state.mjs';
|
|
2
|
+
|
|
3
|
+
/** Read-only summaries only. Never retain recovery payloads, catalog or consent. */
|
|
4
|
+
export function createVoyagesNavigationState({ profile, daily, changed = (_state) => {}, timeoutSignal = () => AbortSignal.timeout(15_000) }) {
|
|
5
|
+
let generation = 0, active = false, disposed = false;
|
|
6
|
+
let request = new AbortController();
|
|
7
|
+
let state = { busy: false, error: '', profile: undefined, daily: undefined };
|
|
8
|
+
const emit = () => { if (!disposed) changed({ ...state }); };
|
|
9
|
+
const invalidate = () => {
|
|
10
|
+
generation++;
|
|
11
|
+
request.abort();
|
|
12
|
+
request = new AbortController();
|
|
13
|
+
state = { busy: false, error: '', profile: undefined, daily: undefined };
|
|
14
|
+
emit();
|
|
15
|
+
};
|
|
16
|
+
const refresh = async () => {
|
|
17
|
+
if (disposed || !active || state.busy) return;
|
|
18
|
+
const current = generation;
|
|
19
|
+
const signal = AbortSignal.any([request.signal, timeoutSignal()]);
|
|
20
|
+
const live = () => !disposed && active && generation === current && !signal.aborted;
|
|
21
|
+
state = { busy: true, error: '', profile: undefined, daily: undefined };
|
|
22
|
+
emit();
|
|
23
|
+
try {
|
|
24
|
+
const [account, assignments] = await Promise.all([profile(signal), daily(signal)]);
|
|
25
|
+
if (!live()) return;
|
|
26
|
+
state.profile = {
|
|
27
|
+
name: account.profile.name, shipName: account.profile.shipName,
|
|
28
|
+
coins: account.profile.coins, level: account.profile.level, totalXp: account.profile.totalXp,
|
|
29
|
+
};
|
|
30
|
+
state.daily = {
|
|
31
|
+
day: assignments.slate.day, endsAt: assignments.slate.endsAt,
|
|
32
|
+
items: assignments.slate.items.map((quest) => ({ id: quest.id, title: quest.definition.title, status: quest.status })),
|
|
33
|
+
};
|
|
34
|
+
} catch (error) {
|
|
35
|
+
if (!disposed && active && generation === current) {
|
|
36
|
+
const code = error?.type === 'dailyQuestError' ? error.data?.code : voyagesErrorCode(error);
|
|
37
|
+
state.error = typeof code === 'string' && /^[a-z_]{1,64}$/.test(code) ? code : 'voyages_request_failed';
|
|
38
|
+
}
|
|
39
|
+
} finally {
|
|
40
|
+
if (!disposed && active && generation === current) { state.busy = false; emit(); }
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
return {
|
|
44
|
+
refresh,
|
|
45
|
+
invalidate,
|
|
46
|
+
activate(next) {
|
|
47
|
+
if (disposed || active === next) return;
|
|
48
|
+
active = next;
|
|
49
|
+
invalidate();
|
|
50
|
+
if (active) return refresh();
|
|
51
|
+
},
|
|
52
|
+
contextChanged() { invalidate(); if (active) return refresh(); },
|
|
53
|
+
disconnect() { invalidate(); },
|
|
54
|
+
dispose() { active = false; invalidate(); disposed = true; request.abort(); },
|
|
55
|
+
};
|
|
56
|
+
}
|