@wolfx/opencode-magic-context 0.27.2 → 0.30.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/dist/agents/language-directive.d.ts +27 -0
- package/dist/agents/language-directive.d.ts.map +1 -0
- package/dist/agents/magic-context-prompt.d.ts +1 -1
- package/dist/agents/magic-context-prompt.d.ts.map +1 -1
- package/dist/config/project-security.d.ts +1 -0
- package/dist/config/project-security.d.ts.map +1 -1
- package/dist/config/schema/magic-context.d.ts +15 -0
- package/dist/config/schema/magic-context.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/refresh-primers.d.ts +1 -0
- package/dist/features/magic-context/dreamer/refresh-primers.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/retrospective-raw-provider.d.ts +0 -1
- package/dist/features/magic-context/dreamer/retrospective-raw-provider.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/storage-task-schedule.d.ts +10 -0
- package/dist/features/magic-context/dreamer/storage-task-schedule.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-config.d.ts +1 -1
- package/dist/features/magic-context/dreamer/task-config.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-executor.d.ts +1 -3
- package/dist/features/magic-context/dreamer/task-executor.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-prompts.d.ts +1 -1
- package/dist/features/magic-context/dreamer/task-prompts.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-registry.d.ts +0 -1
- package/dist/features/magic-context/dreamer/task-registry.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-scheduler.d.ts +1 -0
- package/dist/features/magic-context/dreamer/task-scheduler.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/verify.d.ts +1 -0
- package/dist/features/magic-context/dreamer/verify.d.ts.map +1 -1
- package/dist/features/magic-context/memory/memory-migration.d.ts +1 -0
- package/dist/features/magic-context/memory/memory-migration.d.ts.map +1 -1
- package/dist/features/magic-context/sidekick/agent.d.ts +1 -0
- package/dist/features/magic-context/sidekick/agent.d.ts.map +1 -1
- package/dist/features/magic-context/smart-notes/sandbox-runner.d.ts.map +1 -1
- package/dist/features/magic-context/storage-db.d.ts +2 -21
- package/dist/features/magic-context/storage-db.d.ts.map +1 -1
- package/dist/features/magic-context/storage-schema-helpers.d.ts +30 -0
- package/dist/features/magic-context/storage-schema-helpers.d.ts.map +1 -0
- package/dist/features/magic-context/storage-tags.d.ts +0 -5
- package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
- package/dist/features/magic-context/transform-decision-log.d.ts +1 -0
- package/dist/features/magic-context/transform-decision-log.d.ts.map +1 -1
- package/dist/features/magic-context/types.d.ts +12 -1
- package/dist/features/magic-context/types.d.ts.map +1 -1
- package/dist/features/magic-context/user-memory/review-user-memories.d.ts +1 -0
- package/dist/features/magic-context/user-memory/review-user-memories.d.ts.map +1 -1
- package/dist/hooks/magic-context/apply-operations.d.ts +8 -1
- package/dist/hooks/magic-context/apply-operations.d.ts.map +1 -1
- package/dist/hooks/magic-context/channel2-delivery.d.ts +9 -5
- package/dist/hooks/magic-context/channel2-delivery.d.ts.map +1 -1
- package/dist/hooks/magic-context/command-handler.d.ts +1 -0
- package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-historian.d.ts +1 -0
- package/dist/hooks/magic-context/compartment-runner-historian.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-incremental.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-partial-recomp.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-recomp.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-types.d.ts +1 -0
- package/dist/hooks/magic-context/compartment-runner-types.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-validation.d.ts +1 -1
- package/dist/hooks/magic-context/compartment-runner-validation.d.ts.map +1 -1
- package/dist/hooks/magic-context/ctx-reduce-nudge.d.ts +13 -3
- package/dist/hooks/magic-context/ctx-reduce-nudge.d.ts.map +1 -1
- package/dist/hooks/magic-context/edit-marker.d.ts +11 -0
- package/dist/hooks/magic-context/edit-marker.d.ts.map +1 -0
- package/dist/hooks/magic-context/event-handler.d.ts +1 -4
- package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
- package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
- package/dist/hooks/magic-context/hook.d.ts +2 -2
- package/dist/hooks/magic-context/hook.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-formatting.d.ts.map +1 -1
- package/dist/hooks/magic-context/recomp-orchestrator.d.ts +1 -0
- package/dist/hooks/magic-context/recomp-orchestrator.d.ts.map +1 -1
- package/dist/hooks/magic-context/supersession-reclaim.d.ts +34 -0
- package/dist/hooks/magic-context/supersession-reclaim.d.ts.map +1 -0
- package/dist/hooks/magic-context/system-prompt-hash.d.ts +7 -0
- package/dist/hooks/magic-context/system-prompt-hash.d.ts.map +1 -1
- package/dist/hooks/magic-context/tag-messages.d.ts +8 -0
- package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
- package/dist/hooks/magic-context/tool-drop-target.d.ts +2 -0
- package/dist/hooks/magic-context/tool-drop-target.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +8 -0
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform.d.ts +4 -0
- package/dist/hooks/magic-context/transform.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3911 -5483
- package/dist/plugin/conflict-warning-hook.d.ts.map +1 -1
- package/dist/plugin/dream-timer.d.ts +1 -0
- package/dist/plugin/dream-timer.d.ts.map +1 -1
- package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
- package/dist/plugin/rpc-handlers.d.ts.map +1 -1
- package/dist/plugin/tool-registry.d.ts.map +1 -1
- package/dist/shared/announcement.d.ts +1 -1
- package/dist/shared/announcement.d.ts.map +1 -1
- package/dist/shared/commit-detection.d.ts +29 -0
- package/dist/shared/commit-detection.d.ts.map +1 -0
- package/dist/shared/exit-abort-registry.d.ts +25 -0
- package/dist/shared/exit-abort-registry.d.ts.map +1 -0
- package/dist/shared/harness-provider-map.d.ts +30 -0
- package/dist/shared/harness-provider-map.d.ts.map +1 -0
- package/dist/shared/tag-transcript.d.ts.map +1 -1
- package/dist/shared/transcript.d.ts +15 -0
- package/dist/shared/transcript.d.ts.map +1 -1
- package/dist/shared/tui-preferences.d.ts +1 -1
- package/dist/tools/ctx-note/tools.d.ts.map +1 -1
- package/dist/tui/badge-contrast.d.ts +46 -0
- package/dist/tui/badge-contrast.d.ts.map +1 -0
- package/package.json +78 -77
- package/src/shared/announcement.ts +2 -6
- package/src/shared/commit-detection.test.ts +63 -0
- package/src/shared/commit-detection.ts +53 -0
- package/src/shared/exit-abort-registry.test.ts +50 -0
- package/src/shared/exit-abort-registry.ts +46 -0
- package/src/shared/harness-provider-map.test.ts +63 -0
- package/src/shared/harness-provider-map.ts +56 -0
- package/src/shared/tag-transcript.ts +32 -0
- package/src/shared/transcript-opencode.ts +33 -0
- package/src/shared/transcript.ts +17 -0
- package/src/shared/tui-preferences.ts +2 -2
- package/src/tui/badge-contrast.test.ts +83 -0
- package/src/tui/badge-contrast.ts +84 -0
- package/src/tui/slots/sidebar-content.tsx +2 -1
- package/dist/hooks/is-anthropic-provider.d.ts +0 -2
- package/dist/hooks/is-anthropic-provider.d.ts.map +0 -1
- package/dist/shared/live-server-client.d.ts +0 -50
- package/dist/shared/live-server-client.d.ts.map +0 -1
- package/src/shared/live-server-client.ts +0 -152
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process-global registry of AbortControllers to abort on process exit, backed
|
|
3
|
+
* by a SINGLE `process.once("exit")` listener no matter how many controllers
|
|
4
|
+
* register.
|
|
5
|
+
*
|
|
6
|
+
* Why this exists: the plugin factory runs once per plugin instance, and
|
|
7
|
+
* OpenCode Desktop loads many instances in one Node process (one per open
|
|
8
|
+
* project). Registering a `process.once("exit")` per instance added one listener
|
|
9
|
+
* each, so past Node's default 10-listener cap it logged a
|
|
10
|
+
* `MaxListenersExceededWarning` ("11 exit listeners added to [process]"). One
|
|
11
|
+
* module-global listener that fans out to every registered controller keeps the
|
|
12
|
+
* count at one.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const controllers = new Set<AbortController>();
|
|
16
|
+
let listenerRegistered = false;
|
|
17
|
+
|
|
18
|
+
function abortAll(): void {
|
|
19
|
+
for (const controller of controllers) {
|
|
20
|
+
try {
|
|
21
|
+
controller.abort();
|
|
22
|
+
} catch {
|
|
23
|
+
// best-effort: the process is exiting anyway
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Abort `controller` when the process exits. The underlying `process.once("exit")`
|
|
30
|
+
* listener is installed on the first call only; subsequent calls just add to the
|
|
31
|
+
* fan-out set.
|
|
32
|
+
*/
|
|
33
|
+
export function registerExitAbort(controller: AbortController): void {
|
|
34
|
+
controllers.add(controller);
|
|
35
|
+
if (listenerRegistered) return;
|
|
36
|
+
listenerRegistered = true;
|
|
37
|
+
process.once("exit", abortAll);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Stop tracking `controller` (e.g. when its plugin instance is disposed) so the
|
|
42
|
+
* set doesn't grow without bound as Desktop opens and closes projects.
|
|
43
|
+
*/
|
|
44
|
+
export function unregisterExitAbort(controller: AbortController): void {
|
|
45
|
+
controllers.delete(controller);
|
|
46
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { piModelRefToCanonical, resolveModelRefForPi } from "./harness-provider-map";
|
|
3
|
+
|
|
4
|
+
describe("harness-provider-map", () => {
|
|
5
|
+
describe("resolveModelRefForPi (canonical -> Pi, used when spawning)", () => {
|
|
6
|
+
it("maps the diverging auth-plugin providers, preserving the model id", () => {
|
|
7
|
+
expect(resolveModelRefForPi("openai/gpt-5.5")).toBe("openai-codex/gpt-5.5");
|
|
8
|
+
expect(resolveModelRefForPi("google/antigravity-gemini-3.5-flash")).toBe(
|
|
9
|
+
"google-antigravity/antigravity-gemini-3.5-flash",
|
|
10
|
+
);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("leaves anthropic and every other provider unchanged", () => {
|
|
14
|
+
expect(resolveModelRefForPi("anthropic/claude-opus-4-8")).toBe(
|
|
15
|
+
"anthropic/claude-opus-4-8",
|
|
16
|
+
);
|
|
17
|
+
expect(resolveModelRefForPi("cerebras/gpt-oss-120b")).toBe("cerebras/gpt-oss-120b");
|
|
18
|
+
expect(resolveModelRefForPi("openrouter/openai/gpt-5.5")).toBe(
|
|
19
|
+
"openrouter/openai/gpt-5.5",
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("is idempotent: a config already in Pi form still resolves to Pi form", () => {
|
|
24
|
+
expect(resolveModelRefForPi("openai-codex/gpt-5.5")).toBe("openai-codex/gpt-5.5");
|
|
25
|
+
expect(resolveModelRefForPi("google-antigravity/antigravity-gemini-3.1-pro")).toBe(
|
|
26
|
+
"google-antigravity/antigravity-gemini-3.1-pro",
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("preserves model ids that themselves contain slashes", () => {
|
|
31
|
+
expect(resolveModelRefForPi("openai/some/nested/id")).toBe(
|
|
32
|
+
"openai-codex/some/nested/id",
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("passes through malformed refs (no slash, empty provider) unchanged", () => {
|
|
37
|
+
expect(resolveModelRefForPi("gpt-5.5")).toBe("gpt-5.5");
|
|
38
|
+
expect(resolveModelRefForPi("/gpt-5.5")).toBe("/gpt-5.5");
|
|
39
|
+
expect(resolveModelRefForPi("")).toBe("");
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("piModelRefToCanonical (Pi -> canonical, used by Pi setup write)", () => {
|
|
44
|
+
it("normalizes Pi-native provider ids to the OpenCode form", () => {
|
|
45
|
+
expect(piModelRefToCanonical("openai-codex/gpt-5.5")).toBe("openai/gpt-5.5");
|
|
46
|
+
expect(piModelRefToCanonical("google-antigravity/antigravity-gemini-3.5-flash")).toBe(
|
|
47
|
+
"google/antigravity-gemini-3.5-flash",
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("leaves already-canonical and unmapped providers unchanged", () => {
|
|
52
|
+
expect(piModelRefToCanonical("anthropic/claude-opus-4-8")).toBe(
|
|
53
|
+
"anthropic/claude-opus-4-8",
|
|
54
|
+
);
|
|
55
|
+
expect(piModelRefToCanonical("openai/gpt-5.5")).toBe("openai/gpt-5.5");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("round-trips with resolveModelRefForPi", () => {
|
|
59
|
+
const piForm = "openai-codex/gpt-5.5";
|
|
60
|
+
expect(resolveModelRefForPi(piModelRefToCanonical(piForm))).toBe(piForm);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider-id translation between the canonical (OpenCode) form stored in the
|
|
3
|
+
* shared magic-context config and Pi's harness-native provider ids.
|
|
4
|
+
*
|
|
5
|
+
* OpenCode and Pi now share ONE config, but a few auth-plugin providers were
|
|
6
|
+
* named differently on each side. The model id AFTER the slash is identical;
|
|
7
|
+
* only the provider prefix differs:
|
|
8
|
+
*
|
|
9
|
+
* canonical (OpenCode) Pi
|
|
10
|
+
* -------------------- -------------------
|
|
11
|
+
* openai/<model> openai-codex/<model>
|
|
12
|
+
* google/<model> google-antigravity/<model>
|
|
13
|
+
* anthropic/<model> anthropic/<model> (same; every other provider too)
|
|
14
|
+
*
|
|
15
|
+
* Canonical = OpenCode: the config always stores the OpenCode form. Pi
|
|
16
|
+
* translates at its edges:
|
|
17
|
+
* - read: canonical -> Pi when spawning a configured model (subagent-runner).
|
|
18
|
+
* - write: Pi -> canonical in the Pi setup wizard, so a config written from
|
|
19
|
+
* the Pi side stays readable by OpenCode.
|
|
20
|
+
*
|
|
21
|
+
* OpenCode needs no translation (canonical IS the OpenCode form).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const CANONICAL_TO_PI_PROVIDER: Record<string, string> = {
|
|
25
|
+
openai: "openai-codex",
|
|
26
|
+
google: "google-antigravity",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const PI_TO_CANONICAL_PROVIDER: Record<string, string> = {
|
|
30
|
+
"openai-codex": "openai",
|
|
31
|
+
"google-antigravity": "google",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** Remap only the provider prefix (text before the first "/"), preserving the
|
|
35
|
+
* model id verbatim. No "/", empty provider, or unmapped provider -> unchanged. */
|
|
36
|
+
function remapProviderPrefix(ref: string, map: Record<string, string>): string {
|
|
37
|
+
if (typeof ref !== "string") return ref;
|
|
38
|
+
const slash = ref.indexOf("/");
|
|
39
|
+
if (slash <= 0) return ref;
|
|
40
|
+
const provider = ref.slice(0, slash);
|
|
41
|
+
const mapped = map[provider];
|
|
42
|
+
return mapped ? `${mapped}${ref.slice(slash)}` : ref;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Pi-native `provider/model` -> canonical (OpenCode). Identity when unmapped.
|
|
46
|
+
* Used by the Pi setup wizard so configs it writes stay OpenCode-readable. */
|
|
47
|
+
export function piModelRefToCanonical(ref: string): string {
|
|
48
|
+
return remapProviderPrefix(ref, PI_TO_CANONICAL_PROVIDER);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Canonical (OpenCode) `provider/model` -> Pi-native, for spawning a model on
|
|
52
|
+
* Pi. Idempotent: normalizes any Pi-form prefix back to canonical first, so it
|
|
53
|
+
* is safe on a config that already holds Pi-form ids (hand-edited or pre-fix). */
|
|
54
|
+
export function resolveModelRefForPi(ref: string): string {
|
|
55
|
+
return remapProviderPrefix(piModelRefToCanonical(ref), CANONICAL_TO_PI_PROVIDER);
|
|
56
|
+
}
|
|
@@ -54,6 +54,7 @@ import {
|
|
|
54
54
|
updateTagTokenCount,
|
|
55
55
|
} from "../features/magic-context/storage-tags";
|
|
56
56
|
import { makeToolCompositeKey, type Tagger } from "../features/magic-context/tagger";
|
|
57
|
+
import { applyEditMarkerToInput } from "../hooks/magic-context/edit-marker";
|
|
57
58
|
import { estimateImageTokensFromDataUrl } from "../hooks/magic-context/image-token-estimate";
|
|
58
59
|
import { estimateTokens } from "../hooks/magic-context/read-session-formatting";
|
|
59
60
|
import {
|
|
@@ -692,6 +693,28 @@ function buildAggregateTarget(tagId: number, occurrences: ToolOccurrence[]): Tag
|
|
|
692
693
|
}
|
|
693
694
|
return any ? "truncated" : "absent";
|
|
694
695
|
},
|
|
696
|
+
editMarker(): "truncated" | "absent" {
|
|
697
|
+
// Edit-marker: preserve the tool_use input's filePath + a region
|
|
698
|
+
// hint of the diff, sentinelize the result half. Separate from
|
|
699
|
+
// truncate() so the existing skeleton bytes are never touched.
|
|
700
|
+
// Deterministic + idempotent (re-derived from source each pass; the
|
|
701
|
+
// region-hint clamp self-guards via ...[truncated]).
|
|
702
|
+
const sentinel = `[dropped \u00a7${tagId}\u00a7]`;
|
|
703
|
+
let any = false;
|
|
704
|
+
for (const occ of occurrences) {
|
|
705
|
+
if (occ.kind === "tool_use") {
|
|
706
|
+
const input = occ.part.getToolInput?.();
|
|
707
|
+
if (input) {
|
|
708
|
+
const next = { ...input };
|
|
709
|
+
applyEditMarkerToInput(next);
|
|
710
|
+
if (occ.part.setToolInput?.(next)) any = true;
|
|
711
|
+
}
|
|
712
|
+
} else if (setToolContentOrText(occ.part, sentinel)) {
|
|
713
|
+
any = true;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
return any ? "truncated" : "absent";
|
|
717
|
+
},
|
|
695
718
|
// Non-mutating reclaim predicate (Pi parity with OpenCode's canDrop).
|
|
696
719
|
// Pi sentinelizes BOTH halves, so unlike OpenCode there's no
|
|
697
720
|
// result-part requirement — a target reclaims as long as it still has
|
|
@@ -699,6 +722,15 @@ function buildAggregateTarget(tagId: number, occurrences: ToolOccurrence[]): Tag
|
|
|
699
722
|
canDrop(): boolean {
|
|
700
723
|
return occurrences.length > 0;
|
|
701
724
|
},
|
|
725
|
+
// Non-mutating read of the invocation input (the tool_use occurrence
|
|
726
|
+
// carries the arguments). Used by smart-drops supersession selection.
|
|
727
|
+
readInput(): Record<string, unknown> | null {
|
|
728
|
+
for (const occ of occurrences) {
|
|
729
|
+
const input = occ.part.getToolInput?.();
|
|
730
|
+
if (input) return input;
|
|
731
|
+
}
|
|
732
|
+
return null;
|
|
733
|
+
},
|
|
702
734
|
message: {
|
|
703
735
|
info: { id: messageId, role },
|
|
704
736
|
parts: [],
|
|
@@ -137,6 +137,12 @@ function createOpenCodePart(
|
|
|
137
137
|
} {
|
|
138
138
|
return readOpenCodeToolMetadata(rawPart);
|
|
139
139
|
},
|
|
140
|
+
getToolInput(): Record<string, unknown> | null {
|
|
141
|
+
return readOpenCodeToolInput(rawPart);
|
|
142
|
+
},
|
|
143
|
+
setToolInput(input: Record<string, unknown>): boolean {
|
|
144
|
+
return writeOpenCodeToolInput(rawPart, input);
|
|
145
|
+
},
|
|
140
146
|
replaceWithSentinel(sentinelText: string): boolean {
|
|
141
147
|
// Build a synthetic text part that carries the sentinel as
|
|
142
148
|
// its content. Subsequent passes see this as a normal text
|
|
@@ -267,3 +273,30 @@ function readOpenCodeToolMetadata(part: unknown): {
|
|
|
267
273
|
|
|
268
274
|
return { toolName, inputByteSize, inputTokenCount };
|
|
269
275
|
}
|
|
276
|
+
|
|
277
|
+
/** Non-mutating read of an OpenCode tool part's input object (or null). */
|
|
278
|
+
function readOpenCodeToolInput(part: unknown): Record<string, unknown> | null {
|
|
279
|
+
if (!isRecord(part)) return null;
|
|
280
|
+
const state = isRecord(part.state) ? part.state : null;
|
|
281
|
+
const input = state?.input ?? part.args ?? part.input;
|
|
282
|
+
return isRecord(input) ? input : null;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Replace an OpenCode tool part's input object in place. Returns true if a
|
|
286
|
+
* writable input slot was found. */
|
|
287
|
+
function writeOpenCodeToolInput(part: unknown, input: Record<string, unknown>): boolean {
|
|
288
|
+
if (!isRecord(part)) return false;
|
|
289
|
+
if (isRecord(part.state) && isRecord(part.state.input)) {
|
|
290
|
+
part.state.input = input;
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
if (isRecord(part.args)) {
|
|
294
|
+
part.args = input;
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
if (isRecord(part.input)) {
|
|
298
|
+
part.input = input;
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
return false;
|
|
302
|
+
}
|
package/src/shared/transcript.ts
CHANGED
|
@@ -133,6 +133,23 @@ export interface TranscriptPart {
|
|
|
133
133
|
inputTokenCount: number;
|
|
134
134
|
};
|
|
135
135
|
|
|
136
|
+
/**
|
|
137
|
+
* Non-mutating read of this tool invocation's input object, or null for
|
|
138
|
+
* non-tool parts / parts without an input. Used by smart-drops supersession
|
|
139
|
+
* selection (read `ctx_note`'s action, an edit's `filePath`) without
|
|
140
|
+
* touching the wire. Returns the live object reference; callers must NOT
|
|
141
|
+
* mutate it.
|
|
142
|
+
*/
|
|
143
|
+
getToolInput?(): Record<string, unknown> | null;
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Replace this tool invocation's input object with `input`. Used by the
|
|
147
|
+
* smart-drops edit_marker path to write back a filePath-preserving,
|
|
148
|
+
* region-hint-clamped copy of an edit's arguments. Returns true if the part
|
|
149
|
+
* carried a writable tool input. No-op (false) for non-tool parts.
|
|
150
|
+
*/
|
|
151
|
+
setToolInput?(input: Record<string, unknown>): boolean;
|
|
152
|
+
|
|
136
153
|
/**
|
|
137
154
|
* Replace this part with a sentinel placeholder. Sentinels look like
|
|
138
155
|
* `[dropped §N§]` or `[truncated §N§]` and survive cache-busting
|
|
@@ -11,7 +11,7 @@ import { parse, stringify } from "comment-json";
|
|
|
11
11
|
// Cross-plugin convention (anthropic-auth / aft / magic-context all mirror it):
|
|
12
12
|
// - same file name + env override + lookup order,
|
|
13
13
|
// - byte-identical `computeEffectiveOrder` so the three sort consistently,
|
|
14
|
-
// - a coordinated default-order ladder (anthropic-auth 160,
|
|
14
|
+
// - a coordinated default-order ladder (anthropic-auth 160, MC 170, AFT 180).
|
|
15
15
|
//
|
|
16
16
|
// MC uses `comment-json` (already a dep, Bun-safe) for the WRITE path — a full
|
|
17
17
|
// parse → mutate-one-key → stringify round-trip that preserves comments and
|
|
@@ -64,7 +64,7 @@ export function readTuiPreferencesFileSync(): Record<string, unknown> {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
export const PLUGIN_KEY = "magic-context";
|
|
67
|
-
export const DEFAULT_SLOT_ORDER =
|
|
67
|
+
export const DEFAULT_SLOT_ORDER = 170;
|
|
68
68
|
|
|
69
69
|
export interface MagicContextTuiPrefs {
|
|
70
70
|
forceToTop: boolean;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { badgeTextColor, readableTextColorOn } from "./badge-contrast";
|
|
3
|
+
|
|
4
|
+
describe("badgeTextColor (AFT parity with #186 safety net)", () => {
|
|
5
|
+
// A purple/lavender accent on a dark theme: the badge label should be the
|
|
6
|
+
// theme background (the inverse-of-panel look), the same fixed token the AFT
|
|
7
|
+
// sidebar uses, so two sibling badges never disagree on the same accent.
|
|
8
|
+
const accent = { r: 0.6, g: 0.5, b: 0.9, a: 1 };
|
|
9
|
+
|
|
10
|
+
test("opaque distinct background is used verbatim as the label", () => {
|
|
11
|
+
const background = { r: 0.05, g: 0.05, b: 0.07, a: 1 }; // near-black dark theme
|
|
12
|
+
// Returns the SAME background token rather than a computed color.
|
|
13
|
+
expect(badgeTextColor(accent, background)).toBe(background);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("light theme: background is used verbatim too (near-white label inverse)", () => {
|
|
17
|
+
const background = { r: 0.97, g: 0.97, b: 0.95, a: 1 };
|
|
18
|
+
expect(badgeTextColor(accent, background)).toBe(background);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("transparent background (alpha 0) falls back to a visible pick (#186)", () => {
|
|
22
|
+
// background:"none" resolves to RGBA(0,0,0,0); using it as text would
|
|
23
|
+
// render the label invisible, so fall back to a contrast pick on accent.
|
|
24
|
+
const transparent = { r: 0, g: 0, b: 0, a: 0 };
|
|
25
|
+
const result = badgeTextColor(accent, transparent);
|
|
26
|
+
expect(result).not.toBe(transparent);
|
|
27
|
+
expect(result).toBe(readableTextColorOn(accent));
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("background ~= accent falls back to a visible pick", () => {
|
|
31
|
+
const sameAsAccent = { r: 0.6, g: 0.5, b: 0.9, a: 1 };
|
|
32
|
+
const result = badgeTextColor(accent, sameAsAccent);
|
|
33
|
+
expect(result).toBe(readableTextColorOn(accent));
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("missing alpha is treated as opaque", () => {
|
|
37
|
+
const background = { r: 0.05, g: 0.05, b: 0.07 };
|
|
38
|
+
expect(badgeTextColor(accent, background)).toBe(background);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe("readableTextColorOn", () => {
|
|
43
|
+
test("dark accent gets white text", () => {
|
|
44
|
+
// A typical dark accent (deep blue/purple) should read as white.
|
|
45
|
+
expect(readableTextColorOn({ r: 0.1, g: 0.1, b: 0.3 })).toBe("#ffffff");
|
|
46
|
+
expect(readableTextColorOn({ r: 0, g: 0, b: 0 })).toBe("#ffffff");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("light accent gets black text", () => {
|
|
50
|
+
// Only accents pale enough that white fails the contrast bar go black.
|
|
51
|
+
expect(readableTextColorOn({ r: 0.9, g: 0.9, b: 0.7 })).toBe("#000000");
|
|
52
|
+
expect(readableTextColorOn({ r: 1, g: 1, b: 1 })).toBe("#000000");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("mid-tone orange accent prefers white (white-bias, matches sibling badges)", () => {
|
|
56
|
+
// A typical orange/amber accent reads white ~4:1 and black ~5:1. A
|
|
57
|
+
// higher-contrast-wins pick would flip it to black (the #186 over-
|
|
58
|
+
// correction); the white bias keeps it white, clearing the bold-text bar.
|
|
59
|
+
expect(readableTextColorOn({ r: 0.69, g: 0.455, b: 0.188 })).toBe("#ffffff");
|
|
60
|
+
expect(readableTextColorOn({ r: 0.741, g: 0.482, b: 0.2 })).toBe("#ffffff");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("pure green is treated as light (white fails the contrast bar)", () => {
|
|
64
|
+
// A saturated green is bright enough that white drops below the bar.
|
|
65
|
+
expect(readableTextColorOn({ r: 0, g: 1, b: 0 })).toBe("#000000");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("pure blue is treated as dark (low luma weight)", () => {
|
|
69
|
+
// Blue contributes little to perceived brightness, so a saturated blue
|
|
70
|
+
// badge needs light text.
|
|
71
|
+
expect(readableTextColorOn({ r: 0, g: 0, b: 1 })).toBe("#ffffff");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("does not depend on the (possibly transparent) background alpha", () => {
|
|
75
|
+
// The helper only reads r/g/b — the regression in #186 was using a
|
|
76
|
+
// background color whose alpha could be 0. Two accents with identical
|
|
77
|
+
// rgb resolve identically regardless of any alpha the caller might pass.
|
|
78
|
+
const a = readableTextColorOn({ r: 0.2, g: 0.2, b: 0.2 });
|
|
79
|
+
const b = readableTextColorOn({ r: 0.2, g: 0.2, b: 0.2 });
|
|
80
|
+
expect(a).toBe(b);
|
|
81
|
+
expect(a).toBe("#ffffff");
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pick the text color for the sidebar header badge (a bold label drawn on a
|
|
3
|
+
* `theme.accent` background).
|
|
4
|
+
*
|
|
5
|
+
* Primary rule (matches AFT's sidebar by construction): paint the theme's own
|
|
6
|
+
* `background` color as the label, the inverse-of-panel look. Because it is a
|
|
7
|
+
* fixed theme token rather than an accent-derived computation, MC's badge and
|
|
8
|
+
* AFT's badge agree on EVERY accent automatically, so the same theme can never
|
|
9
|
+
* make one badge black and the other white (issue #198).
|
|
10
|
+
*
|
|
11
|
+
* Fallback rule (the reason a luminance pick exists at all): `theme.background`
|
|
12
|
+
* can be unusable as a label color in two degenerate cases, where it would
|
|
13
|
+
* render the label invisible on the accent:
|
|
14
|
+
* 1. Transparent background. Themes that respect terminal transparency set
|
|
15
|
+
* `background: "none"`, which resolves to `RGBA(0,0,0,0)`; drawing it as
|
|
16
|
+
* text renders fully transparent and the label disappears (issue #186).
|
|
17
|
+
* 2. Background ~= accent. If the theme's background and accent are nearly the
|
|
18
|
+
* same color, background-on-accent text has no contrast.
|
|
19
|
+
* In either case we fall back to a black/white pick that is guaranteed visible
|
|
20
|
+
* on the always-opaque accent.
|
|
21
|
+
*
|
|
22
|
+
* `RGBA` channels from @opentui/core are normalized 0..1 floats (alpha included).
|
|
23
|
+
* We accept the minimal `{ r, g, b, a? }` shape so this stays a pure, trivially
|
|
24
|
+
* testable function independent of the native color class, and we return the
|
|
25
|
+
* passed-in `background` object unchanged on the primary path so it stays the
|
|
26
|
+
* exact same theme token AFT uses.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
type Color = { r: number; g: number; b: number; a?: number };
|
|
30
|
+
|
|
31
|
+
// Below this alpha the theme background is too transparent to read as a label on
|
|
32
|
+
// the accent (issue #186: background:"none" resolves to alpha 0).
|
|
33
|
+
const MIN_OPAQUE_ALPHA = 0.5;
|
|
34
|
+
|
|
35
|
+
// If the theme background and accent are within this per-channel distance they
|
|
36
|
+
// are effectively the same color, so background-on-accent text is unreadable.
|
|
37
|
+
const MIN_CHANNEL_DISTANCE = 0.06;
|
|
38
|
+
|
|
39
|
+
// Luminance midpoint for the fallback pick: accents below this keep white text,
|
|
40
|
+
// accents at/above it (light/pastel/near-white) get black. White-biased relative
|
|
41
|
+
// to the strict equal-contrast crossover (~0.179) so saturated mid-tone accents
|
|
42
|
+
// stay white. Only consulted on the degenerate fallback path.
|
|
43
|
+
const LIGHT_ACCENT_LUMINANCE = 0.5;
|
|
44
|
+
|
|
45
|
+
function srgbChannelToLinear(c: number): number {
|
|
46
|
+
return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function relativeLuminance(bg: Color): number {
|
|
50
|
+
return (
|
|
51
|
+
0.2126 * srgbChannelToLinear(bg.r) +
|
|
52
|
+
0.7152 * srgbChannelToLinear(bg.g) +
|
|
53
|
+
0.0722 * srgbChannelToLinear(bg.b)
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function nearlyEqual(a: Color, b: Color): boolean {
|
|
58
|
+
return (
|
|
59
|
+
Math.abs(a.r - b.r) < MIN_CHANNEL_DISTANCE &&
|
|
60
|
+
Math.abs(a.g - b.g) < MIN_CHANNEL_DISTANCE &&
|
|
61
|
+
Math.abs(a.b - b.b) < MIN_CHANNEL_DISTANCE
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Pure black/white pick by accent luminance. Used as the badge fallback and kept
|
|
67
|
+
* exported for callers that only have the accent.
|
|
68
|
+
*/
|
|
69
|
+
export function readableTextColorOn(bg: Color): string {
|
|
70
|
+
return relativeLuminance(bg) < LIGHT_ACCENT_LUMINANCE ? "#ffffff" : "#000000";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Badge label color on the accent: the theme background (AFT parity) when it is
|
|
75
|
+
* usable, else a guaranteed-visible black/white fallback. Returns the passed-in
|
|
76
|
+
* `background` reference unchanged on the primary path.
|
|
77
|
+
*/
|
|
78
|
+
export function badgeTextColor<T extends Color>(accent: T, background: T): T | string {
|
|
79
|
+
const alpha = background.a ?? 1;
|
|
80
|
+
if (alpha >= MIN_OPAQUE_ALPHA && !nearlyEqual(accent, background)) {
|
|
81
|
+
return background;
|
|
82
|
+
}
|
|
83
|
+
return readableTextColorOn(accent);
|
|
84
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { Show, createEffect, createMemo, createSignal, on, onCleanup } from "solid-js"
|
|
3
3
|
import type { TuiSlotPlugin, TuiPluginApi, TuiThemeCurrent } from "@opencode-ai/plugin/tui"
|
|
4
4
|
import packageJson from "../../../package.json"
|
|
5
|
+
import { badgeTextColor } from '../badge-contrast';
|
|
5
6
|
import { loadSidebarSnapshot, type SidebarSnapshot } from "../data/context-db"
|
|
6
7
|
import { formatThresholdPercent } from "../../shared/format-threshold"
|
|
7
8
|
import {
|
|
@@ -692,7 +693,7 @@ const SidebarContent = (props: {
|
|
|
692
693
|
onMouseDown={() => props.controller.toggleCollapsed()}
|
|
693
694
|
>
|
|
694
695
|
<box paddingLeft={1} paddingRight={1} backgroundColor={props.theme.accent}>
|
|
695
|
-
<text fg={props.theme.background}>
|
|
696
|
+
<text fg={badgeTextColor(props.theme.accent, props.theme.background)}>
|
|
696
697
|
<b>{collapsed() ? "▶ " : "▼ "}{headerLabel()}</b>
|
|
697
698
|
</text>
|
|
698
699
|
</box>
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"is-anthropic-provider.d.ts","sourceRoot":"","sources":["../../src/hooks/is-anthropic-provider.ts"],"names":[],"mappings":"AAAA,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAE/D"}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Live-server client for the Channel 2 ctx_reduce ceiling nudge (a synthetic
|
|
3
|
-
* user `<system-reminder>` delivered via `promptAsync`).
|
|
4
|
-
*
|
|
5
|
-
* WHY a separate client instead of the plugin-provided `input.client`:
|
|
6
|
-
* OpenCode's plugin `input.client` routes through `Server.Default().app.fetch`,
|
|
7
|
-
* which uses a SEPARATE Effect `memoMap` from the live HTTP listener the UI
|
|
8
|
-
* uses. `SessionRunState` lives per-memoMap, so a plugin-origin `promptAsync`
|
|
9
|
-
* observes an "idle" runner while the live turn is still running, `ensureRunning`
|
|
10
|
-
* fails to coalesce, and OpenCode persists duplicate assistant children
|
|
11
|
-
* (upstream bug anomalyco/opencode#28202). Building a `createOpencodeClient`
|
|
12
|
-
* aimed at `input.serverUrl` via `globalThis.fetch` enters the SAME live
|
|
13
|
-
* listener, so `ensureRunning` sees the real run and coalesces — the synthetic
|
|
14
|
-
* message lands at the tail after the current assistant step.
|
|
15
|
-
*
|
|
16
|
-
* The live listener is only reachable on OpenCode Desktop (Electron+Node) and
|
|
17
|
-
* TUI launched with `--port 0`; plain TUI binds an internal listener that 404s
|
|
18
|
-
* `/session/*`. We probe once at init and cache per `serverUrl`. When
|
|
19
|
-
* unreachable, Channel 2 is DISABLED (Channel 1 + 85% force-materialization
|
|
20
|
-
* remain the backstop) — MC deliberately does NOT fall back to the in-process
|
|
21
|
-
* client because that would knowingly trigger #28202.
|
|
22
|
-
*/
|
|
23
|
-
import { createOpencodeClient } from "@opencode-ai/sdk";
|
|
24
|
-
export type LiveServerClient = ReturnType<typeof createOpencodeClient>;
|
|
25
|
-
/**
|
|
26
|
-
* Cached `createOpencodeClient` aimed at the live HTTP listener for the given
|
|
27
|
-
* `(serverUrl, directory)`. One client is reused across deliveries.
|
|
28
|
-
*/
|
|
29
|
-
export declare function getLiveServerClient(serverUrl: string, directory: string): LiveServerClient;
|
|
30
|
-
/**
|
|
31
|
-
* Probe whether `serverUrl` serves OpenCode's HTTP API within `timeoutMs`.
|
|
32
|
-
* `true` only when `/session` proves the API is usable: any 2xx, or 401/403
|
|
33
|
-
* (auth-protected listener still exists). `false` for 404 (plain TUI internal
|
|
34
|
-
* listener), 5xx, connection refused, DNS failure, timeout, or malformed URL.
|
|
35
|
-
* Records the result + timestamp in the per-serverUrl cache.
|
|
36
|
-
*/
|
|
37
|
-
export declare function probeServerReachable(serverUrl: string | undefined, timeoutMs?: number): Promise<boolean>;
|
|
38
|
-
/** Record a probe result directly (test helper / explicit override). */
|
|
39
|
-
export declare function setLiveServerWakeAvailable(serverUrl: string | undefined, available: boolean): void;
|
|
40
|
-
/**
|
|
41
|
-
* Should Channel 2 deliver through the live-server client for `serverUrl`?
|
|
42
|
-
* Returns false when never probed or the last probe failed. A stale decision
|
|
43
|
-
* (older than the TTL) returns false so the caller re-probes before delivering.
|
|
44
|
-
*/
|
|
45
|
-
export declare function useLiveServerWake(serverUrl?: string): boolean;
|
|
46
|
-
/** True when a usable (non-stale) probe decision exists, regardless of outcome. */
|
|
47
|
-
export declare function hasFreshProbe(serverUrl?: string): boolean;
|
|
48
|
-
/** Test helper — reset both caches between cases. */
|
|
49
|
-
export declare function __resetLiveServerClientForTests(): void;
|
|
50
|
-
//# sourceMappingURL=live-server-client.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"live-server-client.d.ts","sourceRoot":"","sources":["../../src/shared/live-server-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAExD,MAAM,MAAM,gBAAgB,GAAG,UAAU,CAAC,OAAO,oBAAoB,CAAC,CAAC;AA0BvE;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,gBAAgB,CAY1F;AAcD;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CACtC,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,SAAS,SAAO,GACjB,OAAO,CAAC,OAAO,CAAC,CAqBlB;AAED,wEAAwE;AACxE,wBAAgB,0BAA0B,CACtC,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,SAAS,EAAE,OAAO,GACnB,IAAI,CAMN;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAM7D;AAED,mFAAmF;AACnF,wBAAgB,aAAa,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAKzD;AAED,qDAAqD;AACrD,wBAAgB,+BAA+B,IAAI,IAAI,CAGtD"}
|