diff --git a/packages/app/src/hooks/use-agent-screen-state-machine.test.ts b/packages/app/src/hooks/use-agent-screen-state-machine.test.ts index 631131a3e..2de63aa10 100644 --- a/packages/app/src/hooks/use-agent-screen-state-machine.test.ts +++ b/packages/app/src/hooks/use-agent-screen-state-machine.test.ts @@ -47,6 +47,19 @@ function createAgent(id: string): Agent { }; } +function createAgentWithStatus({ + id, + status, +}: { + id: string; + status: Agent["status"]; +}): Agent { + return { + ...createAgent(id), + status, + }; +} + function createBaseInput(): AgentScreenMachineInput { return { agent: null, @@ -357,6 +370,86 @@ describe("deriveAgentScreenViewState", () => { expect(handoffReady.agent.id).toBe("draft-agent"); }); + it("keeps optimistic running status while authoritative agent is still bootstrapping", () => { + const memory = createBaseMemory(); + const input: AgentScreenMachineInput = { + ...createBaseInput(), + agent: createAgentWithStatus({ id: "agent-1", status: "idle" }), + placeholderAgent: createAgent("agent-1"), + shouldUseOptimisticStream: true, + }; + + const result = deriveAgentScreenViewState({ input, memory }); + const ready = expectReadyState(result.state); + + expect(ready.source).toBe("optimistic"); + expect(ready.agent.status).toBe("running"); + }); + + it("keeps optimistic running status while authoritative agent is initializing", () => { + const memory = createBaseMemory(); + const input: AgentScreenMachineInput = { + ...createBaseInput(), + agent: createAgentWithStatus({ id: "agent-1", status: "initializing" }), + placeholderAgent: createAgent("agent-1"), + shouldUseOptimisticStream: true, + }; + + const result = deriveAgentScreenViewState({ input, memory }); + const ready = expectReadyState(result.state); + + expect(ready.source).toBe("optimistic"); + expect(ready.agent.status).toBe("running"); + }); + + it("hands off to authoritative once agent reaches running", () => { + const memory = createBaseMemory(); + const input: AgentScreenMachineInput = { + ...createBaseInput(), + agent: createAgentWithStatus({ id: "agent-1", status: "running" }), + placeholderAgent: createAgent("agent-1"), + shouldUseOptimisticStream: true, + }; + + const result = deriveAgentScreenViewState({ input, memory }); + const ready = expectReadyState(result.state); + + expect(ready.source).toBe("authoritative"); + expect(ready.agent.status).toBe("running"); + }); + + it("hands off to authoritative for terminal error states", () => { + const memory = createBaseMemory(); + const input: AgentScreenMachineInput = { + ...createBaseInput(), + agent: createAgentWithStatus({ id: "agent-1", status: "error" }), + placeholderAgent: createAgent("agent-1"), + shouldUseOptimisticStream: true, + }; + + const result = deriveAgentScreenViewState({ input, memory }); + const ready = expectReadyState(result.state); + + expect(ready.source).toBe("authoritative"); + expect(ready.agent.status).toBe("error"); + }); + + it("hands off to authoritative for terminal closed states", () => { + const memory = createBaseMemory(); + const input: AgentScreenMachineInput = { + ...createBaseInput(), + agent: createAgentWithStatus({ id: "agent-1", status: "closed" }), + placeholderAgent: createAgent("agent-1"), + shouldUseOptimisticStream: true, + }; + + const result = deriveAgentScreenViewState({ input, memory }); + const ready = expectReadyState(result.state); + + expect(ready.source).toBe("authoritative"); + expect(ready.agent.status).toBe("closed"); + }); + it("emits history refresh toast only on transition into toast catch-up state", () => { const memory = createBaseMemory({ hasRenderedReady: true, 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 d1a2c39c4..fac82e819 100644 --- a/packages/app/src/hooks/use-agent-screen-state-machine.ts +++ b/packages/app/src/hooks/use-agent-screen-state-machine.ts @@ -93,7 +93,17 @@ export function deriveAgentScreenViewState({ nextMemory.hadInitialSyncFailure = true; } - const candidateAgent = input.agent ?? input.placeholderAgent; + const useOptimisticCreateFlowAgent = + input.shouldUseOptimisticStream && + Boolean(input.placeholderAgent) && + (!input.agent || + input.agent.status === "initializing" || + input.agent.status === "idle"); + + const candidateAgent = + input.agent && useOptimisticCreateFlowAgent && input.placeholderAgent + ? { ...input.agent, status: input.placeholderAgent.status } + : input.agent ?? input.placeholderAgent; if (candidateAgent) { nextMemory.hasRenderedReady = true; nextMemory.lastReadyAgent = candidateAgent; @@ -132,11 +142,13 @@ export function deriveAgentScreenViewState({ }; } - const source: "authoritative" | "optimistic" | "stale" = input.agent - ? "authoritative" - : input.shouldUseOptimisticStream - ? "optimistic" - : "stale"; + const source: "authoritative" | "optimistic" | "stale" = useOptimisticCreateFlowAgent + ? "optimistic" + : input.agent + ? "authoritative" + : input.shouldUseOptimisticStream + ? "optimistic" + : "stale"; let sync: AgentScreenReadySyncState; if (!input.isConnected) { diff --git a/packages/app/src/screens/agent/agent-ready-screen.tsx b/packages/app/src/screens/agent/agent-ready-screen.tsx index e24bc24fd..8e2f2a456 100644 --- a/packages/app/src/screens/agent/agent-ready-screen.tsx +++ b/packages/app/src/screens/agent/agent-ready-screen.tsx @@ -652,6 +652,14 @@ function AgentScreenContent({ }, [optimisticStreamItems, streamItems]); const shouldUseOptimisticStream = isPendingCreateForRoute && optimisticStreamItems.length > 0; + const authoritativeStatus = agent?.status; + const isAuthoritativeBootstrapping = + authoritativeStatus === "initializing" || authoritativeStatus === "idle"; + const showPendingCreateSubmitLoading = + isPendingCreateForRoute && + (!authoritativeStatus || isAuthoritativeBootstrapping); + const canFinalizePendingCreate = + Boolean(authoritativeStatus) && !isAuthoritativeBootstrapping; const placeholderAgent: Agent | null = useMemo(() => { if (!shouldUseOptimisticStream || !resolvedAgentId) { @@ -730,7 +738,7 @@ function AgentScreenContent({ item.kind === "user_message" && item.id === pendingCreate.clientMessageId ); - if (agent && hasUserMessage) { + if (hasUserMessage && canFinalizePendingCreate) { if ( resolvedAgentId && pendingCreate.images && @@ -760,7 +768,7 @@ function AgentScreenContent({ clearPendingCreate(); } }, [ - agent, + canFinalizePendingCreate, clearPendingCreate, isPendingCreateForRoute, markPendingCreateLifecycle, @@ -1239,6 +1247,7 @@ function AgentScreenContent({ agentId={resolvedAgentId} serverId={serverId} autoFocus + isSubmitLoading={showPendingCreateSubmitLoading} onAddImages={handleAddImagesCallback} /> )}