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,127 @@
|
|
|
1
|
+
import type { Context } from "@opencode/plugin/tui/context";
|
|
2
|
+
import type { LocationRef } from "@opencode/client";
|
|
3
|
+
import { Workspace, type WorkspaceDelivery, type WorkspaceSnapshot } from "../workspace-rpc.js";
|
|
4
|
+
import { SessionControls } from "../session-rpc.js";
|
|
5
|
+
import { PullRequests, type PullRequestRecord } from "../pull-request-rpc.js";
|
|
6
|
+
import { Archive } from "../archive-rpc.js";
|
|
7
|
+
import { Carthagent } from "../rpc.js";
|
|
8
|
+
import { Voyages, type VoyagesSnapshot, type VoyagesProfile, type VoyagesItem, type VoyagesQuote } from "../voyages-rpc.js";
|
|
9
|
+
import { DailyQuests, type DailyEvidence, type DailySnapshot, type DailyQuestDetail } from "../daily-quests-rpc.js";
|
|
10
|
+
import { DailySource, type DailySourceReport } from "../daily-source-rpc.js";
|
|
11
|
+
import { formatCloudAccount, formatCloudUsage } from "../../../../lib/cloud/client.mjs";
|
|
12
|
+
|
|
13
|
+
export { Workspace, SessionControls, PullRequests, Archive };
|
|
14
|
+
export type { WorkspaceDelivery, WorkspaceSnapshot, PullRequestRecord };
|
|
15
|
+
export type { VoyagesSnapshot, VoyagesProfile, VoyagesItem, VoyagesQuote };
|
|
16
|
+
|
|
17
|
+
const REQUEST_TIMEOUT_MS = 15_000;
|
|
18
|
+
|
|
19
|
+
type RpcContext = Pick<Context, "client" | "location" | "data">;
|
|
20
|
+
|
|
21
|
+
type SelectedLocation = LocationRef | (() => LocationRef | undefined);
|
|
22
|
+
|
|
23
|
+
function requestOptions(context: RpcContext, selectedLocation?: SelectedLocation, signal?: AbortSignal) {
|
|
24
|
+
const location = typeof selectedLocation === "function" ? selectedLocation() : selectedLocation;
|
|
25
|
+
return {
|
|
26
|
+
location: location ?? context.location ?? context.data.location.default(),
|
|
27
|
+
signal: signal ?? AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function voyagesClient(context: RpcContext, selectedLocation?: SelectedLocation) {
|
|
32
|
+
const rpc = context.client.rpc(Voyages);
|
|
33
|
+
return {
|
|
34
|
+
previewReward: (input: { sessionID: string }, signal?: AbortSignal) => rpc.previewReward(input, requestOptions(context, selectedLocation, signal)),
|
|
35
|
+
claimReward: (input: { sessionID: string; runId: string; snapshot: string }, signal?: AbortSignal) => rpc.claimReward(input, requestOptions(context, selectedLocation, signal)),
|
|
36
|
+
snapshot: (signal?: AbortSignal) => rpc.snapshot({}, requestOptions(context, selectedLocation, signal)) as Promise<VoyagesSnapshot>,
|
|
37
|
+
customize: (input: { version: number; name?: string; shipName?: string; skin?: string; hair?: string; pattern?: string }, signal?: AbortSignal) => rpc.customize(input, requestOptions(context, selectedLocation, signal)),
|
|
38
|
+
equip: (input: { version: number; slot: string; item: string }, signal?: AbortSignal) => rpc.equip(input, requestOptions(context, selectedLocation, signal)),
|
|
39
|
+
quote: (input: { slot: string; item: string }, signal?: AbortSignal) => rpc.quote(input, requestOptions(context, selectedLocation, signal)),
|
|
40
|
+
purchase: (input: { quoteId: string }, signal?: AbortSignal) => rpc.purchase(input, requestOptions(context, selectedLocation, signal)),
|
|
41
|
+
retryPurchase: (input: { quoteId: string }, signal?: AbortSignal) => rpc.retryPurchase(input, requestOptions(context, selectedLocation, signal)),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function dailyQuestsClient(context: RpcContext, selectedLocation?: SelectedLocation) {
|
|
46
|
+
const rpc = context.client.rpc(DailyQuests);
|
|
47
|
+
return {
|
|
48
|
+
snapshot: (signal?: AbortSignal) => rpc.snapshot({}, requestOptions(context, selectedLocation, signal)) as Promise<DailySnapshot>,
|
|
49
|
+
detail: (input: { id: string }, signal?: AbortSignal) => rpc.detail(input, requestOptions(context, selectedLocation, signal)) as Promise<DailyQuestDetail>,
|
|
50
|
+
submit: (input: { accountKey: string; questId: string; id: string; consent: true; evidence: DailyEvidence }, signal?: AbortSignal) => rpc.submit(input, requestOptions(context, selectedLocation, signal)),
|
|
51
|
+
retry: (input: { accountKey: string; id: string }, signal?: AbortSignal) => rpc.retry(input, requestOptions(context, selectedLocation, signal)),
|
|
52
|
+
poll: (input: { id: string }, signal?: AbortSignal) => rpc.poll(input, requestOptions(context, selectedLocation, signal)),
|
|
53
|
+
forgetPayload: (input: { accountKey: string; id: string }, signal?: AbortSignal) => rpc.forgetPayload(input, requestOptions(context, selectedLocation, signal)),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function dailySourceClient(context: RpcContext, selectedLocation?: SelectedLocation) {
|
|
58
|
+
const rpc = context.client.rpc(DailySource);
|
|
59
|
+
return { report: (input: { sessionID: string }, signal?: AbortSignal) => rpc.report(input, requestOptions(context, selectedLocation, signal)) as Promise<{ source: DailySourceReport | null }> };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function workspaceClient(context: RpcContext, selectedLocation?: SelectedLocation) {
|
|
63
|
+
const rpc = context.client.rpc(Workspace);
|
|
64
|
+
return {
|
|
65
|
+
snapshot: (input?: { projectID?: string }, signal?: AbortSignal) => rpc.snapshot(input ?? {}, requestOptions(context, selectedLocation, signal)) as Promise<WorkspaceSnapshot>,
|
|
66
|
+
open: (input: { sessionID: string }, signal?: AbortSignal) => rpc.open(input, requestOptions(context, selectedLocation, signal)) as Promise<{ opened: boolean; directory: string }>,
|
|
67
|
+
createWorktree: (input: { projectID: string; name: string }, signal?: AbortSignal) => rpc.createWorktree(input, requestOptions(context, selectedLocation, signal)) as Promise<{ directory: string }>,
|
|
68
|
+
createTask: (input: { task: string; branch?: string }, signal?: AbortSignal) => rpc.createTask(input, requestOptions(context, selectedLocation, signal)) as Promise<{ taskID: string; sessionID: string; directory: string }>,
|
|
69
|
+
delivery: (input: { sessionID: string }, signal?: AbortSignal) => rpc.delivery(input, requestOptions(context, selectedLocation, signal)) as Promise<WorkspaceDelivery>,
|
|
70
|
+
review: (input: { sessionID: string }, signal?: AbortSignal) => rpc.review(input, requestOptions(context, selectedLocation, signal)) as Promise<{ status: string; summary?: string }>,
|
|
71
|
+
startReview: (input: { sessionID: string }, signal?: AbortSignal) => rpc.startReview(input, requestOptions(context, selectedLocation, signal)) as Promise<{ status: string; summary: string; sessionID?: string }>,
|
|
72
|
+
events: rpc.events,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function sessionControls(context: RpcContext, selectedLocation?: SelectedLocation) {
|
|
77
|
+
const rpc = context.client.rpc(SessionControls);
|
|
78
|
+
return {
|
|
79
|
+
framing: (input: { sessionID: string; enabled?: boolean }, signal?: AbortSignal) => rpc.framing(input, requestOptions(context, selectedLocation, signal)) as Promise<{ enabled: boolean }>,
|
|
80
|
+
compact: (input: { sessionID: string; notes?: string }, signal?: AbortSignal) => rpc.compact(input, requestOptions(context, selectedLocation, signal)) as Promise<{ admitted: boolean }>,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function pullRequestClient(context: RpcContext, selectedLocation?: SelectedLocation) {
|
|
85
|
+
const rpc = context.client.rpc(PullRequests);
|
|
86
|
+
return {
|
|
87
|
+
list: (input: { sessionID: string; taskID: string }, signal?: AbortSignal) => rpc.list(input, requestOptions(context, selectedLocation, signal)) as Promise<{ pullRequests: PullRequestRecord[] }>,
|
|
88
|
+
associate: (input: { sessionID: string; taskID: string; number: number }, signal?: AbortSignal) => rpc.associate(input, requestOptions(context, selectedLocation, signal)) as Promise<{ pullRequest: PullRequestRecord }>,
|
|
89
|
+
refresh: (input: { sessionID: string; taskID: string; number?: number }, signal?: AbortSignal) => rpc.refresh(input, requestOptions(context, selectedLocation, signal)) as Promise<{ pullRequest: PullRequestRecord }>,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function shipClient(context: RpcContext, selectedLocation?: SelectedLocation) {
|
|
94
|
+
const rpc = context.client.rpc(Carthagent);
|
|
95
|
+
return {
|
|
96
|
+
account: (signal?: AbortSignal) => rpc.shipAccount({}, requestOptions(context, selectedLocation, signal)),
|
|
97
|
+
usage: (input: { limit?: number } = {}, signal?: AbortSignal) => rpc.shipUsage(input, requestOptions(context, selectedLocation, signal)),
|
|
98
|
+
billing: (input: { action: "checkout" | "portal" | "quote"; plan?: "builder" | "pro" | "max" }, signal?: AbortSignal) => rpc.shipBilling(input, requestOptions(context, selectedLocation, signal)),
|
|
99
|
+
logout: (input: { session?: string } = {}, signal?: AbortSignal) => rpc.shipLogout(input, requestOptions(context, selectedLocation, signal)),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function archiveClient(context: RpcContext, selectedLocation?: SelectedLocation) {
|
|
104
|
+
const rpc = context.client.rpc(Archive);
|
|
105
|
+
return {
|
|
106
|
+
list: (input: { offset?: number; limit?: number } = {}, signal?: AbortSignal) => rpc.list(input, requestOptions(context, selectedLocation, signal)),
|
|
107
|
+
search: (input: { query: string; offset?: number; limit?: number }, signal?: AbortSignal) => rpc.search(input, requestOptions(context, selectedLocation, signal)),
|
|
108
|
+
read: (input: { id: string }, signal?: AbortSignal) => rpc.read(input, requestOptions(context, selectedLocation, signal)),
|
|
109
|
+
export: (input: { id: string; format?: "json" | "jsonl" }, signal?: AbortSignal) => rpc.export(input, requestOptions(context, selectedLocation, signal)),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function cloudAccountText(account: Record<string, unknown>) {
|
|
114
|
+
return formatCloudAccount(account);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function cloudUsageText(usage: Record<string, unknown>, account?: Record<string, unknown>) {
|
|
118
|
+
return formatCloudUsage(usage, { account } as Parameters<typeof formatCloudUsage>[1]);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function archiveItemText(item: Record<string, unknown>) {
|
|
122
|
+
return `${String(item.id)} · ${String(item.messageCount)} messages${item.createdAt ? ` · ${String(item.createdAt)}` : ""}${item.excerpt ? `\n${String(item.excerpt)}` : ""}`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function rpcEventOptions(_context: RpcContext, signal?: AbortSignal): { signal?: AbortSignal } {
|
|
126
|
+
return signal ? { signal } : {};
|
|
127
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/** One inventory for native OpenCode slash/palette discovery and dispatch. */
|
|
2
|
+
export type CommandId =
|
|
3
|
+
| "help" | "resume" | "new" | "restart" | "theme" | "model" | "thinking"
|
|
4
|
+
| "compact" | "autocompact" | "review" | "export" | "copy" | "stats"
|
|
5
|
+
| "clear" | "abort" | "raw" | "guide" | "workflow" | "architecture" | "evidence"
|
|
6
|
+
| "login" | "account" | "usage" | "billing" | "logout-cloud" | "archive" | "demo" | "voyages" | "quests"
|
|
7
|
+
| "ctg-workspace" | "ctg-open" | "ctg-preview" | "ctg-review" | "ctg-worktree" | "ctg-task";
|
|
8
|
+
|
|
9
|
+
export interface CarthagentCommand {
|
|
10
|
+
id: CommandId;
|
|
11
|
+
name: string;
|
|
12
|
+
aliases?: string[];
|
|
13
|
+
args?: string;
|
|
14
|
+
/** Keep slash input eligible for command dispatch when the composer is submitted. */
|
|
15
|
+
submitSlash?: boolean;
|
|
16
|
+
description: string;
|
|
17
|
+
hidden?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const COMMANDS: readonly CarthagentCommand[] = [
|
|
21
|
+
{ id: "help", name: "help", aliases: ["ctg-help"], submitSlash: true, description: "List Carthagent commands and native shortcuts" },
|
|
22
|
+
{ id: "resume", name: "resume", aliases: ["sessions"], args: "[id|name]", description: "Search and resume native sessions; legacy archives are read-only" },
|
|
23
|
+
{ id: "restart", name: "restart", aliases: ["kill"], description: "Recover the selected session by interrupting its active turn" },
|
|
24
|
+
{ id: "theme", name: "theme", args: "[name]", description: "Pick or set a Carthagent-compatible theme" },
|
|
25
|
+
{ id: "model", name: "model", args: "[name]", description: "Search and select a provider model" },
|
|
26
|
+
{ id: "thinking", name: "thinking", description: "Choose a thinking variant when the model supports it" },
|
|
27
|
+
{ id: "compact", name: "compact", args: "[notes]", description: "Compact the selected session, optionally with notes" },
|
|
28
|
+
{ id: "autocompact", name: "autocompact", args: "[on|off]", description: "Toggle automatic compaction or explicitly enable or disable it for this session" },
|
|
29
|
+
{ id: "review", name: "review", aliases: ["review-mode"], args: "[ask|yes|no|status]", description: "Review the selected session or inspect its review default" },
|
|
30
|
+
{ id: "export", name: "export", args: "[file]", description: "Export the selected session as HTML" },
|
|
31
|
+
{ id: "copy", name: "copy", description: "Copy the full last assistant response" },
|
|
32
|
+
{ id: "stats", name: "stats", description: "Show native session token and cost stats" },
|
|
33
|
+
{ id: "clear", name: "clear", description: "Clear the visible transcript for this session without deleting history" },
|
|
34
|
+
{ id: "abort", name: "abort", description: "Interrupt the selected session" },
|
|
35
|
+
{ id: "raw", name: "raw", args: "<prompt>", description: "Send text without Carthagent delivery framing" },
|
|
36
|
+
{ id: "guide", name: "guide", args: "[task]", description: "Toggle framing or start a delivery-framed prompt" },
|
|
37
|
+
{ id: "workflow", name: "workflow", description: "Open the delivery workspace workflow view" },
|
|
38
|
+
{ id: "architecture", name: "architecture", description: "Open the delivery workspace architecture view" },
|
|
39
|
+
{ id: "evidence", name: "evidence", description: "Open observed delivery evidence" },
|
|
40
|
+
{ id: "login", name: "login", aliases: ["auth"], description: "Connect Ship, Claude, xAI, Codex/OpenAI, or Gemini" },
|
|
41
|
+
{ id: "account", name: "account", aliases: ["cloud"], description: "Show Cloud account, credits, plan, and sessions" },
|
|
42
|
+
{ id: "voyages", name: "voyages", description: "Open Voyages profile, wallet, equipment, shop, and daily quests" },
|
|
43
|
+
{ id: "quests", name: "quests", description: "Open daily coding quests; evidence requires separate explicit consent" },
|
|
44
|
+
{ id: "usage", name: "usage", description: "Show recent Ship usage from the control plane" },
|
|
45
|
+
{ id: "billing", name: "billing", args: "[checkout|portal]", description: "Open Ship checkout or billing portal" },
|
|
46
|
+
{ id: "logout-cloud", name: "logout-cloud", args: "[session-id]", description: "Revoke a Cloud session or sign out this profile" },
|
|
47
|
+
{ id: "archive", name: "archive", args: "[search <query>|read <id>|export <id>]", description: "Browse bounded read-only archived sessions" },
|
|
48
|
+
{ id: "demo", name: "demo", description: "Run a scripted, provider-free delivery walkthrough" },
|
|
49
|
+
{ id: "ctg-workspace", name: "ctg-workspace", args: "[refresh]", description: "Browse and open local CTG workspace projects, tasks, and sessions" },
|
|
50
|
+
{ id: "ctg-open", name: "ctg-open", args: "[search]", description: "Search local CTG sessions and open one in native OpenCode" },
|
|
51
|
+
{ id: "ctg-preview", name: "ctg-preview", description: "Preview CTG delivery evidence for the selected session" },
|
|
52
|
+
{ id: "ctg-review", name: "ctg-review", description: "Inspect CTG review status for the selected session" },
|
|
53
|
+
{ id: "ctg-worktree", name: "ctg-worktree", args: "<project-id> <name>", description: "Create a local CTG worktree" },
|
|
54
|
+
{ id: "ctg-task", name: "ctg-task", args: "<task>", description: "Create a CTG task with a native worktree and session" },
|
|
55
|
+
] as const;
|
|
56
|
+
|
|
57
|
+
export function matchCommand(query: string, commands: readonly CarthagentCommand[] = COMMANDS) {
|
|
58
|
+
const token = query.replace(/^\//, "").toLowerCase();
|
|
59
|
+
if (!token || token.includes(" ")) return [];
|
|
60
|
+
return commands.filter((item) => item.name.startsWith(token) || item.aliases?.some((alias) => alias.startsWith(token)));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function resolveCommand(token: string, commands: readonly CarthagentCommand[] = COMMANDS) {
|
|
64
|
+
const normalized = token.replace(/^\//, "").toLowerCase();
|
|
65
|
+
return commands.find((item) => item.name === normalized || item.aliases?.includes(normalized));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Host-owned commands take priority. These slash names are verified against
|
|
69
|
+
// the V2.0.32 keybind command inventory and are intentionally not shadowed.
|
|
70
|
+
export const NATIVE_COMMAND_COLLISIONS = ["themes", "settings"] as const;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native dialogs resolve before their delayed refocus of the saved renderable.
|
|
3
|
+
* Observe actual action-list focus, then reconcile after focus() has finished
|
|
4
|
+
* installing its input handlers. A fixed timeout from compose can run too early.
|
|
5
|
+
*/
|
|
6
|
+
export function createDailyEditorFocus({ state, editor, defer = queueMicrotask }) {
|
|
7
|
+
let actions;
|
|
8
|
+
let generation = 0;
|
|
9
|
+
let disposed = false;
|
|
10
|
+
const permissions = () => {
|
|
11
|
+
const current = state();
|
|
12
|
+
const enabled = !disposed && !current.busy && current.mode === 'base';
|
|
13
|
+
return {
|
|
14
|
+
editor: enabled && current.phase === 'edit' && current.hasDraft && current.editorFocus,
|
|
15
|
+
actions: enabled && (current.phase !== 'edit' || !current.editorFocus),
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
const reconcile = () => {
|
|
19
|
+
if (disposed) return;
|
|
20
|
+
// Read synchronously too, so a Solid effect subscribes to ownership changes.
|
|
21
|
+
permissions();
|
|
22
|
+
const ticket = ++generation;
|
|
23
|
+
defer(() => {
|
|
24
|
+
if (disposed || ticket !== generation) return;
|
|
25
|
+
// Never capture ownership or the editor across a modal/phase/Tab change.
|
|
26
|
+
const allowed = permissions();
|
|
27
|
+
const input = editor();
|
|
28
|
+
if (!allowed.editor && input && !input.isDestroyed && input.focused) input.blur();
|
|
29
|
+
if (!allowed.actions && actions && !actions.isDestroyed && actions.focused) actions.blur();
|
|
30
|
+
const target = allowed.editor ? input : allowed.actions ? actions : undefined;
|
|
31
|
+
if (target && !target.isDestroyed && !target.focused) target.focus();
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
return {
|
|
35
|
+
reconcile,
|
|
36
|
+
editorEnabled: () => permissions().editor,
|
|
37
|
+
actionsEnabled: () => permissions().actions,
|
|
38
|
+
attachActions(renderable) {
|
|
39
|
+
actions?.off('focused', reconcile);
|
|
40
|
+
actions = renderable;
|
|
41
|
+
if (disposed) return;
|
|
42
|
+
actions.on('focused', reconcile);
|
|
43
|
+
reconcile();
|
|
44
|
+
},
|
|
45
|
+
dispose() {
|
|
46
|
+
disposed = true;
|
|
47
|
+
generation++;
|
|
48
|
+
actions?.off('focused', reconcile);
|
|
49
|
+
actions = undefined;
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { voyagesEventAction } from './voyages-state.mjs';
|
|
2
|
+
|
|
3
|
+
export const dailyCancellationNotice = 'Evidence/consent canceled because the account, connection or location changed. Nothing was resent automatically. Refresh and review again; check the recovery journal for any request already started.';
|
|
4
|
+
|
|
5
|
+
/** Daily-page invalidation only; retain the shared credential/location policy.
|
|
6
|
+
* Reset the native index before either replay removal or protected-state clearing
|
|
7
|
+
* can clamp a former consent selection onto Back. Never focus a renderable here.
|
|
8
|
+
*/
|
|
9
|
+
export function handleDailyQuestsEvent({ controller, event, location, changedLocation = false, interrupted = false, actions, clearReplay, closeDialog, notice }) {
|
|
10
|
+
const action = changedLocation ? String(event.type) === 'server.disconnected' ? 'clear' : 'refresh' : voyagesEventAction(event, location);
|
|
11
|
+
if (action === 'ignore') return;
|
|
12
|
+
if (actions && !actions.isDestroyed) actions.setSelectedIndex(0);
|
|
13
|
+
if (interrupted) notice(dailyCancellationNotice);
|
|
14
|
+
clearReplay();
|
|
15
|
+
controller.invalidate();
|
|
16
|
+
closeDialog();
|
|
17
|
+
if (action === 'refresh') return controller.refresh();
|
|
18
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { dailyRequest } from '../daily-evidence.mjs';
|
|
2
|
+
|
|
3
|
+
const copy = value => value === undefined ? undefined : JSON.parse(JSON.stringify(value));
|
|
4
|
+
function freeze(value) {
|
|
5
|
+
if (value && typeof value === 'object') {
|
|
6
|
+
Object.values(value).forEach(freeze);
|
|
7
|
+
Object.freeze(value);
|
|
8
|
+
}
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
const localError = code => ({ type: 'dailyQuestError', data: { code } });
|
|
12
|
+
const failureMessage = code => `${code}. Refresh required; no success assumed. Sign in again if access expired or was revoked.`;
|
|
13
|
+
|
|
14
|
+
/** Native promise RPC errors are { type, message, data }. Never render messages. */
|
|
15
|
+
export function dailyErrorCode(error) {
|
|
16
|
+
const code = error?.type === 'dailyQuestError' ? error.data?.code : undefined;
|
|
17
|
+
return typeof code === 'string' && code.length >= 1 && code.length <= 64 && !/[^a-z_]/.test(code) ? code : 'daily_request_failed';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Processing status is not a coding assessment, nor independent verification. */
|
|
21
|
+
export function dailySubmissionLabel(submission) {
|
|
22
|
+
if (!submission) return 'No submission';
|
|
23
|
+
if (submission.status === 'queued') return 'Queued (not completed)';
|
|
24
|
+
if (submission.status === 'grading') return 'Grading (not completed)';
|
|
25
|
+
if (submission.status === 'failed') return 'Processing failed (not a coding assessment)';
|
|
26
|
+
if (submission.status === 'graded') {
|
|
27
|
+
if (submission.decision === 'assessed_complete') return 'Classifier-assessed complete (not independently verified)';
|
|
28
|
+
if (submission.decision === 'not_met') return 'Graded: not met';
|
|
29
|
+
if (submission.decision === 'needs_evidence') return 'Graded: needs evidence';
|
|
30
|
+
return 'Graded: assessment unavailable';
|
|
31
|
+
}
|
|
32
|
+
return 'Submission status unavailable';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Offline UI transaction controller. Only explicit submit/retry can upload.
|
|
36
|
+
* Dialog callbacks receive detached copies plus a live() lifetime predicate.
|
|
37
|
+
* Cancel/back/invalidate synchronously revoke any outstanding dialog callback.
|
|
38
|
+
*/
|
|
39
|
+
export function createDailyQuestsState({ api, changed = (_state) => {}, timeoutSignal = () => AbortSignal.timeout(15_000) }) {
|
|
40
|
+
let generation = 0;
|
|
41
|
+
let disposed = false;
|
|
42
|
+
let controller = new AbortController();
|
|
43
|
+
const state = { snapshot: undefined, detail: undefined, busy: false, error: '', draft: undefined, preview: undefined, phase: 'overview' };
|
|
44
|
+
const emit = () => { if (!disposed) changed(copy(state)); };
|
|
45
|
+
const clearDraft = () => { state.draft = undefined; state.preview = undefined; state.phase = 'overview'; };
|
|
46
|
+
const clearProtected = () => { state.snapshot = undefined; state.detail = undefined; clearDraft(); };
|
|
47
|
+
const revoke = () => {
|
|
48
|
+
generation++;
|
|
49
|
+
controller.abort();
|
|
50
|
+
controller = new AbortController();
|
|
51
|
+
state.busy = false;
|
|
52
|
+
state.error = '';
|
|
53
|
+
};
|
|
54
|
+
const invalidate = () => { revoke(); clearProtected(); emit(); };
|
|
55
|
+
const networkSignal = signal => AbortSignal.any([signal, timeoutSignal()]);
|
|
56
|
+
const savedRecord = id => state.snapshot?.records.find(record => record.id === id);
|
|
57
|
+
const knownQuest = id => state.snapshot?.slate.items.some(quest => quest.id === id)
|
|
58
|
+
|| state.snapshot?.records.some(record => record.questId === id);
|
|
59
|
+
|
|
60
|
+
async function loadSnapshot(tx, expectedAccount = tx.accountKey) {
|
|
61
|
+
if (!tx.live()) return false;
|
|
62
|
+
const snapshot = copy(await api.snapshot(networkSignal(tx.signal)));
|
|
63
|
+
if (!tx.live()) return false;
|
|
64
|
+
if (!snapshot?.accountKey || (expectedAccount != null && snapshot.accountKey !== expectedAccount)) {
|
|
65
|
+
throw localError('daily_account_changed');
|
|
66
|
+
}
|
|
67
|
+
state.snapshot = snapshot;
|
|
68
|
+
emit();
|
|
69
|
+
return tx.live();
|
|
70
|
+
}
|
|
71
|
+
async function loadDetail(id, tx) {
|
|
72
|
+
if (!tx.live()) return;
|
|
73
|
+
const detail = copy(await api.detail({ id }, networkSignal(tx.signal)));
|
|
74
|
+
if (!tx.live()) return;
|
|
75
|
+
if (detail?.id !== id) throw localError('daily_request_failed');
|
|
76
|
+
// The detail RPC reconciles acceptance and erases raw journal payloads.
|
|
77
|
+
// Drop the matching in-memory copy before exposing the accepted detail.
|
|
78
|
+
if (detail.latestSubmission && state.snapshot) {
|
|
79
|
+
const submission = detail.latestSubmission;
|
|
80
|
+
const previous = savedRecord(submission.id);
|
|
81
|
+
const record = { id: submission.id, questId: detail.id, createdAt: previous?.createdAt ?? submission.createdAt, request: null, submission: copy(submission) };
|
|
82
|
+
state.snapshot.records = state.snapshot.records.filter(item => item.id !== submission.id);
|
|
83
|
+
state.snapshot.records.push(record);
|
|
84
|
+
}
|
|
85
|
+
state.detail = detail;
|
|
86
|
+
emit();
|
|
87
|
+
}
|
|
88
|
+
async function run(action) {
|
|
89
|
+
if (disposed || state.busy) return;
|
|
90
|
+
const current = generation;
|
|
91
|
+
const signal = controller.signal;
|
|
92
|
+
const tx = { signal, accountKey: state.snapshot?.accountKey, live: () => !disposed && generation === current && !signal.aborted };
|
|
93
|
+
state.busy = true;
|
|
94
|
+
state.error = '';
|
|
95
|
+
emit();
|
|
96
|
+
try { if (tx.live()) await action(tx); }
|
|
97
|
+
catch (error) {
|
|
98
|
+
if (tx.live()) {
|
|
99
|
+
clearProtected();
|
|
100
|
+
state.error = failureMessage(dailyErrorCode(error));
|
|
101
|
+
emit();
|
|
102
|
+
// Read the durable journal only. Never replay a write, assume success,
|
|
103
|
+
// or resurrect raw evidence from a callback after an unknown outcome.
|
|
104
|
+
try { await loadSnapshot(tx); }
|
|
105
|
+
catch (reconcileError) {
|
|
106
|
+
if (tx.live()) {
|
|
107
|
+
clearProtected();
|
|
108
|
+
if (dailyErrorCode(reconcileError) === 'daily_account_changed') state.error = failureMessage('daily_account_changed');
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
} finally {
|
|
113
|
+
if (tx.live()) { state.busy = false; emit(); }
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async function refreshedWrite(tx, questId) {
|
|
117
|
+
// Immediately drop all copies of raw evidence; GET is authoritative.
|
|
118
|
+
clearProtected();
|
|
119
|
+
emit();
|
|
120
|
+
if (await loadSnapshot(tx)) await loadDetail(questId, tx);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
invalidate,
|
|
125
|
+
dispose() { if (!disposed) { invalidate(); disposed = true; controller.abort(); } },
|
|
126
|
+
refresh() {
|
|
127
|
+
return run(async tx => { clearProtected(); emit(); await loadSnapshot(tx, null); });
|
|
128
|
+
},
|
|
129
|
+
select(id) {
|
|
130
|
+
return run(async tx => {
|
|
131
|
+
if (!knownQuest(id)) return;
|
|
132
|
+
clearDraft(); state.detail = undefined; emit();
|
|
133
|
+
await loadDetail(id, tx);
|
|
134
|
+
});
|
|
135
|
+
},
|
|
136
|
+
compose(makeDraft) {
|
|
137
|
+
return run(async tx => {
|
|
138
|
+
const detail = copy(state.detail);
|
|
139
|
+
const accountKey = state.snapshot?.accountKey;
|
|
140
|
+
if (!accountKey || !detail || detail.status !== 'open' || !knownQuest(detail.id)) return;
|
|
141
|
+
clearDraft(); emit();
|
|
142
|
+
if (!tx.live()) return;
|
|
143
|
+
const draft = await makeDraft(copy(detail), tx.live, tx.signal);
|
|
144
|
+
if (!draft || !tx.live() || state.snapshot?.accountKey !== accountKey || state.detail?.id !== detail.id) return;
|
|
145
|
+
state.draft = { accountKey, questId: detail.id, id: draft.id, text: JSON.stringify(draft.evidence, null, 2), provenance: typeof draft.provenance === 'string' ? draft.provenance : '' };
|
|
146
|
+
state.phase = 'edit';
|
|
147
|
+
emit();
|
|
148
|
+
});
|
|
149
|
+
},
|
|
150
|
+
edit(text) {
|
|
151
|
+
if (disposed || state.busy || !state.draft || typeof text !== 'string') return;
|
|
152
|
+
state.draft.text = text; state.preview = undefined; state.phase = 'edit'; state.error = ''; emit();
|
|
153
|
+
},
|
|
154
|
+
preview() {
|
|
155
|
+
if (disposed || state.busy || !state.draft) return;
|
|
156
|
+
state.preview = undefined; state.phase = 'edit'; state.error = '';
|
|
157
|
+
try {
|
|
158
|
+
const request = dailyRequest(state.draft.id, JSON.parse(state.draft.text));
|
|
159
|
+
state.draft.text = JSON.stringify(request.evidence, null, 2);
|
|
160
|
+
state.preview = freeze(copy(request)); state.phase = 'preview';
|
|
161
|
+
} catch { state.error = 'invalid_daily_evidence. Correct the evidence before previewing or submitting.'; }
|
|
162
|
+
emit();
|
|
163
|
+
},
|
|
164
|
+
backToEdit() {
|
|
165
|
+
if (disposed || !state.draft) return;
|
|
166
|
+
if (state.busy) revoke();
|
|
167
|
+
state.preview = undefined; state.phase = 'edit'; state.error = ''; emit();
|
|
168
|
+
},
|
|
169
|
+
cancelDraft() {
|
|
170
|
+
if (disposed) return;
|
|
171
|
+
if (state.busy) revoke();
|
|
172
|
+
clearDraft(); state.error = ''; emit();
|
|
173
|
+
},
|
|
174
|
+
submit(confirm) {
|
|
175
|
+
return run(async tx => {
|
|
176
|
+
if (!state.draft || !state.preview || !state.detail || state.detail.status !== 'open') return;
|
|
177
|
+
const draft = copy(state.draft);
|
|
178
|
+
if (draft.accountKey !== tx.accountKey || draft.questId !== state.detail.id) return;
|
|
179
|
+
const request = freeze(copy(state.preview));
|
|
180
|
+
const shown = { quest: copy(state.detail), request: freeze(copy(request)) };
|
|
181
|
+
if (await confirm(shown, tx.live) !== true || !tx.live()) return;
|
|
182
|
+
if (state.draft?.id !== draft.id || state.snapshot?.accountKey !== draft.accountKey || !state.preview) return;
|
|
183
|
+
await api.submit({ accountKey: draft.accountKey, questId: draft.questId, ...copy(request) }, networkSignal(tx.signal));
|
|
184
|
+
if (tx.live()) await refreshedWrite(tx, draft.questId);
|
|
185
|
+
});
|
|
186
|
+
},
|
|
187
|
+
poll(id) {
|
|
188
|
+
return run(async tx => {
|
|
189
|
+
const record = savedRecord(id);
|
|
190
|
+
const questId = record?.questId ?? (state.detail?.latestSubmission?.id === id ? state.detail.id : undefined);
|
|
191
|
+
if (!questId || !tx.accountKey) return;
|
|
192
|
+
clearDraft(); state.detail = undefined; emit();
|
|
193
|
+
if (!tx.live()) return;
|
|
194
|
+
await api.poll({ id }, networkSignal(tx.signal));
|
|
195
|
+
if (await loadSnapshot(tx)) await loadDetail(questId, tx);
|
|
196
|
+
});
|
|
197
|
+
},
|
|
198
|
+
retry(id, confirm) {
|
|
199
|
+
return run(async tx => {
|
|
200
|
+
const record = copy(savedRecord(id));
|
|
201
|
+
if (!record?.request || !tx.accountKey) return;
|
|
202
|
+
const shown = freeze(copy(record));
|
|
203
|
+
if (await confirm(shown, tx.live) !== true || !tx.live()) return;
|
|
204
|
+
await api.retry({ accountKey: tx.accountKey, id: record.id }, networkSignal(tx.signal));
|
|
205
|
+
if (tx.live()) await refreshedWrite(tx, record.questId);
|
|
206
|
+
});
|
|
207
|
+
},
|
|
208
|
+
forget(id, confirm) {
|
|
209
|
+
return run(async tx => {
|
|
210
|
+
const record = copy(savedRecord(id));
|
|
211
|
+
if (!record?.request || !tx.accountKey) return;
|
|
212
|
+
if (await confirm(freeze(copy(record)), tx.live) !== true || !tx.live()) return;
|
|
213
|
+
await api.forgetPayload({ accountKey: tx.accountKey, id: record.id }, networkSignal(tx.signal));
|
|
214
|
+
if (tx.live()) await refreshedWrite(tx, record.questId);
|
|
215
|
+
});
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
}
|