Responses
diff --git a/apps/web/src/store.test.ts b/apps/web/src/store.test.ts
index 4c9fe09b3465d2ae87d24c81312bce778a9ffa83..03d84c5fcdc6869320fd8b3b1e3b6a0f7c23227d 100644
--- a/apps/web/src/store.test.ts
+++ b/apps/web/src/store.test.ts
@@ -2,7 +2,7 @@ import { ProjectId, ThreadId, TurnId, type OrchestrationReadModel } from "@t3too
import { describe, expect, it } from "vitest";
import { markThreadUnread, syncServerReadModel, type AppState } from "./store";
-import type { Thread } from "./types";
+import { DEFAULT_RUNTIME_MODE, type Thread } from "./types";
function makeThread(overrides: Partial
= {}): Thread {
return {
@@ -11,6 +11,7 @@ function makeThread(overrides: Partial = {}): Thread {
projectId: ProjectId.makeUnsafe("project-1"),
title: "Thread",
model: "gpt-5-codex",
+ runtimeMode: DEFAULT_RUNTIME_MODE,
session: null,
messages: [],
turnDiffSummaries: [],
@@ -38,7 +39,6 @@ function makeState(thread: Thread): AppState {
],
threads: [thread],
threadsHydrated: true,
- runtimeMode: "full-access",
};
}
@@ -48,6 +48,7 @@ function makeReadModelThread(overrides: Partial();
@@ -56,23 +52,14 @@ function readPersistedState(): AppState {
try {
const raw = window.localStorage.getItem(PERSISTED_STATE_KEY);
if (!raw) return initialState;
- const parsed = JSON.parse(raw) as {
- runtimeMode?: RuntimeMode;
- expandedProjectCwds?: string[];
- };
+ const parsed = JSON.parse(raw) as { expandedProjectCwds?: string[] };
persistedExpandedProjectCwds.clear();
for (const cwd of parsed.expandedProjectCwds ?? []) {
if (typeof cwd === "string" && cwd.length > 0) {
persistedExpandedProjectCwds.add(cwd);
}
}
- return {
- ...initialState,
- runtimeMode:
- parsed.runtimeMode === "approval-required" || parsed.runtimeMode === "full-access"
- ? parsed.runtimeMode
- : DEFAULT_RUNTIME_MODE,
- };
+ return { ...initialState };
} catch {
return initialState;
}
@@ -84,7 +71,6 @@ function persistState(state: AppState): void {
window.localStorage.setItem(
PERSISTED_STATE_KEY,
JSON.stringify({
- runtimeMode: state.runtimeMode,
expandedProjectCwds: state.projects
.filter((project) => project.expanded)
.map((project) => project.cwd),
@@ -265,12 +251,13 @@ export function syncServerReadModel(
projectId: thread.projectId,
title: thread.title,
model: resolveModelSlugForProvider(
- inferProviderForThreadModel({
- model: thread.model,
- sessionProviderName: thread.session?.providerName ?? null,
- }),
- thread.model,
- ),
+ inferProviderForThreadModel({
+ model: thread.model,
+ sessionProviderName: thread.session?.providerName ?? null,
+ }),
+ thread.model,
+ ),
+ runtimeMode: thread.runtimeMode,
session: thread.session
? {
sessionId:
@@ -415,11 +402,6 @@ export function setThreadBranch(
return threads === state.threads ? state : { ...state, threads };
}
-export function setRuntimeMode(state: AppState, mode: RuntimeMode): AppState {
- if (state.runtimeMode === mode) return state;
- return { ...state, runtimeMode: mode };
-}
-
// ── Zustand store ────────────────────────────────────────────────────
interface AppStore extends AppState {
@@ -434,7 +416,6 @@ interface AppStore extends AppState {
branch: string | null,
worktreePath: string | null,
) => void;
- setRuntimeMode: (mode: RuntimeMode) => void;
}
export const useStore = create((set) => ({
@@ -453,11 +434,9 @@ export const useStore = create((set) => ({
set((state) => setError(state, threadId, error)),
setThreadBranch: (threadId, branch, worktreePath) =>
set((state) => setThreadBranch(state, threadId, branch, worktreePath)),
- setRuntimeMode: (mode) =>
- set((state) => setRuntimeMode(state, mode)),
}));
-// Persist on every state change (only runtimeMode + expandedProjectCwds)
+// Persist on every state change
useStore.subscribe((state) => persistState(state));
export function StoreProvider({ children }: { children: ReactNode }) {
diff --git a/apps/web/src/types.ts b/apps/web/src/types.ts
index 214b27cb4f8deed191a7ec62e70d7bf7f5742b43..85362f0a028332daaee38e2fcdc17ed2e5e56248 100644
--- a/apps/web/src/types.ts
+++ b/apps/web/src/types.ts
@@ -79,6 +79,7 @@ export interface Thread {
projectId: ProjectId;
title: string;
model: string;
+ runtimeMode: RuntimeMode;
session: ThreadSession | null;
messages: ChatMessage[];
error: string | null;
diff --git a/apps/web/src/worktreeCleanup.test.ts b/apps/web/src/worktreeCleanup.test.ts
index a73b7cd4eb524834b1f009cda98660346b0fe2f8..1da37295d30e81d48214e6793a15302033531d62 100644
--- a/apps/web/src/worktreeCleanup.test.ts
+++ b/apps/web/src/worktreeCleanup.test.ts
@@ -1,7 +1,7 @@
import { ProjectId, ThreadId } from "@t3tools/contracts";
import { describe, expect, it } from "vitest";
-import type { Thread } from "./types";
+import { DEFAULT_RUNTIME_MODE, type Thread } from "./types";
import { formatWorktreePathForDisplay, getOrphanedWorktreePathForThread } from "./worktreeCleanup";
function makeThread(overrides: Partial = {}): Thread {
@@ -11,6 +11,7 @@ function makeThread(overrides: Partial = {}): Thread {
projectId: ProjectId.makeUnsafe("project-1"),
title: "Thread",
model: "gpt-5.3-codex",
+ runtimeMode: DEFAULT_RUNTIME_MODE,
session: null,
messages: [],
turnDiffSummaries: [],
diff --git a/packages/contracts/src/orchestration.ts b/packages/contracts/src/orchestration.ts
index 9a552a78f61690626f439ff44b5d6a8feaaf3a22..e6dd616ea3b24ba67a744dd4fab73ce8592a15bd 100644
--- a/packages/contracts/src/orchestration.ts
+++ b/packages/contracts/src/orchestration.ts
@@ -223,6 +223,7 @@ export const OrchestrationThread = Schema.Struct({
projectId: ProjectId,
title: TrimmedNonEmptyString,
model: TrimmedNonEmptyString,
+ runtimeMode: RuntimeMode,
branch: Schema.NullOr(TrimmedNonEmptyString),
worktreePath: Schema.NullOr(TrimmedNonEmptyString),
latestTurn: Schema.NullOr(OrchestrationLatestTurn),
@@ -277,6 +278,7 @@ export const ThreadCreateCommand = Schema.Struct({
projectId: ProjectId,
title: TrimmedNonEmptyString,
model: TrimmedNonEmptyString,
+ runtimeMode: RuntimeMode,
branch: Schema.NullOr(TrimmedNonEmptyString),
worktreePath: Schema.NullOr(TrimmedNonEmptyString),
createdAt: IsoDateTime,
@@ -298,6 +300,14 @@ export const ThreadMetaUpdateCommand = Schema.Struct({
worktreePath: Schema.optional(Schema.NullOr(TrimmedNonEmptyString)),
});
+export const ThreadRuntimeModeSetCommand = Schema.Struct({
+ type: Schema.Literal("thread.runtime-mode.set"),
+ commandId: CommandId,
+ threadId: ThreadId,
+ runtimeMode: RuntimeMode,
+ createdAt: IsoDateTime,
+});
+
export const ThreadTurnStartCommand = Schema.Struct({
type: Schema.Literal("thread.turn.start"),
commandId: CommandId,
@@ -373,6 +383,7 @@ export const DispatchableClientOrchestrationCommand = Schema.Union([
ThreadCreateCommand,
ThreadDeleteCommand,
ThreadMetaUpdateCommand,
+ ThreadRuntimeModeSetCommand,
ThreadTurnStartCommand,
ThreadTurnInterruptCommand,
ThreadApprovalRespondCommand,
@@ -389,6 +400,7 @@ export const ClientOrchestrationCommand = Schema.Union([
ThreadCreateCommand,
ThreadDeleteCommand,
ThreadMetaUpdateCommand,
+ ThreadRuntimeModeSetCommand,
ClientThreadTurnStartCommand,
ThreadTurnInterruptCommand,
ThreadApprovalRespondCommand,
@@ -477,6 +489,7 @@ export const OrchestrationEventType = Schema.Literals([
"thread.created",
"thread.deleted",
"thread.meta-updated",
+ "thread.runtime-mode-set",
"thread.message-sent",
"thread.turn-start-requested",
"thread.turn-interrupt-requested",
@@ -523,6 +536,7 @@ export const ThreadCreatedPayload = Schema.Struct({
projectId: ProjectId,
title: TrimmedNonEmptyString,
model: TrimmedNonEmptyString,
+ runtimeMode: RuntimeMode,
branch: Schema.NullOr(TrimmedNonEmptyString),
worktreePath: Schema.NullOr(TrimmedNonEmptyString),
createdAt: IsoDateTime,
@@ -543,6 +557,12 @@ export const ThreadMetaUpdatedPayload = Schema.Struct({
updatedAt: IsoDateTime,
});
+export const ThreadRuntimeModeSetPayload = Schema.Struct({
+ threadId: ThreadId,
+ runtimeMode: RuntimeMode,
+ updatedAt: IsoDateTime,
+});
+
export const ThreadMessageSentPayload = Schema.Struct({
threadId: ThreadId,
messageId: MessageId,
@@ -684,6 +704,11 @@ export const OrchestrationEvent = Schema.Union([
type: Schema.Literal("thread.meta-updated"),
payload: ThreadMetaUpdatedPayload,
}),
+ Schema.Struct({
+ ...EventBaseFields,
+ type: Schema.Literal("thread.runtime-mode-set"),
+ payload: ThreadRuntimeModeSetPayload,
+ }),
Schema.Struct({
...EventBaseFields,
type: Schema.Literal("thread.message-sent"),
@@ -768,6 +793,11 @@ export const OrchestrationPersistedEvent = Schema.Union([
eventType: Schema.Literal("thread.meta-updated"),
payload: ThreadMetaUpdatedPayload,
}),
+ Schema.Struct({
+ ...PersistedEventBaseFields,
+ eventType: Schema.Literal("thread.runtime-mode-set"),
+ payload: ThreadRuntimeModeSetPayload,
+ }),
Schema.Struct({
...PersistedEventBaseFields,
eventType: Schema.Literal("thread.message-sent"),
diff --git a/packages/contracts/src/provider.test.ts b/packages/contracts/src/provider.test.ts
index fadde48cd9f15ee85c4c30c3a1d550c58973999e..54e9f043168dcbcbc3abbff0846a5e2e9e314159 100644
--- a/packages/contracts/src/provider.test.ts
+++ b/packages/contracts/src/provider.test.ts
@@ -11,6 +11,7 @@ describe("ProviderSessionStartInput", () => {
provider: "codex",
cwd: "/tmp/workspace",
model: "gpt-5.3-codex",
+ runtimeMode: "full-access",
providerOptions: {
codex: {
binaryPath: "/usr/local/bin/codex",
@@ -18,11 +19,19 @@ describe("ProviderSessionStartInput", () => {
},
},
});
- expect(parsed.runtimeMode).toBeUndefined();
+ expect(parsed.runtimeMode).toBe("full-access");
expect(parsed.providerOptions?.codex?.binaryPath).toBe("/usr/local/bin/codex");
expect(parsed.providerOptions?.codex?.homePath).toBe("/tmp/.codex");
});
+ it("rejects payloads without runtime mode", () => {
+ expect(() =>
+ decodeProviderSessionStartInput({
+ provider: "codex",
+ }),
+ ).toThrow();
+ });
+
it("accepts claude runtime knobs", () => {
const parsed = decodeProviderSessionStartInput({
provider: "claudeCode",
diff --git a/packages/contracts/src/provider.ts b/packages/contracts/src/provider.ts
index d7c83192fc98f0fc087b32b42e16040e0af9e009..fcbbc79338b8b958ec782bf0d83977aae27e3c08 100644
--- a/packages/contracts/src/provider.ts
+++ b/packages/contracts/src/provider.ts
@@ -37,6 +37,7 @@ export const ProviderSession = Schema.Struct({
sessionId: ProviderSessionId,
provider: ProviderKind,
status: ProviderSessionStatus,
+ runtimeMode: RuntimeMode,
cwd: Schema.optional(TrimmedNonEmptyStringSchema),
model: Schema.optional(TrimmedNonEmptyStringSchema),
threadId: Schema.optional(ProviderThreadId),
@@ -79,7 +80,7 @@ export const ProviderSessionStartInput = Schema.Struct({
model: Schema.optional(TrimmedNonEmptyStringSchema),
resumeCursor: Schema.optional(Schema.Unknown),
providerOptions: Schema.optional(ProviderStartOptions),
- runtimeMode: Schema.optional(RuntimeMode),
+ runtimeMode: RuntimeMode,
});
export type ProviderSessionStartInput = typeof ProviderSessionStartInput.Type;