Skip to content

Commit 48a73f3

Browse files
committed
chore(code): wire provider-specific cloud modes
1 parent 74e3d02 commit 48a73f3

8 files changed

Lines changed: 149 additions & 18 deletions

File tree

apps/code/src/renderer/api/posthogClient.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,41 @@ describe("PostHogAPIClient", () => {
4242
);
4343
});
4444

45+
it("preserves Codex-native permission modes for cloud runs", async () => {
46+
const client = new PostHogAPIClient(
47+
"http://localhost:8000",
48+
async () => "token",
49+
async () => "token",
50+
123,
51+
);
52+
53+
const post = vi.fn().mockResolvedValue({
54+
id: "task-123",
55+
title: "Task",
56+
description: "Task",
57+
created_at: "2026-04-14T00:00:00Z",
58+
updated_at: "2026-04-14T00:00:00Z",
59+
origin_product: "user_created",
60+
});
61+
62+
(client as unknown as { api: { post: typeof post } }).api = { post };
63+
64+
await client.runTaskInCloud("task-123", "feature/codex-mode", {
65+
adapter: "codex",
66+
model: "gpt-5.4",
67+
initialPermissionMode: "auto",
68+
});
69+
70+
expect(post).toHaveBeenCalledWith(
71+
"/api/projects/{project_id}/tasks/{id}/run/",
72+
expect.objectContaining({
73+
body: expect.objectContaining({
74+
initial_permission_mode: "auto",
75+
}),
76+
}),
77+
);
78+
});
79+
4580
it("rejects unsupported reasoning effort for cloud Codex runs", async () => {
4681
const client = new PostHogAPIClient(
4782
"http://localhost:8000",

apps/code/src/renderer/api/posthogClient.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { isSupportedReasoningEffort } from "@posthog/agent/adapters/reasoning-effort";
2+
import { type PermissionMode } from "@posthog/agent/execution-mode";
23
import type {
34
ActionabilityJudgmentArtefact,
45
AvailableSuggestedReviewer,
@@ -755,7 +756,7 @@ export class PostHogAPIClient {
755756
runSource?: CloudRunSource;
756757
signalReportId?: string;
757758
githubUserToken?: string;
758-
initialPermissionMode?: string;
759+
initialPermissionMode?: PermissionMode;
759760
},
760761
): Promise<Task> {
761762
const teamId = await this.getTeamId();

apps/code/src/renderer/features/sessions/hooks/useSessionConnection.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ export function useSessionConnection({
7676
typeof task.latest_run.state?.initial_permission_mode === "string"
7777
? task.latest_run.state.initial_permission_mode
7878
: undefined;
79+
const adapter =
80+
task.latest_run.runtime_adapter === "codex" ? "codex" : "claude";
7981
const cleanup = getSessionService().watchCloudTask(
8082
task.id,
8183
runId,
@@ -86,6 +88,7 @@ export function useSessionConnection({
8688
},
8789
task.latest_run?.log_url,
8890
initialMode,
91+
adapter,
8992
);
9093
return cleanup;
9194
}, [
@@ -98,6 +101,7 @@ export function useSessionConnection({
98101
task.id,
99102
task.latest_run?.id,
100103
task.latest_run?.log_url,
104+
task.latest_run?.runtime_adapter,
101105
task.latest_run?.state?.initial_permission_mode,
102106
]);
103107

apps/code/src/renderer/features/sessions/service/service.test.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,9 +172,13 @@ vi.mock("@renderer/stores/connectivityStore", () => ({
172172
getIsOnline: () => mockGetIsOnline(),
173173
}));
174174

175+
const mockSettingsState = vi.hoisted(() => ({
176+
customInstructions: "",
177+
}));
178+
175179
vi.mock("@features/settings/stores/settingsStore", () => ({
176180
useSettingsStore: {
177-
getState: () => ({ customInstructions: "" }),
181+
getState: () => mockSettingsState,
178182
},
179183
}));
180184

@@ -282,6 +286,7 @@ describe("SessionService", () => {
282286
beforeEach(() => {
283287
vi.clearAllMocks();
284288
resetSessionService();
289+
mockSettingsState.customInstructions = "";
285290
mockGetIsOnline.mockReturnValue(true);
286291
mockGetConfigOptionByCategory.mockReturnValue(undefined);
287292
mockSessionStoreSetters.getSessionByTaskId.mockReturnValue(undefined);
@@ -516,6 +521,37 @@ describe("SessionService", () => {
516521
});
517522

518523
describe("watchCloudTask", () => {
524+
it("builds codex cloud mode options using native codex modes", () => {
525+
const service = getSessionService();
526+
527+
service.watchCloudTask(
528+
"task-123",
529+
"run-123",
530+
"https://api.anthropic.com",
531+
123,
532+
undefined,
533+
undefined,
534+
"full-access",
535+
"codex",
536+
);
537+
538+
expect(mockSessionStoreSetters.setSession).toHaveBeenCalledWith(
539+
expect.objectContaining({
540+
configOptions: [
541+
expect.objectContaining({
542+
id: "mode",
543+
currentValue: "full-access",
544+
options: [
545+
expect.objectContaining({ value: "read-only" }),
546+
expect.objectContaining({ value: "auto" }),
547+
expect.objectContaining({ value: "full-access" }),
548+
],
549+
}),
550+
],
551+
}),
552+
);
553+
});
554+
519555
it("resets a same-run preloaded session before the first cloud snapshot", () => {
520556
const service = getSessionService();
521557
mockSessionStoreSetters.getSessionByTaskId.mockReturnValue(

apps/code/src/renderer/features/sessions/service/service.ts

Lines changed: 62 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,10 @@ import {
3333
import { useSettingsStore } from "@features/settings/stores/settingsStore";
3434
import { taskViewedApi } from "@features/sidebar/hooks/useTaskViewed";
3535
import { isNotification, POSTHOG_NOTIFICATIONS } from "@posthog/agent";
36-
import { getAvailableModes } from "@posthog/agent/execution-mode";
36+
import {
37+
getAvailableCodexModes,
38+
getAvailableModes,
39+
} from "@posthog/agent/execution-mode";
3740
import { DEFAULT_GATEWAY_MODEL } from "@posthog/agent/gateway-models";
3841
import { getIsOnline } from "@renderer/stores/connectivityStore";
3942
import { trpcClient } from "@renderer/trpc/client";
@@ -89,15 +92,23 @@ const LOCAL_SESSION_RECOVERY_FAILED_MESSAGE =
8992
* is available in the UI even without a local agent connection.
9093
*/
9194
function buildCloudDefaultConfigOptions(
92-
initialMode = "plan",
95+
initialMode: string | undefined,
96+
adapter: Adapter = "claude",
9397
): SessionConfigOption[] {
94-
const modes = getAvailableModes();
98+
const modes =
99+
adapter === "codex" ? getAvailableCodexModes() : getAvailableModes();
100+
const currentMode =
101+
typeof initialMode === "string"
102+
? initialMode
103+
: adapter === "codex"
104+
? "auto"
105+
: "plan";
95106
return [
96107
{
97108
id: "mode",
98109
name: "Approval Preset",
99110
type: "select",
100-
currentValue: initialMode,
111+
currentValue: currentMode,
101112
options: modes.map((mode) => ({
102113
value: mode.id,
103114
name: mode.name,
@@ -399,7 +410,6 @@ export class SessionService {
399410
.getState()
400411
.getAdapter(taskRunId);
401412
const resolvedAdapter = adapter ?? storedAdapter;
402-
403413
const persistedConfigOptions = getPersistedConfigOptions(taskRunId);
404414

405415
const session = this.createBaseSession(taskRunId, taskId, taskTitle);
@@ -1684,7 +1694,20 @@ export class SessionService {
16841694
// in run state (pending_user_message), NOT via user_message command.
16851695

16861696
// Start the watcher immediately so we don't miss status updates.
1687-
this.watchCloudTask(session.taskId, newRun.id, auth.apiHost, auth.teamId);
1697+
const initialMode =
1698+
typeof newRun.state?.initial_permission_mode === "string"
1699+
? newRun.state.initial_permission_mode
1700+
: undefined;
1701+
this.watchCloudTask(
1702+
session.taskId,
1703+
newRun.id,
1704+
auth.apiHost,
1705+
auth.teamId,
1706+
undefined,
1707+
newRun.log_url,
1708+
initialMode,
1709+
newRun.runtime_adapter ?? session.adapter ?? "claude",
1710+
);
16881711

16891712
// Invalidate task queries so the UI picks up the new run metadata
16901713
queryClient.invalidateQueries({ queryKey: ["tasks"] });
@@ -2211,6 +2234,7 @@ export class SessionService {
22112234
onStatusChange?: () => void,
22122235
logUrl?: string,
22132236
initialMode?: string,
2237+
adapter: Adapter = "claude",
22142238
): () => void {
22152239
const taskRunId = runId;
22162240
const startToken = ++this.nextCloudTaskWatchToken;
@@ -2226,10 +2250,21 @@ export class SessionService {
22262250
existingWatcher.onStatusChange = onStatusChange;
22272251
// Ensure configOptions is populated on revisit
22282252
const existing = sessionStoreSetters.getSessionByTaskId(taskId);
2229-
if (existing && !existing.configOptions?.length) {
2230-
sessionStoreSetters.updateSession(existing.taskRunId, {
2231-
configOptions: buildCloudDefaultConfigOptions(initialMode),
2232-
});
2253+
if (existing) {
2254+
const existingMode = getConfigOptionByCategory(
2255+
existing.configOptions,
2256+
"mode",
2257+
)?.currentValue;
2258+
const currentMode =
2259+
typeof existingMode === "string" ? existingMode : initialMode;
2260+
const shouldRefreshConfigOptions =
2261+
!existing.configOptions?.length || existing.adapter !== adapter;
2262+
if (shouldRefreshConfigOptions) {
2263+
sessionStoreSetters.updateSession(existing.taskRunId, {
2264+
adapter,
2265+
configOptions: buildCloudDefaultConfigOptions(currentMode, adapter),
2266+
});
2267+
}
22332268
}
22342269
return () => {};
22352270
}
@@ -2263,14 +2298,28 @@ export class SessionService {
22632298
const session = this.createBaseSession(taskRunId, taskId, taskTitle);
22642299
session.status = "disconnected";
22652300
session.isCloud = true;
2266-
session.configOptions = buildCloudDefaultConfigOptions(initialMode);
2301+
session.adapter = adapter;
2302+
session.configOptions = buildCloudDefaultConfigOptions(
2303+
initialMode,
2304+
adapter,
2305+
);
22672306
sessionStoreSetters.setSession(session);
22682307
} else {
22692308
// Ensure cloud flag and configOptions are set on existing sessions
22702309
const updates: Partial<AgentSession> = {};
22712310
if (!existing.isCloud) updates.isCloud = true;
2272-
if (!existing.configOptions?.length) {
2273-
updates.configOptions = buildCloudDefaultConfigOptions(initialMode);
2311+
if (existing.adapter !== adapter) updates.adapter = adapter;
2312+
if (!existing.configOptions?.length || existing.adapter !== adapter) {
2313+
const existingMode = getConfigOptionByCategory(
2314+
existing.configOptions,
2315+
"mode",
2316+
)?.currentValue;
2317+
const currentMode =
2318+
typeof existingMode === "string" ? existingMode : initialMode;
2319+
updates.configOptions = buildCloudDefaultConfigOptions(
2320+
currentMode,
2321+
adapter,
2322+
);
22742323
}
22752324
if (Object.keys(updates).length > 0) {
22762325
sessionStoreSetters.updateSession(existing.taskRunId, updates);

apps/code/src/renderer/features/task-detail/hooks/usePreviewConfig.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,12 @@ export function usePreviewConfig(
9292
) {
9393
initialMode = lastUsedInitialTaskMode;
9494
} else {
95+
const fallbackDefault = adapter === "codex" ? "auto" : "plan";
9596
initialMode =
96-
typeof serverDefault === "string" ? serverDefault : "plan";
97+
typeof serverDefault === "string" &&
98+
availableValues.includes(serverDefault)
99+
? serverDefault
100+
: fallbackDefault;
97101
}
98102

99103
const withMode = options.map((opt) =>

apps/code/src/renderer/sagas/task/task-creation.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ describe("TaskCreationSaga", () => {
166166
runSource: "manual",
167167
signalReportId: undefined,
168168
githubUserToken: undefined,
169-
initialPermissionMode: "plan",
169+
initialPermissionMode: "auto",
170170
},
171171
);
172172
expect(sendRunCommandMock).not.toHaveBeenCalled();

apps/code/src/renderer/sagas/task/task-creation.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,9 @@ export class TaskCreationSaga extends Saga<
308308
runSource: input.cloudRunSource ?? "manual",
309309
signalReportId: input.signalReportId,
310310
githubUserToken,
311-
initialPermissionMode: input.executionMode ?? "plan",
311+
initialPermissionMode:
312+
input.executionMode ??
313+
(input.adapter === "codex" ? "auto" : "plan"),
312314
});
313315
},
314316
rollback: async () => {

0 commit comments

Comments
 (0)