Skip to content

Instantly share code, notes, and snippets.

@scode
Last active March 7, 2026 04:45
Show Gist options
  • Select an option

  • Save scode/3c872a5a1a3257f3c627d0c6d2c5729c to your computer and use it in GitHub Desktop.

Select an option

Save scode/3c872a5a1a3257f3c627d0c6d2c5729c to your computer and use it in GitHub Desktop.
commit fdfaf8150288279683b3441f80ac665bd44cbd93
Author: Peter Schuller <peter.schuller@infidyne.com>
Date: Fri Mar 6 20:45:11 2026 -0800
ai slop w/o steering or validating at all: "fix" https://github.com/pingdotgg/t3code/issues/263
diff --git a/apps/server/src/checkpointing/Utils.test.ts b/apps/server/src/checkpointing/Utils.test.ts
new file mode 100644
index 00000000..559b4b3e
--- /dev/null
+++ b/apps/server/src/checkpointing/Utils.test.ts
@@ -0,0 +1,42 @@
+import assert from "node:assert/strict";
+import { describe, it } from "vitest";
+import { ProjectId } from "@t3tools/contracts";
+import { resolveThreadWorkspaceCwd } from "./Utils";
+
+describe("resolveThreadWorkspaceCwd", () => {
+ it("expands tilde-prefixed worktree paths", () => {
+ const cwd = resolveThreadWorkspaceCwd({
+ thread: {
+ projectId: ProjectId.makeUnsafe("project-1"),
+ worktreePath: "~/git/saltybox",
+ },
+ projects: [
+ {
+ id: ProjectId.makeUnsafe("project-1"),
+ workspaceRoot: "/tmp/unused",
+ },
+ ],
+ });
+
+ assert.equal(cwd?.startsWith("/"), true);
+ assert.equal(cwd?.includes("git/saltybox"), true);
+ });
+
+ it("expands tilde-prefixed workspace roots", () => {
+ const cwd = resolveThreadWorkspaceCwd({
+ thread: {
+ projectId: ProjectId.makeUnsafe("project-1"),
+ worktreePath: null,
+ },
+ projects: [
+ {
+ id: ProjectId.makeUnsafe("project-1"),
+ workspaceRoot: "~/git/saltybox",
+ },
+ ],
+ });
+
+ assert.equal(cwd?.startsWith("/"), true);
+ assert.equal(cwd?.includes("git/saltybox"), true);
+ });
+});
diff --git a/apps/server/src/checkpointing/Utils.ts b/apps/server/src/checkpointing/Utils.ts
index 3cd92f85..c803a928 100644
--- a/apps/server/src/checkpointing/Utils.ts
+++ b/apps/server/src/checkpointing/Utils.ts
@@ -1,5 +1,6 @@
import { Encoding } from "effect";
import { CheckpointRef, ProjectId, type ThreadId } from "@t3tools/contracts";
+import { expandHomePathSync } from "../os-jank";
export const CHECKPOINT_REFS_PREFIX = "refs/t3/checkpoints";
@@ -21,8 +22,10 @@ export function resolveThreadWorkspaceCwd(input: {
}): string | undefined {
const worktreeCwd = input.thread.worktreePath ?? undefined;
if (worktreeCwd) {
- return worktreeCwd;
+ return expandHomePathSync(worktreeCwd);
}
- return input.projects.find((project) => project.id === input.thread.projectId)?.workspaceRoot;
+ const workspaceRoot = input.projects.find((project) => project.id === input.thread.projectId)
+ ?.workspaceRoot;
+ return workspaceRoot ? expandHomePathSync(workspaceRoot) : undefined;
}
diff --git a/apps/server/src/codexAppServerManager.ts b/apps/server/src/codexAppServerManager.ts
index c80b9f75..b98e6378 100644
--- a/apps/server/src/codexAppServerManager.ts
+++ b/apps/server/src/codexAppServerManager.ts
@@ -21,6 +21,7 @@ import {
} from "@t3tools/contracts";
import { normalizeModelSlug } from "@t3tools/shared/model";
import { Effect, ServiceMap } from "effect";
+import { expandHomePathSync } from "./os-jank";
type PendingRequestKey = string;
@@ -519,7 +520,7 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve
let context: CodexSessionContext | undefined;
try {
- const resolvedCwd = input.cwd ?? process.cwd();
+ const resolvedCwd = expandHomePathSync(input.cwd ?? process.cwd());
const session: ProviderSession = {
provider: "codex",
@@ -533,8 +534,10 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve
};
const codexOptions = readCodexProviderOptions(input);
- const codexBinaryPath = codexOptions.binaryPath ?? "codex";
- const codexHomePath = codexOptions.homePath;
+ const codexBinaryPath = expandHomePathSync(codexOptions.binaryPath ?? "codex");
+ const codexHomePath = codexOptions.homePath
+ ? expandHomePathSync(codexOptions.homePath)
+ : undefined;
const child = spawn(codexBinaryPath, ["app-server"], {
cwd: resolvedCwd,
env: {
diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts
index 4f352435..95cd588c 100644
--- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts
+++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts
@@ -344,6 +344,45 @@ describe("ProviderCommandReactor", () => {
});
});
+ it("forwards codex launch options through session start", async () => {
+ const harness = await createHarness();
+ const now = new Date().toISOString();
+
+ await Effect.runPromise(
+ harness.engine.dispatch({
+ type: "thread.turn.start",
+ commandId: CommandId.makeUnsafe("cmd-turn-start-provider-options"),
+ threadId: ThreadId.makeUnsafe("thread-1"),
+ message: {
+ messageId: asMessageId("user-message-provider-options"),
+ role: "user",
+ text: "hello custom codex",
+ attachments: [],
+ },
+ provider: "codex",
+ providerOptions: {
+ codex: {
+ binaryPath: "/opt/homebrew/bin/codex",
+ homePath: "/tmp/custom-codex-home",
+ },
+ },
+ interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
+ runtimeMode: "approval-required",
+ createdAt: now,
+ }),
+ );
+
+ await waitFor(() => harness.startSession.mock.calls.length === 1);
+ expect(harness.startSession.mock.calls[0]?.[1]).toMatchObject({
+ providerOptions: {
+ codex: {
+ binaryPath: "/opt/homebrew/bin/codex",
+ homePath: "/tmp/custom-codex-home",
+ },
+ },
+ });
+ });
+
it("forwards plan interaction mode to the provider turn request", async () => {
const harness = await createHarness();
const now = new Date().toISOString();
diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
index d34791bc..ae4617ff 100644
--- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
+++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
@@ -3,6 +3,7 @@ import {
CommandId,
EventId,
type OrchestrationEvent,
+ type ProviderStartOptions,
type ProviderModelOptions,
type ProviderKind,
type ProviderServiceTier,
@@ -202,6 +203,7 @@ const make = Effect.gen(function* () {
readonly provider?: ProviderKind;
readonly model?: string;
readonly modelOptions?: ProviderModelOptions;
+ readonly providerOptions?: ProviderStartOptions;
readonly serviceTier?: ProviderServiceTier | null;
},
) {
@@ -239,6 +241,9 @@ const make = Effect.gen(function* () {
...(desiredModel ? { model: desiredModel } : {}),
...(options?.serviceTier !== undefined ? { serviceTier: options.serviceTier } : {}),
...(options?.modelOptions !== undefined ? { modelOptions: options.modelOptions } : {}),
+ ...(options?.providerOptions !== undefined
+ ? { providerOptions: options.providerOptions }
+ : {}),
...(input?.resumeCursor !== undefined ? { resumeCursor: input.resumeCursor } : {}),
runtimeMode: desiredRuntimeMode,
});
@@ -325,6 +330,7 @@ const make = Effect.gen(function* () {
readonly model?: string;
readonly serviceTier?: ProviderServiceTier | null;
readonly modelOptions?: ProviderModelOptions;
+ readonly providerOptions?: ProviderStartOptions;
readonly interactionMode?: "default" | "plan";
readonly createdAt: string;
}) {
@@ -337,6 +343,7 @@ const make = Effect.gen(function* () {
...(input.model !== undefined ? { model: input.model } : {}),
...(input.serviceTier !== undefined ? { serviceTier: input.serviceTier } : {}),
...(input.modelOptions !== undefined ? { modelOptions: input.modelOptions } : {}),
+ ...(input.providerOptions !== undefined ? { providerOptions: input.providerOptions } : {}),
});
const normalizedInput = toNonEmptyProviderInput(input.messageText);
const normalizedAttachments = input.attachments ?? [];
@@ -472,6 +479,9 @@ const make = Effect.gen(function* () {
...(event.payload.model !== undefined ? { model: event.payload.model } : {}),
...(event.payload.serviceTier !== undefined ? { serviceTier: event.payload.serviceTier } : {}),
...(event.payload.modelOptions !== undefined ? { modelOptions: event.payload.modelOptions } : {}),
+ ...(event.payload.providerOptions !== undefined
+ ? { providerOptions: event.payload.providerOptions }
+ : {}),
interactionMode: event.payload.interactionMode,
createdAt: event.payload.createdAt,
});
diff --git a/apps/server/src/orchestration/decider.ts b/apps/server/src/orchestration/decider.ts
index f0364164..47bfc3d9 100644
--- a/apps/server/src/orchestration/decider.ts
+++ b/apps/server/src/orchestration/decider.ts
@@ -303,6 +303,9 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
...(command.model !== undefined ? { model: command.model } : {}),
...(command.serviceTier !== undefined ? { serviceTier: command.serviceTier } : {}),
...(command.modelOptions !== undefined ? { modelOptions: command.modelOptions } : {}),
+ ...(command.providerOptions !== undefined
+ ? { providerOptions: command.providerOptions }
+ : {}),
assistantDeliveryMode: command.assistantDeliveryMode ?? DEFAULT_ASSISTANT_DELIVERY_MODE,
runtimeMode:
readModel.threads.find((entry) => entry.id === command.threadId)?.runtimeMode ??
diff --git a/apps/server/src/os-jank.ts b/apps/server/src/os-jank.ts
index 3f5e2129..65593bb1 100644
--- a/apps/server/src/os-jank.ts
+++ b/apps/server/src/os-jank.ts
@@ -2,6 +2,18 @@ import * as OS from "node:os";
import { Effect, Path } from "effect";
import { execFileSync } from "node:child_process";
+export function expandHomePathSync(input: string): string {
+ if (input === "~") {
+ return OS.homedir();
+ }
+
+ if (input.startsWith("~/") || input.startsWith("~\\")) {
+ return `${OS.homedir()}${input.slice(1)}`;
+ }
+
+ return input;
+}
+
export function fixPath(): void {
if (process.platform !== "darwin") return;
@@ -20,14 +32,7 @@ export function fixPath(): void {
}
export const expandHomePath = Effect.fn(function* (input: string) {
- const { join, sep } = yield* Path.Path;
- if (input === "~") {
- return OS.homedir();
- }
- if (input.startsWith(`~${sep}`)) {
- return join(OS.homedir(), input.slice(sep.length));
- }
- return input;
+ return expandHomePathSync(input);
});
export const resolveStateDir = Effect.fn(function* (raw: string | undefined) {
diff --git a/apps/server/src/wsServer.test.ts b/apps/server/src/wsServer.test.ts
index 2c30b7cc..98da500f 100644
--- a/apps/server/src/wsServer.test.ts
+++ b/apps/server/src/wsServer.test.ts
@@ -1143,6 +1143,61 @@ describe("WebSocket Server", () => {
expect(response.error?.message).toContain("exceeds current turn count");
});
+ it("expands tilde-prefixed workspace and worktree paths in dispatched commands", async () => {
+ server = await createTestServer({ cwd: "/test" });
+ const addr = server.address();
+ const port = typeof addr === "object" && addr !== null ? addr.port : 0;
+
+ const ws = await connectWs(port);
+ connections.push(ws);
+ await waitForMessage(ws);
+
+ const createdAt = new Date().toISOString();
+ const projectRoot = "~/tmp/ws-tilde-project";
+ const worktreePath = "~/tmp/ws-tilde-worktree";
+
+ const createProjectResponse = await sendRequest(ws, ORCHESTRATION_WS_METHODS.dispatchCommand, {
+ type: "project.create",
+ commandId: "cmd-tilde-project-create",
+ projectId: "project-tilde",
+ title: "Tilde Project",
+ workspaceRoot: projectRoot,
+ defaultModel: "gpt-5-codex",
+ createdAt,
+ });
+ expect(createProjectResponse.error).toBeUndefined();
+
+ const createThreadResponse = await sendRequest(ws, ORCHESTRATION_WS_METHODS.dispatchCommand, {
+ type: "thread.create",
+ commandId: "cmd-tilde-thread-create",
+ threadId: "thread-tilde",
+ projectId: "project-tilde",
+ title: "Tilde Thread",
+ model: "gpt-5-codex",
+ runtimeMode: "full-access",
+ interactionMode: "default",
+ branch: null,
+ worktreePath,
+ createdAt,
+ });
+ expect(createThreadResponse.error).toBeUndefined();
+
+ const snapshotResponse = await sendRequest(ws, ORCHESTRATION_WS_METHODS.getSnapshot, {});
+ expect(snapshotResponse.error).toBeUndefined();
+
+ const snapshot = snapshotResponse.result as
+ | {
+ projects?: Array<{ id: string; workspaceRoot: string }>;
+ threads?: Array<{ id: string; worktreePath: string | null }>;
+ }
+ | undefined;
+ const project = snapshot?.projects?.find((entry: { id: string }) => entry.id === "project-tilde");
+ const thread = snapshot?.threads?.find((entry: { id: string }) => entry.id === "thread-tilde");
+
+ expect(project?.workspaceRoot).toBe(path.join(os.homedir(), "tmp/ws-tilde-project"));
+ expect(thread?.worktreePath).toBe(path.join(os.homedir(), "tmp/ws-tilde-worktree"));
+ });
+
it("keeps orchestration domain push behavior for provider runtime events", async () => {
const runtimeEventPubSub = Effect.runSync(PubSub.unbounded<ProviderRuntimeEvent>());
const emitRuntimeEvent = (event: ProviderRuntimeEvent) => {
diff --git a/apps/server/src/wsServer.ts b/apps/server/src/wsServer.ts
index 934ff92a..a1bdbbbf 100644
--- a/apps/server/src/wsServer.ts
+++ b/apps/server/src/wsServer.ts
@@ -72,6 +72,7 @@ import {
} from "./attachmentStore.ts";
import { parseBase64DataUrl } from "./imageMime.ts";
import { AnalyticsService } from "./telemetry/Services/AnalyticsService.ts";
+import { expandHomePath } from "./os-jank";
/**
* ServerShape - Service API for server lifecycle control.
@@ -298,83 +299,136 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return<
const normalizeDispatchCommand = Effect.fnUntraced(function* (input: {
readonly command: ClientOrchestrationCommand;
}) {
- if (input.command.type !== "thread.turn.start") {
- return input.command as OrchestrationCommand;
- }
- const turnStartCommand = input.command;
-
- const normalizedAttachments = yield* Effect.forEach(
- turnStartCommand.message.attachments,
- (attachment) =>
- Effect.gen(function* () {
- const parsed = parseBase64DataUrl(attachment.dataUrl);
- if (!parsed || !parsed.mimeType.startsWith("image/")) {
- return yield* new RouteRequestError({
- message: `Invalid image attachment payload for '${attachment.name}'.`,
- });
- }
+ const normalizeAbsolutePath = (value: string) =>
+ expandHomePath(value.trim()).pipe(Effect.map((expanded) => path.resolve(expanded)));
+ const normalizeOptionalAbsolutePath = (value: string | null) =>
+ value === null ? Effect.succeed(null) : normalizeAbsolutePath(value);
- const bytes = Buffer.from(parsed.base64, "base64");
- if (bytes.byteLength === 0 || bytes.byteLength > PROVIDER_SEND_TURN_MAX_IMAGE_BYTES) {
- return yield* new RouteRequestError({
- message: `Image attachment '${attachment.name}' is empty or too large.`,
- });
- }
+ switch (input.command.type) {
+ case "project.create":
+ return {
+ ...input.command,
+ workspaceRoot: yield* normalizeAbsolutePath(input.command.workspaceRoot),
+ } satisfies OrchestrationCommand;
+ case "project.meta.update":
+ return {
+ ...input.command,
+ ...(input.command.workspaceRoot !== undefined
+ ? { workspaceRoot: yield* normalizeAbsolutePath(input.command.workspaceRoot) }
+ : {}),
+ } satisfies OrchestrationCommand;
+ case "thread.create":
+ return {
+ ...input.command,
+ worktreePath: yield* normalizeOptionalAbsolutePath(input.command.worktreePath),
+ } satisfies OrchestrationCommand;
+ case "thread.meta.update":
+ return {
+ ...input.command,
+ ...(input.command.worktreePath !== undefined
+ ? { worktreePath: yield* normalizeOptionalAbsolutePath(input.command.worktreePath) }
+ : {}),
+ } satisfies OrchestrationCommand;
+ case "thread.turn.start": {
+ const turnStartCommand = input.command;
+ const normalizedAttachments = yield* Effect.forEach(
+ turnStartCommand.message.attachments,
+ (attachment) =>
+ Effect.gen(function* () {
+ const parsed = parseBase64DataUrl(attachment.dataUrl);
+ if (!parsed || !parsed.mimeType.startsWith("image/")) {
+ return yield* new RouteRequestError({
+ message: `Invalid image attachment payload for '${attachment.name}'.`,
+ });
+ }
- const attachmentId = createAttachmentId(turnStartCommand.threadId);
- if (!attachmentId) {
- return yield* new RouteRequestError({
- message: "Failed to create a safe attachment id.",
- });
- }
+ const bytes = Buffer.from(parsed.base64, "base64");
+ if (bytes.byteLength === 0 || bytes.byteLength > PROVIDER_SEND_TURN_MAX_IMAGE_BYTES) {
+ return yield* new RouteRequestError({
+ message: `Image attachment '${attachment.name}' is empty or too large.`,
+ });
+ }
- const persistedAttachment = {
- type: "image" as const,
- id: attachmentId,
- name: attachment.name,
- mimeType: parsed.mimeType.toLowerCase(),
- sizeBytes: bytes.byteLength,
- };
-
- const attachmentPath = resolveAttachmentPath({
- stateDir: serverConfig.stateDir,
- attachment: persistedAttachment,
- });
- if (!attachmentPath) {
- return yield* new RouteRequestError({
- message: `Failed to resolve persisted path for '${attachment.name}'.`,
- });
- }
+ const attachmentId = createAttachmentId(turnStartCommand.threadId);
+ if (!attachmentId) {
+ return yield* new RouteRequestError({
+ message: "Failed to create a safe attachment id.",
+ });
+ }
- yield* fileSystem.makeDirectory(path.dirname(attachmentPath), { recursive: true }).pipe(
- Effect.mapError(
- () =>
- new RouteRequestError({
- message: `Failed to create attachment directory for '${attachment.name}'.`,
- }),
- ),
- );
- yield* fileSystem.writeFile(attachmentPath, bytes).pipe(
- Effect.mapError(
- () =>
- new RouteRequestError({
- message: `Failed to persist attachment '${attachment.name}'.`,
- }),
- ),
- );
+ const persistedAttachment = {
+ type: "image" as const,
+ id: attachmentId,
+ name: attachment.name,
+ mimeType: parsed.mimeType.toLowerCase(),
+ sizeBytes: bytes.byteLength,
+ };
- return persistedAttachment;
- }),
- { concurrency: 1 },
- );
+ const attachmentPath = resolveAttachmentPath({
+ stateDir: serverConfig.stateDir,
+ attachment: persistedAttachment,
+ });
+ if (!attachmentPath) {
+ return yield* new RouteRequestError({
+ message: `Failed to resolve persisted path for '${attachment.name}'.`,
+ });
+ }
- return {
- ...turnStartCommand,
- message: {
- ...turnStartCommand.message,
- attachments: normalizedAttachments,
- },
- } satisfies OrchestrationCommand;
+ yield* fileSystem.makeDirectory(path.dirname(attachmentPath), { recursive: true }).pipe(
+ Effect.mapError(
+ () =>
+ new RouteRequestError({
+ message: `Failed to create attachment directory for '${attachment.name}'.`,
+ }),
+ ),
+ );
+ yield* fileSystem.writeFile(attachmentPath, bytes).pipe(
+ Effect.mapError(
+ () =>
+ new RouteRequestError({
+ message: `Failed to persist attachment '${attachment.name}'.`,
+ }),
+ ),
+ );
+
+ return persistedAttachment;
+ }),
+ { concurrency: 1 },
+ );
+
+ return {
+ ...turnStartCommand,
+ ...(turnStartCommand.providerOptions?.codex
+ ? {
+ providerOptions: {
+ codex: {
+ ...(turnStartCommand.providerOptions.codex.binaryPath
+ ? {
+ binaryPath: yield* expandHomePath(
+ turnStartCommand.providerOptions.codex.binaryPath,
+ ),
+ }
+ : {}),
+ ...(turnStartCommand.providerOptions.codex.homePath
+ ? {
+ homePath: yield* expandHomePath(
+ turnStartCommand.providerOptions.codex.homePath,
+ ),
+ }
+ : {}),
+ },
+ },
+ }
+ : {}),
+ message: {
+ ...turnStartCommand.message,
+ attachments: normalizedAttachments,
+ },
+ } satisfies OrchestrationCommand;
+ }
+ default:
+ return input.command as OrchestrationCommand;
+ }
});
// HTTP server — serves static files or redirects to Vite dev server
diff --git a/apps/web/src/appSettings.ts b/apps/web/src/appSettings.ts
index e58f7af4..f5df5ad1 100644
--- a/apps/web/src/appSettings.ts
+++ b/apps/web/src/appSettings.ts
@@ -1,6 +1,10 @@
import { useCallback, useSyncExternalStore } from "react";
import { Option, Schema } from "effect";
-import { type ProviderKind, type ProviderServiceTier } from "@t3tools/contracts";
+import {
+ type ProviderKind,
+ type ProviderServiceTier,
+ type ProviderStartOptions,
+} from "@t3tools/contracts";
import { getDefaultModel, getModelOptions, normalizeModelSlug } from "@t3tools/shared/model";
const APP_SETTINGS_STORAGE_KEY = "t3code:app-settings:v1";
@@ -57,6 +61,21 @@ export function resolveAppServiceTier(serviceTier: AppServiceTier): ProviderServ
return serviceTier === "auto" ? null : serviceTier;
}
+export function resolveCodexProviderOptions(settings: AppSettings): ProviderStartOptions | undefined {
+ const binaryPath = settings.codexBinaryPath.trim();
+ const homePath = settings.codexHomePath.trim();
+ if (binaryPath.length === 0 && homePath.length === 0) {
+ return undefined;
+ }
+
+ return {
+ codex: {
+ ...(binaryPath.length > 0 ? { binaryPath } : {}),
+ ...(homePath.length > 0 ? { homePath } : {}),
+ },
+ };
+}
+
export function shouldShowFastTierIcon(
model: string | null | undefined,
serviceTier: AppServiceTier,
diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx
index 46231694..b154bb4d 100644
--- a/apps/web/src/components/ChatView.tsx
+++ b/apps/web/src/components/ChatView.tsx
@@ -201,6 +201,7 @@ import {
getAppModelOptions,
resolveAppModelSelection,
resolveAppServiceTier,
+ resolveCodexProviderOptions,
shouldShowFastTierIcon,
type AppServiceTier,
useAppSettings,
@@ -573,6 +574,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
const setStoreThreadError = useStore((store) => store.setError);
const setStoreThreadBranch = useStore((store) => store.setThreadBranch);
const { settings } = useAppSettings();
+ const codexProviderOptionsForDispatch = resolveCodexProviderOptions(settings);
const navigate = useNavigate();
const rawSearch = useSearch({
strict: false,
@@ -2564,6 +2566,9 @@ export default function ChatView({ threadId }: ChatViewProps) {
...(selectedModelOptionsForDispatch
? { modelOptions: selectedModelOptionsForDispatch }
: {}),
+ ...(codexProviderOptionsForDispatch
+ ? { providerOptions: codexProviderOptionsForDispatch }
+ : {}),
provider: selectedProvider,
assistantDeliveryMode: settings.enableAssistantStreaming ? "streaming" : "buffered",
runtimeMode,
@@ -2839,6 +2844,9 @@ export default function ChatView({ threadId }: ChatViewProps) {
...(selectedModelOptionsForDispatch
? { modelOptions: selectedModelOptionsForDispatch }
: {}),
+ ...(codexProviderOptionsForDispatch
+ ? { providerOptions: codexProviderOptionsForDispatch }
+ : {}),
assistantDeliveryMode: settings.enableAssistantStreaming ? "streaming" : "buffered",
runtimeMode,
interactionMode: nextInteractionMode,
@@ -2868,6 +2876,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
runtimeMode,
selectedModel,
selectedModelOptionsForDispatch,
+ codexProviderOptionsForDispatch,
selectedProvider,
setComposerDraftInteractionMode,
setThreadError,
@@ -2938,6 +2947,9 @@ export default function ChatView({ threadId }: ChatViewProps) {
...(selectedModelOptionsForDispatch
? { modelOptions: selectedModelOptionsForDispatch }
: {}),
+ ...(codexProviderOptionsForDispatch
+ ? { providerOptions: codexProviderOptionsForDispatch }
+ : {}),
assistantDeliveryMode: settings.enableAssistantStreaming ? "streaming" : "buffered",
runtimeMode,
interactionMode: "default",
@@ -2985,6 +2997,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
runtimeMode,
selectedModel,
selectedModelOptionsForDispatch,
+ codexProviderOptionsForDispatch,
selectedProvider,
settings.enableAssistantStreaming,
syncServerReadModel,
diff --git a/packages/contracts/src/index.ts b/packages/contracts/src/index.ts
index 0f37a935..436a01e7 100644
--- a/packages/contracts/src/index.ts
+++ b/packages/contracts/src/index.ts
@@ -2,6 +2,7 @@ export * from "./baseSchemas";
export * from "./ipc";
export * from "./terminal";
export * from "./provider";
+export * from "./providerOptions";
export * from "./providerRuntime";
export * from "./model";
export * from "./ws";
diff --git a/packages/contracts/src/orchestration.test.ts b/packages/contracts/src/orchestration.test.ts
index 25a641ed..478226ba 100644
--- a/packages/contracts/src/orchestration.test.ts
+++ b/packages/contracts/src/orchestration.test.ts
@@ -186,6 +186,31 @@ it.effect("accepts provider-scoped model options in thread.turn.start", () =>
}),
);
+it.effect("accepts provider-scoped launch options in thread.turn.start", () =>
+ Effect.gen(function* () {
+ const parsed = yield* decodeThreadTurnStartCommand({
+ type: "thread.turn.start",
+ commandId: "cmd-turn-provider-options",
+ threadId: "thread-1",
+ message: {
+ messageId: "msg-provider-options",
+ role: "user",
+ text: "hello",
+ attachments: [],
+ },
+ providerOptions: {
+ codex: {
+ binaryPath: "~/bin/codex",
+ homePath: "~/.codex-alt",
+ },
+ },
+ createdAt: "2026-01-01T00:00:00.000Z",
+ });
+ assert.strictEqual(parsed.providerOptions?.codex?.binaryPath, "~/bin/codex");
+ assert.strictEqual(parsed.providerOptions?.codex?.homePath, "~/.codex-alt");
+ }),
+);
+
it.effect(
"decodes thread.turn-start-requested defaults for provider, runtime mode, and interaction mode",
() =>
diff --git a/packages/contracts/src/orchestration.ts b/packages/contracts/src/orchestration.ts
index aa7bd827..3e3553c8 100644
--- a/packages/contracts/src/orchestration.ts
+++ b/packages/contracts/src/orchestration.ts
@@ -1,5 +1,6 @@
import { Option, Schema, SchemaIssue, Struct } from "effect";
import { ProviderModelOptions } from "./model";
+import { ProviderStartOptions } from "./providerOptions";
import {
ApprovalRequestId,
CheckpointRef,
@@ -366,6 +367,7 @@ export const ThreadTurnStartCommand = Schema.Struct({
model: Schema.optional(TrimmedNonEmptyString),
serviceTier: Schema.optional(Schema.NullOr(ProviderServiceTier)),
modelOptions: Schema.optional(ProviderModelOptions),
+ providerOptions: Schema.optional(ProviderStartOptions),
assistantDeliveryMode: Schema.optional(AssistantDeliveryMode),
runtimeMode: RuntimeMode.pipe(Schema.withDecodingDefault(() => DEFAULT_RUNTIME_MODE)),
interactionMode: ProviderInteractionMode.pipe(
@@ -388,6 +390,7 @@ const ClientThreadTurnStartCommand = Schema.Struct({
model: Schema.optional(TrimmedNonEmptyString),
serviceTier: Schema.optional(Schema.NullOr(ProviderServiceTier)),
modelOptions: Schema.optional(ProviderModelOptions),
+ providerOptions: Schema.optional(ProviderStartOptions),
assistantDeliveryMode: Schema.optional(AssistantDeliveryMode),
runtimeMode: RuntimeMode,
interactionMode: ProviderInteractionMode,
@@ -668,6 +671,7 @@ export const ThreadTurnStartRequestedPayload = Schema.Struct({
model: Schema.optional(TrimmedNonEmptyString),
serviceTier: Schema.optional(Schema.NullOr(ProviderServiceTier)),
modelOptions: Schema.optional(ProviderModelOptions),
+ providerOptions: Schema.optional(ProviderStartOptions),
assistantDeliveryMode: Schema.optional(AssistantDeliveryMode),
runtimeMode: RuntimeMode.pipe(Schema.withDecodingDefault(() => DEFAULT_RUNTIME_MODE)),
interactionMode: ProviderInteractionMode.pipe(
diff --git a/packages/contracts/src/provider.ts b/packages/contracts/src/provider.ts
index 9ca7068a..79d278a2 100644
--- a/packages/contracts/src/provider.ts
+++ b/packages/contracts/src/provider.ts
@@ -1,6 +1,7 @@
import { Schema } from "effect";
import { TrimmedNonEmptyString } from "./baseSchemas";
import { ProviderModelOptions } from "./model";
+import { ProviderStartOptions } from "./providerOptions";
import {
ApprovalRequestId,
EventId,
@@ -48,15 +49,6 @@ export const ProviderSession = Schema.Struct({
});
export type ProviderSession = typeof ProviderSession.Type;
-const CodexProviderStartOptions = Schema.Struct({
- binaryPath: Schema.optional(TrimmedNonEmptyStringSchema),
- homePath: Schema.optional(TrimmedNonEmptyStringSchema),
-});
-
-const ProviderStartOptions = Schema.Struct({
- codex: Schema.optional(CodexProviderStartOptions),
-});
-
export const ProviderSessionStartInput = Schema.Struct({
threadId: ThreadId,
provider: Schema.optional(ProviderKind),
diff --git a/packages/contracts/src/providerOptions.ts b/packages/contracts/src/providerOptions.ts
new file mode 100644
index 00000000..f332c7dd
--- /dev/null
+++ b/packages/contracts/src/providerOptions.ts
@@ -0,0 +1,13 @@
+import { Schema } from "effect";
+import { TrimmedNonEmptyString } from "./baseSchemas";
+
+export const CodexProviderStartOptions = Schema.Struct({
+ binaryPath: Schema.optional(TrimmedNonEmptyString),
+ homePath: Schema.optional(TrimmedNonEmptyString),
+});
+export type CodexProviderStartOptions = typeof CodexProviderStartOptions.Type;
+
+export const ProviderStartOptions = Schema.Struct({
+ codex: Schema.optional(CodexProviderStartOptions),
+});
+export type ProviderStartOptions = typeof ProviderStartOptions.Type;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment