diff --git a/packages/app/src/components/agent-activity.tsx b/packages/app/src/components/agent-activity.tsx
index bbf6d1f00..e2da089f5 100644
--- a/packages/app/src/components/agent-activity.tsx
+++ b/packages/app/src/components/agent-activity.tsx
@@ -61,7 +61,7 @@ function GroupedTextItem({ item }: { item: GroupedTextMessage }) {
return (
-
+
{formatTimestamp(item.startTimestamp)}
{isThought && (
@@ -260,17 +260,21 @@ const stylesheet = StyleSheet.create((theme) => ({
backgroundColor: theme.colors.muted,
borderLeftWidth: 3,
borderLeftColor: theme.colors.palette.purple[500],
+ paddingVertical: theme.spacing[2],
},
timestamp: {
color: theme.colors.mutedForeground,
fontSize: theme.fontSize.xs,
marginBottom: theme.spacing[1],
},
+ thoughtTimestamp: {
+ marginBottom: theme.spacing[0],
+ },
thoughtLabel: {
color: theme.colors.palette.purple[500],
fontSize: theme.fontSize.xs,
fontWeight: theme.fontWeight.semibold,
- marginBottom: theme.spacing[1],
+ marginBottom: theme.spacing[0],
},
text: {
color: theme.colors.foreground,
diff --git a/packages/app/src/components/agent-stream-view.tsx b/packages/app/src/components/agent-stream-view.tsx
index 6c8ac90e2..7ae78886b 100644
--- a/packages/app/src/components/agent-stream-view.tsx
+++ b/packages/app/src/components/agent-stream-view.tsx
@@ -19,18 +19,16 @@ import {
UserMessage,
ActivityLog,
ToolCall,
+ AgentThoughtMessage,
} from "./message";
import { ToolCallBottomSheet } from "./tool-call-bottom-sheet";
import type { StreamItem } from "@/types/stream";
-import type {
- SelectedToolCall,
- PendingPermission,
- AgentInfo,
-} from "@/types/shared";
+import type { SelectedToolCall, PendingPermission } from "@/types/shared";
+import type { Agent } from "@/contexts/session-context";
export interface AgentStreamViewProps {
agentId: string;
- agent: AgentInfo;
+ agent: Agent;
streamItems: StreamItem[];
pendingPermissions: Map;
onPermissionResponse: (requestId: string, optionId: string) => void;
@@ -53,6 +51,7 @@ export function AgentStreamView({
const hasAutoScrolledOnce = useRef(false);
const isProgrammaticScrollRef = useRef(false);
const isNearBottomRef = useRef(true);
+ const isUserScrollingRef = useRef(false);
useEffect(() => {
hasScrolledInitially.current = false;
@@ -77,12 +76,41 @@ export function AgentStreamView({
const { contentOffset } = event.nativeEvent;
const threshold = Math.max(insets.bottom, 32);
const nearBottom = contentOffset.y <= threshold;
+
+ if (isProgrammaticScrollRef.current) {
+ if (nearBottom && !isNearBottomRef.current) {
+ isNearBottomRef.current = true;
+ setIsNearBottom(true);
+ }
+ return;
+ }
+
+ if (!nearBottom && !isUserScrollingRef.current) {
+ return;
+ }
+
+ if (isNearBottomRef.current === nearBottom) {
+ return;
+ }
+
isNearBottomRef.current = nearBottom;
setIsNearBottom(nearBottom);
},
[insets.bottom]
);
+ const handleScrollBeginDrag = useCallback(() => {
+ isUserScrollingRef.current = true;
+ }, []);
+
+ const handleMomentumScrollBegin = useCallback(() => {
+ isUserScrollingRef.current = true;
+ }, []);
+
+ const handleScrollEnd = useCallback(() => {
+ isUserScrollingRef.current = false;
+ }, []);
+
const scrollToBottomInternal = useCallback(
({ animated }: { animated: boolean }) => {
const list = flatListRef.current;
@@ -156,10 +184,8 @@ export function AgentStreamView({
case "thought":
return (
-
);
@@ -235,6 +261,10 @@ export function AgentStreamView({
}}
style={stylesheet.list}
onScroll={handleScroll}
+ onScrollBeginDrag={handleScrollBeginDrag}
+ onScrollEndDrag={handleScrollEnd}
+ onMomentumScrollBegin={handleMomentumScrollBegin}
+ onMomentumScrollEnd={handleScrollEnd}
scrollEventThrottle={16}
ListEmptyComponent={
diff --git a/packages/app/src/components/create-agent-modal.tsx b/packages/app/src/components/create-agent-modal.tsx
index e49ad8834..0ec7ed6f4 100644
--- a/packages/app/src/components/create-agent-modal.tsx
+++ b/packages/app/src/components/create-agent-modal.tsx
@@ -27,24 +27,30 @@ import { useRecentPaths } from "@/hooks/use-recent-paths";
import { useSession } from "@/contexts/session-context";
import { useRouter } from "expo-router";
import { generateMessageId } from "@/types/stream";
+import {
+ listAgentTypeDefinitions,
+ type AgentType,
+ type AgentTypeDefinition,
+} from "@server/server/acp/agent-types";
interface CreateAgentModalProps {
isVisible: boolean;
onClose: () => void;
}
-const MODES = [
- {
- value: "plan",
- label: "Plan",
- description: "Plan and design before implementing",
- },
- {
- value: "bypassPermissions",
- label: "Bypass Permissions",
- description: "Skip permission prompts for faster execution",
- },
-] as const;
+const agentTypeDefinitions = listAgentTypeDefinitions();
+
+const agentTypeDefinitionMap: Record =
+ {} as Record;
+for (const definition of agentTypeDefinitions) {
+ agentTypeDefinitionMap[definition.id] = definition;
+}
+
+const fallbackDefinition = agentTypeDefinitionMap.claude ?? agentTypeDefinitions[0];
+const DEFAULT_AGENT_TYPE: AgentType = fallbackDefinition
+ ? fallbackDefinition.id
+ : "claude";
+const DEFAULT_MODE_FOR_DEFAULT_AGENT = fallbackDefinition?.defaultModeId;
export function CreateAgentModal({
isVisible,
@@ -58,11 +64,41 @@ export function CreateAgentModal({
const router = useRouter();
const [workingDir, setWorkingDir] = useState("");
- const [selectedMode, setSelectedMode] = useState("plan");
+ const [selectedAgentType, setSelectedAgentType] = useState(
+ DEFAULT_AGENT_TYPE
+ );
+ const [selectedMode, setSelectedMode] = useState(
+ DEFAULT_MODE_FOR_DEFAULT_AGENT ?? ""
+ );
const [worktreeName, setWorktreeName] = useState("");
const [errorMessage, setErrorMessage] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [pendingRequestId, setPendingRequestId] = useState(null);
+ const agentDefinition = agentTypeDefinitionMap[selectedAgentType];
+ const modeOptions = agentDefinition?.availableModes ?? [];
+
+ useEffect(() => {
+ if (!agentDefinition) {
+ return;
+ }
+
+ const availableModeIds = agentDefinition.availableModes.map(
+ (mode) => mode.id
+ );
+
+ if (availableModeIds.length === 0) {
+ if (selectedMode !== "") {
+ setSelectedMode("");
+ }
+ return;
+ }
+
+ if (!availableModeIds.includes(selectedMode)) {
+ const fallbackModeId =
+ agentDefinition.defaultModeId ?? availableModeIds[0];
+ setSelectedMode(fallbackModeId);
+ }
+ }, [agentDefinition, selectedMode]);
// Use ref instead of state to survive state resets in onDismiss
const pendingNavigationAgentIdRef = useRef(null);
@@ -164,11 +200,15 @@ export function CreateAgentModal({
setPendingRequestId(requestId);
setErrorMessage("");
+ const modeId =
+ modeOptions.length > 0 && selectedMode !== "" ? selectedMode : undefined;
+
// Create the agent
try {
createAgent({
cwd: path,
- initialMode: selectedMode,
+ agentType: selectedAgentType,
+ initialMode: modeId,
worktreeName: worktree || undefined,
requestId,
});
@@ -178,7 +218,17 @@ export function CreateAgentModal({
setIsLoading(false);
setPendingRequestId(null);
}
- }, [workingDir, worktreeName, selectedMode, isLoading, validateWorktreeName, addRecentPath, createAgent]);
+ }, [
+ workingDir,
+ worktreeName,
+ selectedMode,
+ selectedAgentType,
+ modeOptions,
+ isLoading,
+ validateWorktreeName,
+ addRecentPath,
+ createAgent,
+ ]);
const renderFooter = useCallback(
(props: BottomSheetFooterProps) => (
@@ -254,7 +304,8 @@ export function CreateAgentModal({
// Reset all state
setWorkingDir("");
setWorktreeName("");
- setSelectedMode("plan");
+ setSelectedAgentType(DEFAULT_AGENT_TYPE);
+ setSelectedMode(DEFAULT_MODE_FOR_DEFAULT_AGENT ?? "");
setErrorMessage("");
setIsLoading(false);
setPendingRequestId(null);
@@ -338,6 +389,58 @@ export function CreateAgentModal({
)}
+ {/* Agent Type Selector */}
+
+ Agent Type
+
+ {agentTypeDefinitions.map((definition) => {
+ const isSelected = selectedAgentType === definition.id;
+ return (
+ setSelectedAgentType(definition.id)}
+ disabled={isLoading}
+ style={[
+ styles.agentTypeOption,
+ isSelected && styles.agentTypeOptionSelected,
+ isLoading && styles.agentTypeOptionDisabled,
+ ]}
+ >
+
+
+ {isSelected && }
+
+
+
+ {definition.label}
+
+ {definition.description ? (
+
+ {definition.description}
+
+ ) : null}
+
+ {definition.availableModes.length > 0
+ ? `Modes: ${definition.availableModes
+ .map((mode) => mode.name)
+ .join(", ")}`
+ : "Modes: none"}
+
+
+
+
+ );
+ })}
+
+
+
{/* Worktree Name Input (Optional) */}
Worktree Name (Optional)
@@ -364,41 +467,50 @@ export function CreateAgentModal({
{/* Mode Selector */}
Mode
-
- {MODES.map((mode) => (
- setSelectedMode(mode.value)}
- disabled={isLoading}
- style={[
- styles.modeOption,
- selectedMode === mode.value && styles.modeOptionSelected,
- isLoading && styles.modeOptionDisabled,
- ]}
- >
-
-
+ This agent type does not expose selectable modes.
+
+ ) : (
+
+ {modeOptions.map((mode) => {
+ const isSelected = selectedMode === mode.id;
+ return (
+ setSelectedMode(mode.id)}
+ disabled={isLoading}
style={[
- styles.radioOuter,
- selectedMode === mode.value
- ? styles.radioOuterSelected
- : styles.radioOuterUnselected,
+ styles.modeOption,
+ isSelected && styles.modeOptionSelected,
+ isLoading && styles.modeOptionDisabled,
]}
>
- {selectedMode === mode.value && (
-
- )}
-
-
- {mode.label}
-
- {mode.description}
-
-
-
-
- ))}
-
+
+
+ {isSelected && }
+
+
+ {mode.name}
+ {mode.description ? (
+
+ {mode.description}
+
+ ) : null}
+
+
+
+ );
+ })}
+
+ )}
@@ -516,6 +628,45 @@ const styles = StyleSheet.create((theme) => ({
color: theme.colors.mutedForeground,
fontSize: theme.fontSize.sm,
},
+ agentTypeContainer: {
+ gap: theme.spacing[3],
+ },
+ agentTypeOption: {
+ backgroundColor: theme.colors.background,
+ borderRadius: theme.borderRadius.lg,
+ borderWidth: theme.borderWidth[1],
+ borderColor: theme.colors.border,
+ padding: theme.spacing[4],
+ },
+ agentTypeOptionSelected: {
+ borderColor: theme.colors.palette.blue[500],
+ backgroundColor: theme.colors.muted,
+ },
+ agentTypeOptionDisabled: {
+ opacity: theme.opacity[50],
+ },
+ agentTypeOptionContent: {
+ flexDirection: "row",
+ alignItems: "center",
+ },
+ agentTypeTextContainer: {
+ flex: 1,
+ },
+ agentTypeLabel: {
+ color: theme.colors.foreground,
+ fontSize: theme.fontSize.base,
+ fontWeight: theme.fontWeight.semibold,
+ marginBottom: theme.spacing[1],
+ },
+ agentTypeDescription: {
+ color: theme.colors.mutedForeground,
+ fontSize: theme.fontSize.sm,
+ marginBottom: theme.spacing[1],
+ },
+ agentTypeMeta: {
+ color: theme.colors.mutedForeground,
+ fontSize: theme.fontSize.sm,
+ },
footer: {
borderTopWidth: theme.borderWidth[1],
borderTopColor: theme.colors.border,
diff --git a/packages/app/src/components/message.tsx b/packages/app/src/components/message.tsx
index 24f63f74f..24dc066c8 100644
--- a/packages/app/src/components/message.tsx
+++ b/packages/app/src/components/message.tsx
@@ -1,5 +1,5 @@
import { View, Text, Pressable, Animated } from "react-native";
-import { useState, useEffect, useRef, memo } from "react";
+import { useState, useEffect, useRef, memo, useMemo } from "react";
import Markdown from "react-native-markdown-display";
import {
Circle,
@@ -16,6 +16,7 @@ import {
Pencil,
Eye,
SquareTerminal,
+ Brain,
} from "lucide-react-native";
import { StyleSheet } from "react-native-unistyles";
import { baseColors, theme } from "@/styles/theme";
@@ -48,7 +49,10 @@ const userMessageStylesheet = StyleSheet.create((theme) => ({
},
}));
-export const UserMessage = memo(function UserMessage({ message, timestamp }: UserMessageProps) {
+export const UserMessage = memo(function UserMessage({
+ message,
+ timestamp,
+}: UserMessageProps) {
return (
@@ -64,7 +68,7 @@ interface AssistantMessageProps {
isStreaming?: boolean;
}
-const assistantMessageStylesheet = StyleSheet.create((theme) => ({
+export const assistantMessageStylesheet = StyleSheet.create((theme) => ({
container: {
marginBottom: theme.spacing[3],
paddingHorizontal: theme.spacing[4],
@@ -133,6 +137,20 @@ const assistantMessageStylesheet = StyleSheet.create((theme) => ({
},
}));
+const markdownStyles = {
+ body: assistantMessageStylesheet.markdownBody,
+ paragraph: assistantMessageStylesheet.markdownParagraph,
+ strong: assistantMessageStylesheet.markdownStrong,
+ em: assistantMessageStylesheet.markdownEm,
+ code_inline: assistantMessageStylesheet.markdownCodeInline,
+ code_block: assistantMessageStylesheet.markdownCodeBlock,
+ fence: assistantMessageStylesheet.markdownFence,
+ link: assistantMessageStylesheet.markdownLink,
+ bullet_list: assistantMessageStylesheet.markdownList,
+ ordered_list: assistantMessageStylesheet.markdownList,
+ list_item: assistantMessageStylesheet.markdownListItem,
+};
+
export const AssistantMessage = memo(function AssistantMessage({
message,
timestamp,
@@ -162,20 +180,6 @@ export const AssistantMessage = memo(function AssistantMessage({
}
}, [isStreaming, fadeAnim]);
- const markdownStyles = {
- body: assistantMessageStylesheet.markdownBody,
- paragraph: assistantMessageStylesheet.markdownParagraph,
- strong: assistantMessageStylesheet.markdownStrong,
- em: assistantMessageStylesheet.markdownEm,
- code_inline: assistantMessageStylesheet.markdownCodeInline,
- code_block: assistantMessageStylesheet.markdownCodeBlock,
- fence: assistantMessageStylesheet.markdownFence,
- link: assistantMessageStylesheet.markdownLink,
- bullet_list: assistantMessageStylesheet.markdownList,
- ordered_list: assistantMessageStylesheet.markdownList,
- list_item: assistantMessageStylesheet.markdownListItem,
- };
-
return (
{message}
@@ -376,6 +380,79 @@ export const ActivityLog = memo(function ActivityLog({
);
});
+interface AgentThoughtMessageProps {
+ message: string;
+}
+
+const agentThoughtStylesheet = StyleSheet.create((theme) => ({
+ container: {
+ marginHorizontal: theme.spacing[2],
+ marginBottom: theme.spacing[2],
+ },
+ card: {
+ backgroundColor: theme.colors.card,
+ borderRadius: theme.borderRadius.lg,
+ borderWidth: theme.borderWidth[1],
+ borderColor: theme.colors.border,
+ paddingVertical: theme.spacing[2],
+ paddingHorizontal: theme.spacing[3],
+ flexDirection: "row",
+ alignItems: "flex-start",
+ gap: theme.spacing[3],
+ },
+ iconContainer: {
+ width: 28,
+ height: 28,
+ borderRadius: 14,
+ alignItems: "center",
+ justifyContent: "center",
+ flexShrink: 0,
+ paddingTop: theme.spacing[1],
+ },
+ icon: {
+ color: theme.colors.mutedForeground,
+ opacity: 0.8,
+ },
+ textContainer: {
+ flex: 1,
+ paddingVertical: theme.spacing[1],
+ },
+ label: {
+ color: theme.colors.mutedForeground,
+ fontSize: theme.fontSize.xs,
+ fontWeight: theme.fontWeight.semibold,
+ textTransform: "uppercase",
+ marginBottom: theme.spacing[1],
+ },
+ text: {
+ color: theme.colors.mutedForeground,
+ fontSize: theme.fontSize.base,
+ lineHeight: 20,
+ },
+}));
+
+export function AgentThoughtMessage({ message }: AgentThoughtMessageProps) {
+ const messageText = useMemo(() => {
+ return message
+ .trim()
+ .replace(/^\*\*|\*\*$/g, "")
+ .trim();
+ }, [message]);
+
+ return (
+
+
+
+
+
+
+ {messageText}
+
+
+
+ );
+}
+
interface ToolCallProps {
toolName: string;
kind?: string; // Optional kind for ACP tool calls
diff --git a/packages/app/src/contexts/session-context.tsx b/packages/app/src/contexts/session-context.tsx
index a7af862ac..0a765f84c 100644
--- a/packages/app/src/contexts/session-context.tsx
+++ b/packages/app/src/contexts/session-context.tsx
@@ -9,6 +9,7 @@ import type {
WSInboundMessage,
} from "@server/server/messages";
import type { AgentStatus, AgentUpdate, AgentNotification } from "@server/server/acp/types";
+import type { AgentType } from "@server/server/acp/agent-types";
import { parseSessionUpdate } from "@/types/agent-activity";
import { ScrollView } from "react-native";
import * as FileSystem from 'expo-file-system';
@@ -58,7 +59,7 @@ export interface Agent {
status: AgentStatus;
createdAt: Date;
lastActivityAt: Date;
- type: "claude";
+ type: AgentType;
sessionId: string | null;
error: string | null;
currentModeId: string | null;
@@ -118,7 +119,13 @@ interface SessionContextValue {
initializeAgent: (params: { agentId: string; requestId?: string }) => void;
sendAgentMessage: (agentId: string, message: string, imageUris?: string[]) => Promise;
sendAgentAudio: (agentId: string, audioBlob: Blob, requestId?: string) => Promise;
- createAgent: (options: { cwd: string; initialMode?: string; worktreeName?: string; requestId?: string }) => void;
+ createAgent: (options: {
+ cwd: string;
+ agentType: AgentType;
+ initialMode?: string;
+ worktreeName?: string;
+ requestId?: string;
+ }) => void;
setAgentMode: (agentId: string, modeId: string) => void;
respondToPermission: (requestId: string, agentId: string, sessionId: string, selectedOptionIds: string[]) => void;
}
@@ -720,7 +727,7 @@ export function SessionProvider({ children, serverUrl }: SessionProviderProps) {
}
}, [ws]);
- const createAgent = useCallback((options: { cwd: string; initialMode?: string; worktreeName?: string; requestId?: string }) => {
+ const createAgent = useCallback((options: { cwd: string; agentType: AgentType; initialMode?: string; worktreeName?: string; requestId?: string }) => {
const msg: WSInboundMessage = {
type: "session",
message: {
diff --git a/packages/app/src/server/agents/agent-type-definitions.ts b/packages/app/src/server/agents/agent-type-definitions.ts
deleted file mode 100644
index f2e7340b9..000000000
--- a/packages/app/src/server/agents/agent-type-definitions.ts
+++ /dev/null
@@ -1,69 +0,0 @@
-export type AgentType = "claude" | "codex";
-
-export interface AgentModeDefinition {
- id: string;
- name: string;
- description?: string;
-}
-
-export interface AgentTypeDefinition {
- id: AgentType;
- label: string;
- spawn: {
- command: string;
- args: string[];
- };
- supportsSessionPersistence: boolean;
- availableModes: AgentModeDefinition[];
- defaultModeId: string;
-}
-
-export const agentTypeDefinitions: Record = {
- claude: {
- id: "claude",
- label: "Claude Code",
- spawn: {
- command: "npx",
- args: ["@boudra/claude-code-acp"],
- },
- supportsSessionPersistence: true,
- availableModes: [
- {
- id: "plan",
- name: "Plan",
- description: "Plan and design before implementing",
- },
- {
- id: "bypassPermissions",
- name: "Bypass Permissions",
- description: "Skip permission prompts for faster execution",
- },
- ],
- defaultModeId: "plan",
- },
- codex: {
- id: "codex",
- label: "Codex",
- spawn: {
- command: "npx",
- args: ["@zed-industries/codex-acp"],
- },
- supportsSessionPersistence: false,
- availableModes: [
- {
- id: "default",
- name: "Default",
- description: "Standard coding mode",
- },
- ],
- defaultModeId: "default",
- },
-} as const;
-
-export function getAgentTypeDefinition(type: AgentType): AgentTypeDefinition {
- return agentTypeDefinitions[type];
-}
-
-export function getAgentModes(type: AgentType): AgentModeDefinition[] {
- return agentTypeDefinitions[type].availableModes;
-}
diff --git a/packages/app/src/types/shared.ts b/packages/app/src/types/shared.ts
index 4d5895c53..7665ae832 100644
--- a/packages/app/src/types/shared.ts
+++ b/packages/app/src/types/shared.ts
@@ -1,4 +1,3 @@
-import type { AgentStatus } from '@server/server/acp/types';
import type { RequestPermissionRequest } from '@agentclientprotocol/sdk';
import type { ToolCallPayload } from './stream';
@@ -22,12 +21,4 @@ export interface PendingPermission {
options: RequestPermissionRequest['options'];
}
-/**
- * Agent info for UI display
- */
-export interface AgentInfo {
- id: string;
- status: AgentStatus;
- createdAt: Date;
- type: 'claude';
-}
\ No newline at end of file
+// Agent info interface is provided by SessionContext's Agent type
diff --git a/packages/server/agents.json b/packages/server/agents.json
index 99430e83f..76d15eb7c 100644
--- a/packages/server/agents.json
+++ b/packages/server/agents.json
@@ -284,7 +284,7 @@
"sessionId": "20b0b2c2-18ec-409b-b932-a20c9ca8e0d3"
},
"createdAt": "2025-10-26T09:45:29.563Z",
- "lastActivityAt": "2025-10-26T13:11:22.795Z",
+ "lastActivityAt": "2025-10-26T14:53:00.404Z",
"cwd": "/Users/moboudra/dev/voice-dev"
},
{
@@ -380,7 +380,18 @@
"sessionId": "addbd42f-dbb1-4536-bc2b-e70a64587de6"
},
"createdAt": "2025-10-26T14:02:59.844Z",
- "lastActivityAt": "2025-10-26T14:08:58.462Z",
+ "lastActivityAt": "2025-10-26T16:45:15.700Z",
+ "cwd": "/Users/moboudra/dev/voice-dev"
+ },
+ {
+ "id": "f1df26aa-4b20-499e-a84b-1d61828cf7fe",
+ "title": "Prepare Concise Session Title",
+ "sessionId": "019a215a-22dd-7930-9db7-e38d256ac597",
+ "options": {
+ "type": "codex"
+ },
+ "createdAt": "2025-10-26T16:29:01.354Z",
+ "lastActivityAt": "2025-10-26T16:29:17.523Z",
"cwd": "/Users/moboudra/dev/voice-dev"
}
]
\ No newline at end of file
diff --git a/packages/server/package.json b/packages/server/package.json
index 612144ef2..641746058 100644
--- a/packages/server/package.json
+++ b/packages/server/package.json
@@ -20,6 +20,7 @@
"@agentclientprotocol/sdk": "^0.4.9",
"@ai-sdk/openai": "^2.0.52",
"@boudra/claude-code-acp": "^0.8.5",
+ "@zed-industries/codex-acp": "^0.3.9",
"@deepgram/sdk": "^3.4.0",
"@modelcontextprotocol/sdk": "^1.20.1",
"@openrouter/ai-sdk-provider": "^1.2.0",
diff --git a/packages/server/src/server/acp/agent-manager.test.ts b/packages/server/src/server/acp/agent-manager.test.ts
index 0c9daa4f5..fb6d0e31d 100644
--- a/packages/server/src/server/acp/agent-manager.test.ts
+++ b/packages/server/src/server/acp/agent-manager.test.ts
@@ -38,6 +38,7 @@ describe("AgentManager", () => {
const agentId = await manager.createAgent({
cwd: tmpDir,
+ type: "claude",
initialMode: "plan",
});
createdAgents.push({ manager, agentId });
@@ -96,10 +97,11 @@ describe("AgentManager", () => {
}, 120000);
describe("persistence", () => {
- it.only("should load persisted agent and send new prompt", async () => {
+ it("should load persisted agent and send new prompt", async () => {
const manager = new AgentManager();
const agentId = await manager.createAgent({
cwd: tmpDir,
+ type: "claude",
});
createdAgents.push({ manager, agentId });
@@ -115,7 +117,9 @@ describe("AgentManager", () => {
expect(status).toBe("completed");
- const agentBeforeKill = manager.listAgents().find((a) => a.id === agentId);
+ const agentBeforeKill = manager
+ .listAgents()
+ .find((a) => a.id === agentId);
const claudeSessionId = manager.getClaudeSessionId(agentId);
console.log("Before kill - ACP Session ID:", agentBeforeKill?.sessionId);
console.log("Before kill - Claude Session ID:", claudeSessionId);
@@ -127,7 +131,9 @@ describe("AgentManager", () => {
await newManager.initialize();
// Update the tracking to use the new manager instance
- const trackingIndex = createdAgents.findIndex((a) => a.agentId === agentId);
+ const trackingIndex = createdAgents.findIndex(
+ (a) => a.agentId === agentId
+ );
if (trackingIndex >= 0) {
createdAgents[trackingIndex] = { manager: newManager, agentId };
}
diff --git a/packages/server/src/server/acp/agent-manager.ts b/packages/server/src/server/acp/agent-manager.ts
index 7a39b7fb5..bbd0bcfea 100644
--- a/packages/server/src/server/acp/agent-manager.ts
+++ b/packages/server/src/server/acp/agent-manager.ts
@@ -17,6 +17,10 @@ import {
} from "@agentclientprotocol/sdk";
import { v4 as uuidv4 } from "uuid";
import { expandTilde } from "../terminal-mcp/tmux.js";
+import {
+ getAgentModes,
+ getAgentTypeDefinition,
+} from "./agent-types.js";
import type {
AgentStatus,
AgentInfo,
@@ -86,6 +90,84 @@ function getAgentError(state: ManagedAgentState): string | undefined {
return undefined;
}
+function normalizeModes(modes?: SessionMode[] | null): SessionMode[] {
+ if (!modes) {
+ return [];
+ }
+ return modes.map((mode) => ({
+ id: mode.id,
+ name: mode.name,
+ description: mode.description ?? null,
+ }));
+}
+
+function getStaticModes(type: AgentOptions["type"]): SessionMode[] {
+ const staticModes = getAgentModes(type);
+ return staticModes.map((mode) => ({
+ id: mode.id,
+ name: mode.name,
+ description: mode.description ?? null,
+ }));
+}
+
+function buildAvailableModes(
+ type: AgentOptions["type"],
+ runtimeModes?: SessionMode[] | null
+): SessionMode[] | null {
+ const runtimeList = normalizeModes(runtimeModes);
+ if (runtimeList.length > 0) {
+ return runtimeList;
+ }
+
+ const staticModes = getStaticModes(type);
+ return staticModes.length > 0 ? staticModes : null;
+}
+
+function resolveModeSelection({
+ type,
+ requestedModeId,
+ runtimeModes,
+}: {
+ type: AgentOptions["type"];
+ requestedModeId?: string | null;
+ runtimeModes?: SessionMode[] | null;
+}): {
+ availableModes: SessionMode[] | null;
+ modeId: string | null;
+ wasAdjusted: boolean;
+} {
+ const availableModes = buildAvailableModes(type, runtimeModes);
+
+ if (!availableModes || availableModes.length === 0) {
+ return {
+ availableModes: null,
+ modeId: null,
+ wasAdjusted: Boolean(requestedModeId),
+ };
+ }
+
+ if (requestedModeId) {
+ const match = availableModes.find((mode) => mode.id === requestedModeId);
+ if (match) {
+ return {
+ availableModes,
+ modeId: requestedModeId,
+ wasAdjusted: false,
+ };
+ }
+ }
+
+ const definition = getAgentTypeDefinition(type);
+ const fallbackModeId =
+ definition.defaultModeId ?? (availableModes[0]?.id ?? null);
+
+ return {
+ availableModes,
+ modeId: fallbackModeId,
+ wasAdjusted: Boolean(requestedModeId),
+ };
+}
+
/**
* Client implementation for ACP callbacks
*/
@@ -236,10 +318,26 @@ export class AgentManager {
}
const createdAt = new Date();
- const agentOptions: AgentOptions = {
- type: "claude",
- sessionId: null,
- };
+ const agentOptions: AgentOptions =
+ options.type === "claude"
+ ? {
+ type: "claude",
+ sessionId: null,
+ }
+ : {
+ type: "codex",
+ };
+
+ const modeSelection = resolveModeSelection({
+ type: options.type,
+ requestedModeId: options.initialMode ?? null,
+ });
+
+ if (options.initialMode && modeSelection.wasAdjusted) {
+ console.warn(
+ `[AgentManager] Invalid initial mode '${options.initialMode}' for agent type '${options.type}'. Falling back to '${modeSelection.modeId ?? "none"}'.`
+ );
+ }
const agent: ManagedAgent = {
id: agentId,
@@ -254,9 +352,7 @@ export class AgentManager {
currentAssistantMessageId: null,
currentThoughtId: null,
titleGenerationTriggered: false,
- pendingSessionMode: options.initialPrompt
- ? null
- : options.initialMode ?? null,
+ pendingSessionMode: options.initialPrompt ? null : modeSelection.modeId,
state: {
type: "uninitialized",
persistedSessionId: null,
@@ -593,14 +689,17 @@ export class AgentManager {
const runtime = this.getRuntime(agent);
const sessionId = runtime?.sessionId ?? null;
const currentModeId = runtime?.currentModeId ?? null;
- const availableModes = runtime?.availableModes ?? null;
+ const availableModes = buildAvailableModes(
+ agent.options.type,
+ runtime?.availableModes ?? null
+ );
return {
id: agent.id,
status,
createdAt: agent.createdAt,
lastActivityAt: agent.lastActivityAt,
- type: "claude" as const,
+ type: agent.options.type,
sessionId,
error: error ?? null,
currentModeId,
@@ -659,17 +758,21 @@ export class AgentManager {
const status = getAgentStatusFromState(agent.state);
const error = getAgentError(agent.state);
const runtime = this.getRuntime(agent);
+ const availableModes = buildAvailableModes(
+ agent.options.type,
+ runtime?.availableModes ?? null
+ );
const info: AgentInfo = {
id: agent.id,
status,
createdAt: agent.createdAt,
lastActivityAt: agent.lastActivityAt,
- type: "claude",
+ type: agent.options.type,
sessionId: runtime?.sessionId ?? null,
error: error ?? null,
currentModeId: runtime?.currentModeId ?? null,
- availableModes: runtime?.availableModes ?? null,
+ availableModes,
title: agent.title,
cwd: agent.cwd,
};
@@ -701,7 +804,10 @@ export class AgentManager {
throw new Error(`Agent ${agentId} not found`);
}
const runtime = this.getRuntime(agent);
- return runtime?.availableModes ?? null;
+ return buildAvailableModes(
+ agent.options.type,
+ runtime?.availableModes ?? null
+ );
}
/**
@@ -721,9 +827,12 @@ export class AgentManager {
throw new Error(`Agent ${agentId} has no active session`);
}
- // Validate mode is available if we have the list
- const availableModes = runtime.availableModes ?? [];
- if (availableModes.length > 0) {
+ const availableModes = buildAvailableModes(
+ agent.options.type,
+ runtime.availableModes ?? null
+ );
+
+ if (availableModes && availableModes.length > 0) {
const mode = availableModes.find((m) => m.id === modeId);
if (!mode) {
const availableIds = availableModes.map((m) => m.id).join(", ");
@@ -742,6 +851,7 @@ export class AgentManager {
const updatedRuntime: AgentRuntime = {
...runtime,
currentModeId: modeId,
+ availableModes,
};
switch (agent.state.type) {
@@ -832,11 +942,13 @@ export class AgentManager {
// Create the init promise
const initPromise = (async () => {
try {
+ const definition = getAgentTypeDefinition(agent.options.type);
const hasPersistedSession =
- (agent.options.type === "claude" &&
+ definition.supportsSessionPersistence &&
+ ((agent.options.type === "claude" &&
agent.options.sessionId !== null) ||
- !!persistedSessionId;
- const mode = hasPersistedSession ? "resume" : "new";
+ !!persistedSessionId);
+ const mode: "new" | "resume" = hasPersistedSession ? "resume" : "new";
console.log(
`[Agent ${agentId}] Starting lazy initialization (mode: ${mode})`
@@ -965,7 +1077,8 @@ export class AgentManager {
throw new Error(errorMessage);
}
- const agentProcess = spawn("npx", ["@boudra/claude-code-acp"], {
+ const definition = getAgentTypeDefinition(agent.options.type);
+ const agentProcess = spawn(definition.spawn.command, definition.spawn.args, {
stdio: ["pipe", "pipe", "pipe"],
cwd,
});
@@ -1040,11 +1153,15 @@ export class AgentManager {
},
});
+ const supportsResume = definition.supportsSessionPersistence;
+ const canResume = supportsResume && mode === "resume";
+
let sessionResponse:
| Awaited>
| Awaited>;
let effectiveSessionId: string;
- if (mode === "resume") {
+
+ if (canResume) {
const sessionIdToResume =
(agent.options.type === "claude" && agent.options.sessionId) ||
resumeSessionId ||
@@ -1064,7 +1181,13 @@ export class AgentManager {
});
effectiveSessionId = sessionIdToResume;
} else {
- console.log(`[Agent ${agentId}] Creating new session`);
+ if (mode === "resume" && !supportsResume) {
+ console.log(
+ `[Agent ${agentId}] Resume requested but unsupported for type '${definition.id}', starting new session`
+ );
+ } else {
+ console.log(`[Agent ${agentId}] Creating new session`);
+ }
const newSessionResponse = await connection.newSession({
cwd,
mcpServers: [],
@@ -1074,16 +1197,37 @@ export class AgentManager {
}
runtime.sessionId = effectiveSessionId;
- runtime.currentModeId = sessionResponse.modes?.currentModeId ?? null;
- runtime.availableModes = sessionResponse.modes?.availableModes ?? null;
- const claudeSessionId = sessionResponse._meta?.claudeSessionId as
- | string
- | undefined;
+ const modeSelection = resolveModeSelection({
+ type: agent.options.type,
+ requestedModeId: sessionResponse.modes?.currentModeId ?? null,
+ runtimeModes: sessionResponse.modes?.availableModes ?? null,
+ });
+
+ runtime.availableModes = modeSelection.availableModes;
+ runtime.currentModeId = modeSelection.modeId;
+
+ if (
+ sessionResponse.modes?.currentModeId &&
+ modeSelection.wasAdjusted
+ ) {
+ console.warn(
+ `[Agent ${agentId}] Mode '${sessionResponse.modes.currentModeId}' not available for type '${agent.options.type}', using '${modeSelection.modeId ?? "none"}' instead.`
+ );
+ }
+
+ const claudeSessionId =
+ sessionResponse._meta?.claudeSessionId !== undefined
+ ? (sessionResponse._meta?.claudeSessionId as string | undefined)
+ : undefined;
console.log(
`[Agent ${agentId}] Session ${
- mode === "new" ? "created" : "loaded"
- }: ACP=${effectiveSessionId}, Claude=${claudeSessionId || "N/A"}`
+ canResume ? "loaded" : "created"
+ }: ACP=${effectiveSessionId}${
+ agent.options.type === "claude"
+ ? `, Claude=${claudeSessionId || "N/A"}`
+ : ""
+ }`
);
await this.updateAgent(agentId, (managedAgent) => {
diff --git a/packages/server/src/server/acp/agent-persistence.ts b/packages/server/src/server/acp/agent-persistence.ts
index a4b1220ff..4bcba02f6 100644
--- a/packages/server/src/server/acp/agent-persistence.ts
+++ b/packages/server/src/server/acp/agent-persistence.ts
@@ -13,7 +13,9 @@ export const AgentOptionsSchema = z.discriminatedUnion("type", [
type: z.literal("claude"),
sessionId: z.string().nullable(), // Claude's internal session ID (null until first prompt)
}),
- // Add more agent types here in the future
+ z.object({
+ type: z.literal("codex"),
+ }),
]);
export type AgentOptions = z.infer;
diff --git a/packages/server/src/server/acp/agent-types.ts b/packages/server/src/server/acp/agent-types.ts
new file mode 100644
index 000000000..00312eaf2
--- /dev/null
+++ b/packages/server/src/server/acp/agent-types.ts
@@ -0,0 +1,90 @@
+export type AgentType = "claude" | "codex";
+
+export interface AgentModeDefinition {
+ id: string;
+ name: string;
+ description?: string | null;
+}
+
+export interface AgentTypeDefinition {
+ id: AgentType;
+ label: string;
+ description?: string;
+ spawn: {
+ command: string;
+ args: string[];
+ };
+ supportsSessionPersistence: boolean;
+ availableModes: AgentModeDefinition[];
+ defaultModeId: string;
+}
+
+const CLAUDE_MODES: AgentModeDefinition[] = [
+ {
+ id: "plan",
+ name: "Plan",
+ description: "Plan and design before implementing",
+ },
+ {
+ id: "bypassPermissions",
+ name: "Bypass Permissions",
+ description: "Skip permission prompts for faster execution",
+ },
+];
+
+const CODEX_MODES: AgentModeDefinition[] = [
+ {
+ id: "read-only",
+ name: "Read Only",
+ description: "Codex can read files and answer questions. Codex requires approval to make edits, run commands, or access network",
+ },
+ {
+ id: "auto",
+ name: "Auto",
+ description: "Codex can read files, make edits, and run commands in the workspace. Codex requires approval to work outside the workspace or access network",
+ },
+ {
+ id: "full-access",
+ name: "Full Access",
+ description: "Codex can read files, make edits, and run commands with network access, without approval. Exercise caution",
+ },
+];
+
+const agentTypeDefinitions: Record = {
+ claude: {
+ id: "claude",
+ label: "Claude Code",
+ description: "Full Claude Code agent with session persistence",
+ spawn: {
+ command: "npx",
+ args: ["@boudra/claude-code-acp"],
+ },
+ supportsSessionPersistence: true,
+ availableModes: CLAUDE_MODES,
+ defaultModeId: "plan",
+ },
+ codex: {
+ id: "codex",
+ label: "Codex",
+ description: "Zed Codex ACP integration (no session resume)",
+ spawn: {
+ command: "npx",
+ args: ["@zed-industries/codex-acp"],
+ },
+ supportsSessionPersistence: false,
+ availableModes: CODEX_MODES,
+ defaultModeId: "auto",
+ },
+} as const;
+
+export function getAgentTypeDefinition(type: AgentType): AgentTypeDefinition {
+ return agentTypeDefinitions[type];
+}
+
+export function getAgentModes(type: AgentType): AgentModeDefinition[] {
+ return agentTypeDefinitions[type].availableModes;
+}
+
+export function listAgentTypeDefinitions(): AgentTypeDefinition[] {
+ return Object.values(agentTypeDefinitions);
+}
diff --git a/packages/server/src/server/acp/mcp-server.ts b/packages/server/src/server/acp/mcp-server.ts
index ed044f1e7..8acf1c889 100644
--- a/packages/server/src/server/acp/mcp-server.ts
+++ b/packages/server/src/server/acp/mcp-server.ts
@@ -121,6 +121,7 @@ export async function createAgentMcpServer(
const agentId = await agentManager.createAgent({
cwd: resolvedCwd,
+ type: "claude",
initialPrompt,
initialMode,
});
diff --git a/packages/server/src/server/acp/test-agent.ts b/packages/server/src/server/acp/test-agent.ts
deleted file mode 100644
index 4903489d0..000000000
--- a/packages/server/src/server/acp/test-agent.ts
+++ /dev/null
@@ -1,88 +0,0 @@
-#!/usr/bin/env tsx
-
-import { AgentManager } from "./agent-manager.js";
-
-async function main() {
- console.log("=== ACP Agent Manager Test ===\n");
-
- const manager = new AgentManager();
-
- try {
- console.log("1. Creating agent...");
- const agentId = await manager.createAgent({
- cwd: process.cwd(),
- });
- console.log(` ✓ Agent created: ${agentId}\n`);
-
- console.log("2. Subscribing to updates...");
- manager.subscribeToUpdates(agentId, (update) => {
- console.log(` [UPDATE] ${new Date().toISOString()}`);
- console.log(` Type: ${(update.notification as any).type}`);
- console.log(` Notification:`, JSON.stringify(update.notification, null, 2));
- console.log();
- });
- console.log(" ✓ Subscribed\n");
-
- console.log("3. Checking initial status...");
- const status = manager.getAgentStatus(agentId);
- console.log(` Status: ${status}\n`);
-
- console.log("4. Listing all agents...");
- const agents = manager.listAgents();
- console.log(` Total agents: ${agents.length}`);
- agents.forEach((agent) => {
- console.log(
- ` - ${agent.id}: ${agent.status} (created: ${agent.createdAt.toISOString()})`
- );
- });
- console.log();
-
- console.log("5. Sending prompt: 'List files in current directory'");
- await manager.sendPrompt(
- agentId,
- "List files in current directory using ls"
- );
- console.log(" ✓ Prompt sent\n");
-
- console.log("6. Waiting for completion (30 seconds max)...");
- const startTime = Date.now();
- const maxWait = 30000; // 30 seconds
-
- await new Promise((resolve, reject) => {
- const checkInterval = setInterval(() => {
- const currentStatus = manager.getAgentStatus(agentId);
- const elapsed = Date.now() - startTime;
-
- if (currentStatus === "completed") {
- clearInterval(checkInterval);
- console.log(` ✓ Agent completed in ${elapsed}ms\n`);
- resolve();
- } else if (currentStatus === "failed") {
- clearInterval(checkInterval);
- const agents = manager.listAgents();
- const agent = agents.find((a) => a.id === agentId);
- reject(new Error(`Agent failed: ${agent?.error || "Unknown error"}`));
- } else if (elapsed > maxWait) {
- clearInterval(checkInterval);
- console.log(` ⚠ Timeout after ${elapsed}ms (status: ${currentStatus})\n`);
- resolve();
- }
- }, 500);
- });
-
- console.log("7. Cleaning up...");
- await manager.killAgent(agentId);
- console.log(" ✓ Agent killed\n");
-
- console.log("=== Test completed successfully ===");
- } catch (error) {
- console.error("\n❌ Test failed:");
- console.error(error);
- process.exit(1);
- }
-}
-
-main().catch((error) => {
- console.error("Fatal error:", error);
- process.exit(1);
-});
diff --git a/packages/server/src/server/acp/test-comprehensive.ts b/packages/server/src/server/acp/test-comprehensive.ts
deleted file mode 100755
index d3857f64a..000000000
--- a/packages/server/src/server/acp/test-comprehensive.ts
+++ /dev/null
@@ -1,610 +0,0 @@
-#!/usr/bin/env tsx
-
-import { AgentManager } from "./agent-manager.js";
-import { mkdtemp, rm, readFile, access } from "fs/promises";
-import { tmpdir } from "os";
-import { join } from "path";
-import type { AgentUpdate, AgentStatus } from "./types.js";
-
-/**
- * Comprehensive Test Suite for ACP Agent Functionality
- *
- * This test validates all core features with a REAL Claude Code agent (no mocking):
- *
- * 1. Directory Control - Agent runs in specified directory
- * 2. Permission Mode: Auto Approve - File operations are automatically approved
- * 3. Permission Mode: Reject All - File operations are blocked
- * 4. Multiple Prompts - Agent can handle sequential prompts
- * 5. Update Streaming - All update types stream correctly (message chunks, tool calls, etc.)
- * 6. State Management - Status accurately reflects actual agent state
- *
- * Run with: npx tsx src/server/acp/test-comprehensive.ts
- */
-
-interface TestContext {
- manager: AgentManager;
- tmpDir: string;
-}
-
-interface CollectedUpdates {
- all: AgentUpdate[];
- messageChunks: string[];
- toolCalls: any[];
- toolResults: any[];
- statusUpdates: any[];
- commands: any[];
-}
-
-async function main() {
- const manager = new AgentManager();
- const tmpDir = await mkdtemp(join(tmpdir(), "acp-test-"));
-
- console.log("=== Comprehensive ACP Agent Test Suite ===");
- console.log(`Test directory: ${tmpDir}\n`);
-
- const ctx: TestContext = { manager, tmpDir };
-
- try {
- await testDirectoryAndInitialPrompt(ctx);
- await testPermissionAutoApprove(ctx);
- await testPermissionRejectAll(ctx);
- await testMultiplePrompts(ctx);
- await testUpdateStreaming(ctx);
- await testStateManagement(ctx);
-
- console.log("\n✅ ALL TESTS PASSED!");
- } catch (error) {
- console.error("\n❌ TEST SUITE FAILED:", error);
- throw error;
- } finally {
- // Cleanup
- console.log("\n=== Cleanup ===");
- const agents = manager.listAgents();
- console.log(`Killing ${agents.length} agents...`);
- for (const agent of agents) {
- await manager.killAgent(agent.id);
- }
- console.log(`Removing test directory: ${tmpDir}`);
- await rm(tmpDir, { recursive: true, force: true });
- console.log("Cleanup complete");
- }
-}
-
-/**
- * TEST 1: Directory Control and Initial Prompt
- * Verifies that the agent runs in the specified directory
- * and can execute the initial prompt successfully
- */
-async function testDirectoryAndInitialPrompt(ctx: TestContext) {
- console.log("\n=== TEST 1: Directory Control and Initial Prompt ===");
-
- const updates = createUpdateCollector();
- let unsubscribe: (() => void) | null = null;
-
- // Create agent without initial prompt first, so we can subscribe
- const agentId = await ctx.manager.createAgent({
- cwd: ctx.tmpDir,
- });
-
- console.log(`✓ Agent created: ${agentId}`);
-
- // Subscribe before sending prompt
- unsubscribe = ctx.manager.subscribeToUpdates(agentId, (update) => {
- collectUpdate(updates, update);
- });
-
- // Now send the prompt
- await ctx.manager.sendPrompt(
- agentId,
- "Run pwd and confirm you're in the test directory. Output the full path."
- );
-
- try {
- // Wait for processing to complete
- await waitForStatus(ctx.manager, agentId, "completed", 60000);
-
- console.log(`✓ Agent completed processing`);
- console.log(` - Total updates: ${updates.all.length}`);
- console.log(` - Message chunks: ${updates.messageChunks.length}`);
- console.log(` - Tool calls: ${updates.toolCalls.length}`);
-
- // Verify the output contains the tmpDir path
- const fullMessage = updates.messageChunks.join("");
- if (!fullMessage.includes(ctx.tmpDir)) {
- throw new Error(
- `Expected output to contain tmpDir path "${ctx.tmpDir}", but got: ${fullMessage}`
- );
- }
-
- console.log(`✓ Output contains correct directory path`);
-
- // Verify we got tool calls (pwd command should be executed via terminal)
- if (updates.toolCalls.length === 0) {
- console.log(`⚠ Warning: No tool calls detected (expected pwd command)`);
- } else {
- console.log(`✓ Tool calls detected: ${updates.toolCalls.length}`);
- }
-
- // Verify status transitions
- const statuses = extractStatusTransitions(updates);
- if (statuses.length > 0) {
- console.log(`✓ Status transitions: ${statuses.join(" → ")}`);
- }
-
- console.log("✅ TEST 1 PASSED\n");
- } finally {
- if (unsubscribe) unsubscribe();
- await ctx.manager.killAgent(agentId);
- }
-}
-
-/**
- * TEST 2: Permission Mode - Auto Approve
- * Verifies that file operations are automatically approved
- */
-async function testPermissionAutoApprove(ctx: TestContext) {
- console.log("\n=== TEST 2: Permission Mode - Auto Approve ===");
-
- const updates = createUpdateCollector();
- const testFile = join(ctx.tmpDir, "test-auto-approve.txt");
-
- const agentId = await ctx.manager.createAgent({
- cwd: ctx.tmpDir,
- });
-
- console.log(`✓ Agent created: ${agentId}`);
-
- const unsubscribe = ctx.manager.subscribeToUpdates(agentId, (update) => {
- collectUpdate(updates, update);
- });
-
- await ctx.manager.sendPrompt(
- agentId,
- `Create a file called "test-auto-approve.txt" with the content "hello from auto approve". You can write the file however you prefer.`
- );
-
- try {
- // Wait for completion
- await waitForStatus(ctx.manager, agentId, "completed", 60000);
-
- console.log(`✓ Agent completed processing`);
- console.log(` - Total updates: ${updates.all.length}`);
- console.log(` - Tool calls: ${updates.toolCalls.length}`);
-
- // Verify file was created
- try {
- const content = await readFile(testFile, "utf-8");
- console.log(`✓ File created with content: "${content.trim()}"`);
-
- if (!content.includes("hello from auto approve")) {
- throw new Error(
- `Expected file content to include "hello from auto approve", got: ${content}`
- );
- }
-
- console.log(`✓ File content is correct`);
- } catch (error) {
- throw new Error(`File was not created or could not be read: ${error}`);
- }
-
- console.log("✅ TEST 2 PASSED\n");
- } finally {
- unsubscribe();
- await ctx.manager.killAgent(agentId);
- }
-}
-
-/**
- * TEST 3: Permission Mode - Reject All
- * Verifies that file operations are blocked
- */
-async function testPermissionRejectAll(ctx: TestContext) {
- console.log("\n=== TEST 3: Permission Mode - Reject All ===");
-
- const updates = createUpdateCollector();
- const testFile = join(ctx.tmpDir, "test-blocked.txt");
-
- const agentId = await ctx.manager.createAgent({
- cwd: ctx.tmpDir,
- });
-
- console.log(`✓ Agent created: ${agentId}`);
-
- const unsubscribe = ctx.manager.subscribeToUpdates(agentId, (update) => {
- collectUpdate(updates, update);
- });
-
- await ctx.manager.sendPrompt(
- agentId,
- `Create a file called "test-blocked.txt" with the content "this should be blocked".`
- );
-
- try {
- // Wait for completion (agent should complete even if permission denied)
- await waitForStatus(ctx.manager, agentId, "completed", 60000);
-
- console.log(`✓ Agent completed processing`);
- console.log(` - Total updates: ${updates.all.length}`);
-
- // Verify file was NOT created
- try {
- await access(testFile);
- throw new Error(
- "File should not have been created with reject_all mode"
- );
- } catch (error: any) {
- if (error.code === "ENOENT") {
- console.log(`✓ File was correctly blocked from creation`);
- } else {
- throw error;
- }
- }
-
- // Check that the response indicates permission denial
- const fullMessage = updates.messageChunks.join("").toLowerCase();
- const hasPermissionMessage =
- fullMessage.includes("permission") ||
- fullMessage.includes("denied") ||
- fullMessage.includes("blocked") ||
- fullMessage.includes("unable") ||
- fullMessage.includes("could not") ||
- fullMessage.includes("cannot");
-
- if (hasPermissionMessage) {
- console.log(`✓ Agent indicated permission denial in response`);
- } else {
- console.log(
- `⚠ Warning: Agent response doesn't clearly indicate permission denial`
- );
- console.log(` Response: ${fullMessage.substring(0, 200)}...`);
- }
-
- console.log("✅ TEST 3 PASSED\n");
- } finally {
- unsubscribe();
- await ctx.manager.killAgent(agentId);
- }
-}
-
-/**
- * TEST 4: Multiple Prompts
- * Verifies that an agent can handle multiple sequential prompts
- */
-async function testMultiplePrompts(ctx: TestContext) {
- console.log("\n=== TEST 4: Multiple Prompts ===");
-
- const updates1 = createUpdateCollector();
- const updates2 = createUpdateCollector();
- let collectingFirst = true;
-
- const agentId = await ctx.manager.createAgent({
- cwd: ctx.tmpDir,
- });
-
- console.log(`✓ Agent created: ${agentId}`);
-
- const unsubscribe = ctx.manager.subscribeToUpdates(agentId, (update) => {
- if (collectingFirst) {
- collectUpdate(updates1, update);
- } else {
- collectUpdate(updates2, update);
- }
- });
-
- try {
- // Send first prompt
- console.log("Sending prompt 1...");
- await ctx.manager.sendPrompt(agentId, "Echo 'first prompt response'");
-
- await waitForStatus(ctx.manager, agentId, "completed", 30000);
- console.log(`✓ First prompt completed`);
- console.log(` - Updates: ${updates1.all.length}`);
- console.log(` - Message: ${updates1.messageChunks.join("").substring(0, 100)}...`);
-
- // Switch to collecting second set of updates
- collectingFirst = false;
-
- // Send second prompt
- console.log("Sending prompt 2...");
- await ctx.manager.sendPrompt(agentId, "Echo 'second prompt response'");
-
- await waitForStatus(ctx.manager, agentId, "completed", 30000);
- console.log(`✓ Second prompt completed`);
- console.log(` - Updates: ${updates2.all.length}`);
- console.log(` - Message: ${updates2.messageChunks.join("").substring(0, 100)}...`);
-
- // Verify both prompts got responses
- if (updates1.all.length === 0 || updates2.all.length === 0) {
- throw new Error("Both prompts should generate updates");
- }
-
- console.log(`✓ Both prompts executed successfully`);
- console.log("✅ TEST 4 PASSED\n");
- } finally {
- unsubscribe();
- await ctx.manager.killAgent(agentId);
- }
-}
-
-/**
- * TEST 5: Update Streaming
- * Verifies that all update types are received correctly
- */
-async function testUpdateStreaming(ctx: TestContext) {
- console.log("\n=== TEST 5: Update Streaming ===");
-
- const updates = createUpdateCollector();
-
- const agentId = await ctx.manager.createAgent({
- cwd: ctx.tmpDir,
- });
-
- console.log(`✓ Agent created: ${agentId}`);
-
- const unsubscribe = ctx.manager.subscribeToUpdates(agentId, (update) => {
- collectUpdate(updates, update);
- });
-
- await ctx.manager.sendPrompt(
- agentId,
- "List the files in the current directory using the Bash tool. Then explain what you found."
- );
-
- try {
- await waitForStatus(ctx.manager, agentId, "completed", 60000);
-
- console.log(`✓ Agent completed processing`);
- console.log(`\nUpdate Statistics:`);
- console.log(` - Total updates: ${updates.all.length}`);
- console.log(` - Message chunks: ${updates.messageChunks.length}`);
- console.log(` - Tool calls: ${updates.toolCalls.length}`);
- console.log(` - Tool results: ${updates.toolResults.length}`);
- console.log(` - Status updates: ${updates.statusUpdates.length}`);
- console.log(` - Commands: ${updates.commands.length}`);
-
- // Verify message can be reconstructed
- const fullMessage = updates.messageChunks.join("");
- console.log(`\nReconstructed message length: ${fullMessage.length} chars`);
- console.log(`First 150 chars: ${fullMessage.substring(0, 150)}...`);
-
- if (fullMessage.length === 0) {
- throw new Error("Expected to receive message chunks");
- }
-
- console.log(`✓ Message chunks successfully reconstructed`);
-
- // Verify we got tool calls
- if (updates.toolCalls.length === 0) {
- throw new Error("Expected at least one tool call");
- }
-
- console.log(`✓ Tool calls captured: ${updates.toolCalls.length}`);
-
- // Log tool call details
- updates.toolCalls.forEach((call, i) => {
- console.log(` Tool ${i + 1}: ${JSON.stringify(call).substring(0, 100)}...`);
- });
-
- console.log("✅ TEST 5 PASSED\n");
- } finally {
- unsubscribe();
- await ctx.manager.killAgent(agentId);
- }
-}
-
-/**
- * TEST 6: State Management
- * Verifies that agent status accurately reflects actual state
- */
-async function testStateManagement(ctx: TestContext) {
- console.log("\n=== TEST 6: State Management ===");
-
- const statusLog: Array<{ time: Date; status: AgentStatus }> = [];
-
- const agentId = await ctx.manager.createAgent({
- cwd: ctx.tmpDir,
- });
-
- console.log(`✓ Agent created: ${agentId}`);
-
- await ctx.manager.sendPrompt(
- agentId,
- "Sleep for 2 seconds using a bash command, then echo 'done'"
- );
-
- // Track status changes
- const checkInterval = setInterval(() => {
- const status = ctx.manager.getAgentStatus(agentId);
- statusLog.push({ time: new Date(), status });
- }, 500);
-
- try {
- // Wait for completion
- await waitForStatus(ctx.manager, agentId, "completed", 60000);
-
- clearInterval(checkInterval);
-
- console.log(`✓ Agent completed processing`);
- console.log(`\nStatus transitions logged: ${statusLog.length}`);
-
- // Analyze status transitions
- const transitions: string[] = [];
- let lastStatus: AgentStatus | null = null;
-
- for (const entry of statusLog) {
- if (entry.status !== lastStatus) {
- transitions.push(entry.status);
- lastStatus = entry.status;
- }
- }
-
- console.log(`Status transition sequence: ${transitions.join(" → ")}`);
-
- // Verify expected transitions occurred
- const expectedStates = ["ready", "processing", "completed"];
- for (const expectedState of expectedStates) {
- if (!transitions.includes(expectedState as AgentStatus)) {
- console.log(
- `⚠ Warning: Expected state "${expectedState}" not observed`
- );
- } else {
- console.log(`✓ State "${expectedState}" observed`);
- }
- }
-
- // Verify final status is completed
- const finalStatus = ctx.manager.getAgentStatus(agentId);
- if (finalStatus !== "completed") {
- throw new Error(
- `Expected final status to be "completed", got "${finalStatus}"`
- );
- }
-
- console.log(`✓ Final status is correct: ${finalStatus}`);
-
- // Kill the agent and verify status changes
- await ctx.manager.killAgent(agentId);
-
- // Agent should be removed from manager
- try {
- ctx.manager.getAgentStatus(agentId);
- throw new Error("Agent should be removed after killing");
- } catch (error: any) {
- if (error.message.includes("not found")) {
- console.log(`✓ Agent correctly removed after kill`);
- } else {
- throw error;
- }
- }
-
- console.log("✅ TEST 6 PASSED\n");
- } finally {
- clearInterval(checkInterval);
- }
-}
-
-/**
- * Helper: Create an update collector
- */
-function createUpdateCollector(): CollectedUpdates {
- return {
- all: [],
- messageChunks: [],
- toolCalls: [],
- toolResults: [],
- statusUpdates: [],
- commands: [],
- };
-}
-
-/**
- * Helper: Collect and categorize an update
- */
-function collectUpdate(collector: CollectedUpdates, update: AgentUpdate): void {
- collector.all.push(update);
-
- const notification = update.notification as any;
-
- // The notification structure can be:
- // 1. { type: "sessionUpdate", sessionUpdate: { ... } } - for status updates
- // 2. { sessionUpdate: "agent_message_chunk", content: { ... } } - for message chunks
- // 3. { sessionUpdate: "tool_call", ... } - for tool calls
- // 4. { availableCommands: [...] } - for available commands
-
- // Message chunks
- if (notification.sessionUpdate === "agent_message_chunk" && notification.content) {
- const content = notification.content;
- if (content.type === "text" && content.text) {
- collector.messageChunks.push(content.text);
- }
- }
-
- // Tool calls
- if (notification.sessionUpdate === "tool_call") {
- collector.toolCalls.push(notification);
- }
-
- // Tool results
- if (notification.sessionUpdate === "tool_result") {
- collector.toolResults.push(notification);
- }
-
- // Status updates from the wrapper
- if (notification.type === "sessionUpdate" && notification.sessionUpdate?.status) {
- collector.statusUpdates.push(notification.sessionUpdate.status);
- }
-
- // Status updates from our custom type
- if (notification.sessionUpdate === "status_change") {
- collector.statusUpdates.push(notification.status);
- }
-
- // Available commands
- if (notification.availableCommands) {
- collector.commands.push(notification.availableCommands);
- }
-}
-
-/**
- * Helper: Extract status transitions from updates
- */
-function extractStatusTransitions(collector: CollectedUpdates): string[] {
- const statuses: string[] = [];
- let lastStatus: string | null = null;
-
- for (const statusUpdate of collector.statusUpdates) {
- if (statusUpdate !== lastStatus) {
- statuses.push(String(statusUpdate));
- lastStatus = statusUpdate;
- }
- }
-
- return statuses;
-}
-
-/**
- * Helper: Wait for agent to reach a specific status
- */
-async function waitForStatus(
- manager: AgentManager,
- agentId: string,
- targetStatus: AgentStatus,
- timeoutMs: number
-): Promise {
- const startTime = Date.now();
-
- while (Date.now() - startTime < timeoutMs) {
- const currentStatus = manager.getAgentStatus(agentId);
-
- if (currentStatus === targetStatus) {
- return;
- }
-
- if (currentStatus === "failed") {
- const agent = manager.listAgents().find((a) => a.id === agentId);
- throw new Error(
- `Agent failed while waiting for status "${targetStatus}": ${agent?.error}`
- );
- }
-
- // Wait a bit before checking again
- await sleep(500);
- }
-
- throw new Error(
- `Timeout waiting for status "${targetStatus}" (current: ${manager.getAgentStatus(agentId)})`
- );
-}
-
-/**
- * Helper: Sleep for specified milliseconds
- */
-function sleep(ms: number): Promise {
- return new Promise((resolve) => setTimeout(resolve, ms));
-}
-
-// Run the test suite
-main().catch((error) => {
- console.error("Fatal error:", error);
- process.exit(1);
-});
diff --git a/packages/server/src/server/acp/test-concurrent.ts b/packages/server/src/server/acp/test-concurrent.ts
deleted file mode 100644
index 3bda9146f..000000000
--- a/packages/server/src/server/acp/test-concurrent.ts
+++ /dev/null
@@ -1,80 +0,0 @@
-#!/usr/bin/env tsx
-
-import { AgentManager } from "./agent-manager.js";
-
-async function main() {
- console.log("=== ACP Concurrent Agents Test ===\n");
-
- const manager = new AgentManager();
-
- try {
- console.log("1. Creating multiple agents...");
- const agent1Id = await manager.createAgent({
- cwd: process.cwd(),
- });
- const agent2Id = await manager.createAgent({
- cwd: process.cwd(),
- });
- console.log(` ✓ Agent 1: ${agent1Id}`);
- console.log(` ✓ Agent 2: ${agent2Id}\n`);
-
- console.log("2. Setting up update handlers...");
- let agent1Updates = 0;
- let agent2Updates = 0;
-
- manager.subscribeToUpdates(agent1Id, () => {
- agent1Updates++;
- });
-
- manager.subscribeToUpdates(agent2Id, () => {
- agent2Updates++;
- });
- console.log(" ✓ Handlers set up\n");
-
- console.log("3. Sending prompts to both agents...");
- await manager.sendPrompt(agent1Id, "Echo 'Agent 1 says hello'");
- await manager.sendPrompt(agent2Id, "Echo 'Agent 2 says hello'");
- console.log(" ✓ Prompts sent\n");
-
- console.log("4. Waiting for activity (5 seconds)...");
- await new Promise((resolve) => setTimeout(resolve, 5000));
- console.log(` Agent 1 updates: ${agent1Updates}`);
- console.log(` Agent 2 updates: ${agent2Updates}\n`);
-
- console.log("5. Listing all agents...");
- const agents = manager.listAgents();
- console.log(` Total agents: ${agents.length}`);
- agents.forEach((agent) => {
- console.log(` - ${agent.id.slice(0, 8)}: ${agent.status}`);
- });
- console.log();
-
- console.log("6. Testing cancellation on agent 1...");
- const status1Before = manager.getAgentStatus(agent1Id);
- console.log(` Agent 1 status before cancel: ${status1Before}`);
-
- await manager.cancelAgent(agent1Id);
- const status1After = manager.getAgentStatus(agent1Id);
- console.log(` Agent 1 status after cancel: ${status1After}\n`);
-
- console.log("7. Cleaning up...");
- await manager.killAgent(agent1Id);
- await manager.killAgent(agent2Id);
- console.log(" ✓ All agents killed\n");
-
- console.log("8. Verifying cleanup...");
- const remainingAgents = manager.listAgents();
- console.log(` Remaining agents: ${remainingAgents.length}\n`);
-
- console.log("=== Concurrent test completed successfully ===");
- } catch (error) {
- console.error("\n❌ Test failed:");
- console.error(error);
- process.exit(1);
- }
-}
-
-main().catch((error) => {
- console.error("Fatal error:", error);
- process.exit(1);
-});
diff --git a/packages/server/src/server/acp/test-integration.ts b/packages/server/src/server/acp/test-integration.ts
deleted file mode 100755
index 87110423f..000000000
--- a/packages/server/src/server/acp/test-integration.ts
+++ /dev/null
@@ -1,166 +0,0 @@
-#!/usr/bin/env tsx
-
-/**
- * Integration test for Agent MCP Server with Session
- *
- * This test simulates a Session-like scenario where:
- * 1. AgentManager is created
- * 2. Agent MCP server is set up
- * 3. Tools are called via the MCP client
- * 4. Agent updates are streamed to a mock WebSocket handler
- * 5. Cleanup is performed
- */
-
-import { AgentManager } from "./agent-manager.js";
-import { createAgentMcpServer } from "./mcp-server.js";
-import { experimental_createMCPClient } from "ai";
-import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
-import type { AgentUpdate } from "./types.js";
-
-async function sleep(ms: number): Promise {
- return new Promise((resolve) => setTimeout(resolve, ms));
-}
-
-async function main() {
- console.log("=== Agent MCP Integration Test ===\n");
-
- // Step 1: Create AgentManager
- console.log("1. Creating AgentManager...");
- const agentManager = new AgentManager();
- console.log(" ✓ AgentManager created\n");
-
- // Step 2: Create Agent MCP Server
- console.log("2. Creating Agent MCP Server...");
- const server = await createAgentMcpServer({ agentManager });
- console.log(" ✓ Agent MCP Server created\n");
-
- // Step 3: Set up transport and client
- console.log("3. Setting up MCP transport and client...");
- const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
- await server.connect(serverTransport);
- const mcpClient = await experimental_createMCPClient({
- transport: clientTransport,
- });
- console.log(" ✓ MCP client connected\n");
-
- // Step 4: Get tools from client
- console.log("4. Getting tools from MCP client...");
- const tools = await mcpClient.tools();
- const toolNames = Object.keys(tools);
- console.log(` ✓ Got ${toolNames.length} tools: ${toolNames.join(", ")}\n`);
-
- // Step 5: Verify all expected tools are present
- console.log("5. Verifying expected tools...");
- const expectedTools = [
- "create_coding_agent",
- "send_agent_prompt",
- "get_agent_status",
- "list_agents",
- "cancel_agent",
- "kill_agent",
- ];
-
- const missingTools = expectedTools.filter((t) => !toolNames.includes(t));
- if (missingTools.length > 0) {
- console.error(` ✗ Missing tools: ${missingTools.join(", ")}`);
- process.exit(1);
- }
- console.log(" ✓ All expected tools present\n");
-
- // Step 6: Test direct AgentManager usage (bypassing MCP tools for simplicity)
- console.log("6. Creating agent directly via AgentManager...");
- const receivedUpdates: AgentUpdate[] = [];
-
- const agentId = await agentManager.createAgent({
- initialPrompt: "Create a simple hello world function in TypeScript",
- cwd: process.cwd(),
- });
- console.log(` ✓ Agent created with ID: ${agentId}\n`);
-
- // Step 7: Subscribe to agent updates (simulating Session behavior)
- console.log("7. Subscribing to agent updates...");
- const unsubscribe = agentManager.subscribeToUpdates(agentId, (update) => {
- const updateType = (update.notification as any).type || "unknown";
- console.log(` 📡 Received update for agent ${update.agentId}: ${updateType}`);
- receivedUpdates.push(update);
- });
- console.log(" ✓ Subscribed to agent updates\n");
-
- // Step 8: Wait a bit for agent initialization
- console.log("8. Waiting for agent to initialize...");
- await sleep(2000);
- console.log(" ✓ Wait complete\n");
-
- // Step 9: Test list_agents via manager
- console.log("9. Listing agents via AgentManager...");
- const agents = agentManager.listAgents();
- console.log(` ✓ Found ${agents.length} agent(s)`);
- console.log(` Agents:`, agents.map(a => ({ id: a.id, status: a.status })));
- console.log();
-
- // Step 10: Test get_agent_status
- console.log("10. Getting agent status...");
- const status = agentManager.getAgentStatus(agentId);
- console.log(` ✓ Agent status: ${status}\n`);
-
- // Step 11: Test send_agent_prompt
- console.log("11. Sending prompt to agent...");
- try {
- await agentManager.sendPrompt(agentId, "Show me what you created");
- console.log(" ✓ Prompt sent successfully\n");
- } catch (error) {
- console.log(` ⚠ Prompt send failed (expected if agent not ready): ${error instanceof Error ? error.message : String(error)}\n`);
- }
-
- // Step 12: Wait a bit for any agent processing
- console.log("12. Waiting for agent processing...");
- await sleep(3000);
- console.log(" ✓ Wait complete\n");
-
- // Step 13: Test cancel_agent
- console.log("13. Cancelling agent...");
- try {
- await agentManager.cancelAgent(agentId);
- console.log(" ✓ Agent cancelled successfully\n");
- } catch (error) {
- console.log(` ⚠ Cancel failed (expected if agent not processing): ${error instanceof Error ? error.message : String(error)}\n`);
- }
-
- // Step 14: Unsubscribe from updates
- console.log("14. Unsubscribing from agent updates...");
- unsubscribe();
- console.log(" ✓ Unsubscribed\n");
-
- // Step 15: Test kill_agent
- console.log("15. Killing agent...");
- await agentManager.killAgent(agentId);
- console.log(" ✓ Agent killed successfully\n");
-
- // Step 16: Verify agent is gone
- console.log("16. Verifying agent is removed...");
- await sleep(1000);
- const finalAgents = agentManager.listAgents();
- if (finalAgents.some((a) => a.id === agentId)) {
- console.error(" ✗ Agent still exists after kill");
- process.exit(1);
- }
- console.log(" ✓ Agent successfully removed\n");
-
- // Step 17: Close MCP client
- console.log("17. Closing MCP client...");
- await mcpClient.close();
- console.log(" ✓ MCP client closed\n");
-
- // Summary
- console.log("=== Test Summary ===");
- console.log(`✓ All tests passed`);
- console.log(`✓ Received ${receivedUpdates.length} agent updates during test`);
- console.log("\nIntegration test completed successfully!");
-}
-
-// Run the test
-main().catch((error) => {
- console.error("\n❌ Test failed with error:");
- console.error(error);
- process.exit(1);
-});
diff --git a/packages/server/src/server/acp/test-simple.ts b/packages/server/src/server/acp/test-simple.ts
deleted file mode 100644
index 03352930f..000000000
--- a/packages/server/src/server/acp/test-simple.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-#!/usr/bin/env tsx
-
-import { AgentManager } from "./agent-manager.js";
-
-async function main() {
- console.log("=== ACP Simple Test ===\n");
-
- const manager = new AgentManager();
- let updateCount = 0;
- let toolCallCount = 0;
- let messageChunks: string[] = [];
-
- try {
- console.log("1. Creating agent...");
- const agentId = await manager.createAgent({
- cwd: process.cwd(),
- });
- console.log(` ✓ Created: ${agentId}\n`);
-
- console.log("2. Setting up minimal update handler...");
- manager.subscribeToUpdates(agentId, (update) => {
- updateCount++;
- const sessionUpdate = (update.notification as any).sessionUpdate;
-
- if (sessionUpdate === "tool_call") {
- toolCallCount++;
- console.log(` [Tool Call] ${(update.notification as any).title}`);
- } else if (sessionUpdate === "agent_message_chunk") {
- const text = (update.notification as any).content?.text || "";
- if (text.trim()) {
- messageChunks.push(text);
- }
- }
- });
- console.log(" ✓ Handler set up\n");
-
- console.log("3. Sending prompt...");
- await manager.sendPrompt(agentId, "List 3 files in current directory");
- console.log(" ✓ Prompt sent\n");
-
- console.log("4. Waiting for completion (10 seconds)...");
- await new Promise((resolve) => setTimeout(resolve, 10000));
-
- console.log("\n=== Results ===");
- console.log(`Total updates: ${updateCount}`);
- console.log(`Tool calls: ${toolCallCount}`);
- console.log(`Message length: ${messageChunks.join("").length} chars`);
- console.log(`First few chunks: ${messageChunks.slice(0, 5).join("")}\n`);
-
- console.log("5. Cleaning up...");
- await manager.killAgent(agentId);
- console.log(" ✓ Agent killed\n");
-
- console.log("=== Simple test completed ===");
- } catch (error) {
- console.error("\n❌ Test failed:");
- console.error(error);
- process.exit(1);
- }
-}
-
-main().catch((error) => {
- console.error("Fatal error:", error);
- process.exit(1);
-});
diff --git a/packages/server/src/server/acp/types.ts b/packages/server/src/server/acp/types.ts
index 9a8905c65..1def2fb37 100644
--- a/packages/server/src/server/acp/types.ts
+++ b/packages/server/src/server/acp/types.ts
@@ -4,6 +4,7 @@ import type {
ClientSideConnection,
} from "@agentclientprotocol/sdk";
import type { ChildProcess } from "child_process";
+import type { AgentModeDefinition, AgentType } from "./agent-types.js";
/**
* Extended update types with messageId for proper deduplication
@@ -68,13 +69,10 @@ export type AgentStatus =
| "killed";
/**
- * Session mode definition from ACP
+ * Session mode definition from ACP.
+ * Alias to the shared agent mode definition used across the app.
*/
-export interface SessionMode {
- id: string;
- name: string;
- description?: string | null;
-}
+export type SessionMode = AgentModeDefinition;
/**
* Runtime state for an initialized agent
@@ -117,7 +115,7 @@ export interface AgentInfo {
status: AgentStatus;
createdAt: Date;
lastActivityAt: Date;
- type: "claude";
+ type: AgentType;
sessionId: string | null;
error: string | null;
currentModeId: string | null;
@@ -141,6 +139,7 @@ export interface AgentUpdate {
*/
export interface CreateAgentOptions {
cwd: string;
+ type: AgentType;
initialPrompt?: string;
initialMode?: string;
}
diff --git a/packages/server/src/server/messages.ts b/packages/server/src/server/messages.ts
index 82d5950f1..6c8afddbf 100644
--- a/packages/server/src/server/messages.ts
+++ b/packages/server/src/server/messages.ts
@@ -1,4 +1,6 @@
import { z } from "zod";
+import type { AgentType } from "./acp/agent-types.js";
+import { listAgentTypeDefinitions } from "./acp/agent-types.js";
const AgentModeSchema = z.object({
id: z.string(),
@@ -6,12 +8,14 @@ const AgentModeSchema = z.object({
description: z.string().nullable().optional(),
});
+const agentTypes = listAgentTypeDefinitions().map((definition) => definition.id);
+
const AgentInfoSchema = z.object({
id: z.string(),
status: z.string(),
createdAt: z.date(),
lastActivityAt: z.date(),
- type: z.literal("claude"),
+ type: z.enum(agentTypes as [AgentType, ...AgentType[]]),
sessionId: z.string().nullable(),
error: z.string().nullable(),
currentModeId: z.string().nullable(),
@@ -95,6 +99,7 @@ export const CreateAgentRequestMessageSchema = z.object({
cwd: z.string(),
initialMode: z.string().optional(),
worktreeName: z.string().optional(),
+ agentType: z.enum(agentTypes as [AgentType, ...AgentType[]]).optional(),
requestId: z.string().optional(),
});
@@ -220,7 +225,7 @@ export const AgentCreatedMessageSchema = z.object({
payload: z.object({
agentId: z.string(),
status: z.string(),
- type: z.literal("claude"),
+ type: z.enum(agentTypes as [AgentType, ...AgentType[]]),
currentModeId: z.string().optional(),
availableModes: z.array(AgentModeSchema).optional(),
title: z.string().optional(),
diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts
index 3fa5b61b4..938464277 100644
--- a/packages/server/src/server/session.ts
+++ b/packages/server/src/server/session.ts
@@ -29,6 +29,7 @@ import { createTerminalMcpServer } from "./terminal-mcp/index.js";
import { AgentManager } from "./acp/agent-manager.js";
import { createAgentMcpServer } from "./acp/mcp-server.js";
import type { AgentUpdate } from "./acp/types.js";
+import type { AgentType } from "./acp/agent-types.js";
import {
generateAgentTitle,
isTitleGeneratorInitialized,
@@ -493,12 +494,13 @@ export class Session {
break;
case "create_agent_request":
- await this.handleCreateAgentRequest(
- msg.cwd,
- msg.initialMode,
- msg.requestId,
- msg.worktreeName
- );
+ await this.handleCreateAgentRequest({
+ cwd: msg.cwd,
+ initialMode: msg.initialMode,
+ requestId: msg.requestId,
+ worktreeName: msg.worktreeName,
+ agentType: msg.agentType,
+ });
break;
case "initialize_agent_request":
@@ -820,12 +822,19 @@ export class Session {
/**
* Handle create agent request
*/
- private async handleCreateAgentRequest(
- cwd: string,
- initialMode?: string,
- requestId?: string,
- worktreeName?: string
- ): Promise {
+ private async handleCreateAgentRequest({
+ cwd,
+ initialMode,
+ requestId,
+ worktreeName,
+ agentType,
+ }: {
+ cwd: string;
+ initialMode?: string;
+ requestId?: string;
+ worktreeName?: string;
+ agentType?: AgentType;
+ }): Promise {
console.log(
`[Session ${this.clientId}] Creating agent in ${cwd} with mode ${
initialMode || "default"
@@ -859,8 +868,11 @@ export class Session {
);
}
+ const normalizedType: AgentType = agentType ?? "claude";
+
const agentId = await this.agentManager.createAgent({
cwd: effectiveCwd,
+ type: normalizedType,
initialMode,
});
@@ -892,7 +904,7 @@ export class Session {
payload: {
agentId,
status: info.status,
- type: "claude",
+ type: info.type,
currentModeId: info.currentModeId ?? undefined,
availableModes: info.availableModes ?? undefined,
title: info.title ?? undefined,
@@ -1419,7 +1431,7 @@ export class Session {
payload: {
agentId,
status: agentInfo.status,
- type: "claude",
+ type: agentInfo.type,
currentModeId: agentInfo.currentModeId ?? undefined,
availableModes: agentInfo.availableModes ?? undefined,
title: agentInfo.title || undefined,