Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions apps/code/src/renderer/api/posthogClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,41 @@ describe("PostHogAPIClient", () => {
);
});

it("preserves Codex-native permission modes for cloud runs", async () => {
const client = new PostHogAPIClient(
"http://localhost:8000",
async () => "token",
async () => "token",
123,
);

const post = vi.fn().mockResolvedValue({
id: "task-123",
title: "Task",
description: "Task",
created_at: "2026-04-14T00:00:00Z",
updated_at: "2026-04-14T00:00:00Z",
origin_product: "user_created",
});

(client as unknown as { api: { post: typeof post } }).api = { post };

await client.runTaskInCloud("task-123", "feature/codex-mode", {
adapter: "codex",
model: "gpt-5.4",
initialPermissionMode: "auto",
});

expect(post).toHaveBeenCalledWith(
"/api/projects/{project_id}/tasks/{id}/run/",
expect.objectContaining({
body: expect.objectContaining({
initial_permission_mode: "auto",
}),
}),
);
});

it("rejects unsupported reasoning effort for cloud Codex runs", async () => {
const client = new PostHogAPIClient(
"http://localhost:8000",
Expand Down
3 changes: 2 additions & 1 deletion apps/code/src/renderer/api/posthogClient.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { isSupportedReasoningEffort } from "@posthog/agent/adapters/reasoning-effort";
import { type PermissionMode } from "@posthog/agent/execution-mode";
import type {
ActionabilityJudgmentArtefact,
AvailableSuggestedReviewer,
Expand Down Expand Up @@ -755,7 +756,7 @@ export class PostHogAPIClient {
runSource?: CloudRunSource;
signalReportId?: string;
githubUserToken?: string;
initialPermissionMode?: string;
initialPermissionMode?: PermissionMode;
},
): Promise<Task> {
const teamId = await this.getTeamId();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ export function useSessionConnection({
typeof task.latest_run.state?.initial_permission_mode === "string"
? task.latest_run.state.initial_permission_mode
: undefined;
const adapter =
task.latest_run.runtime_adapter === "codex" ? "codex" : "claude";
const cleanup = getSessionService().watchCloudTask(
task.id,
runId,
Expand All @@ -86,6 +88,7 @@ export function useSessionConnection({
},
task.latest_run?.log_url,
initialMode,
adapter,
);
return cleanup;
}, [
Expand All @@ -98,6 +101,7 @@ export function useSessionConnection({
task.id,
task.latest_run?.id,
task.latest_run?.log_url,
task.latest_run?.runtime_adapter,
task.latest_run?.state?.initial_permission_mode,
]);

Expand Down
38 changes: 37 additions & 1 deletion apps/code/src/renderer/features/sessions/service/service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,9 +172,13 @@ vi.mock("@renderer/stores/connectivityStore", () => ({
getIsOnline: () => mockGetIsOnline(),
}));

const mockSettingsState = vi.hoisted(() => ({
customInstructions: "",
}));

vi.mock("@features/settings/stores/settingsStore", () => ({
useSettingsStore: {
getState: () => ({ customInstructions: "" }),
getState: () => mockSettingsState,
},
}));

Expand Down Expand Up @@ -282,6 +286,7 @@ describe("SessionService", () => {
beforeEach(() => {
vi.clearAllMocks();
resetSessionService();
mockSettingsState.customInstructions = "";
mockGetIsOnline.mockReturnValue(true);
mockGetConfigOptionByCategory.mockReturnValue(undefined);
mockSessionStoreSetters.getSessionByTaskId.mockReturnValue(undefined);
Expand Down Expand Up @@ -516,6 +521,37 @@ describe("SessionService", () => {
});

describe("watchCloudTask", () => {
it("builds codex cloud mode options using native codex modes", () => {
const service = getSessionService();

service.watchCloudTask(
"task-123",
"run-123",
"https://api.anthropic.com",
123,
undefined,
undefined,
"full-access",
"codex",
);

expect(mockSessionStoreSetters.setSession).toHaveBeenCalledWith(
expect.objectContaining({
configOptions: [
expect.objectContaining({
id: "mode",
currentValue: "full-access",
options: [
expect.objectContaining({ value: "read-only" }),
expect.objectContaining({ value: "auto" }),
expect.objectContaining({ value: "full-access" }),
],
}),
],
}),
);
});

it("resets a same-run preloaded session before the first cloud snapshot", () => {
const service = getSessionService();
mockSessionStoreSetters.getSessionByTaskId.mockReturnValue(
Expand Down
75 changes: 62 additions & 13 deletions apps/code/src/renderer/features/sessions/service/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ import {
import { useSettingsStore } from "@features/settings/stores/settingsStore";
import { taskViewedApi } from "@features/sidebar/hooks/useTaskViewed";
import { isNotification, POSTHOG_NOTIFICATIONS } from "@posthog/agent";
import { getAvailableModes } from "@posthog/agent/execution-mode";
import {
getAvailableCodexModes,
getAvailableModes,
} from "@posthog/agent/execution-mode";
import { DEFAULT_GATEWAY_MODEL } from "@posthog/agent/gateway-models";
import { getIsOnline } from "@renderer/stores/connectivityStore";
import { trpcClient } from "@renderer/trpc/client";
Expand Down Expand Up @@ -89,15 +92,23 @@ const LOCAL_SESSION_RECOVERY_FAILED_MESSAGE =
* is available in the UI even without a local agent connection.
*/
function buildCloudDefaultConfigOptions(
initialMode = "plan",
initialMode: string | undefined,
adapter: Adapter = "claude",
): SessionConfigOption[] {
const modes = getAvailableModes();
const modes =
adapter === "codex" ? getAvailableCodexModes() : getAvailableModes();
const currentMode =
typeof initialMode === "string"
? initialMode
: adapter === "codex"
? "auto"
: "plan";
return [
{
id: "mode",
name: "Approval Preset",
type: "select",
currentValue: initialMode,
currentValue: currentMode,
options: modes.map((mode) => ({
value: mode.id,
name: mode.name,
Expand Down Expand Up @@ -399,7 +410,6 @@ export class SessionService {
.getState()
.getAdapter(taskRunId);
const resolvedAdapter = adapter ?? storedAdapter;

const persistedConfigOptions = getPersistedConfigOptions(taskRunId);

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

// Start the watcher immediately so we don't miss status updates.
this.watchCloudTask(session.taskId, newRun.id, auth.apiHost, auth.teamId);
const initialMode =
typeof newRun.state?.initial_permission_mode === "string"
? newRun.state.initial_permission_mode
: undefined;
this.watchCloudTask(
session.taskId,
newRun.id,
auth.apiHost,
auth.teamId,
undefined,
newRun.log_url,
initialMode,
newRun.runtime_adapter ?? session.adapter ?? "claude",
);

// Invalidate task queries so the UI picks up the new run metadata
queryClient.invalidateQueries({ queryKey: ["tasks"] });
Expand Down Expand Up @@ -2211,6 +2234,7 @@ export class SessionService {
onStatusChange?: () => void,
logUrl?: string,
initialMode?: string,
adapter: Adapter = "claude",
): () => void {
const taskRunId = runId;
const startToken = ++this.nextCloudTaskWatchToken;
Expand All @@ -2226,10 +2250,21 @@ export class SessionService {
existingWatcher.onStatusChange = onStatusChange;
// Ensure configOptions is populated on revisit
const existing = sessionStoreSetters.getSessionByTaskId(taskId);
if (existing && !existing.configOptions?.length) {
sessionStoreSetters.updateSession(existing.taskRunId, {
configOptions: buildCloudDefaultConfigOptions(initialMode),
});
if (existing) {
const existingMode = getConfigOptionByCategory(
existing.configOptions,
"mode",
)?.currentValue;
const currentMode =
typeof existingMode === "string" ? existingMode : initialMode;
const shouldRefreshConfigOptions =
!existing.configOptions?.length || existing.adapter !== adapter;
if (shouldRefreshConfigOptions) {
sessionStoreSetters.updateSession(existing.taskRunId, {
adapter,
configOptions: buildCloudDefaultConfigOptions(currentMode, adapter),
});
}
}
return () => {};
}
Expand Down Expand Up @@ -2263,14 +2298,28 @@ export class SessionService {
const session = this.createBaseSession(taskRunId, taskId, taskTitle);
session.status = "disconnected";
session.isCloud = true;
session.configOptions = buildCloudDefaultConfigOptions(initialMode);
session.adapter = adapter;
session.configOptions = buildCloudDefaultConfigOptions(
initialMode,
adapter,
);
sessionStoreSetters.setSession(session);
} else {
// Ensure cloud flag and configOptions are set on existing sessions
const updates: Partial<AgentSession> = {};
if (!existing.isCloud) updates.isCloud = true;
if (!existing.configOptions?.length) {
updates.configOptions = buildCloudDefaultConfigOptions(initialMode);
if (existing.adapter !== adapter) updates.adapter = adapter;
if (!existing.configOptions?.length || existing.adapter !== adapter) {
const existingMode = getConfigOptionByCategory(
existing.configOptions,
"mode",
)?.currentValue;
const currentMode =
typeof existingMode === "string" ? existingMode : initialMode;
updates.configOptions = buildCloudDefaultConfigOptions(
currentMode,
adapter,
);
}
if (Object.keys(updates).length > 0) {
sessionStoreSetters.updateSession(existing.taskRunId, updates);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,12 @@ export function usePreviewConfig(
) {
initialMode = lastUsedInitialTaskMode;
} else {
const fallbackDefault = adapter === "codex" ? "auto" : "plan";
initialMode =
typeof serverDefault === "string" ? serverDefault : "plan";
typeof serverDefault === "string" &&
availableValues.includes(serverDefault)
? serverDefault
: fallbackDefault;
}

const withMode = options.map((opt) =>
Expand Down
2 changes: 1 addition & 1 deletion apps/code/src/renderer/sagas/task/task-creation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ describe("TaskCreationSaga", () => {
runSource: "manual",
signalReportId: undefined,
githubUserToken: undefined,
initialPermissionMode: "plan",
initialPermissionMode: "auto",
},
);
expect(sendRunCommandMock).not.toHaveBeenCalled();
Expand Down
4 changes: 3 additions & 1 deletion apps/code/src/renderer/sagas/task/task-creation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,9 @@ export class TaskCreationSaga extends Saga<
runSource: input.cloudRunSource ?? "manual",
signalReportId: input.signalReportId,
githubUserToken,
initialPermissionMode: input.executionMode ?? "plan",
initialPermissionMode:
input.executionMode ??
(input.adapter === "codex" ? "auto" : "plan"),
});
},
rollback: async () => {
Expand Down
Loading