carthagent 1.0.7 → 1.1.0-preview.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +140 -83
- package/bin/ctg.mjs +14 -732
- package/docs/glyph-review/host-chrome-glyph-compatibility-v4.md +83 -0
- package/docs/glyph-review/test-isolation-v1.md +26 -0
- package/docs/glyph-review/typecheck-base.log +1 -0
- package/docs/glyph-review/typecheck-v4.log +1 -0
- package/docs/glyph-review/unicode-inventory-v4.csv +402 -0
- package/docs/help-dispatch-review.md +69 -0
- package/docs/legacy-cli.md +150 -0
- package/docs/live-theme-plugin-api.md +18 -0
- package/docs/native-cli.md +102 -0
- package/docs/native-compatibility.md +16 -0
- package/docs/native-daily-quests.md +132 -0
- package/docs/native-glyph-pty.md +59 -0
- package/docs/native-manual-testing.md +336 -0
- package/docs/native-slash-acceptance-handoff.md +49 -0
- package/docs/native-terminal-paired-failure-2026-10-17.md +40 -0
- package/docs/native-terminal-performance.md +310 -0
- package/docs/native-terminal-semantics-evidence/README.md +25 -0
- package/docs/native-terminal-semantics-evidence/controlled-host-model-selftest.json +16 -0
- package/docs/native-terminal-semantics-evidence/current-acceptance-status.json +59 -0
- package/docs/native-terminal-semantics-evidence/final-binary-focused-85bb44a.json +154 -0
- package/docs/native-terminal-semantics-evidence/final-binary-full-incomplete-85bb44a.json +160 -0
- package/docs/native-terminal-semantics-evidence/final-binary-layout-85bb44a.json +149 -0
- package/docs/native-terminal-semantics-evidence/final-binary-layout-interactive-85bb44a.json +159 -0
- package/docs/native-terminal-semantics-evidence/final-binary-model-picker-escape-unchanged-85bb44a.json +44 -0
- package/docs/native-terminal-semantics-evidence/final-binary-model-picker-exact-identity-85bb44a.json +47 -0
- package/docs/native-terminal-semantics-evidence/final-binary-model-picker-unchanged-model-incomplete-85bb44a.json +58 -0
- package/docs/native-terminal-semantics-evidence/final-binary-model-selftest.json +16 -0
- package/docs/native-terminal-semantics-evidence/final-binary-picker-cancel-incomplete-85bb44a.json +62 -0
- package/docs/native-terminal-semantics-evidence/focused-clipboard-title-stale-binary.json +161 -0
- package/docs/native-terminal-semantics-evidence/layout-stale-binary-observation-not-accepted.json +153 -0
- package/docs/native-terminal-semantics-evidence/picker-latest-incomplete-attempt.json +62 -0
- package/docs/native-terminal-semantics-evidence/picker-previous-probe-pass.json +61 -0
- package/docs/native-terminal-semantics.md +89 -0
- package/docs/native-upgrade.md +177 -0
- package/docs/opencode-composed-source-review.md +89 -0
- package/docs/opencode-migration.md +225 -9
- package/docs/opencode-owned-runtime.md +241 -0
- package/docs/opencode-runtime.md +12 -0
- package/docs/opencode-theme-provider-proof/README.md +55 -0
- package/docs/opencode-theme-provider-proof/live-theme-api-base.patch +122 -0
- package/docs/opencode-theme-provider-proof/plugin-theme-provider.patch +107 -0
- package/docs/opencode-theme-provider-proof/session-ui-base.patch +1217 -0
- package/docs/owned-host-controls-proof.md +39 -0
- package/docs/owned-runtime-release-manifest.md +42 -0
- package/docs/owned-visual-clear-proof.md +40 -0
- package/docs/patches/opencode-v2.0.32-glyph-spinner-test-isolation-v1.patch +50 -0
- package/docs/patches/opencode-v2.0.32-glyphs.patch +3670 -0
- package/docs/patches/opencode-v2.0.32-help-dispatch.patch +156 -0
- package/docs/patches/opencode-v2.0.32-live-theme-api.patch +122 -0
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v1.patch +5272 -0
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v2.patch +6052 -0
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v3.patch +8657 -0
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v4.patch +9928 -0
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v5.patch +13965 -0
- package/docs/patches/opencode-v2.0.32-session-ui.patch +5137 -0
- package/docs/release-notes-native-draft.md +73 -0
- package/docs/release-notes-preview.10.md +121 -0
- package/docs/release-notes-preview.11.md +76 -0
- package/docs/release-notes-preview.12.md +55 -0
- package/docs/release-notes-preview.13.md +68 -0
- package/docs/release-notes-preview.9.md +130 -0
- package/docs/source-preview.md +120 -0
- package/evaluate.mjs +28 -1
- package/lib/budget.mjs +1 -1
- package/lib/codegraph.mjs +1 -1
- package/lib/delivery-review.mjs +11 -1
- package/lib/delivery.mjs +3 -2
- package/lib/environment.mjs +8 -0
- package/lib/guidance.mjs +1 -1
- package/lib/opencode/cli-runtime.mjs +1171 -0
- package/lib/opencode/cli.mjs +252 -0
- package/lib/opencode/credentials.mjs +63 -8
- package/lib/opencode/demo.mjs +53 -0
- package/lib/opencode/evaluation-budget-plugin/index.ts +3 -0
- package/lib/opencode/evaluation.mjs +32 -12
- package/lib/opencode/headless.mjs +8 -4
- package/lib/opencode/provider-policy.mjs +23 -0
- package/lib/opencode/review.mjs +37 -16
- package/lib/opencode/runtime-artifact.mjs +335 -0
- package/lib/opencode/runtime-github-transport.mjs +121 -0
- package/lib/opencode/runtime-release.json +36 -0
- package/lib/opencode/runtime.mjs +299 -20
- package/lib/opencode/service.mjs +77 -35
- package/lib/opencode/session-options.mjs +6 -2
- package/lib/opencode/update.mjs +222 -0
- package/lib/opencode/workspace.mjs +1 -1
- package/lib/planning-advice.mjs +2 -2
- package/lib/providers/experiential.mjs +2 -2
- package/lib/tui/framing.mjs +1 -80
- package/lib/update.mjs +10 -1
- package/package.json +33 -7
- package/plugins/carthagent/README.md +23 -0
- package/plugins/carthagent/src/bootstrap.ts +1 -1
- package/plugins/carthagent/src/daily-evidence.mjs +57 -0
- package/plugins/carthagent/src/daily-quests-rpc.ts +50 -0
- package/plugins/carthagent/src/daily-source-rpc.ts +52 -0
- package/plugins/carthagent/src/index.ts +71 -24
- package/plugins/carthagent/src/rpc.ts +51 -6
- package/plugins/carthagent/src/server/codegraph.ts +12 -5
- package/plugins/carthagent/src/server/daily-quests-rpc.ts +277 -0
- package/plugins/carthagent/src/server/daily-source-rpc.ts +211 -0
- package/plugins/carthagent/src/server/delivery.ts +227 -112
- package/plugins/carthagent/src/server/framing.mjs +80 -0
- package/plugins/carthagent/src/server/policy.ts +1 -1
- package/plugins/carthagent/src/server/providers.ts +59 -31
- package/plugins/carthagent/src/server/session-controls.ts +3 -1
- package/plugins/carthagent/src/server/ship-agents.ts +77 -0
- package/plugins/carthagent/src/server/ship-context.mjs +60 -0
- package/plugins/carthagent/src/server/ship-guidance.ts +41 -0
- package/plugins/carthagent/src/server/ship-rpc.ts +69 -0
- package/plugins/carthagent/src/server/voyages-rpc.ts +223 -0
- package/plugins/carthagent/src/testing.ts +5 -0
- package/plugins/carthagent/src/tui.tsx +496 -0
- package/plugins/carthagent/src/ui/client.ts +125 -0
- package/plugins/carthagent/src/ui/commands.ts +70 -0
- package/plugins/carthagent/src/ui/daily-editor-focus.mjs +52 -0
- package/plugins/carthagent/src/ui/daily-editor.mjs +4 -0
- package/plugins/carthagent/src/ui/daily-quests-events.mjs +18 -0
- package/plugins/carthagent/src/ui/daily-quests-state.mjs +218 -0
- package/plugins/carthagent/src/ui/daily-quests.tsx +235 -0
- package/plugins/carthagent/src/ui/host-state.ts +42 -0
- package/plugins/carthagent/src/ui/model-command.ts +32 -0
- package/plugins/carthagent/src/ui/native-navigation.ts +23 -0
- package/plugins/carthagent/src/ui/native-review.d.mts +33 -0
- package/plugins/carthagent/src/ui/native-review.mjs +159 -0
- package/plugins/carthagent/src/ui/resume-session.mjs +34 -0
- package/plugins/carthagent/src/ui/select-mouse.ts +17 -0
- package/plugins/carthagent/src/ui/session-preferences.d.mts +7 -0
- package/plugins/carthagent/src/ui/session-preferences.mjs +14 -0
- package/plugins/carthagent/src/ui/theme-provision.d.mts +3 -0
- package/plugins/carthagent/src/ui/theme-provision.mjs +134 -0
- package/plugins/carthagent/src/ui/voyages-navigation-state.mjs +56 -0
- package/plugins/carthagent/src/ui/voyages-navigation.tsx +131 -0
- package/plugins/carthagent/src/ui/voyages-state.mjs +142 -0
- package/plugins/carthagent/src/ui/voyages.tsx +129 -0
- package/plugins/carthagent/src/ui/workspace-live.ts +61 -0
- package/plugins/carthagent/src/ui/workspace.tsx +379 -0
- package/plugins/carthagent/src/voyages-rpc.ts +60 -0
- package/plugins/carthagent/src/workspace-rpc.ts +36 -0
- package/plugins/carthagent/tests/commands.test.ts +107 -0
- package/plugins/carthagent/tests/session-preferences.test.mjs +41 -0
- package/plugins/carthagent/tests/voyages-navigation.test.tsx +269 -0
- package/plugins/carthagent/themes/carthage.json +67 -0
- package/plugins/carthagent/tui.ts +26 -1
- package/script/ctg/build-owned-runtime.sh +166 -0
- package/tests/budget-engine.test.mjs +4 -3
- package/tests/cli.test.mjs +52 -96
- package/tests/daily-evidence.test.mjs +62 -0
- package/tests/delivery-review.test.mjs +11 -0
- package/tests/delivery.test.mjs +22 -1
- package/tests/fixtures/native-discovery-plugin/server.ts +15 -0
- package/tests/generate-owned-runtime-release.test.mjs +354 -0
- package/tests/installed-runtime-contract.test.mjs +43 -0
- package/tests/native-cli-host-smoke.mjs +457 -0
- package/tests/native-daily-editor-focus.test.mjs +213 -0
- package/tests/native-daily-editor.test.mjs +43 -0
- package/tests/native-daily-quests-layout.test.mjs +53 -0
- package/tests/native-daily-quests-rpc.test.mjs +456 -0
- package/tests/native-daily-quests-ui.test.mjs +778 -0
- package/tests/native-daily-source.test.mjs +451 -0
- package/tests/native-demo-ui.test.mjs +48 -0
- package/tests/native-evaluation-admission.test.mjs +24 -0
- package/tests/native-fixture-browser-cleanup.test.mjs +65 -0
- package/tests/native-fixture-runtime.test.mjs +111 -0
- package/tests/native-fixture-stop-diagnostics.test.mjs +67 -0
- package/tests/native-glyph-pty.test.mjs +107 -0
- package/tests/native-manual-projects.test.mjs +170 -0
- package/tests/native-model-command.test.mjs +67 -0
- package/tests/native-owned-runtime-env.test.mjs +140 -0
- package/tests/native-paired-guards.test.mjs +464 -0
- package/tests/native-provider-policy.test.mjs +31 -0
- package/tests/native-pty-invocation.test.mjs +114 -0
- package/tests/native-pty-runtime.test.mjs +45 -0
- package/tests/native-resume-session.test.mjs +46 -0
- package/tests/native-review-consumer.test.mjs +286 -0
- package/tests/native-runtime-identity.test.mjs +214 -0
- package/tests/native-select-mouse.test.mjs +23 -0
- package/tests/native-ship-parity.test.mjs +526 -0
- package/tests/native-ship-plan.test.mjs +85 -0
- package/tests/native-ship-rpc.test.mjs +71 -0
- package/tests/native-ui-commands.test.mjs +6 -0
- package/tests/native-voyages-layout.test.mjs +28 -0
- package/tests/native-voyages-navigation.test.mjs +169 -0
- package/tests/native-voyages-rpc.test.mjs +395 -0
- package/tests/native-voyages-ui.test.mjs +277 -0
- package/tests/opencode-atomic-write.test.mjs +55 -0
- package/tests/opencode-cli.test.mjs +1312 -0
- package/tests/opencode-codegraph-plugin.test.mjs +47 -0
- package/tests/opencode-delivery.test.mjs +16 -0
- package/tests/opencode-providers.test.mjs +245 -18
- package/tests/opencode-runtime.test.mjs +546 -8
- package/tests/opencode-session-controls.test.mjs +15 -0
- package/tests/opencode-ship-rpc.test.mjs +71 -0
- package/tests/opencode-update.test.mjs +475 -0
- package/tests/opencode-workspace.test.mjs +51 -2
- package/tests/owned-runtime-publish-workflow.test.mjs +477 -0
- package/tests/release-workflow.test.mjs +46 -0
- package/tests/runtime-artifact.test.mjs +269 -0
- package/tests/runtime-development-trust.test.mjs +230 -0
- package/tests/runtime-github-transport.test.mjs +132 -0
- package/tests/runtime-token-env.test.mjs +23 -0
- package/tests/server.test.mjs +3 -1
- package/tests/ship-telemetry.test.mjs +1 -1
- package/tests/source-preview.test.mjs +253 -0
- package/tests/test_native_terminal_probe.py +76 -0
- package/tests/test_native_terminal_semantics.py +232 -0
- package/tests/update.test.mjs +42 -5
- package/tests/verify-draft-runtime-gate.test.mjs +280 -0
- package/tests/verify-owned-runtime-release.test.mjs +371 -0
- package/tests/workspace-live.test.mjs +127 -0
- package/tools/benchmark/diagnose-request.mjs +59 -36
- package/tools/benchmark/eval-common.mjs +3 -3
- package/tools/benchmark/evaluators.test.mjs +4 -4
- package/tools/benchmark/guard.test.mjs +35 -2
- package/tools/benchmark/guard.ts +20 -7
- package/tools/benchmark/planning-startup.test.mjs +30 -34
- package/tools/benchmark/runner.mjs +12 -7
- package/tools/benchmark/runner.test.mjs +23 -7
- package/tools/benchmark/upstream-error.test.mjs +16 -2
- package/tools/generate-owned-runtime-release.mjs +217 -0
- package/tools/installed-runtime-contract.mjs +16 -0
- package/tools/manual-runtime-manifest.mjs +39 -0
- package/tools/native-fixture-browser-cleanup.mjs +59 -0
- package/tools/native-fixture-stop-diagnostics.mjs +43 -0
- package/tools/native-fixture.mjs +199 -40
- package/tools/native-glyph-helpers.py +127 -0
- package/tools/native-glyph-pty.py +144 -0
- package/tools/native-manual-projects.mjs +259 -0
- package/tools/native-manual.mjs +338 -0
- package/tools/native-model-picker-acceptance.py +306 -0
- package/tools/native-owned-runtime-env.sh +47 -0
- package/tools/native-paired-guards.mjs +282 -0
- package/tools/native-paired-manifest.mjs +72 -0
- package/tools/native-paired-orchestration.sh +51 -0
- package/tools/native-plugin-readiness.mjs +33 -0
- package/tools/native-plugin-readiness.test.mjs +53 -0
- package/tools/native-pty-invocation.mjs +26 -0
- package/tools/native-pty-runtime.mjs +83 -0
- package/tools/native-pty-smoke.py +147 -24
- package/tools/native-public-repo-shell-result.mjs +13 -0
- package/tools/native-public-repo-shell-result.test.mjs +28 -0
- package/tools/native-runtime-identity.mjs +201 -0
- package/tools/native-slash-acceptance-env.py +38 -0
- package/tools/native-terminal-semantics.py +876 -0
- package/tools/owned-shell-observer/index.ts +16 -0
- package/tools/owned-shell-observer/package.json +1 -0
- package/tools/owned-visual-clear-env.py +48 -0
- package/tools/owned-visual-clear-model.py +21 -0
- package/tools/pinned-owned-runtime-inputs.mjs +46 -0
- package/tools/prepare-native-manual.mjs +122 -0
- package/tools/run-native-terminal-paired.sh +403 -0
- package/tools/runtime-token-env.mjs +8 -0
- package/tools/seed-native-switch-fixture.mjs +79 -0
- package/tools/test-native-controls.mjs +3 -1
- package/tools/test-native-daily-quests.mjs +65 -0
- package/tools/test-native-default-cli.mjs +106 -0
- package/tools/test-native-delivery-host.mjs +102 -20
- package/tools/test-native-evaluation.mjs +7 -3
- package/tools/test-native-glyph-pty.mjs +94 -0
- package/tools/test-native-host.mjs +87 -63
- package/tools/test-native-outcomes.mjs +3 -1
- package/tools/test-native-package.mjs +206 -38
- package/tools/test-native-performance.mjs +26 -0
- package/tools/test-native-product.mjs +90 -0
- package/tools/test-native-pty.mjs +34 -9
- package/tools/test-native-public-repo.mjs +224 -0
- package/tools/test-native-refresh-reentry.mjs +106 -0
- package/tools/test-native-report.mjs +27 -20
- package/tools/test-native-review.mjs +75 -0
- package/tools/test-native-runtime.mjs +120 -0
- package/tools/test-native-ship-models.mjs +176 -0
- package/tools/test-native-ship-walkthrough.mjs +96 -0
- package/tools/test-native-slash-commands.mjs +125 -0
- package/tools/test-native-slash-commands.py +242 -0
- package/tools/test-native-swe-host.mjs +224 -55
- package/tools/test-native-tasks.mjs +77 -0
- package/tools/test-native-terminal-performance.py +887 -0
- package/tools/test-native-terminal-readiness.sh +85 -0
- package/tools/test-native-terminal-semantics-installed.mjs +38 -0
- package/tools/test-native-visual-clear-installed.mjs +48 -0
- package/tools/test-owned-host-controls.mjs +238 -0
- package/tools/test-owned-opencode-runtime.mjs +35 -0
- package/tools/test-owned-visual-clear-env.py +120 -0
- package/tools/test-owned-visual-clear.py +373 -0
- package/tools/verify-draft-runtime-gate.mjs +138 -0
- package/tools/verify-owned-runtime-release.mjs +114 -0
- package/tools/verify-release-version.mjs +21 -1
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
diff --git a/packages/tui/src/component/prompt/autocomplete.tsx b/packages/tui/src/component/prompt/autocomplete.tsx
|
|
2
|
+
index c669491..2c0c63a 100644
|
|
3
|
+
--- a/packages/tui/src/component/prompt/autocomplete.tsx
|
|
4
|
+
+++ b/packages/tui/src/component/prompt/autocomplete.tsx
|
|
5
|
+
@@ -472,15 +472,28 @@ export function Autocomplete(props: {
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const commands = createMemo((): AutocompleteOption[] => {
|
|
9
|
+
- const results: AutocompleteOption[] = keymapCommands().flatMap((command) => {
|
|
10
|
+
+ const registered = keymapCommands()
|
|
11
|
+
+ let help: KeymapCommand | undefined
|
|
12
|
+
+ const results: AutocompleteOption[] = registered.flatMap((command) => {
|
|
13
|
+
const slash = command.slash
|
|
14
|
+
if (!slash) return []
|
|
15
|
+
- return [slash.name, ...(slash.aliases ?? [])].map((name) => ({
|
|
16
|
+
+ const names = [slash.name, ...(slash.aliases ?? [])]
|
|
17
|
+
+ if (names.includes("help") && command.id !== "help.show") help = command
|
|
18
|
+
+ return names.filter((name) => name !== "help").map((name) => ({
|
|
19
|
+
display: `/${name}`,
|
|
20
|
+
description: command.description ?? command.title,
|
|
21
|
+
onSelect: slash.arguments ? () => insertSlash(name) : command.run,
|
|
22
|
+
}))
|
|
23
|
+
})
|
|
24
|
+
+ const helpCommand = help ?? registered.find((command) => command.id === "help.show")
|
|
25
|
+
+ if (helpCommand?.slash) {
|
|
26
|
+
+ const slash = helpCommand.slash
|
|
27
|
+
+ results.push({
|
|
28
|
+
+ display: "/help",
|
|
29
|
+
+ description: helpCommand.description ?? helpCommand.title,
|
|
30
|
+
+ onSelect: slash.arguments ? () => insertSlash("help") : helpCommand.run,
|
|
31
|
+
+ })
|
|
32
|
+
+ }
|
|
33
|
+
const commandNames = new Set<string>()
|
|
34
|
+
|
|
35
|
+
for (const serverCommand of data.location.command.list(location.current) ?? []) {
|
|
36
|
+
diff --git a/packages/tui/src/component/prompt/index.tsx b/packages/tui/src/component/prompt/index.tsx
|
|
37
|
+
index a9f699c..63fe438 100644
|
|
38
|
+
--- a/packages/tui/src/component/prompt/index.tsx
|
|
39
|
+
+++ b/packages/tui/src/component/prompt/index.tsx
|
|
40
|
+
@@ -173,11 +173,15 @@ function formatEditorContext(selection: EditorSelection) {
|
|
41
|
+
function argumentSlash(input: string, commands: readonly KeymapCommand[]) {
|
|
42
|
+
const head = parseSlashHead(input, /\s/)
|
|
43
|
+
if (!head) return
|
|
44
|
+
- const command = commands.find(
|
|
45
|
+
+ const matches = commands.filter(
|
|
46
|
+
(command) =>
|
|
47
|
+
command.slash?.arguments &&
|
|
48
|
+
(command.slash.name === head.name || command.slash.aliases?.includes(head.name) === true),
|
|
49
|
+
)
|
|
50
|
+
+ // The built-in help action is a fallback; let an extension own the natural
|
|
51
|
+
+ // `/help` spelling regardless of which layer happened to register first.
|
|
52
|
+
+ const command =
|
|
53
|
+
+ head.name === "help" ? (matches.findLast((item) => item.id !== "help.show") ?? matches[0]) : matches[0]
|
|
54
|
+
if (!command) return
|
|
55
|
+
return { command, input: head.arguments }
|
|
56
|
+
}
|
|
57
|
+
diff --git a/packages/tui/test/plugin-keymap.test.tsx b/packages/tui/test/plugin-keymap.test.tsx
|
|
58
|
+
index 0790e9a..23f502c 100644
|
|
59
|
+
--- a/packages/tui/test/plugin-keymap.test.tsx
|
|
60
|
+
+++ b/packages/tui/test/plugin-keymap.test.tsx
|
|
61
|
+
@@ -59,6 +59,38 @@ test("setup layers before and after await reach keyboard, palette, and slash acr
|
|
62
|
+
await setup.waitForFrame((frame) => frame.includes("Sync keymap ran 1"))
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
+test("a plugin /help overrides native /help for natural autocomplete selection and restores it on disable", async () => {
|
|
66
|
+
+ await using plugin = await copyHelpPlugin()
|
|
67
|
+
+ const requests: URL[] = []
|
|
68
|
+
+ await using setup = await createAppFixture({
|
|
69
|
+
+ config: { animations: false, plugins: [plugin.directory] },
|
|
70
|
+
+ fetch(url) {
|
|
71
|
+
+ requests.push(url)
|
|
72
|
+
+ },
|
|
73
|
+
+ })
|
|
74
|
+
+ await setup.ready
|
|
75
|
+
+ await setup.waitForFrame((frame) => frame.includes("Help override ready"))
|
|
76
|
+
+
|
|
77
|
+
+ await setup.mockInput.typeText("/help")
|
|
78
|
+
+ await setup.waitForFrame((frame) => frame.includes("/help"))
|
|
79
|
+
+ setup.mockInput.pressEnter()
|
|
80
|
+
+ await setup.waitForFrame((frame) => frame.includes("Plugin help handler 2"))
|
|
81
|
+
+ expect(requests.some((url) => url.pathname === "/session")).toBe(false)
|
|
82
|
+
+
|
|
83
|
+
+ setup.mockInput.pressEscape()
|
|
84
|
+
+ await openPlugin(setup, "help-override")
|
|
85
|
+
+ await setup.waitForFrame((frame) => frame.includes("disable"))
|
|
86
|
+
+ setup.mockInput.pressEnter()
|
|
87
|
+
+ await setup.waitForFrame((frame) => frame.includes("inactive"))
|
|
88
|
+
+ setup.mockInput.pressEscape()
|
|
89
|
+
+
|
|
90
|
+
+ await setup.mockInput.typeText("/help")
|
|
91
|
+
+ await setup.waitForFrame((frame) => frame.includes("/help"))
|
|
92
|
+
+ setup.mockInput.pressEnter()
|
|
93
|
+
+ await setup.waitForFrame((frame) => frame.includes("Press ctrl+p to see all available actions"))
|
|
94
|
+
+ expect(requests.some((url) => url.pathname === "/session")).toBe(false)
|
|
95
|
+
+})
|
|
96
|
+
+
|
|
97
|
+
test("a component layer ends when its component unmounts while the plugin stays active", async () => {
|
|
98
|
+
await using plugin = await copyPlugin()
|
|
99
|
+
await using setup = await createAppFixture({ config: { animations: false, plugins: [plugin.directory] } })
|
|
100
|
+
@@ -138,6 +170,41 @@ async function copyPlugin() {
|
|
101
|
+
return { directory, entry, [Symbol.asyncDispose]: root[Symbol.asyncDispose] }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
+async function copyHelpPlugin() {
|
|
105
|
+
+ const root = await tmpdir()
|
|
106
|
+
+ const directory = path.join(root.path, "help-override")
|
|
107
|
+
+ const entry = path.join(directory, "tui.tsx")
|
|
108
|
+
+ await mkdir(directory)
|
|
109
|
+
+ await symlink(path.join(import.meta.dir, "../node_modules"), path.join(directory, "node_modules"))
|
|
110
|
+
+ await Bun.write(entry, `
|
|
111
|
+
+ import { Plugin } from "@opencode/plugin/tui"
|
|
112
|
+
+ export default Plugin.define({
|
|
113
|
+
+ id: "fixture.help-override",
|
|
114
|
+
+ setup(context) {
|
|
115
|
+
+ context.keymap.layer(() => ({
|
|
116
|
+
+ mode: "global",
|
|
117
|
+
+ commands: [
|
|
118
|
+
+ {
|
|
119
|
+
+ id: "fixture.help-override.first",
|
|
120
|
+
+ title: "First plugin help",
|
|
121
|
+
+ slash: { name: "help" },
|
|
122
|
+
+ run: () => context.ui.dialog.show(() => <text>Plugin help handler 1</text>),
|
|
123
|
+
+ },
|
|
124
|
+
+ {
|
|
125
|
+
+ id: "fixture.help-override.second",
|
|
126
|
+
+ title: "Second plugin help",
|
|
127
|
+
+ slash: { name: "help" },
|
|
128
|
+
+ run: () => context.ui.dialog.show(() => <text>Plugin help handler 2</text>),
|
|
129
|
+
+ },
|
|
130
|
+
+ ],
|
|
131
|
+
+ }))
|
|
132
|
+
+ context.ui.toast.show({ message: "Help override ready" })
|
|
133
|
+
+ },
|
|
134
|
+
+ })
|
|
135
|
+
+ `)
|
|
136
|
+
+ return { directory, [Symbol.asyncDispose]: root[Symbol.asyncDispose] }
|
|
137
|
+
+}
|
|
138
|
+
+
|
|
139
|
+
async function search(setup: Fixture, query: string) {
|
|
140
|
+
setup.mockInput.pressKey("p", { ctrl: true })
|
|
141
|
+
await setup.waitForFrame((frame) => frame.includes("Commands"))
|
|
142
|
+
@@ -145,11 +212,11 @@ async function search(setup: Fixture, query: string) {
|
|
143
|
+
await setup.mockInput.typeText(query)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
-async function openPlugin(setup: Fixture) {
|
|
147
|
+
+async function openPlugin(setup: Fixture, query = "setup-keymap") {
|
|
148
|
+
await setup.mockInput.typeText("/plugins")
|
|
149
|
+
await setup.waitForFrame((frame) => frame.includes("/plugins"))
|
|
150
|
+
setup.mockInput.pressEnter()
|
|
151
|
+
- await setup.waitForFrame((frame) => frame.includes("fixture.setup-keymap"))
|
|
152
|
+
+ await setup.waitForFrame((frame) => frame.includes(query === "help-override" ? "fixture.help-override" : "fixture.setup-keymap"))
|
|
153
|
+
await setup.waitFor(() => setup.renderer.currentFocusedEditor instanceof InputRenderable)
|
|
154
|
+
- await setup.mockInput.typeText("setup-keymap")
|
|
155
|
+
+ await setup.mockInput.typeText(query)
|
|
156
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
diff --git a/packages/plugin/src/tui/context.ts b/packages/plugin/src/tui/context.ts
|
|
2
|
+
index fe641b3..21ec351 100644
|
|
3
|
+
--- a/packages/plugin/src/tui/context.ts
|
|
4
|
+
+++ b/packages/plugin/src/tui/context.ts
|
|
5
|
+
@@ -490,6 +490,13 @@ export interface Keymap {
|
|
6
|
+
export interface UI {
|
|
7
|
+
readonly dialog: Dialog
|
|
8
|
+
readonly toast: Toast
|
|
9
|
+
+ /** Live host theme controls. Theme files must already be available to the host. */
|
|
10
|
+
+ readonly theme: {
|
|
11
|
+
+ /** Returns the selected theme name and active color mode. */
|
|
12
|
+
+ current(): { readonly name: string; readonly mode: "dark" | "light" }
|
|
13
|
+
+ /** Switches the live theme and optionally locks its color mode; returns false if either is unavailable. */
|
|
14
|
+
+ set(selection: { readonly name: string; readonly mode?: "dark" | "light" }): boolean
|
|
15
|
+
+ }
|
|
16
|
+
readonly format: {
|
|
17
|
+
path(value: string): string
|
|
18
|
+
}
|
|
19
|
+
diff --git a/packages/tui/src/context/theme.tsx b/packages/tui/src/context/theme.tsx
|
|
20
|
+
index ed568e6..2d7d363 100644
|
|
21
|
+
--- a/packages/tui/src/context/theme.tsx
|
|
22
|
+
+++ b/packages/tui/src/context/theme.tsx
|
|
23
|
+
@@ -94,6 +94,7 @@ type Themes = {
|
|
24
|
+
mode: Accessor<"dark" | "light">
|
|
25
|
+
modes: Accessor<readonly ("dark" | "light")[]>
|
|
26
|
+
supports(mode: "dark" | "light"): boolean
|
|
27
|
+
+ supportsThemeMode(theme: string, mode: "dark" | "light"): boolean
|
|
28
|
+
locked: Accessor<boolean>
|
|
29
|
+
lock(): void
|
|
30
|
+
unlock(): void
|
|
31
|
+
@@ -329,6 +330,10 @@ const themeContext = createSimpleContext({
|
|
32
|
+
mode,
|
|
33
|
+
modes,
|
|
34
|
+
supports: (requested) => modes().includes(requested),
|
|
35
|
+
+ supportsThemeMode: (theme, requested) => {
|
|
36
|
+
+ const source = available()[theme]
|
|
37
|
+
+ return source !== undefined && themeModes(parseTheme(source, theme)).includes(requested)
|
|
38
|
+
+ },
|
|
39
|
+
locked: () => store.lock !== undefined,
|
|
40
|
+
lock: () => pin(mode()),
|
|
41
|
+
unlock: free,
|
|
42
|
+
diff --git a/packages/tui/src/plugin/api.tsx b/packages/tui/src/plugin/api.tsx
|
|
43
|
+
index 35a4eac..4150ba9 100644
|
|
44
|
+
--- a/packages/tui/src/plugin/api.tsx
|
|
45
|
+
+++ b/packages/tui/src/plugin/api.tsx
|
|
46
|
+
@@ -204,6 +204,16 @@ export function createPluginContext(input: {
|
|
47
|
+
ui: {
|
|
48
|
+
dialog: dialogApi,
|
|
49
|
+
toast: toastApi,
|
|
50
|
+
+ theme: {
|
|
51
|
+
+ current: () => ({ name: host.themes.selected, mode: host.themes.mode() }),
|
|
52
|
+
+ set(selection) {
|
|
53
|
+
+ if (!selection || typeof selection.name !== "string" || !host.themes.has(selection.name)) return false
|
|
54
|
+
+ if (selection.mode !== undefined && !host.themes.supportsThemeMode(selection.name, selection.mode)) return false
|
|
55
|
+
+ if (!host.themes.set(selection.name)) return false
|
|
56
|
+
+ if (selection.mode !== undefined) host.themes.setMode(selection.mode)
|
|
57
|
+
+ return true
|
|
58
|
+
+ },
|
|
59
|
+
+ },
|
|
60
|
+
format: {
|
|
61
|
+
path: (value) => abbreviateHome(value, host.paths.home),
|
|
62
|
+
},
|
|
63
|
+
diff --git a/packages/tui/test/plugin-theme.test.tsx b/packages/tui/test/plugin-theme.test.tsx
|
|
64
|
+
new file mode 100644
|
|
65
|
+
index 0000000..9a46b4d
|
|
66
|
+
--- /dev/null
|
|
67
|
+
+++ b/packages/tui/test/plugin-theme.test.tsx
|
|
68
|
+
@@ -0,0 +1,54 @@
|
|
69
|
+
+import { expect, test } from "bun:test"
|
|
70
|
+
+import { createPluginContext, type Registry, type usePluginHost } from "../src/plugin/api"
|
|
71
|
+
+
|
|
72
|
+
+function setup() {
|
|
73
|
+
+ let selected = "opencode"
|
|
74
|
+
+ let mode: "dark" | "light" = "dark"
|
|
75
|
+
+ const available = new Set(["opencode", "custom"])
|
|
76
|
+
+ const host = {
|
|
77
|
+
+ app: {},
|
|
78
|
+
+ client: {},
|
|
79
|
+
+ keymap: {},
|
|
80
|
+
+ shortcuts: {},
|
|
81
|
+
+ keymapState: {},
|
|
82
|
+
+ sessionTabs: {},
|
|
83
|
+
+ themes: {
|
|
84
|
+
+ get selected() {
|
|
85
|
+
+ return selected
|
|
86
|
+
+ },
|
|
87
|
+
+ mode: () => mode,
|
|
88
|
+
+ has: (name: string) => available.has(name),
|
|
89
|
+
+ supportsThemeMode: (name: string, requested: "dark" | "light") => requested === "dark" || name === "custom",
|
|
90
|
+
+ set(name: string) {
|
|
91
|
+
+ if (!available.has(name)) return false
|
|
92
|
+
+ selected = name
|
|
93
|
+
+ return true
|
|
94
|
+
+ },
|
|
95
|
+
+ setMode(requested: "dark" | "light") {
|
|
96
|
+
+ mode = requested
|
|
97
|
+
+ return true
|
|
98
|
+
+ },
|
|
99
|
+
+ },
|
|
100
|
+
+ } as unknown as ReturnType<typeof usePluginHost>
|
|
101
|
+
+ const registry: Registry = { has: () => false, set() {}, remove() {}, active: () => true }
|
|
102
|
+
+ const theme = createPluginContext({ host, id: "test", options: undefined, owned: [], registry }).ui.theme
|
|
103
|
+
+ return { theme, current: () => ({ selected, mode }) }
|
|
104
|
+
+}
|
|
105
|
+
+
|
|
106
|
+
+test("plugin theme setter applies available theme and mode live through the host service", () => {
|
|
107
|
+
+ const harness = setup()
|
|
108
|
+
+ expect(harness.theme.current()).toEqual({ name: "opencode", mode: "dark" })
|
|
109
|
+
+ expect(harness.theme.set({ name: "custom", mode: "light" })).toBe(true)
|
|
110
|
+
+ expect(harness.theme.current()).toEqual({ name: "custom", mode: "light" })
|
|
111
|
+
+ expect(harness.current()).toEqual({ selected: "custom", mode: "light" })
|
|
112
|
+
+})
|
|
113
|
+
+
|
|
114
|
+
+test("plugin theme setter rejects unknown themes and unsupported modes without partial changes", () => {
|
|
115
|
+
+ const harness = setup()
|
|
116
|
+
+ expect(harness.theme.set({ name: "missing", mode: "dark" })).toBe(false)
|
|
117
|
+
+ expect(harness.theme.set({ name: "custom", mode: "light" })).toBe(true)
|
|
118
|
+
+ expect(harness.theme.set({ name: "opencode", mode: "light" })).toBe(false)
|
|
119
|
+
+ expect(harness.theme.current()).toEqual({ name: "custom", mode: "light" })
|
|
120
|
+
+ expect(harness.theme.set({ name: "opencode" })).toBe(true)
|
|
121
|
+
+ expect(harness.theme.current()).toEqual({ name: "opencode", mode: "light" })
|
|
122
|
+
+})
|