mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
refactor(agent): consolidate agent type definitions and add multi-agent-type support
This commit is contained in:
@@ -61,7 +61,7 @@ function GroupedTextItem({ item }: { item: GroupedTextMessage }) {
|
||||
|
||||
return (
|
||||
<View style={[stylesheet.card, isThought && stylesheet.thoughtCard]}>
|
||||
<Text style={stylesheet.timestamp}>
|
||||
<Text style={[stylesheet.timestamp, isThought && stylesheet.thoughtTimestamp]}>
|
||||
{formatTimestamp(item.startTimestamp)}
|
||||
</Text>
|
||||
{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,
|
||||
|
||||
@@ -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<string, PendingPermission>;
|
||||
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 (
|
||||
<ActivityLog
|
||||
type="info"
|
||||
<AgentThoughtMessage
|
||||
message={item.text}
|
||||
timestamp={item.timestamp.getTime()}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -235,6 +261,10 @@ export function AgentStreamView({
|
||||
}}
|
||||
style={stylesheet.list}
|
||||
onScroll={handleScroll}
|
||||
onScrollBeginDrag={handleScrollBeginDrag}
|
||||
onScrollEndDrag={handleScrollEnd}
|
||||
onMomentumScrollBegin={handleMomentumScrollBegin}
|
||||
onMomentumScrollEnd={handleScrollEnd}
|
||||
scrollEventThrottle={16}
|
||||
ListEmptyComponent={
|
||||
<View style={stylesheet.invertedWrapper}>
|
||||
|
||||
@@ -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<AgentType, AgentTypeDefinition> =
|
||||
{} as Record<AgentType, AgentTypeDefinition>;
|
||||
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<AgentType>(
|
||||
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<string | null>(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<string | null>(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({
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Agent Type Selector */}
|
||||
<View style={styles.formSection}>
|
||||
<Text style={styles.label}>Agent Type</Text>
|
||||
<View style={styles.agentTypeContainer}>
|
||||
{agentTypeDefinitions.map((definition) => {
|
||||
const isSelected = selectedAgentType === definition.id;
|
||||
return (
|
||||
<Pressable
|
||||
key={definition.id}
|
||||
onPress={() => setSelectedAgentType(definition.id)}
|
||||
disabled={isLoading}
|
||||
style={[
|
||||
styles.agentTypeOption,
|
||||
isSelected && styles.agentTypeOptionSelected,
|
||||
isLoading && styles.agentTypeOptionDisabled,
|
||||
]}
|
||||
>
|
||||
<View style={styles.agentTypeOptionContent}>
|
||||
<View
|
||||
style={[
|
||||
styles.radioOuter,
|
||||
isSelected
|
||||
? styles.radioOuterSelected
|
||||
: styles.radioOuterUnselected,
|
||||
]}
|
||||
>
|
||||
{isSelected && <View style={styles.radioInner} />}
|
||||
</View>
|
||||
<View style={styles.agentTypeTextContainer}>
|
||||
<Text style={styles.agentTypeLabel}>
|
||||
{definition.label}
|
||||
</Text>
|
||||
{definition.description ? (
|
||||
<Text style={styles.agentTypeDescription}>
|
||||
{definition.description}
|
||||
</Text>
|
||||
) : null}
|
||||
<Text style={styles.agentTypeMeta}>
|
||||
{definition.availableModes.length > 0
|
||||
? `Modes: ${definition.availableModes
|
||||
.map((mode) => mode.name)
|
||||
.join(", ")}`
|
||||
: "Modes: none"}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Worktree Name Input (Optional) */}
|
||||
<View style={styles.formSection}>
|
||||
<Text style={styles.label}>Worktree Name (Optional)</Text>
|
||||
@@ -364,41 +467,50 @@ export function CreateAgentModal({
|
||||
{/* Mode Selector */}
|
||||
<View style={styles.formSection}>
|
||||
<Text style={styles.label}>Mode</Text>
|
||||
<View style={styles.modeContainer}>
|
||||
{MODES.map((mode) => (
|
||||
<Pressable
|
||||
key={mode.value}
|
||||
onPress={() => setSelectedMode(mode.value)}
|
||||
disabled={isLoading}
|
||||
style={[
|
||||
styles.modeOption,
|
||||
selectedMode === mode.value && styles.modeOptionSelected,
|
||||
isLoading && styles.modeOptionDisabled,
|
||||
]}
|
||||
>
|
||||
<View style={styles.modeOptionContent}>
|
||||
<View
|
||||
{modeOptions.length === 0 ? (
|
||||
<Text style={styles.helperText}>
|
||||
This agent type does not expose selectable modes.
|
||||
</Text>
|
||||
) : (
|
||||
<View style={styles.modeContainer}>
|
||||
{modeOptions.map((mode) => {
|
||||
const isSelected = selectedMode === mode.id;
|
||||
return (
|
||||
<Pressable
|
||||
key={mode.id}
|
||||
onPress={() => 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 && (
|
||||
<View style={styles.radioInner} />
|
||||
)}
|
||||
</View>
|
||||
<View style={styles.modeTextContainer}>
|
||||
<Text style={styles.modeLabel}>{mode.label}</Text>
|
||||
<Text style={styles.modeDescription}>
|
||||
{mode.description}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</Pressable>
|
||||
))}
|
||||
</View>
|
||||
<View style={styles.modeOptionContent}>
|
||||
<View
|
||||
style={[
|
||||
styles.radioOuter,
|
||||
isSelected
|
||||
? styles.radioOuterSelected
|
||||
: styles.radioOuterUnselected,
|
||||
]}
|
||||
>
|
||||
{isSelected && <View style={styles.radioInner} />}
|
||||
</View>
|
||||
<View style={styles.modeTextContainer}>
|
||||
<Text style={styles.modeLabel}>{mode.name}</Text>
|
||||
{mode.description ? (
|
||||
<Text style={styles.modeDescription}>
|
||||
{mode.description}
|
||||
</Text>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</BottomSheetScrollView>
|
||||
</BottomSheetModal>
|
||||
@@ -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,
|
||||
|
||||
@@ -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 (
|
||||
<View style={userMessageStylesheet.container}>
|
||||
<View style={userMessageStylesheet.bubble}>
|
||||
@@ -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 (
|
||||
<View style={assistantMessageStylesheet.container}>
|
||||
<Markdown style={markdownStyles}>{message}</Markdown>
|
||||
@@ -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 (
|
||||
<View style={agentThoughtStylesheet.container}>
|
||||
<View style={agentThoughtStylesheet.card}>
|
||||
<View style={agentThoughtStylesheet.iconContainer}>
|
||||
<Brain size={18} style={agentThoughtStylesheet.icon} />
|
||||
</View>
|
||||
<View style={agentThoughtStylesheet.textContainer}>
|
||||
<Text style={agentThoughtStylesheet.text}>{messageText}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
interface ToolCallProps {
|
||||
toolName: string;
|
||||
kind?: string; // Optional kind for ACP tool calls
|
||||
|
||||
@@ -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<void>;
|
||||
sendAgentAudio: (agentId: string, audioBlob: Blob, requestId?: string) => Promise<void>;
|
||||
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: {
|
||||
|
||||
@@ -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<AgentType, AgentTypeDefinition> = {
|
||||
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;
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
// Agent info interface is provided by SessionContext's Agent type
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -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",
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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<ReturnType<typeof connection.newSession>>
|
||||
| Awaited<ReturnType<typeof connection.loadSession>>;
|
||||
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) => {
|
||||
|
||||
@@ -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<typeof AgentOptionsSchema>;
|
||||
|
||||
90
packages/server/src/server/acp/agent-types.ts
Normal file
90
packages/server/src/server/acp/agent-types.ts
Normal file
@@ -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<AgentType, AgentTypeDefinition> = {
|
||||
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);
|
||||
}
|
||||
@@ -121,6 +121,7 @@ export async function createAgentMcpServer(
|
||||
|
||||
const agentId = await agentManager.createAgent({
|
||||
cwd: resolvedCwd,
|
||||
type: "claude",
|
||||
initialPrompt,
|
||||
initialMode,
|
||||
});
|
||||
|
||||
@@ -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<void>((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);
|
||||
});
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
// Run the test suite
|
||||
main().catch((error) => {
|
||||
console.error("Fatal error:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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<void> {
|
||||
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);
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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<void> {
|
||||
private async handleCreateAgentRequest({
|
||||
cwd,
|
||||
initialMode,
|
||||
requestId,
|
||||
worktreeName,
|
||||
agentType,
|
||||
}: {
|
||||
cwd: string;
|
||||
initialMode?: string;
|
||||
requestId?: string;
|
||||
worktreeName?: string;
|
||||
agentType?: AgentType;
|
||||
}): Promise<void> {
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user