Skip to content

Commit e4eebb9

Browse files
fix(evaluations): preserve native OpenCode configuration
Match v1 and v2 configuration precedence and filenames, and ignore empty override flags. Keep keyboard palette coverage deterministic by seeding its remembered-project prerequisite without removing navigation assertions.
1 parent 5dc36b6 commit e4eebb9

6 files changed

Lines changed: 257 additions & 91 deletions

File tree

‎apps/web/src/components/CommandPalette.browser.test.tsx‎

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
import { EnvironmentId, ProjectId, ThreadId } from "@t3tools/contracts";
2+
import { scopedProjectKey } from "@t3tools/client-runtime/environment";
23
import { describe, expect, it, vi } from "vite-plus/test";
34
import { page, userEvent } from "vite-plus/test/browser";
45

56
import { act, render } from "../../test/render";
67
import { openCommandPalette } from "../commandPaletteBus";
78
import { useCommandPaletteStore } from "../commandPaletteStore";
9+
import {
10+
LAST_USED_PROJECT_STORAGE_KEY,
11+
lastUsedProjectKeySchema,
12+
} from "../hooks/useHandleNewThread";
13+
import { removeLocalStorageItem, setLocalStorageItem } from "../hooks/useLocalStorage";
814
import { makeThreadFixture } from "../test-fixtures";
915
import type { Project, Thread } from "../types";
1016
import { CommandPalette } from "./CommandPalette";
@@ -98,16 +104,33 @@ describe("CommandPalette open intents", () => {
98104
updatedAt: "2026-03-01T00:00:00.000Z",
99105
scripts: [],
100106
});
101-
setup({
102-
projects: [project("acme", "Acme"), project("beta", "Beta"), project("gamma", "Gamma")],
103-
});
107+
const acme = project("acme", "Acme");
108+
setup({ projects: [acme, project("beta", "Beta"), project("gamma", "Gamma")] });
109+
// Keep the starting row independent of remembered project preferences.
110+
const previousLastUsedProject = window.localStorage.getItem(LAST_USED_PROJECT_STORAGE_KEY);
111+
using _rememberedProject = {
112+
[Symbol.dispose]() {
113+
if (previousLastUsedProject === null) {
114+
removeLocalStorageItem(LAST_USED_PROJECT_STORAGE_KEY);
115+
} else {
116+
window.localStorage.setItem(LAST_USED_PROJECT_STORAGE_KEY, previousLastUsedProject);
117+
}
118+
},
119+
};
120+
setLocalStorageItem(
121+
LAST_USED_PROJECT_STORAGE_KEY,
122+
scopedProjectKey({ environmentId, projectId: acme.id }),
123+
lastUsedProjectKeySchema,
124+
);
104125
await using _screen = await render(<CommandPalette>{null}</CommandPalette>);
105126

106127
openCommandPalette({});
107128
await expect
108129
.element(page.getByPlaceholder("Search commands, projects, and threads..."))
109130
.toHaveFocus();
110-
await expect.element(page.getByRole("option").first()).toHaveAttribute("data-highlighted");
131+
await expect
132+
.element(page.getByRole("option", { name: /^New thread in Acme\b/ }))
133+
.toHaveAttribute("data-highlighted");
111134
await userEvent.keyboard("{ArrowDown}");
112135
await expect
113136
.element(page.getByRole("option", { name: /^New thread in\.\.\./ }))

‎packages/provider-opencode/src/server/blindEvaluation.ts‎

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ function mergeConfig(target: Config, source: Config): Config {
5353

5454
export const makeBlindEvaluationEnvironment = Effect.fn("OpenCode.makeBlindEvaluationEnvironment")(
5555
function* (input: {
56+
readonly generation: "v1" | "v2";
5657
readonly operation: TextGenerationOperation;
5758
readonly directory: string;
5859
readonly environment: NodeJS.ProcessEnv;
@@ -82,35 +83,34 @@ export const makeBlindEvaluationEnvironment = Effect.fn("OpenCode.makeBlindEvalu
8283
);
8384

8485
let routing: Config = {};
86+
const configuredDirectory = input.environment.OPENCODE_CONFIG_DIR
87+
? path.resolve(input.directory, expandHomePath(input.environment.OPENCODE_CONFIG_DIR, home))
88+
: undefined;
8589
const configHome = input.environment.XDG_CONFIG_HOME;
90+
// 2.x replaces the global directory; 1.x loads this directory after the explicit config file.
8691
const globalDirectory =
87-
configHome !== undefined && configHome.trim().length > 0
88-
? path.join(configHome, "opencode")
89-
: path.join(home, ".config", "opencode");
90-
const directories = [
91-
globalDirectory,
92-
...(input.environment.OPENCODE_CONFIG_DIR === undefined
93-
? []
94-
: [
95-
path.resolve(
96-
input.directory,
97-
expandHomePath(input.environment.OPENCODE_CONFIG_DIR, home),
98-
),
99-
]),
100-
];
101-
for (const directory of directories) {
102-
for (const name of ["config.json", "opencode.json", "opencode.jsonc"]) {
103-
routing = mergeConfig(routing, yield* readConfig(path.join(directory, name)));
104-
}
92+
input.generation === "v2" && configuredDirectory !== undefined
93+
? configuredDirectory
94+
: configHome !== undefined && configHome.trim().length > 0
95+
? path.join(configHome, "opencode")
96+
: path.join(home, ".config", "opencode");
97+
const configNames = ["opencode.json", "opencode.jsonc"];
98+
for (const name of [...(input.generation === "v1" ? ["config.json"] : []), ...configNames]) {
99+
routing = mergeConfig(routing, yield* readConfig(path.join(globalDirectory, name)));
105100
}
106-
if (input.environment.OPENCODE_CONFIG !== undefined) {
101+
if (input.environment.OPENCODE_CONFIG) {
107102
const configuredPath = path.resolve(
108103
input.directory,
109104
expandHomePath(input.environment.OPENCODE_CONFIG, home),
110105
);
111106
routing = mergeConfig(routing, yield* readConfig(configuredPath));
112107
}
113-
if (input.environment.OPENCODE_CONFIG_CONTENT !== undefined) {
108+
if (input.generation === "v1" && configuredDirectory !== undefined) {
109+
for (const name of configNames) {
110+
routing = mergeConfig(routing, yield* readConfig(path.join(configuredDirectory, name)));
111+
}
112+
}
113+
if (input.environment.OPENCODE_CONFIG_CONTENT) {
114114
const content = readRoutingConfig(
115115
input.environment.OPENCODE_CONFIG_CONTENT,
116116
resolveFile(input.directory),

‎packages/provider-opencode/src/server/textGeneration.test.ts‎

Lines changed: 128 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import * as Duration from "effect/Duration";
99
import * as Effect from "effect/Effect";
1010
import * as Exit from "effect/Exit";
1111
import * as Fiber from "effect/Fiber";
12+
import * as FileSystem from "effect/FileSystem";
1213
import * as Layer from "effect/Layer";
1314
import * as Schema from "effect/Schema";
1415
import * as TestClock from "effect/testing/TestClock";
@@ -282,20 +283,21 @@ function withOpenCodeTextGeneration<A, E, R>(
282283
settings: OpenCodeSettings,
283284
effectFn: (textGeneration: ProviderTextGenerationRuntime) => Effect.Effect<A, E, R>,
284285
environment?: NodeJS.ProcessEnv,
286+
directory = process.cwd(),
285287
) {
286288
return Effect.gen(function* () {
287289
const serverOwner = yield* OpenCodeServerOwner.make({
288290
binaryPath: settings.binaryPath,
289-
directory: process.cwd(),
291+
directory,
290292
...(settingsSecretLiteral(settings.serverPassword)
291293
? { serverPassword: settingsSecretLiteral(settings.serverPassword) }
292294
: {}),
293295
...(environment ? { environment } : {}),
294296
});
295-
const textGeneration = yield* OpenCodeTextGeneration.makeOpenCodeTextGeneration(
296-
settings,
297-
environment ? { environment } : {},
298-
).pipe(Effect.provideService(OpenCodeServerOwner.OpenCodeServerOwner, serverOwner));
297+
const textGeneration = yield* OpenCodeTextGeneration.makeOpenCodeTextGeneration(settings, {
298+
directory,
299+
...(environment ? { environment } : {}),
300+
}).pipe(Effect.provideService(OpenCodeServerOwner.OpenCodeServerOwner, serverOwner));
299301
return yield* effectFn(textGeneration);
300302
}).pipe(Effect.scoped);
301303
}
@@ -470,66 +472,134 @@ it.layer(layerOpenCodeTextGenerationTest)("OpenCodeTextGeneration", (it) => {
470472
),
471473
);
472474

473-
it.effect("isolates blind evaluation from shared servers, tools, and custom instructions", () =>
474-
withOpenCodeTextGeneration(
475-
DEFAULT_OPENCODE_SETTINGS,
476-
(textGeneration) =>
477-
Effect.gen(function* () {
478-
runtimeMock.state.promptResult = {
479-
data: {
480-
info: { providerID: "openai", modelID: "gpt-5", variant: "high" },
481-
parts: [
482-
{ type: "step-start" },
483-
{ type: "reasoning", text: "Check the evidence." },
484-
{ type: "text", text: JSON.stringify(VALID_EVALUATION) },
485-
{ type: "step-finish", reason: "stop" },
486-
{ type: "snapshot", snapshot: "unchanged" },
487-
],
488-
},
489-
};
475+
it.effect.each([
476+
"configured overrides",
477+
"empty file override",
478+
"empty directory override",
479+
"empty inline override",
480+
])(
481+
"isolates blind evaluation from shared servers, tools, and custom instructions (%s)",
482+
(overrides) =>
483+
Effect.gen(function* () {
484+
const fs = yield* FileSystem.FileSystem;
485+
const directory = yield* fs.makeTempDirectoryScoped({ prefix: "t3-opencode-routing-" });
486+
const configHome = `${directory}/global`;
487+
const configDirectory = `${directory}/override`;
488+
const configFile = `${directory}/explicit.json`;
489+
yield* fs.makeDirectory(`${configHome}/opencode`, { recursive: true });
490+
yield* fs.makeDirectory(configDirectory);
491+
yield* fs.writeFileString(
492+
`${configHome}/opencode/config.json`,
493+
JSON.stringify({
494+
provider: { openai: { options: { apiKey: "global-key" } } },
495+
enabled_providers: ["openai"],
496+
disabled_providers: ["global-disabled"],
497+
}),
498+
);
499+
yield* fs.writeFileString(
500+
`${directory}/opencode.json`,
501+
JSON.stringify({
502+
provider: { openai: { options: { baseURL: "http://ignored-cwd.internal" } } },
503+
}),
504+
);
505+
yield* fs.writeFileString(
506+
configFile,
507+
JSON.stringify({
508+
provider: { openai: { options: { baseURL: "http://explicit.internal" } } },
509+
}),
510+
);
511+
yield* fs.writeFileString(
512+
`${configDirectory}/config.json`,
513+
JSON.stringify({ disabled_providers: ["ignored-directory-file"] }),
514+
);
515+
yield* fs.writeFileString(
516+
`${configDirectory}/opencode.json`,
517+
JSON.stringify({
518+
provider: { openai: { options: { baseURL: "http://directory-json.internal" } } },
519+
}),
520+
);
521+
yield* fs.writeFileString(
522+
`${configDirectory}/opencode.jsonc`,
523+
JSON.stringify({
524+
provider: { openai: { options: { baseURL: "http://directory-jsonc.internal" } } },
525+
}),
526+
);
527+
return yield* withOpenCodeTextGeneration(
528+
DEFAULT_OPENCODE_SETTINGS,
529+
(textGeneration) =>
530+
Effect.gen(function* () {
531+
runtimeMock.state.promptResult = {
532+
data: {
533+
info: { providerID: "openai", modelID: "gpt-5", variant: "high" },
534+
parts: [
535+
{ type: "step-start" },
536+
{ type: "reasoning", text: "Check the evidence." },
537+
{ type: "text", text: JSON.stringify(VALID_EVALUATION) },
538+
{ type: "step-finish", reason: "stop" },
539+
{ type: "snapshot", snapshot: "unchanged" },
540+
],
541+
},
542+
};
490543

491-
const verdict = yield* textGeneration.generateModelEvaluation({
492-
...DEFAULT_EVALUATION_INPUT,
493-
modelSelection: {
494-
...DEFAULT_TEST_MODEL_SELECTION,
495-
options: [{ id: "variant", value: "high" }],
496-
},
497-
});
544+
const verdict = yield* textGeneration.generateModelEvaluation({
545+
...DEFAULT_EVALUATION_INPUT,
546+
modelSelection: {
547+
...DEFAULT_TEST_MODEL_SELECTION,
548+
options: [{ id: "variant", value: "high" }],
549+
},
550+
});
498551

499-
expect(verdict).toEqual(VALID_EVALUATION);
500-
expect(textGeneration.supportedOperations).toContain("generateModelEvaluation");
501-
expect(runtimeMock.state.promptUrls).toEqual(["http://127.0.0.1:4301"]);
502-
expect(runtimeMock.state.promptInputs[0]).toMatchObject({
503-
model: { providerID: "openai", modelID: "gpt-5" },
504-
variant: "high",
505-
});
506-
expect(runtimeMock.state.sessionCreateInputs[0]?.permission).toEqual([
507-
{ permission: "*", pattern: "*", action: "deny" },
508-
]);
509-
const environment = runtimeMock.state.startEnvironments[0];
510-
expect(environment?.XDG_CONFIG_HOME).not.toBe("/user-config");
511-
expect(environment?.OPENCODE_CONFIG).toBeUndefined();
512-
expect(environment?.OPENCODE_DISABLE_PROJECT_CONFIG).toBe("1");
513-
expect(environment?.OPENCODE_DISABLE_CLAUDE_CODE).toBe("1");
514-
expect(JSON.parse(environment?.OPENCODE_CONFIG_CONTENT ?? "null")).toEqual({
515-
provider: { custom: { options: { baseURL: "http://model.internal" } } },
516-
enabled_providers: ["custom"],
517-
});
518-
expect(runtimeMock.state.closeCalls).toEqual(["http://127.0.0.1:4301"]);
519-
}),
520-
{
521-
XDG_CONFIG_HOME: "/user-config",
522-
OPENCODE_CONFIG: "/user-config/opencode.json",
523-
OPENCODE_CONFIG_CONTENT: `{
552+
expect(verdict).toEqual(VALID_EVALUATION);
553+
expect(textGeneration.supportedOperations).toContain("generateModelEvaluation");
554+
expect(runtimeMock.state.promptUrls).toEqual(["http://127.0.0.1:4301"]);
555+
expect(runtimeMock.state.promptInputs[0]).toMatchObject({
556+
model: { providerID: "openai", modelID: "gpt-5" },
557+
variant: "high",
558+
});
559+
expect(runtimeMock.state.sessionCreateInputs[0]?.permission).toEqual([
560+
{ permission: "*", pattern: "*", action: "deny" },
561+
]);
562+
const environment = runtimeMock.state.startEnvironments[0];
563+
expect(environment?.XDG_CONFIG_HOME).not.toBe(configHome);
564+
expect(environment?.OPENCODE_CONFIG).toBeUndefined();
565+
expect(environment?.OPENCODE_DISABLE_PROJECT_CONFIG).toBe("1");
566+
expect(environment?.OPENCODE_DISABLE_CLAUDE_CODE).toBe("1");
567+
expect(JSON.parse(environment?.OPENCODE_CONFIG_CONTENT ?? "null")).toEqual({
568+
provider: {
569+
openai: {
570+
options: {
571+
baseURL:
572+
overrides === "empty directory override"
573+
? "http://explicit.internal"
574+
: "http://directory-jsonc.internal",
575+
apiKey: overrides === "empty inline override" ? "global-key" : "inline-key",
576+
},
577+
},
578+
},
579+
enabled_providers: ["openai"],
580+
disabled_providers: ["global-disabled"],
581+
});
582+
expect(runtimeMock.state.closeCalls).toEqual(["http://127.0.0.1:4301"]);
583+
}),
584+
{
585+
XDG_CONFIG_HOME: configHome,
586+
OPENCODE_CONFIG: overrides === "empty file override" ? "" : configFile,
587+
OPENCODE_CONFIG_DIR: overrides === "empty directory override" ? "" : configDirectory,
588+
OPENCODE_CONFIG_CONTENT:
589+
overrides === "empty inline override"
590+
? ""
591+
: `{
524592
// Routing survives, but customization does not.
525-
"provider": { "custom": { "options": { "baseURL": "http://model.internal" } } },
526-
"enabled_providers": ["custom"],
593+
"provider": { "openai": { "options": { "apiKey": "inline-key" } } },
594+
"enabled_providers": ["openai"],
527595
"instructions": ["/secret/instructions.md"],
528596
"plugin": ["secret-plugin"],
529597
"mcp": { "private": { "type": "remote", "url": "http://private" } }
530598
}`,
531-
},
532-
),
599+
},
600+
directory,
601+
);
602+
}).pipe(Effect.scoped),
533603
);
534604

535605
it.effect("fails blind evaluation instead of dropping an unavailable or invalid variant", () =>

‎packages/provider-opencode/src/server/textGeneration.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,7 @@ export const makeOpenCodeTextGeneration = Effect.fn("makeOpenCodeTextGeneration"
404404

405405
const isolatedOutput = Effect.gen(function* () {
406406
const environment = yield* BlindEvaluation.makeBlindEvaluationEnvironment({
407+
generation: "v1",
407408
operation: input.operation,
408409
directory: processDirectory,
409410
environment: processEnvironment,

0 commit comments

Comments
 (0)