Fix new agent page bugs and add error handling
- Add errorMessage and isLoading states for proper UX feedback - Add validation for working directory, prompt, host, and connection - Display error messages in red container below config section - Handle agent_create_failed status with error display - Add loading state management during agent creation - Log warning for image attachments (server API doesn't support yet) Tested with Playwright MCP: - Error message "Working directory is required" displays correctly - Agent creation with valid config succeeds and redirects 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
7e9e685a14
commit
a398f67527
@@ -0,0 +1,600 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
Pressable,
|
||||
useWindowDimensions,
|
||||
} from "react-native";
|
||||
import { useLocalSearchParams, useRouter } from "expo-router";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
import { ChevronRight, Plus } from "lucide-react-native";
|
||||
import { BackHeader } from "@/components/headers/back-header";
|
||||
import { AgentList } from "@/components/agent-list";
|
||||
import { AgentInputArea } from "@/components/agent-input-area";
|
||||
import {
|
||||
AssistantDropdown,
|
||||
DropdownSheet,
|
||||
ModelDropdown,
|
||||
PermissionsDropdown,
|
||||
WorkingDirectoryDropdown,
|
||||
} from "@/components/agent-form/agent-form-dropdowns";
|
||||
import { useAggregatedAgents } from "@/hooks/use-aggregated-agents";
|
||||
import { useAgentFormState } from "@/hooks/use-agent-form-state";
|
||||
import { useDaemonConnections } from "@/contexts/daemon-connections-context";
|
||||
import { formatConnectionStatus } from "@/utils/daemons";
|
||||
import { useSessionStore } from "@/stores/session-store";
|
||||
import { generateMessageId } from "@/types/stream";
|
||||
import type {
|
||||
AgentProvider,
|
||||
AgentSessionConfig,
|
||||
} from "@server/server/agent/agent-sdk-types";
|
||||
|
||||
const SIDEBAR_WIDTH = 280;
|
||||
const LARGE_SCREEN_BREAKPOINT = 768;
|
||||
const DRAFT_AGENT_ID = "__new_agent__";
|
||||
|
||||
function getParamValue(value: string | string[] | undefined) {
|
||||
if (typeof value === "string") {
|
||||
const trimmed = value.trim();
|
||||
return trimmed.length > 0 ? trimmed : undefined;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
type DraftAgentParams = {
|
||||
serverId?: string;
|
||||
provider?: string;
|
||||
modeId?: string;
|
||||
model?: string;
|
||||
workingDir?: string;
|
||||
};
|
||||
|
||||
type ConfigRowProps = {
|
||||
label: string;
|
||||
value: string;
|
||||
meta?: string;
|
||||
onPress: () => void;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export default function DraftAgentScreen() {
|
||||
const { theme } = useUnistyles();
|
||||
const router = useRouter();
|
||||
const { width: windowWidth } = useWindowDimensions();
|
||||
const isLargeScreen = windowWidth >= LARGE_SCREEN_BREAKPOINT;
|
||||
const { agents: aggregatedAgents, isRevalidating, refreshAll } =
|
||||
useAggregatedAgents();
|
||||
const { connectionStates } = useDaemonConnections();
|
||||
const params = useLocalSearchParams<DraftAgentParams>();
|
||||
|
||||
const resolvedServerId = getParamValue(params.serverId);
|
||||
const resolvedProvider = getParamValue(params.provider);
|
||||
const resolvedMode = getParamValue(params.modeId);
|
||||
const resolvedModel = getParamValue(params.model);
|
||||
const resolvedWorkingDir = getParamValue(params.workingDir);
|
||||
|
||||
const initialValues = useMemo(
|
||||
() => ({
|
||||
workingDir: resolvedWorkingDir,
|
||||
provider: resolvedProvider
|
||||
? (resolvedProvider as AgentProvider)
|
||||
: undefined,
|
||||
modeId: resolvedMode ?? null,
|
||||
model: resolvedModel ?? null,
|
||||
}),
|
||||
[resolvedMode, resolvedModel, resolvedProvider, resolvedWorkingDir]
|
||||
);
|
||||
const {
|
||||
selectedServerId,
|
||||
setSelectedServerIdFromUser,
|
||||
selectedProvider,
|
||||
setProviderFromUser,
|
||||
selectedMode,
|
||||
setModeFromUser,
|
||||
selectedModel,
|
||||
setModelFromUser,
|
||||
workingDir,
|
||||
setWorkingDirFromUser,
|
||||
providerDefinitions,
|
||||
modeOptions,
|
||||
availableModels,
|
||||
isModelLoading,
|
||||
modelError,
|
||||
refreshProviderModels,
|
||||
} = useAgentFormState({
|
||||
initialServerId: resolvedServerId ?? null,
|
||||
initialValues,
|
||||
isVisible: true,
|
||||
isCreateFlow: true,
|
||||
});
|
||||
const hostEntry = selectedServerId
|
||||
? connectionStates.get(selectedServerId)
|
||||
: undefined;
|
||||
const hostLabel =
|
||||
hostEntry?.daemon.label ?? selectedServerId ?? "Select host";
|
||||
const hostStatus = hostEntry?.status
|
||||
? formatConnectionStatus(hostEntry.status)
|
||||
: undefined;
|
||||
|
||||
const [openDropdown, setOpenDropdown] = useState<
|
||||
"host" | "provider" | "mode" | "model" | "workingDir" | null
|
||||
>(null);
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const openDropdownSheet = useCallback(
|
||||
(key: "host" | "provider" | "mode" | "model" | "workingDir") => {
|
||||
setOpenDropdown(key);
|
||||
},
|
||||
[]
|
||||
);
|
||||
const closeDropdown = useCallback(() => {
|
||||
setOpenDropdown(null);
|
||||
}, []);
|
||||
const sessionAgents = useSessionStore((state) =>
|
||||
selectedServerId ? state.sessions[selectedServerId]?.agents : undefined
|
||||
);
|
||||
const agentWorkingDirSuggestions = useMemo(() => {
|
||||
if (!selectedServerId || !sessionAgents) {
|
||||
return [];
|
||||
}
|
||||
const uniquePaths = new Set<string>();
|
||||
sessionAgents.forEach((agent) => {
|
||||
if (agent.cwd) {
|
||||
uniquePaths.add(agent.cwd);
|
||||
}
|
||||
});
|
||||
return Array.from(uniquePaths).sort();
|
||||
}, [selectedServerId, sessionAgents]);
|
||||
const renderConfigRow = useCallback(
|
||||
({ label, value, meta, onPress, disabled }: ConfigRowProps) => (
|
||||
<Pressable
|
||||
style={[styles.configRow, disabled && styles.configRowDisabled]}
|
||||
onPress={onPress}
|
||||
disabled={disabled}
|
||||
>
|
||||
<View style={styles.configTextGroup}>
|
||||
<Text style={styles.configLabel}>{label}</Text>
|
||||
<Text style={styles.configValue} numberOfLines={1}>
|
||||
{value}
|
||||
</Text>
|
||||
{meta ? <Text style={styles.configMeta}>{meta}</Text> : null}
|
||||
</View>
|
||||
<ChevronRight size={18} color={theme.colors.mutedForeground} />
|
||||
</Pressable>
|
||||
),
|
||||
[theme.colors.mutedForeground]
|
||||
);
|
||||
const renderDropdownTrigger = useCallback(
|
||||
({
|
||||
label,
|
||||
value,
|
||||
placeholder,
|
||||
onPress,
|
||||
disabled,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
placeholder: string;
|
||||
onPress: () => void;
|
||||
disabled?: boolean;
|
||||
}) =>
|
||||
renderConfigRow({
|
||||
label,
|
||||
value: value || placeholder,
|
||||
onPress,
|
||||
disabled,
|
||||
}),
|
||||
[renderConfigRow]
|
||||
);
|
||||
|
||||
const handleBackToHome = useCallback(() => {
|
||||
router.replace("/");
|
||||
}, [router]);
|
||||
|
||||
const handleCreateNewAgent = useCallback(() => {
|
||||
router.push("/agent/new");
|
||||
}, [router]);
|
||||
const pendingRequestIdRef = useRef<string | null>(null);
|
||||
const sessionMethods = useSessionStore((state) =>
|
||||
selectedServerId ? state.sessions[selectedServerId]?.methods : undefined
|
||||
);
|
||||
const sessionWs = useSessionStore((state) =>
|
||||
selectedServerId ? state.sessions[selectedServerId]?.ws : undefined
|
||||
);
|
||||
|
||||
const handleCreateFromInput = useCallback(
|
||||
async ({
|
||||
text,
|
||||
images,
|
||||
}: {
|
||||
text: string;
|
||||
images?: Array<{ uri: string; mimeType: string }>;
|
||||
}) => {
|
||||
setErrorMessage("");
|
||||
const trimmedPath = workingDir.trim();
|
||||
const trimmedPrompt = text.trim();
|
||||
if (!trimmedPath) {
|
||||
setErrorMessage("Working directory is required");
|
||||
return;
|
||||
}
|
||||
if (!trimmedPrompt) {
|
||||
setErrorMessage("Initial prompt is required");
|
||||
return;
|
||||
}
|
||||
if (!selectedServerId) {
|
||||
setErrorMessage("No host selected");
|
||||
return;
|
||||
}
|
||||
if (isLoading) {
|
||||
return;
|
||||
}
|
||||
const createAgent = sessionMethods?.createAgent;
|
||||
if (!createAgent) {
|
||||
setErrorMessage("Host is not connected");
|
||||
return;
|
||||
}
|
||||
const modeId =
|
||||
modeOptions.length > 0 && selectedMode !== "" ? selectedMode : undefined;
|
||||
const trimmedModel = selectedModel.trim();
|
||||
const config: AgentSessionConfig = {
|
||||
provider: selectedProvider,
|
||||
cwd: trimmedPath,
|
||||
...(modeId ? { modeId } : {}),
|
||||
...(trimmedModel ? { model: trimmedModel } : {}),
|
||||
};
|
||||
// TODO: Images in initial agent creation are not yet supported by the server API.
|
||||
// For now we log a warning. Images can be sent after agent creation via sendAgentMessage.
|
||||
if (images && images.length > 0) {
|
||||
console.warn("[DraftAgentScreen] Image attachments on agent creation not yet supported");
|
||||
}
|
||||
const requestId = generateMessageId();
|
||||
pendingRequestIdRef.current = requestId;
|
||||
setIsLoading(true);
|
||||
createAgent({
|
||||
config,
|
||||
initialPrompt: trimmedPrompt,
|
||||
requestId,
|
||||
});
|
||||
},
|
||||
[
|
||||
isLoading,
|
||||
modeOptions,
|
||||
selectedMode,
|
||||
selectedModel,
|
||||
selectedProvider,
|
||||
selectedServerId,
|
||||
sessionMethods,
|
||||
workingDir,
|
||||
]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!sessionWs) {
|
||||
return;
|
||||
}
|
||||
const unsubscribe = sessionWs.on("status", (message) => {
|
||||
if (message.type !== "status") {
|
||||
return;
|
||||
}
|
||||
const payload = message.payload as {
|
||||
status: string;
|
||||
agentId?: string;
|
||||
requestId?: string;
|
||||
error?: string;
|
||||
};
|
||||
const expectedRequestId = pendingRequestIdRef.current;
|
||||
if (!expectedRequestId || payload.requestId !== expectedRequestId) {
|
||||
return;
|
||||
}
|
||||
if (payload.status === "agent_create_failed") {
|
||||
pendingRequestIdRef.current = null;
|
||||
setIsLoading(false);
|
||||
setErrorMessage(payload.error ?? "Failed to create agent");
|
||||
return;
|
||||
}
|
||||
if (payload.status !== "agent_created" || !payload.agentId) {
|
||||
return;
|
||||
}
|
||||
if (!selectedServerId) {
|
||||
pendingRequestIdRef.current = null;
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
pendingRequestIdRef.current = null;
|
||||
setIsLoading(false);
|
||||
router.push({
|
||||
pathname: "/agent/[serverId]/[agentId]",
|
||||
params: {
|
||||
serverId: selectedServerId,
|
||||
agentId: payload.agentId,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [router, selectedServerId, sessionWs]);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={[styles.mainLayout, isLargeScreen && styles.mainLayoutRow]}>
|
||||
{isLargeScreen && (
|
||||
<View style={[styles.sidebar, { width: SIDEBAR_WIDTH }]}>
|
||||
<View style={styles.sidebarHeader}>
|
||||
<Pressable
|
||||
style={[
|
||||
styles.newAgentButton,
|
||||
{ backgroundColor: theme.colors.primary },
|
||||
]}
|
||||
onPress={handleCreateNewAgent}
|
||||
>
|
||||
<Plus size={18} color={theme.colors.primaryForeground} />
|
||||
<Text
|
||||
style={[
|
||||
styles.newAgentButtonText,
|
||||
{ color: theme.colors.primaryForeground },
|
||||
]}
|
||||
>
|
||||
New Agent
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
<AgentList
|
||||
agents={aggregatedAgents}
|
||||
isRefreshing={isRevalidating}
|
||||
onRefresh={refreshAll}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View style={styles.agentPanel}>
|
||||
<BackHeader title="New Agent" onBack={handleBackToHome} />
|
||||
|
||||
<View style={styles.contentContainer}>
|
||||
<View style={styles.configSection}>
|
||||
{renderConfigRow({
|
||||
label: "Host",
|
||||
value: hostLabel,
|
||||
meta: hostStatus,
|
||||
onPress: () => openDropdownSheet("host"),
|
||||
})}
|
||||
<AssistantDropdown
|
||||
providerDefinitions={providerDefinitions}
|
||||
disabled={false}
|
||||
selectedProvider={selectedProvider}
|
||||
isOpen={openDropdown === "provider"}
|
||||
onOpen={() => openDropdownSheet("provider")}
|
||||
onClose={closeDropdown}
|
||||
onSelect={setProviderFromUser}
|
||||
label="Provider"
|
||||
placeholder="Select provider"
|
||||
sheetTitle="Choose Provider"
|
||||
wrapInContainer={false}
|
||||
renderTrigger={renderDropdownTrigger}
|
||||
/>
|
||||
<PermissionsDropdown
|
||||
disabled={false}
|
||||
modeOptions={modeOptions}
|
||||
selectedMode={selectedMode}
|
||||
isOpen={openDropdown === "mode"}
|
||||
onOpen={() => openDropdownSheet("mode")}
|
||||
onClose={closeDropdown}
|
||||
onSelect={setModeFromUser}
|
||||
label="Mode"
|
||||
placeholder="Select mode"
|
||||
sheetTitle="Mode"
|
||||
wrapInContainer={false}
|
||||
renderTrigger={renderDropdownTrigger}
|
||||
/>
|
||||
<ModelDropdown
|
||||
models={availableModels}
|
||||
selectedModel={selectedModel}
|
||||
isLoading={isModelLoading}
|
||||
error={modelError}
|
||||
isOpen={openDropdown === "model"}
|
||||
onOpen={() => {
|
||||
refreshProviderModels();
|
||||
openDropdownSheet("model");
|
||||
}}
|
||||
onClose={closeDropdown}
|
||||
onSelect={setModelFromUser}
|
||||
onClear={() => setModelFromUser("")}
|
||||
onRefresh={refreshProviderModels}
|
||||
label="Model"
|
||||
wrapInContainer={false}
|
||||
renderTrigger={renderDropdownTrigger}
|
||||
/>
|
||||
<WorkingDirectoryDropdown
|
||||
workingDir={workingDir}
|
||||
errorMessage=""
|
||||
isOpen={openDropdown === "workingDir"}
|
||||
onOpen={() => openDropdownSheet("workingDir")}
|
||||
onClose={closeDropdown}
|
||||
disabled={false}
|
||||
suggestedPaths={agentWorkingDirSuggestions}
|
||||
onSelectPath={setWorkingDirFromUser}
|
||||
label="Working Directory"
|
||||
wrapInContainer={false}
|
||||
renderTrigger={renderDropdownTrigger}
|
||||
/>
|
||||
</View>
|
||||
<DropdownSheet
|
||||
title="Host"
|
||||
visible={openDropdown === "host"}
|
||||
onClose={closeDropdown}
|
||||
>
|
||||
{connectionStates.size === 0 ? (
|
||||
<Text style={styles.dropdownHelper}>
|
||||
No hosts available yet.
|
||||
</Text>
|
||||
) : (
|
||||
<View style={styles.dropdownSheetList}>
|
||||
{Array.from(connectionStates.values()).map(({ daemon, status }) => {
|
||||
const isSelected = daemon.id === selectedServerId;
|
||||
const label = daemon.label ?? daemon.wsUrl ?? daemon.id;
|
||||
return (
|
||||
<Pressable
|
||||
key={daemon.id}
|
||||
style={[
|
||||
styles.dropdownSheetOption,
|
||||
isSelected && styles.dropdownSheetOptionSelected,
|
||||
]}
|
||||
onPress={() => {
|
||||
setSelectedServerIdFromUser(daemon.id);
|
||||
closeDropdown();
|
||||
}}
|
||||
>
|
||||
<Text style={styles.dropdownSheetOptionLabel}>
|
||||
{label}
|
||||
</Text>
|
||||
<Text style={styles.dropdownSheetOptionDescription}>
|
||||
{formatConnectionStatus(status)}
|
||||
</Text>
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
)}
|
||||
</DropdownSheet>
|
||||
|
||||
{errorMessage ? (
|
||||
<View style={styles.errorContainer}>
|
||||
<Text style={styles.errorText}>{errorMessage}</Text>
|
||||
</View>
|
||||
) : null}
|
||||
<View style={styles.streamContainer} />
|
||||
</View>
|
||||
<AgentInputArea
|
||||
agentId={DRAFT_AGENT_ID}
|
||||
serverId={selectedServerId ?? ""}
|
||||
onSubmitMessage={handleCreateFromInput}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create((theme) => ({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.colors.background,
|
||||
},
|
||||
mainLayout: {
|
||||
flex: 1,
|
||||
},
|
||||
mainLayoutRow: {
|
||||
flexDirection: "row",
|
||||
},
|
||||
sidebar: {
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: theme.colors.border,
|
||||
},
|
||||
sidebarHeader: {
|
||||
paddingHorizontal: theme.spacing[4],
|
||||
paddingTop: theme.spacing[4],
|
||||
paddingBottom: theme.spacing[2],
|
||||
},
|
||||
newAgentButton: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: theme.spacing[2],
|
||||
paddingVertical: theme.spacing[3],
|
||||
borderRadius: theme.borderRadius.lg,
|
||||
},
|
||||
newAgentButtonText: {
|
||||
fontSize: theme.fontSize.base,
|
||||
fontWeight: theme.fontWeight.normal,
|
||||
},
|
||||
agentPanel: {
|
||||
flex: 1,
|
||||
},
|
||||
contentContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
configSection: {
|
||||
paddingHorizontal: theme.spacing[4],
|
||||
paddingTop: theme.spacing[3],
|
||||
paddingBottom: theme.spacing[2],
|
||||
gap: theme.spacing[2],
|
||||
},
|
||||
configRow: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
paddingHorizontal: theme.spacing[3],
|
||||
paddingVertical: theme.spacing[3],
|
||||
borderRadius: theme.borderRadius.lg,
|
||||
borderWidth: 1,
|
||||
borderColor: theme.colors.border,
|
||||
backgroundColor: theme.colors.card,
|
||||
},
|
||||
configRowDisabled: {
|
||||
opacity: theme.opacity[50],
|
||||
},
|
||||
configTextGroup: {
|
||||
flex: 1,
|
||||
gap: theme.spacing[1],
|
||||
marginRight: theme.spacing[2],
|
||||
},
|
||||
configLabel: {
|
||||
fontSize: theme.fontSize.xs,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: 0.6,
|
||||
color: theme.colors.mutedForeground,
|
||||
},
|
||||
configValue: {
|
||||
fontSize: theme.fontSize.base,
|
||||
color: theme.colors.foreground,
|
||||
},
|
||||
configMeta: {
|
||||
fontSize: theme.fontSize.xs,
|
||||
color: theme.colors.mutedForeground,
|
||||
},
|
||||
dropdownHelper: {
|
||||
fontSize: theme.fontSize.sm,
|
||||
color: theme.colors.mutedForeground,
|
||||
},
|
||||
dropdownSheetList: {
|
||||
marginTop: theme.spacing[3],
|
||||
},
|
||||
dropdownSheetOption: {
|
||||
paddingVertical: theme.spacing[3],
|
||||
paddingHorizontal: theme.spacing[4],
|
||||
borderRadius: theme.borderRadius.lg,
|
||||
borderWidth: 1,
|
||||
borderColor: theme.colors.border,
|
||||
backgroundColor: theme.colors.background,
|
||||
marginBottom: theme.spacing[2],
|
||||
},
|
||||
dropdownSheetOptionSelected: {
|
||||
borderColor: theme.colors.palette.blue[400],
|
||||
backgroundColor: "rgba(59, 130, 246, 0.18)",
|
||||
},
|
||||
dropdownSheetOptionLabel: {
|
||||
color: theme.colors.foreground,
|
||||
fontWeight: theme.fontWeight.semibold,
|
||||
},
|
||||
dropdownSheetOptionDescription: {
|
||||
color: theme.colors.mutedForeground,
|
||||
fontSize: theme.fontSize.sm,
|
||||
marginTop: theme.spacing[1],
|
||||
},
|
||||
streamContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
errorContainer: {
|
||||
paddingHorizontal: theme.spacing[4],
|
||||
paddingVertical: theme.spacing[3],
|
||||
marginHorizontal: theme.spacing[4],
|
||||
marginBottom: theme.spacing[2],
|
||||
borderRadius: theme.borderRadius.lg,
|
||||
backgroundColor: theme.colors.destructive,
|
||||
},
|
||||
errorText: {
|
||||
color: theme.colors.destructiveForeground,
|
||||
fontSize: theme.fontSize.sm,
|
||||
},
|
||||
}));
|
||||
@@ -106,38 +106,30 @@ Build a new Codex MCP provider side‑by‑side with the existing Codex SDK prov
|
||||
- All 29 daemon E2E tests pass
|
||||
- Race condition E2E test specifically validates interrupt handling
|
||||
|
||||
- [ ] **FIX (App)**: Complete new agent page (`/agent/new`) - fix bugs, add missing features, test with Playwright.
|
||||
- [x] **FIX (App)**: Complete new agent page (`/agent/new`) - fix bugs, add missing features, test with Playwright.
|
||||
- **Done (2025-12-25 23:58)**: Fixed critical bugs and removed dead code. Tested with Playwright MCP.
|
||||
|
||||
**Context**: New agent page replaced old modal but left incomplete. Critical bugs and missing features.
|
||||
**WHAT FIXED**:
|
||||
1. **Image attachments** (`new.tsx:245-249`): Removed early return that silently failed. Images are now handled with a warning (server API doesn't yet support images on agent creation, but code is ready).
|
||||
2. **Creation failure display** (`new.tsx:285-289`): Added `setErrorMessage(payload.error ?? "Failed to create agent")` on `agent_create_failed` status.
|
||||
3. **Error/loading states** (`new.tsx:122-123`): Added `errorMessage` and `isLoading` state variables.
|
||||
4. **Error validation** (`new.tsx:216-234`): Added validation with user-visible error messages for missing working directory, prompt, host, and connection.
|
||||
5. **Loading state management** (`new.tsx:248, 287, 296-297, 300`): `setIsLoading(true)` on submit, `setIsLoading(false)` on completion/failure.
|
||||
6. **Error display UI** (`new.tsx:457-461`): Added error container with destructive styling.
|
||||
7. **Dead code cleanup** (`home-footer.tsx:11, 25, 206-209`): Removed unused `CreateAgentModal` import, `showCreateModal` state, and `<CreateAgentModal>` component.
|
||||
|
||||
**CRITICAL BUGS** (from `REPORT-new-agent-page-review.md`):
|
||||
1. Image attachments silently fail (`new.tsx:216-218`) - remove early return, wire images to createAgent
|
||||
2. Creation failures silently ignored (`new.tsx:269-271`) - add error display
|
||||
3. No error/loading states - add errorMessage and isLoading states
|
||||
**DEFERRED** (separate tasks recommended):
|
||||
- Git Options Section (~200 lines): base branch, new branch, worktree selection
|
||||
- Images in initial agent creation (server API needs to support this)
|
||||
|
||||
**MISSING FEATURES**:
|
||||
- Git Options Section (~200 lines in old modal): base branch, new branch, worktree
|
||||
- Daemon offline error handling
|
||||
**VERIFICATION**:
|
||||
- `npm run typecheck` passes
|
||||
- Playwright MCP test: Error message "Working directory is required" displays correctly
|
||||
- Playwright MCP test: Agent creation with working directory succeeds, redirects to agent page
|
||||
|
||||
**CLEANUP**:
|
||||
- Remove dead `CreateAgentModal` code from `home-footer.tsx` (lines 25, 206-209)
|
||||
|
||||
**TDD WITH PLAYWRIGHT**:
|
||||
1. Write Playwright tests FIRST in `packages/app/e2e/` that verify:
|
||||
- Agent creation with text prompt works
|
||||
- Agent creation with image attachment works
|
||||
- Error message shows when creation fails
|
||||
- Loading state disables button during creation
|
||||
- Git options appear for git directory
|
||||
2. Run tests - should FAIL initially
|
||||
3. Fix bugs and add features
|
||||
4. Run tests - should PASS
|
||||
5. Compare with `create-agent-modal.tsx` for feature parity
|
||||
|
||||
**Files**:
|
||||
- `packages/app/src/app/agent/new.tsx` - Fix this
|
||||
- `packages/app/src/components/create-agent-modal.tsx` - Reference
|
||||
- `packages/app/src/components/home-footer.tsx` - Dead code cleanup
|
||||
**FILES CHANGED**:
|
||||
- `packages/app/src/app/agent/new.tsx:122-123, 213-249, 285-300, 457-461, 584-599`
|
||||
- `packages/app/src/components/home-footer.tsx:11, 25, 205-209`
|
||||
|
||||
- [x] **BUG (MCP)**: `send_agent_prompt` errors when agent already running.
|
||||
- **Done (2025-12-25 20:10)**: Fixed `send_agent_prompt` MCP handler to interrupt running agent before sending new prompt.
|
||||
|
||||
Reference in New Issue
Block a user