Skip to content

Commit ad85ad6

Browse files
authored
feat: add codex (#381)
1 parent ada84ae commit ad85ad6

17 files changed

Lines changed: 1069 additions & 94 deletions

File tree

apps/array/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@
121121
"cmdk": "^1.1.1",
122122
"date-fns": "^3.3.1",
123123
"detect-libc": "^1.0.3",
124+
"dotenv": "^17.2.3",
124125
"electron-log": "^5.4.3",
125126
"electron-store": "^11.0.0",
126127
"file-icon": "^6.0.0",

apps/array/src/main/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ declare const __BUILD_DATE__: string | undefined;
33

44
import "reflect-metadata";
55
import dns from "node:dns";
6+
67
import { mkdirSync } from "node:fs";
78
import os from "node:os";
89
import path from "node:path";

apps/array/src/main/services/agent/schemas.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ export const credentialsSchema = z.object({
99

1010
export type Credentials = z.infer<typeof credentialsSchema>;
1111

12+
// Agent framework schema
13+
export const agentFrameworkSchema = z.enum(["claude", "codex"]);
14+
export type AgentFramework = z.infer<typeof agentFrameworkSchema>;
15+
1216
// Session config schema
1317
export const sessionConfigSchema = z.object({
1418
taskId: z.string(),
@@ -18,11 +22,13 @@ export const sessionConfigSchema = z.object({
1822
logUrl: z.string().optional(),
1923
sdkSessionId: z.string().optional(),
2024
model: z.string().optional(),
25+
framework: agentFrameworkSchema.optional(),
2126
});
2227

2328
export type SessionConfig = z.infer<typeof sessionConfigSchema>;
2429

2530
// Start session input/output
31+
2632
export const startSessionInput = z.object({
2733
taskId: z.string(),
2834
taskRunId: z.string(),
@@ -33,6 +39,7 @@ export const startSessionInput = z.object({
3339
permissionMode: z.string().optional(),
3440
autoProgress: z.boolean().optional(),
3541
model: z.string().optional(),
42+
framework: agentFrameworkSchema.optional().default("claude"),
3643
executionMode: z.enum(["plan"]).optional(),
3744
runMode: z.enum(["local", "cloud"]).optional(),
3845
createPR: z.boolean().optional(),

apps/array/src/main/services/agent/service.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ interface SessionConfig {
129129
logUrl?: string;
130130
sdkSessionId?: string;
131131
model?: string;
132+
framework?: "claude" | "codex";
132133
}
133134

134135
interface ManagedSession {
@@ -230,6 +231,7 @@ export class AgentService extends TypedEventEmitter<AgentServiceEvents> {
230231
logUrl,
231232
sdkSessionId,
232233
model,
234+
framework,
233235
} = config;
234236

235237
if (!isRetry) {
@@ -255,6 +257,7 @@ export class AgentService extends TypedEventEmitter<AgentServiceEvents> {
255257
try {
256258
const { clientStreams } = await agent.runTaskV2(taskId, taskRunId, {
257259
skipGitBranch: true,
260+
framework,
258261
});
259262

260263
const connection = this.createClientConnection(
@@ -457,7 +460,15 @@ export class AgentService extends TypedEventEmitter<AgentServiceEvents> {
457460
process.env.ANTHROPIC_API_KEY = token;
458461
process.env.ANTHROPIC_AUTH_TOKEN = token;
459462

460-
process.env.ANTHROPIC_BASE_URL = getLlmGatewayUrl(credentials.apiHost);
463+
const llmGatewayUrl = getLlmGatewayUrl(credentials.apiHost);
464+
process.env.ANTHROPIC_BASE_URL = llmGatewayUrl;
465+
466+
const openaiBaseUrl = llmGatewayUrl.endsWith("/v1")
467+
? llmGatewayUrl
468+
: `${llmGatewayUrl}/v1`;
469+
process.env.OPENAI_BASE_URL = openaiBaseUrl;
470+
process.env.OPENAI_API_KEY = token;
471+
process.env.LLM_GATEWAY_URL = llmGatewayUrl;
461472

462473
process.env.CLAUDE_CODE_EXECUTABLE = getClaudeCliPath();
463474

@@ -599,6 +610,7 @@ export class AgentService extends TypedEventEmitter<AgentServiceEvents> {
599610
logUrl: "logUrl" in params ? params.logUrl : undefined,
600611
sdkSessionId: "sdkSessionId" in params ? params.sdkSessionId : undefined,
601612
model: "model" in params ? params.model : undefined,
613+
framework: "framework" in params ? params.framework : "claude",
602614
};
603615
}
604616

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { useSettingsStore } from "@features/settings/stores/settingsStore";
2+
import { Select, Text } from "@radix-ui/themes";
3+
import {
4+
type AgentFramework,
5+
AVAILABLE_FRAMEWORKS,
6+
} from "@shared/types/models";
7+
import { useSessionForTask } from "../stores/sessionStore";
8+
9+
interface FrameworkSelectorProps {
10+
taskId?: string;
11+
disabled?: boolean;
12+
onFrameworkChange?: (framework: AgentFramework) => void;
13+
}
14+
15+
export function FrameworkSelector({
16+
taskId,
17+
disabled,
18+
onFrameworkChange,
19+
}: FrameworkSelectorProps) {
20+
const defaultFramework = useSettingsStore((state) => state.defaultFramework);
21+
const setDefaultFramework = useSettingsStore(
22+
(state) => state.setDefaultFramework,
23+
);
24+
const session = useSessionForTask(taskId);
25+
26+
// Use session framework if available, otherwise fall back to default
27+
const activeFramework = session?.framework ?? defaultFramework;
28+
29+
// Disable if there's an active session (can't change framework mid-session)
30+
const isDisabled = disabled || session?.status === "connected";
31+
32+
const handleChange = (value: string) => {
33+
const framework = value as AgentFramework;
34+
setDefaultFramework(framework);
35+
onFrameworkChange?.(framework);
36+
};
37+
38+
const currentFramework = AVAILABLE_FRAMEWORKS.find(
39+
(f) => f.id === activeFramework,
40+
);
41+
const displayName = currentFramework?.name ?? activeFramework;
42+
43+
return (
44+
<Select.Root
45+
value={activeFramework}
46+
onValueChange={handleChange}
47+
disabled={isDisabled}
48+
size="1"
49+
>
50+
<Select.Trigger
51+
variant="ghost"
52+
style={{
53+
fontSize: "var(--font-size-1)",
54+
color: "var(--gray-11)",
55+
padding: "4px 8px",
56+
marginLeft: "4px",
57+
height: "auto",
58+
minHeight: "unset",
59+
}}
60+
>
61+
<Text size="1" style={{ fontFamily: "var(--font-mono)" }}>
62+
{displayName}
63+
</Text>
64+
</Select.Trigger>
65+
<Select.Content position="popper" sideOffset={4}>
66+
{AVAILABLE_FRAMEWORKS.filter((f) => f.enabled).map((framework) => (
67+
<Select.Item key={framework.id} value={framework.id}>
68+
{framework.name}
69+
</Select.Item>
70+
))}
71+
</Select.Content>
72+
</Select.Root>
73+
);
74+
}

apps/array/src/renderer/features/sessions/stores/sessionStore.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export interface AgentSession {
3939
logUrl?: string;
4040
processedLineCount?: number;
4141
model?: string;
42+
framework?: "claude" | "codex";
4243
}
4344

4445
interface SessionState {
@@ -267,6 +268,7 @@ function createBaseSession(
267268
taskRunId: string,
268269
taskId: string,
269270
isCloud: boolean,
271+
framework?: "claude" | "codex",
270272
): AgentSession {
271273
return {
272274
taskRunId,
@@ -277,6 +279,7 @@ function createBaseSession(
277279
status: "connecting",
278280
isPromptPending: false,
279281
isCloud,
282+
framework,
280283
};
281284
}
282285

@@ -471,7 +474,7 @@ const useStore = create<SessionStore>()(
471474
return;
472475
}
473476

474-
const defaultModel = useSettingsStore.getState().defaultModel;
477+
const { defaultModel, defaultFramework } = useSettingsStore.getState();
475478
const result = await trpcVanilla.agent.start.mutate({
476479
taskId,
477480
taskRunId: taskRun.id,
@@ -480,9 +483,15 @@ const useStore = create<SessionStore>()(
480483
apiHost: auth.apiHost,
481484
projectId: auth.projectId,
482485
model: defaultModel,
486+
framework: defaultFramework,
483487
});
484488

485-
const session = createBaseSession(taskRun.id, taskId, false);
489+
const session = createBaseSession(
490+
taskRun.id,
491+
taskId,
492+
false,
493+
defaultFramework,
494+
);
486495
session.channel = result.channel;
487496
session.status = "connected";
488497
session.model = defaultModel;

apps/array/src/renderer/features/settings/stores/settingsStore.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import type { WorkspaceMode } from "@shared/types";
2-
import { DEFAULT_MODEL } from "@shared/types/models";
2+
import {
3+
type AgentFramework,
4+
DEFAULT_FRAMEWORK,
5+
DEFAULT_MODEL,
6+
} from "@shared/types/models";
37
import { create } from "zustand";
48
import { persist } from "zustand/middleware";
59

@@ -14,6 +18,7 @@ interface SettingsStore {
1418
lastUsedWorkspaceMode: WorkspaceMode;
1519
createPR: boolean;
1620
defaultModel: string;
21+
defaultFramework: AgentFramework;
1722

1823
setAutoRunTasks: (autoRun: boolean) => void;
1924
setDefaultRunMode: (mode: DefaultRunMode) => void;
@@ -22,6 +27,7 @@ interface SettingsStore {
2227
setLastUsedWorkspaceMode: (mode: WorkspaceMode) => void;
2328
setCreatePR: (createPR: boolean) => void;
2429
setDefaultModel: (model: string) => void;
30+
setDefaultFramework: (framework: AgentFramework) => void;
2531
}
2632

2733
export const useSettingsStore = create<SettingsStore>()(
@@ -34,6 +40,7 @@ export const useSettingsStore = create<SettingsStore>()(
3440
lastUsedWorkspaceMode: "worktree",
3541
createPR: true,
3642
defaultModel: DEFAULT_MODEL,
43+
defaultFramework: DEFAULT_FRAMEWORK,
3744

3845
setAutoRunTasks: (autoRun) => set({ autoRunTasks: autoRun }),
3946
setDefaultRunMode: (mode) => set({ defaultRunMode: mode }),
@@ -43,6 +50,7 @@ export const useSettingsStore = create<SettingsStore>()(
4350
setLastUsedWorkspaceMode: (mode) => set({ lastUsedWorkspaceMode: mode }),
4451
setCreatePR: (createPR) => set({ createPR }),
4552
setDefaultModel: (model) => set({ defaultModel: model }),
53+
setDefaultFramework: (framework) => set({ defaultFramework: framework }),
4654
}),
4755
{
4856
name: "settings-storage",

apps/array/src/renderer/features/task-detail/components/TaskInputEditor.tsx

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import "@features/message-editor/components/message-editor.css";
22
import { EditorToolbar } from "@features/message-editor/components/EditorToolbar";
33
import type { MessageEditorHandle } from "@features/message-editor/components/MessageEditor";
44
import { useTiptapEditor } from "@features/message-editor/tiptap/useTiptapEditor";
5+
import { FrameworkSelector } from "@features/sessions/components/FrameworkSelector";
56
import { ArrowUp, GitBranchIcon } from "@phosphor-icons/react";
67
import { Box, Flex, IconButton, Text, Tooltip } from "@radix-ui/themes";
78
import { EditorContent } from "@tiptap/react";
@@ -172,12 +173,15 @@ export const TaskInputEditor = forwardRef<
172173
</Flex>
173174

174175
<Flex justify="between" align="center" px="3" pb="3">
175-
<EditorToolbar
176-
disabled={isCreatingTask}
177-
onInsertChip={insertChip}
178-
attachTooltip="Attach files from anywhere"
179-
iconSize={16}
180-
/>
176+
<Flex align="center" gap="1">
177+
<EditorToolbar
178+
disabled={isCreatingTask}
179+
onInsertChip={insertChip}
180+
attachTooltip="Attach files from anywhere"
181+
iconSize={16}
182+
/>
183+
<FrameworkSelector disabled={isCreatingTask} />
184+
</Flex>
181185

182186
<Flex align="center" gap="4">
183187
{!isCloudMode && (

apps/array/src/shared/types/models.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,33 @@ export const AVAILABLE_MODELS: ModelOption[] = [
6565

6666
export const DEFAULT_MODEL = "claude-opus-4-5";
6767

68+
// Agent frameworks
69+
export type AgentFramework = "claude" | "codex";
70+
71+
export interface FrameworkOption {
72+
id: AgentFramework;
73+
name: string;
74+
description: string;
75+
enabled: boolean;
76+
}
77+
78+
export const AVAILABLE_FRAMEWORKS: FrameworkOption[] = [
79+
{
80+
id: "claude",
81+
name: "Claude Code",
82+
description: "Anthropic's Claude Code agent",
83+
enabled: true,
84+
},
85+
{
86+
id: "codex",
87+
name: "OpenAI Codex",
88+
description: "OpenAI's Codex agent",
89+
enabled: true,
90+
},
91+
];
92+
93+
export const DEFAULT_FRAMEWORK: AgentFramework = "claude";
94+
6895
export function getModelById(id: string): ModelOption | undefined {
6996
return AVAILABLE_MODELS.find((m) => m.id === id && m.enabled);
7097
}

packages/agent/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ export type {
1414
export { POSTHOG_NOTIFICATIONS } from "./src/acp-extensions.js";
1515
export type {
1616
AcpConnectionConfig,
17+
AgentFramework,
1718
InProcessAcpConnection,
18-
} from "./src/adapters/claude/claude.js";
19-
export { createAcpConnection } from "./src/adapters/claude/claude.js";
19+
} from "./src/adapters/connection.js";
20+
export { createAcpConnection } from "./src/adapters/connection.js";
2021
export { Agent } from "./src/agent.js";
2122
export type {
2223
AgentEvent,

0 commit comments

Comments
 (0)