From 81ee887d030f611b2b0d0464b777033e189a5410 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Wed, 25 Mar 2026 13:09:17 +0700 Subject: [PATCH] Reduce unnecessary re-renders in agent panel and input components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Narrow zustand selectors to only subscribe to fields each component actually uses. Split nested objects (availableModes, projectPlacement) into separate selectors with structural equality checks. Remove the updatedAt processing-spinner hack in agent-input-area — the server already guarantees agent status is "running" before ACKing the send request, so isProcessing can clear on submit success directly. --- CLAUDE.md | 8 +- .../agent-form/agent-form-dropdowns.tsx | 48 ++---- .../app/src/components/agent-input-area.tsx | 57 ++----- .../app/src/components/agent-input-submit.ts | 1 + .../app/src/components/agent-status-bar.tsx | 37 ++++- .../app/src/components/agent-stream-view.tsx | 4 +- .../components/combined-model-selector.tsx | 8 +- .../app/src/hooks/use-agent-form-state.ts | 5 +- .../hooks/use-agent-screen-state-machine.ts | 22 ++- packages/app/src/panels/agent-panel.tsx | 145 ++++++++---------- 10 files changed, 150 insertions(+), 185 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 9035fe370..44c854c88 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -46,9 +46,7 @@ See [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md) for full setup, build sync requir - **NEVER add auth checks to tests** — agent providers handle their own auth. - **Always run typecheck after every change.** -## Orchestrator mode +## Debugging -- Prefix agent titles with "🎭" (e.g., "🎭 Feature Implementation") -- Launch agents in the most permissive mode -- Set cwd to the repository root -- When agent control tool calls fail, list agents first — it may be a wait timeout + +Find the complete daemon logs and traces in the $PASEO_HOME/daemon.log diff --git a/packages/app/src/components/agent-form/agent-form-dropdowns.tsx b/packages/app/src/components/agent-form/agent-form-dropdowns.tsx index 4f89d1ebd..6137eb76d 100644 --- a/packages/app/src/components/agent-form/agent-form-dropdowns.tsx +++ b/packages/app/src/components/agent-form/agent-form-dropdowns.tsx @@ -537,14 +537,10 @@ export function AgentConfigRow({ }, [modeOptions]); const modelSelectOptions: ComboSelectOption[] = useMemo(() => { - const opts: ComboSelectOption[] = [{ id: "", label: "Auto" }]; - for (const model of models) { - opts.push({ - id: model.id, - label: model.label, - }); - } - return opts; + return models.map((model) => ({ + id: model.id, + label: model.label, + })); }, [models]); const thinkingSelectOptions: ComboSelectOption[] = useMemo( @@ -586,7 +582,7 @@ export function AgentConfigRow({ title="Select model" value={selectedModel} options={modelSelectOptions} - placeholder="Auto" + placeholder={isModelLoading ? "Loading..." : "Select model"} disabled={disabled} isLoading={isModelLoading} onSelect={onSelectModel} @@ -777,10 +773,8 @@ export function ModelDropdown({ const [isOpen, setIsOpen] = useState(false); const anchorRef = useRef(null); - const selectedLabel = selectedModel - ? (models.find((model) => model.id === selectedModel)?.label ?? selectedModel) - : "Automatic"; - const placeholder = isLoading && models.length === 0 ? "Loading..." : "Automatic"; + const selectedLabel = models.find((model) => model.id === selectedModel)?.label ?? selectedModel ?? "Select model"; + const placeholder = isLoading && models.length === 0 ? "Loading..." : "Select model"; const helperText = error ? undefined : isLoading @@ -790,34 +784,20 @@ export function ModelDropdown({ : undefined; const options = useMemo(() => { - const opts: ComboSelectOption[] = [ - { - id: "", - label: "Automatic (provider default)", - description: "Let the assistant pick the recommended model.", - }, - ]; - for (const model of models) { - opts.push({ - id: model.id, - label: model.label, - description: model.description, - }); - } - return opts; + return models.map((model) => ({ + id: model.id, + label: model.label, + description: model.description, + })); }, [models]); const handleOpen = useCallback(() => setIsOpen(true), []); const handleOpenChange = useCallback((open: boolean) => setIsOpen(open), []); const handleSelect = useCallback( (id: string) => { - if (id === "") { - onClear(); - } else { - onSelect(id); - } + onSelect(id); }, - [onClear, onSelect], + [onSelect], ); return ( diff --git a/packages/app/src/components/agent-input-area.tsx b/packages/app/src/components/agent-input-area.tsx index f79f10029..4f4d6c441 100644 --- a/packages/app/src/components/agent-input-area.tsx +++ b/packages/app/src/components/agent-input-area.tsx @@ -1,6 +1,7 @@ import { View, Pressable, Text, ActivityIndicator, Platform } from "react-native"; import { useState, useEffect, useRef, useCallback } from "react"; import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles"; +import { useShallow } from "zustand/shallow"; import { ArrowUp, Square, Pencil, AudioLines } from "lucide-react-native"; import Animated from "react-native-reanimated"; import { useSafeAreaInsets } from "react-native-safe-area-context"; @@ -126,7 +127,14 @@ export function AgentInputArea({ agentDirectoryStatus === "revalidating" || agentDirectoryStatus === "error_after_ready"); - const agent = useSessionStore((state) => state.sessions[serverId]?.agents?.get(agentId)); + const agentState = useSessionStore( + useShallow((state) => { + const agent = state.sessions[serverId]?.agents?.get(agentId) ?? null; + return { + status: agent?.status ?? null, + }; + }), + ); const queuedMessagesRaw = useSessionStore((state) => state.sessions[serverId]?.queuedMessages?.get(agentId), @@ -272,41 +280,8 @@ export function AgentInputArea({ onSubmitMessageRef.current = onSubmitMessage; }, [onSubmitMessage]); - const isAgentRunning = agent?.status === "running"; - const agentUpdatedAtMs = agent?.updatedAt?.getTime() ?? 0; - - const prevIsAgentRunningRef = useRef(isAgentRunning); - const latestAgentUpdatedAtRef = useRef(agentUpdatedAtMs); - useEffect(() => { - const previousUpdatedAt = latestAgentUpdatedAtRef.current; - if (agentUpdatedAtMs < previousUpdatedAt) { - if (isProcessing && !isAgentRunning) { - prevIsAgentRunningRef.current = false; - setIsProcessing(false); - } - return; - } - - const wasRunning = prevIsAgentRunningRef.current; - let shouldClearProcessing = false; - - if (isProcessing) { - const hasEnteredRunning = !wasRunning && isAgentRunning; - const hasFreshRunningUpdateWhileRunning = - wasRunning && isAgentRunning && agentUpdatedAtMs > previousUpdatedAt; - const hasStoppedRunning = wasRunning && !isAgentRunning; - - shouldClearProcessing = - hasEnteredRunning || hasFreshRunningUpdateWhileRunning || hasStoppedRunning; - } - - prevIsAgentRunningRef.current = isAgentRunning; - latestAgentUpdatedAtRef.current = agentUpdatedAtMs; - - if (shouldClearProcessing) { - setIsProcessing(false); - } - }, [agentUpdatedAtMs, isAgentRunning, isProcessing]); + const isAgentRunning = agentState.status === "running"; + const hasAgent = agentState.status !== null; const updateQueue = useCallback( (updater: (current: QueuedMessage[]) => QueuedMessage[]) => { @@ -348,7 +323,7 @@ export function AgentInputArea({ message, imageAttachments, forceSend, - isAgentRunning: agent?.status === "running", + isAgentRunning: agentState.status === "running", // Parent-managed submits are still valid submit paths even when the // transport is disconnected, because the parent decides the failure mode. canSubmit: Boolean(sendAgentMessageRef.current || onSubmitMessageRef.current), @@ -481,7 +456,7 @@ export function AgentInputArea({ }); function handleCancelAgent() { - if (!agent || agent.status !== "running" || isCancellingAgent) { + if (!isAgentRunning || isCancellingAgent) { return; } if (!isConnected || !client) { @@ -495,7 +470,7 @@ export function AgentInputArea({ const isVoiceModeForAgent = voice?.isVoiceModeForAgent(serverId, agentId) ?? false; const handleToggleRealtimeVoice = useCallback(() => { - if (!voice || !isConnected || !agent) { + if (!voice || !isConnected || !hasAgent) { return; } if (voice.isVoiceSwitching) { @@ -512,7 +487,7 @@ export function AgentInputArea({ toast.error(message); } }); - }, [agent, agentId, isConnected, serverId, toast, voice]); + }, [agentId, hasAgent, isConnected, serverId, toast, voice]); function handleEditQueuedMessage(id: string) { const item = queuedMessages.find((q) => q.id === id); @@ -604,7 +579,7 @@ export function AgentInputArea({ const rightContent = ( - {!isVoiceModeForAgent && agent ? ( + {!isVoiceModeForAgent && hasAgent ? ( ( try { await input.submitMessage({ message: trimmedMessage, imageAttachments }); input.clearDraft("sent"); + input.setIsProcessing(false); return "submitted"; } catch (error) { input.onSubmitError?.(error); diff --git a/packages/app/src/components/agent-status-bar.tsx b/packages/app/src/components/agent-status-bar.tsx index c9d834d2f..c2317653a 100644 --- a/packages/app/src/components/agent-status-bar.tsx +++ b/packages/app/src/components/agent-status-bar.tsx @@ -1,6 +1,8 @@ import { useCallback, useMemo, useRef, useState } from "react"; import { View, Text, Platform, Pressable, Keyboard } from "react-native"; import { StyleSheet, useUnistyles } from "react-native-unistyles"; +import { useShallow } from "zustand/shallow"; +import { useStoreWithEqualityFn } from "zustand/traditional"; import { Brain, ChevronDown, ShieldAlert, ShieldCheck, ShieldOff } from "lucide-react-native"; import { getProviderIcon } from "@/components/provider-icons"; import { CombinedModelSelector } from "@/components/combined-model-selector"; @@ -603,8 +605,29 @@ function ControlledStatusBar({ ); } +const EMPTY_MODES: AgentMode[] = []; + export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) { - const agent = useSessionStore((state) => state.sessions[serverId]?.agents?.get(agentId)); + const agent = useSessionStore( + useShallow((state) => { + const currentAgent = state.sessions[serverId]?.agents?.get(agentId) ?? null; + return currentAgent + ? { + provider: currentAgent.provider, + cwd: currentAgent.cwd, + currentModeId: currentAgent.currentModeId, + runtimeModelId: currentAgent.runtimeInfo?.model ?? null, + model: currentAgent.model, + thinkingOptionId: currentAgent.thinkingOptionId, + } + : null; + }), + ); + const availableModes = useStoreWithEqualityFn( + useSessionStore, + (state) => state.sessions[serverId]?.agents?.get(agentId)?.availableModes ?? EMPTY_MODES, + (a, b) => a === b || JSON.stringify(a) === JSON.stringify(b), + ); const client = useSessionStore((state) => state.sessions[serverId]?.client ?? null); const modelsQuery = useQuery({ @@ -631,23 +654,23 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) { const models = modelsQuery.data ?? null; const displayMode = - agent?.availableModes?.find((mode) => mode.id === agent.currentModeId)?.label || + availableModes.find((mode) => mode.id === agent?.currentModeId)?.label || agent?.currentModeId || "default"; const modelSelection = resolveAgentModelSelection({ models, - runtimeModelId: agent?.runtimeInfo?.model, + runtimeModelId: agent?.runtimeModelId, configuredModelId: agent?.model, explicitThinkingOptionId: agent?.thinkingOptionId, }); const modeOptions = useMemo(() => { - return (agent?.availableModes ?? []).map((mode) => ({ + return availableModes.map((mode) => ({ id: mode.id, label: mode.label, })); - }, [agent?.availableModes]); + }, [availableModes]); const modelOptions = useMemo(() => { return (models ?? []).map((model) => ({ id: model.id, label: model.label })); @@ -668,9 +691,7 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) { 0 - ? modeOptions - : [{ id: agent.currentModeId ?? "", label: displayMode }] + modeOptions.length > 0 ? modeOptions : [{ id: agent.currentModeId ?? "", label: displayMode }] } selectedModeId={agent.currentModeId ?? undefined} onSelectMode={(modeId) => { diff --git a/packages/app/src/components/agent-stream-view.tsx b/packages/app/src/components/agent-stream-view.tsx index b7ec45512..6e136bbd5 100644 --- a/packages/app/src/components/agent-stream-view.tsx +++ b/packages/app/src/components/agent-stream-view.tsx @@ -39,7 +39,7 @@ import { import type { StreamItem } from "@/types/stream"; import type { PendingPermission } from "@/types/shared"; import type { AgentPermissionResponse } from "@server/server/agent/agent-sdk-types"; -import type { Agent } from "@/contexts/session-context"; +import type { AgentScreenAgent } from "@/hooks/use-agent-screen-state-machine"; import { useSessionStore } from "@/stores/session-store"; import { useFileExplorerActions } from "@/hooks/use-file-explorer-actions"; import type { DaemonClient } from "@server/client/daemon-client"; @@ -82,7 +82,7 @@ export interface AgentStreamViewHandle { export interface AgentStreamViewProps { agentId: string; serverId?: string; - agent: Agent; + agent: AgentScreenAgent; streamItems: StreamItem[]; pendingPermissions: Map; routeBottomAnchorRequest?: BottomAnchorRouteRequest | null; diff --git a/packages/app/src/components/combined-model-selector.tsx b/packages/app/src/components/combined-model-selector.tsx index c60fd4e10..d9edea21f 100644 --- a/packages/app/src/components/combined-model-selector.tsx +++ b/packages/app/src/components/combined-model-selector.tsx @@ -11,11 +11,11 @@ const INLINE_MODEL_THRESHOLD = 8; type DrillDownView = { provider: string }; -function resolveDefaultModelLabel(models: AgentModelDefinition[] | undefined): string | null { +function resolveDefaultModelLabel(models: AgentModelDefinition[] | undefined): string { if (!models || models.length === 0) { - return null; + return "Select model"; } - return (models.find((model) => model.isDefault) ?? models[0])?.label ?? null; + return (models.find((model) => model.isDefault) ?? models[0])?.label ?? "Select model"; } interface CombinedModelSelectorProps { @@ -75,7 +75,7 @@ export function CombinedModelSelector({ const models = allProviderModels.get(selectedProvider); if (!models) return isLoading ? "Loading..." : "Select model"; const model = models.find((m) => m.id === selectedModel); - return model?.label ?? resolveDefaultModelLabel(models) ?? "Select model"; + return model?.label ?? resolveDefaultModelLabel(models); }, [allProviderModels, selectedProvider, selectedModel, isLoading]); return ( diff --git a/packages/app/src/hooks/use-agent-form-state.ts b/packages/app/src/hooks/use-agent-form-state.ts index dd18ca1c8..3413794fe 100644 --- a/packages/app/src/hooks/use-agent-form-state.ts +++ b/packages/app/src/hooks/use-agent-form-state.ts @@ -693,6 +693,7 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg const agentDefinition = providerDefinitionMap.get(formState.provider); const modeOptions = agentDefinition?.modes ?? []; const effectiveModel = resolveEffectiveModel(availableModels, formState.model); + const resolvedModelId = effectiveModel?.id ?? formState.model; const availableThinkingOptions = effectiveModel?.thinkingOptions ?? []; const isModelLoading = providerModelsQuery.isLoading || providerModelsQuery.isFetching; const modelError = @@ -709,7 +710,7 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg setProviderFromUser, selectedMode: formState.modeId, setModeFromUser, - selectedModel: formState.model, + selectedModel: resolvedModelId, setModelFromUser, selectedThinkingOptionId: formState.thinkingOptionId, setThinkingOptionFromUser, @@ -735,7 +736,7 @@ export function useAgentFormState(options: UseAgentFormStateOptions = {}): UseAg formState.serverId, formState.provider, formState.modeId, - formState.model, + resolvedModelId, formState.thinkingOptionId, formState.workingDir, setSelectedServerId, diff --git a/packages/app/src/hooks/use-agent-screen-state-machine.ts b/packages/app/src/hooks/use-agent-screen-state-machine.ts index 06fabad30..2c37b70a6 100644 --- a/packages/app/src/hooks/use-agent-screen-state-machine.ts +++ b/packages/app/src/hooks/use-agent-screen-state-machine.ts @@ -1,5 +1,17 @@ import { useRef } from "react"; -import type { Agent } from "@/contexts/session-context"; + +export interface AgentScreenAgent { + serverId: string; + id: string; + status: "initializing" | "idle" | "running" | "error" | "closed"; + cwd: string; + projectPlacement?: { + checkout?: { + cwd?: string; + isGit?: boolean; + }; + } | null; +} export type AgentScreenMissingState = | { kind: "idle" } @@ -8,8 +20,8 @@ export type AgentScreenMissingState = | { kind: "error"; message: string }; export interface AgentScreenMachineInput { - agent: Agent | null; - placeholderAgent: Agent | null; + agent: AgentScreenAgent | null; + placeholderAgent: AgentScreenAgent | null; missingAgentState: AgentScreenMissingState; isConnected: boolean; isArchivingCurrentAgent: boolean; @@ -31,7 +43,7 @@ export type AgentScreenToastLatch = "none" | "history_refresh" | "sync_error"; export interface AgentScreenMachineMemory { hasRenderedReady: boolean; - lastReadyAgent: Agent | null; + lastReadyAgent: AgentScreenAgent | null; activeToastLatch: AgentScreenToastLatch; hadInitialSyncFailure: boolean; } @@ -70,7 +82,7 @@ export type AgentScreenViewState = } | { tag: "ready"; - agent: Agent; + agent: AgentScreenAgent; source: "authoritative" | "optimistic" | "stale"; sync: AgentScreenReadySyncState; isArchiving: boolean; diff --git a/packages/app/src/panels/agent-panel.tsx b/packages/app/src/panels/agent-panel.tsx index 9a5f98123..880c8a4b9 100644 --- a/packages/app/src/panels/agent-panel.tsx +++ b/packages/app/src/panels/agent-panel.tsx @@ -2,6 +2,8 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { ActivityIndicator, Platform, Text, View } from "react-native"; import ReanimatedAnimated from "react-native-reanimated"; import { StyleSheet, useUnistyles } from "react-native-unistyles"; +import { useShallow } from "zustand/shallow"; +import { useStoreWithEqualityFn } from "zustand/traditional"; import { Bot } from "lucide-react-native"; import invariant from "tiny-invariant"; import { AgentStreamView, type AgentStreamViewHandle } from "@/components/agent-stream-view"; @@ -16,6 +18,7 @@ import { useAgentAttentionClear } from "@/hooks/use-agent-attention-clear"; import { useAgentInitialization } from "@/hooks/use-agent-initialization"; import { useAgentScreenStateMachine, + type AgentScreenAgent, type AgentScreenMissingState, } from "@/hooks/use-agent-screen-state-machine"; import { useArchiveAgent } from "@/hooks/use-archive-agent"; @@ -78,11 +81,21 @@ function useAgentPanelDescriptor( target: { kind: "agent"; agentId: string }, context: { serverId: string }, ): PanelDescriptor { - const agent = useSessionStore( - (state) => state.sessions[context.serverId]?.agents?.get(target.agentId) ?? null, + const descriptorState = useSessionStore( + useShallow((state) => { + const agent = state.sessions[context.serverId]?.agents?.get(target.agentId) ?? null; + return { + provider: agent?.provider ?? "codex", + title: agent?.title ?? null, + status: agent?.status ?? null, + pendingPermissionCount: agent?.pendingPermissions.length ?? 0, + requiresAttention: agent?.requiresAttention ?? false, + attentionReason: agent?.attentionReason ?? null, + }; + }), ); - const provider = agent?.provider ?? "codex"; - const label = resolveWorkspaceAgentTabLabel(agent?.title); + const provider = descriptorState.provider; + const label = resolveWorkspaceAgentTabLabel(descriptorState.title); const icon = provider === "claude" ? ClaudeIcon : provider === "codex" ? CodexIcon : Bot; return { @@ -90,12 +103,12 @@ function useAgentPanelDescriptor( subtitle: `${formatProviderLabel(provider)} agent`, titleState: label ? "ready" : "loading", icon, - statusBucket: agent + statusBucket: descriptorState.status ? deriveSidebarStateBucket({ - status: agent.status, - pendingPermissionCount: agent.pendingPermissions.length, - requiresAttention: agent.requiresAttention, - attentionReason: agent.attentionReason, + status: descriptorState.status, + pendingPermissionCount: descriptorState.pendingPermissionCount, + requiresAttention: descriptorState.requiresAttention, + attentionReason: descriptorState.attentionReason, }) : null, }; @@ -243,8 +256,25 @@ function AgentPanelBody({ addImagesRef.current = addImages; }, []); - const agent = useSessionStore((state) => - agentId ? state.sessions[serverId]?.agents?.get(agentId) : undefined, + const agentState = useSessionStore( + useShallow((state) => { + const agent = agentId ? state.sessions[serverId]?.agents?.get(agentId) ?? null : null; + return { + serverId: agent?.serverId ?? null, + id: agent?.id ?? null, + status: agent?.status ?? null, + cwd: agent?.cwd ?? null, + archivedAt: agent?.archivedAt ?? null, + requiresAttention: agent?.requiresAttention ?? false, + attentionReason: agent?.attentionReason ?? null, + }; + }), + ); + const projectPlacement = useStoreWithEqualityFn( + useSessionStore, + (state) => + agentId ? (state.sessions[serverId]?.agents?.get(agentId)?.projectPlacement ?? null) : null, + (a, b) => a === b || JSON.stringify(a) === JSON.stringify(b), ); const streamItemsRaw = useSessionStore((state) => agentId ? state.sessions[serverId]?.agentStreamTail?.get(agentId) : undefined, @@ -318,52 +348,14 @@ function AgentPanelBody({ agentId, client, isConnected, - requiresAttention: agent?.requiresAttention, - attentionReason: agent?.attentionReason, + requiresAttention: agentState.requiresAttention, + attentionReason: agentState.attentionReason, isScreenFocused: isPaneFocused, }); useEffect(() => { clearOnAgentBlurRef.current = attentionController.clearOnAgentBlur; }, [attentionController.clearOnAgentBlur]); - // --------------------------------------------------------------------------- - // DEBUG: track which selector values change between renders - // --------------------------------------------------------------------------- - const debugPrevRef = useRef>({}); - useEffect(() => { - const prev = debugPrevRef.current; - const curr: Record = { - agent, - "agent?.status": agent?.status, - "agent?.cwd": agent?.cwd, - "agent?.updatedAt": agent?.updatedAt, - "agent?.requiresAttention": agent?.requiresAttention, - streamItemsRaw, - "streamItems.length": streamItems.length, - allPendingPermissions, - isInitializingFromMap, - historySyncGeneration, - hasAppliedAuthoritativeHistory, - agentHistorySyncGeneration, - hasSession, - isPaneFocused, - isConnected, - }; - const changed: string[] = []; - for (const key of Object.keys(curr)) { - if (!Object.is(prev[key], curr[key])) { - changed.push(key); - } - } - if (changed.length > 0 && Object.keys(prev).length > 0) { - console.log("[AgentPanelBody] values changed:", changed.join(", "), { - changed: Object.fromEntries(changed.map((k) => [k, { prev: prev[k], curr: curr[k] }])), - }); - } - debugPrevRef.current = curr; - }); - // --------------------------------------------------------------------------- - const { style: animatedKeyboardStyle } = useKeyboardShiftStyle({ mode: "translate", }); @@ -488,49 +480,34 @@ function AgentPanelBody({ }, [optimisticStreamItems, streamItems]); const shouldUseOptimisticStream = isPendingCreateForPanel && optimisticStreamItems.length > 0; - const authoritativeStatus = agent?.status; + const authoritativeStatus = agentState.status; const isAuthoritativeBootstrapping = authoritativeStatus === "initializing" || authoritativeStatus === "idle"; const showPendingCreateSubmitLoading = isPendingCreateForPanel && (!authoritativeStatus || isAuthoritativeBootstrapping); const canFinalizePendingCreate = Boolean(authoritativeStatus) && !isAuthoritativeBootstrapping; - const placeholderAgent: Agent | null = useMemo(() => { + const agent: AgentScreenAgent | null = + agentState.serverId && agentState.id && agentState.status && agentState.cwd + ? { + serverId: agentState.serverId, + id: agentState.id, + status: agentState.status, + cwd: agentState.cwd, + projectPlacement, + } + : null; + + const placeholderAgent: AgentScreenAgent | null = useMemo(() => { if (!shouldUseOptimisticStream || !agentId) { return null; } - const now = new Date(); return { serverId, id: agentId, - provider: "claude", status: "running", - createdAt: now, - updatedAt: now, - lastUserMessageAt: now, - lastActivityAt: now, - capabilities: { - supportsStreaming: true, - supportsSessionPersistence: false, - supportsDynamicModes: false, - supportsMcpServers: false, - supportsReasoningStream: false, - supportsToolInvocations: false, - }, - currentModeId: null, - availableModes: [], - pendingPermissions: [], - persistence: null, - runtimeInfo: { - provider: "claude", - sessionId: null, - model: null, - modeId: null, - }, - title: "Agent", cwd: ".", - model: null, - labels: {}, + projectPlacement: null, }; }, [agentId, serverId, shouldUseOptimisticStream]); @@ -642,7 +619,7 @@ function AgentPanelBody({ if (!agentId) { return; } - if (agent || shouldUseOptimisticStream) { + if (agentState.id || shouldUseOptimisticStream) { if (missingAgentState.kind !== "idle") { setMissingAgentState({ kind: "idle" }); } @@ -717,7 +694,7 @@ function AgentPanelBody({ setMissingAgentState({ kind: "error", message }); }); }, [ - agent, + agentState.id, agentId, client, ensureAgentIsInitialized, @@ -803,7 +780,7 @@ function AgentPanelBody({ - {agentId && !isArchivingCurrentAgent && !agent?.archivedAt ? ( + {agentId && !isArchivingCurrentAgent && !agentState.archivedAt ? ( - ) : agentId && agent?.archivedAt ? ( + ) : agentId && agentState.archivedAt ? ( ) : null}