Show optimistic chat for new workspaces

This commit is contained in:
Mohamed Boudra
2026-05-25 21:26:31 +07:00
parent d271597ee8
commit 30a8bffc24
11 changed files with 452 additions and 140 deletions
@@ -0,0 +1,90 @@
/**
* @vitest-environment jsdom
*/
import { act, renderHook } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { useCreateFlowStore } from "@/stores/create-flow-store";
import type { UserMessageImageAttachment } from "@/types/stream";
import type { AgentAttachment } from "@server/shared/messages";
import { useDraftAgentCreateFlow, type DraftCreateAttempt } from "./use-draft-agent-create-flow";
describe("useDraftAgentCreateFlow", () => {
beforeEach(() => {
useCreateFlowStore.setState({ pendingByDraftId: {} });
});
it("renders a prepared new-workspace create attempt as optimistic chat before continuing it", async () => {
const image: UserMessageImageAttachment = {
id: "image-1",
mimeType: "image/png",
storageType: "web-indexeddb",
storageKey: "image-key",
createdAt: 123,
};
const attachment = {
type: "review",
cwd: "/repo",
summary: "Review",
} as unknown as AgentAttachment;
const attempt: DraftCreateAttempt = {
clientMessageId: "msg-prepared",
text: "build this",
timestamp: new Date("2026-05-25T00:00:00.000Z"),
images: [image],
attachments: [attachment],
};
const createRequest = vi.fn(
async (ctx: {
attempt: DraftCreateAttempt;
text: string;
images?: UserMessageImageAttachment[];
attachments?: AgentAttachment[];
cwd: string;
}) => ({
agentId: "agent-1",
result: { id: "agent-1", ctx },
}),
);
const onCreateSuccess = vi.fn();
const { result } = renderHook(() =>
useDraftAgentCreateFlow({
draftId: "draft-1",
getPendingServerId: () => "server-1",
initialAttempt: attempt,
buildDraftAgent: (currentAttempt) => ({ currentAttempt }),
createRequest,
onCreateSuccess,
}),
);
expect(result.current.isSubmitting).toBe(true);
expect(result.current.draftAgent).toEqual({ currentAttempt: attempt });
expect(result.current.optimisticStreamItems).toEqual([
{
kind: "user_message",
id: "msg-prepared",
text: "build this",
timestamp: attempt.timestamp,
optimistic: true,
images: [image],
attachments: [attachment],
},
]);
expect(createRequest).not.toHaveBeenCalled();
await act(async () => {
await result.current.continueCreateFromAttempt({ attempt, cwd: "/repo" });
});
expect(createRequest).toHaveBeenCalledTimes(1);
expect(createRequest).toHaveBeenCalledWith({
attempt,
text: "build this",
images: [image],
attachments: [attachment],
cwd: "/repo",
});
expect(onCreateSuccess).toHaveBeenCalledTimes(1);
});
});
@@ -81,6 +81,7 @@ interface CreateRequestContext {
interface UseDraftAgentCreateFlowOptions<TDraftAgent, TCreateResult> {
draftId: string;
getPendingServerId: () => string | null;
initialAttempt?: CreateAttempt | null;
allowEmptyText?: boolean;
validateBeforeSubmit?: (ctx: SubmitContext) => string | null;
onBeforeSubmit?: (ctx: CreateRequestContext) => void;
@@ -94,6 +95,7 @@ interface UseDraftAgentCreateFlowOptions<TDraftAgent, TCreateResult> {
export function useDraftAgentCreateFlow<TDraftAgent, TCreateResult>({
draftId,
getPendingServerId,
initialAttempt = null,
allowEmptyText = false,
validateBeforeSubmit,
onBeforeSubmit,
@@ -103,10 +105,17 @@ export function useDraftAgentCreateFlow<TDraftAgent, TCreateResult>({
onCreateSuccess,
onCreateError,
}: UseDraftAgentCreateFlowOptions<TDraftAgent, TCreateResult>) {
const [machine, dispatch] = useReducer(reducer, {
tag: "draft",
errorMessage: "",
} as DraftAgentMachineState);
const [machine, dispatch] = useReducer(
reducer,
initialAttempt,
(attempt): DraftAgentMachineState =>
attempt
? { tag: "creating", attempt }
: {
tag: "draft",
errorMessage: "",
},
);
const setPendingCreateAttempt = useCreateFlowStore((state) => state.setPending);
const updatePendingAgentId = useCreateFlowStore((state) => state.updateAgentId);
@@ -150,6 +159,73 @@ export function useDraftAgentCreateFlow<TDraftAgent, TCreateResult>({
return buildDraftAgent(machine.attempt);
}, [buildDraftAgent, machine]);
const runCreateAttempt = useCallback(
async ({ attempt, cwd }: { attempt: CreateAttempt; cwd: string }) => {
const pendingServerId = getPendingServerId();
if (!pendingServerId) {
const error = new Error("No host selected");
dispatch({ type: "DRAFT_SET_ERROR", message: error.message });
throw error;
}
onBeforeSubmit?.({
attempt,
text: attempt.text,
images: attempt.images,
attachments: attempt.attachments,
cwd,
});
try {
const createResult = await createRequest({
attempt,
text: attempt.text,
images: attempt.images,
attachments: attempt.attachments,
cwd,
});
if (createResult.agentId) {
updatePendingAgentId({ draftId, agentId: createResult.agentId });
appendOptimisticUserMessageToAgentStream(
pendingServerId,
createResult.agentId,
buildOptimisticUserMessage({
id: attempt.clientMessageId,
text: attempt.text,
timestamp: attempt.timestamp,
images: attempt.images,
attachments: attempt.attachments,
}),
{ placement: "tail", skipIfUserMessageExists: true },
);
markPendingCreateLifecycle({ draftId, lifecycle: "sent" });
}
await onCreateSuccess({ result: createResult.result, attempt });
} catch (error) {
const resolved = error instanceof Error ? error : new Error("Failed to create agent");
dispatch({ type: "CREATE_FAILED", message: resolved.message });
markPendingCreateLifecycle({ draftId, lifecycle: "abandoned" });
clearPendingCreateAttempt({ draftId });
onCreateError?.(resolved);
throw error;
}
},
[
appendOptimisticUserMessageToAgentStream,
clearPendingCreateAttempt,
createRequest,
draftId,
getPendingServerId,
markPendingCreateLifecycle,
onBeforeSubmit,
onCreateError,
onCreateSuccess,
updatePendingAgentId,
],
);
const handleCreateFromInput = useCallback(
async ({ text, attachments, cwd }: SubmitContext) => {
if (isSubmitting) {
@@ -206,71 +282,32 @@ export function useDraftAgentCreateFlow<TDraftAgent, TCreateResult>({
: {}),
});
onBeforeSubmit?.({
attempt,
text: trimmedPrompt,
images,
attachments: wirePayload.attachments,
cwd,
});
dispatch({ type: "SUBMIT", attempt });
onCreateStart?.();
try {
const createResult = await createRequest({
attempt,
text: trimmedPrompt,
images,
attachments: wirePayload.attachments,
cwd,
});
if (createResult.agentId) {
updatePendingAgentId({ draftId, agentId: createResult.agentId });
appendOptimisticUserMessageToAgentStream(
pendingServerId,
createResult.agentId,
buildOptimisticUserMessage({
id: attempt.clientMessageId,
text: attempt.text,
timestamp: attempt.timestamp,
images: attempt.images,
attachments: attempt.attachments,
}),
{ placement: "tail", skipIfUserMessageExists: true },
);
markPendingCreateLifecycle({ draftId, lifecycle: "sent" });
}
await onCreateSuccess({ result: createResult.result, attempt });
} catch (error) {
const resolved = error instanceof Error ? error : new Error("Failed to create agent");
dispatch({ type: "CREATE_FAILED", message: resolved.message });
markPendingCreateLifecycle({ draftId, lifecycle: "abandoned" });
clearPendingCreateAttempt({ draftId });
onCreateError?.(resolved);
throw error;
}
await runCreateAttempt({ attempt, cwd });
},
[
appendOptimisticUserMessageToAgentStream,
clearPendingCreateAttempt,
createRequest,
allowEmptyText,
draftId,
getPendingServerId,
isSubmitting,
markPendingCreateLifecycle,
onBeforeSubmit,
onCreateError,
onCreateStart,
onCreateSuccess,
runCreateAttempt,
setPendingCreateAttempt,
updatePendingAgentId,
validateBeforeSubmit,
allowEmptyText,
],
);
const continueCreateFromAttempt = useCallback(
async ({ attempt, cwd }: { attempt: CreateAttempt; cwd: string }) => {
if (!isSubmitting) {
dispatch({ type: "SUBMIT", attempt });
}
await runCreateAttempt({ attempt, cwd });
},
[isSubmitting, runCreateAttempt],
);
return {
machine,
formErrorMessage,
@@ -278,6 +315,7 @@ export function useDraftAgentCreateFlow<TDraftAgent, TCreateResult>({
optimisticStreamItems,
draftAgent,
handleCreateFromInput,
continueCreateFromAttempt,
};
}
@@ -0,0 +1,44 @@
import { describe, expect, it } from "vitest";
import { buildDraftPanelDescriptor } from "@/panels/draft-panel-descriptor";
function TestIcon() {
return null;
}
describe("buildDraftPanelDescriptor", () => {
it("uses the initial prompt title and running loader bucket during create", () => {
const descriptor = buildDraftPanelDescriptor({
isCreating: true,
pendingPrompt: "Build the dashboard",
icon: TestIcon,
});
expect(descriptor).toMatchObject({
label: "Build the dashboard",
subtitle: "Creating agent",
titleState: "ready",
statusBucket: "running",
});
});
it("falls back to the draft title for empty create prompts", () => {
const descriptor = buildDraftPanelDescriptor({
isCreating: true,
pendingPrompt: " ",
icon: TestIcon,
});
expect(descriptor.label).toBe("New Agent");
});
it("keeps ordinary draft tabs labeled as new agents", () => {
const descriptor = buildDraftPanelDescriptor({ isCreating: false, icon: TestIcon });
expect(descriptor).toMatchObject({
label: "New Agent",
subtitle: "New Agent",
titleState: "ready",
statusBucket: null,
});
});
});
+74 -1
View File
@@ -1,4 +1,5 @@
import type { DaemonClient } from "@server/client/daemon-client";
import { SquarePen } from "lucide-react-native";
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { ActivityIndicator, Text, View } from "react-native";
import ReanimatedAnimated from "react-native-reanimated";
@@ -36,6 +37,7 @@ import { useArchiveAgent } from "@/hooks/use-archive-agent";
import { useKeyboardShiftStyle } from "@/hooks/use-keyboard-shift-style";
import { usePaneContext, usePaneFocus } from "@/panels/pane-context";
import type { PanelDescriptor, PanelRegistration } from "@/panels/panel-registry";
import { buildDraftPanelDescriptor } from "@/panels/draft-panel-descriptor";
import {
type HostRuntimeConnectionStatus,
useHostRuntimeClient,
@@ -48,6 +50,7 @@ import {
deriveRouteBottomAnchorIntent,
deriveRouteBottomAnchorRequest,
} from "@/screens/agent/agent-ready-screen-bottom-anchor";
import { WorkspaceDraftAgentTab } from "@/screens/workspace/workspace-draft-agent-tab";
import { useCreateFlowStore } from "@/stores/create-flow-store";
import { buildDraftStoreKey, generateDraftId } from "@/stores/draft-keys";
import { usePanelStore } from "@/stores/panel-store";
@@ -302,12 +305,82 @@ function AgentPanel() {
);
}
function DraftPanel() {
const {
serverId,
workspaceId,
tabId,
target,
openFileInWorkspace,
openImportSheet,
retargetCurrentTab,
} = usePaneContext();
const { isInteractive } = usePaneFocus();
invariant(target.kind === "draft", "DraftPanel requires draft target");
const handleCreated = useCallback(
(agentSnapshot: Parameters<typeof normalizeAgentSnapshot>[0]) => {
const normalized = normalizeAgentSnapshot(agentSnapshot, serverId);
useSessionStore.getState().setAgents(serverId, (prev) => {
const next = new Map(prev);
next.set(agentSnapshot.id, normalized);
return next;
});
retargetCurrentTab({ kind: "agent", agentId: agentSnapshot.id });
},
[retargetCurrentTab, serverId],
);
return (
<WorkspaceDraftAgentTab
serverId={serverId}
workspaceId={workspaceId}
tabId={tabId}
draftId={target.draftId}
initialSetup={target.setup}
isPaneFocused={isInteractive}
onOpenWorkspaceFile={openFileInWorkspace}
onCreated={handleCreated}
onOpenImportSheet={openImportSheet}
/>
);
}
export function AgentConversationPanel() {
const { target } = usePaneContext();
if (target.kind === "draft") {
return <DraftPanel />;
}
if (target.kind === "agent") {
return <AgentPanel />;
}
invariant(false, "AgentConversationPanel requires an agent or draft target");
}
export const agentPanelRegistration: PanelRegistration<"agent"> = {
kind: "agent",
component: AgentPanel,
component: AgentConversationPanel,
useDescriptor: useAgentPanelDescriptor,
};
export function useDraftPanelDescriptor(
target: { kind: "draft"; draftId: string },
context: { serverId: string },
) {
const pendingCreate = useCreateFlowStore((state) => {
const pending = state.pendingByDraftId[target.draftId];
return pending?.serverId === context.serverId && pending.lifecycle === "active"
? pending
: null;
});
return buildDraftPanelDescriptor({
isCreating: Boolean(pendingCreate),
pendingPrompt: pendingCreate?.text,
icon: SquarePen,
});
}
const EMPTY_STREAM_ITEMS: StreamItem[] = [];
const EMPTY_PENDING_PERMISSIONS = new Map<string, PendingPermission>();
const EMPTY_PENDING_PERMISSION_LIST: PendingPermission[] = [];
@@ -0,0 +1,28 @@
import type { ComponentType } from "react";
import type { PanelDescriptor, PanelIconProps } from "@/panels/panel-registry";
export function buildDraftPanelDescriptor(input: {
isCreating: boolean;
pendingPrompt?: string | null;
icon: ComponentType<PanelIconProps>;
}): PanelDescriptor {
const { icon, isCreating, pendingPrompt } = input;
const creatingLabel = pendingPrompt?.trim() || "New Agent";
if (isCreating) {
return {
label: creatingLabel,
subtitle: "Creating agent",
titleState: "ready",
icon,
statusBucket: "running",
};
}
return {
label: "New Agent",
subtitle: "New Agent",
titleState: "ready",
icon,
statusBucket: null,
};
}
+2 -59
View File
@@ -1,65 +1,8 @@
import { SquarePen } from "lucide-react-native";
import { useCallback } from "react";
import invariant from "tiny-invariant";
import { WorkspaceDraftAgentTab } from "@/screens/workspace/workspace-draft-agent-tab";
import { usePaneContext, usePaneFocus } from "@/panels/pane-context";
import { AgentConversationPanel, useDraftPanelDescriptor } from "@/panels/agent-panel";
import type { PanelRegistration } from "@/panels/panel-registry";
import { useSessionStore } from "@/stores/session-store";
import { normalizeAgentSnapshot } from "@/utils/agent-snapshots";
function useDraftPanelDescriptor() {
return {
label: "New Agent",
subtitle: "New Agent",
titleState: "ready" as const,
icon: SquarePen,
statusBucket: null,
};
}
function DraftPanel() {
const {
serverId,
workspaceId,
tabId,
target,
openFileInWorkspace,
openImportSheet,
retargetCurrentTab,
} = usePaneContext();
const { isInteractive } = usePaneFocus();
invariant(target.kind === "draft", "DraftPanel requires draft target");
const handleCreated = useCallback(
(agentSnapshot: Parameters<typeof normalizeAgentSnapshot>[0]) => {
const normalized = normalizeAgentSnapshot(agentSnapshot, serverId);
useSessionStore.getState().setAgents(serverId, (prev) => {
const next = new Map(prev);
next.set(agentSnapshot.id, normalized);
return next;
});
retargetCurrentTab({ kind: "agent", agentId: agentSnapshot.id });
},
[retargetCurrentTab, serverId],
);
return (
<WorkspaceDraftAgentTab
serverId={serverId}
workspaceId={workspaceId}
tabId={tabId}
draftId={target.draftId}
initialSetup={target.setup}
isPaneFocused={isInteractive}
onOpenWorkspaceFile={openFileInWorkspace}
onCreated={handleCreated}
onOpenImportSheet={openImportSheet}
/>
);
}
export const draftPanelRegistration: PanelRegistration<"draft"> = {
kind: "draft",
component: DraftPanel,
component: AgentConversationPanel,
useDescriptor: useDraftPanelDescriptor,
};
@@ -23,9 +23,11 @@ import { useGithubSearchQuery } from "@/git/use-github-search-query";
import { useKeyboardShiftStyle } from "@/hooks/use-keyboard-shift-style";
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
import { normalizeWorkspaceDescriptor, useSessionStore } from "@/stores/session-store";
import { buildDraftStoreKey, generateDraftId } from "@/stores/draft-keys";
import { generateDraftId } from "@/stores/draft-keys";
import { useDraftStore } from "@/stores/draft-store";
import { useCreateFlowStore } from "@/stores/create-flow-store";
import { useWorkspaceDraftSubmissionStore } from "@/stores/workspace-draft-submission-store";
import { generateMessageId } from "@/types/stream";
import { toErrorMessage } from "@/utils/error-messages";
import { navigateToPreparedWorkspaceTab } from "@/utils/workspace-navigation";
import type { ComposerAttachment, UserComposerAttachment } from "@/attachments/types";
@@ -458,27 +460,29 @@ function submitWorkspaceDraft(input: SubmitDraftInput): void {
composerState,
} = input;
const draftId = generateDraftId();
useDraftStore.getState().saveDraftInput({
draftKey: buildDraftStoreKey({
serverId,
agentId: draftId,
draftId,
}),
draft: {
text,
attachments: attachments.filter(
(attachment): attachment is UserComposerAttachment => attachment.kind !== "review",
),
},
const clientMessageId = generateMessageId();
const timestamp = Date.now();
const wirePayload = splitComposerAttachmentsForSubmit(attachments);
useCreateFlowStore.getState().setPending({
serverId,
draftId,
agentId: null,
clientMessageId,
text: text.trim(),
timestamp,
...(wirePayload.images.length > 0 ? { images: wirePayload.images } : {}),
...(wirePayload.attachments.length > 0 ? { attachments: wirePayload.attachments } : {}),
});
useWorkspaceDraftSubmissionStore.getState().setPending({
serverId,
workspaceId,
draftId,
text,
text: text.trim(),
attachments,
cwd: workspaceDirectory,
provider,
clientMessageId,
timestamp,
...(composerState.modeOptions.length > 0 && composerState.selectedMode !== ""
? { modeId: composerState.selectedMode }
: {}),
@@ -11,11 +11,15 @@ import { composerWorkspaceAttachment } from "@/attachments/composer-workspace-at
import type { ImageAttachment } from "@/components/message-input";
import { useAgentInputDraft } from "@/hooks/use-agent-input-draft";
import type { CreateAgentInitialValues } from "@/hooks/use-agent-form-state";
import { useDraftAgentCreateFlow } from "@/hooks/use-draft-agent-create-flow";
import {
useDraftAgentCreateFlow,
type DraftCreateAttempt,
} from "@/hooks/use-draft-agent-create-flow";
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
import { buildWorkspaceDraftAgentConfig } from "@/screens/workspace/workspace-draft-agent-config";
import { buildDraftStoreKey } from "@/stores/draft-keys";
import { usePanelStore } from "@/stores/panel-store";
import { useCreateFlowStore } from "@/stores/create-flow-store";
import type { Agent } from "@/stores/session-store";
import { useWorkspaceExecutionAuthority } from "@/stores/session-store-hooks";
import { useWorkspaceDraftSubmissionStore } from "@/stores/workspace-draft-submission-store";
@@ -348,10 +352,33 @@ export function WorkspaceDraftAgentTab({
const pending = state.pendingByDraftId[draftId] ?? null;
return pending?.serverId === serverId && pending.workspaceId === workspaceId ? pending : null;
});
const pendingCreateAttempt = useCreateFlowStore((state) => {
const pending = state.pendingByDraftId[draftId] ?? null;
return pending?.serverId === serverId && pending.lifecycle === "active" ? pending : null;
});
const consumePendingAutoSubmit = useWorkspaceDraftSubmissionStore(
(state) => state.consumePending,
);
const autoSubmitConfig = resolveAutoSubmitConfig(pendingAutoSubmit);
const initialCreateAttempt = useMemo<DraftCreateAttempt | null>(() => {
if (!pendingAutoSubmit || !pendingCreateAttempt) {
return null;
}
if (pendingAutoSubmit.clientMessageId !== pendingCreateAttempt.clientMessageId) {
return null;
}
return {
clientMessageId: pendingCreateAttempt.clientMessageId,
text: pendingCreateAttempt.text,
timestamp: new Date(pendingCreateAttempt.timestamp),
...(pendingCreateAttempt.images && pendingCreateAttempt.images.length > 0
? { images: pendingCreateAttempt.images }
: {}),
...(pendingCreateAttempt.attachments && pendingCreateAttempt.attachments.length > 0
? { attachments: pendingCreateAttempt.attachments }
: {}),
};
}, [pendingAutoSubmit, pendingCreateAttempt]);
const allowsEmptyAutoSubmit = pendingAutoSubmit?.allowEmptyText === true;
const isCompact = useIsCompactFormFactor();
const workspaceAttachmentScopeKey = useWorkspaceAttachmentScopeKey({
@@ -390,9 +417,11 @@ export function WorkspaceDraftAgentTab({
optimisticStreamItems,
draftAgent,
handleCreateFromInput,
continueCreateFromAttempt,
} = useDraftAgentCreateFlow<Agent, AgentSnapshotPayload>({
draftId,
getPendingServerId: () => serverId,
initialAttempt: initialCreateAttempt,
allowEmptyText: allowsEmptyAutoSubmit,
validateBeforeSubmit: ({ text }) =>
validateDraftSubmission({
@@ -442,7 +471,6 @@ export function WorkspaceDraftAgentTab({
draftInput.isHydrated &&
draftWorkingDirectory &&
client &&
!isSubmitting &&
!composerState.isModelLoading,
);
const autoSubmitKeyRef = useRef<string | null>(null);
@@ -461,19 +489,31 @@ export function WorkspaceDraftAgentTab({
autoSubmitKeyRef.current = submitKey;
setDraftText("");
setDraftAttachments([]);
void handleCreateFromInput({
text: submission.text,
attachments: submission.attachments,
cwd: submission.cwd,
}).catch(() => {
const preparedAttempt =
initialCreateAttempt?.clientMessageId === submission.clientMessageId
? initialCreateAttempt
: null;
const createPromise = preparedAttempt
? continueCreateFromAttempt({
attempt: preparedAttempt,
cwd: submission.cwd,
})
: handleCreateFromInput({
text: submission.text,
attachments: submission.attachments,
cwd: submission.cwd,
});
void createPromise.catch(() => {
setDraftText(submission.text);
setDraftAttachments(composerWorkspaceAttachment.userAttachmentsOnly(submission.attachments));
autoSubmitKeyRef.current = null;
});
}, [
continueCreateFromAttempt,
consumePendingAutoSubmit,
draftId,
handleCreateFromInput,
initialCreateAttempt,
isReadyForPendingAutoSubmit,
serverId,
setDraftAttachments,
@@ -10,6 +10,8 @@ export interface PendingWorkspaceDraftSubmission {
attachments: ComposerAttachment[];
cwd: string;
provider: AgentProvider;
clientMessageId: string;
timestamp: number;
modeId?: string;
model?: string;
thinkingOptionId?: string;
@@ -1772,13 +1772,13 @@ export function reconcileWorkspaceTabs(
continue;
}
if (group.tabs.some((tab) => tab.tabId === originalFocusedTabId)) {
reconciledFocusedTabId = canonicalTabId;
reconciledFocusedTabId = keeper.tabId;
}
if (keeper.tabId !== canonicalTabId || !workspaceTabTargetsEqual(keeper.target, group.target)) {
if (!workspaceTabTargetsEqual(keeper.target, group.target)) {
nextLayout = withNormalizedParentTabMap({
root: replaceTabInTree(asInternalLayout(nextLayout).root, {
tabId: keeper.tabId,
nextTabId: canonicalTabId,
nextTabId: keeper.tabId,
target: group.target,
}),
focusedPaneId: nextLayout.focusedPaneId,
@@ -1354,6 +1354,56 @@ describe("workspace-layout-store actions", () => {
expect(findPaneById(layout.root, "main")?.focusedTabId).toBe("agent_agent-1");
});
it("reconcileTabs preserves a draft-origin agent tab id when there is no duplicate", () => {
const workspaceKey = createWorkspaceKey();
workspaceLayoutStore.setState((state) => ({
...state,
layoutByWorkspace: {
...state.layoutByWorkspace,
[workspaceKey]: {
root: {
kind: "pane",
pane: {
id: "main",
tabIds: ["draft-agent"],
focusedTabId: "draft-agent",
tabs: [
{
tabId: "draft-agent",
target: { kind: "agent", agentId: "agent-1" },
createdAt: 1,
},
],
} as SplitPane,
},
focusedPaneId: "main",
},
},
}));
workspaceLayoutStore.getState().reconcileTabs(workspaceKey, {
agentsHydrated: true,
terminalsHydrated: true,
activeAgentIds: ["agent-1"],
autoOpenAgentIds: ["agent-1"],
knownAgentIds: ["agent-1"],
standaloneTerminalIds: [],
hasActivePendingDraftCreate: false,
});
const layout = workspaceLayoutStore.getState().layoutByWorkspace[workspaceKey];
expect(collectAllTabs(layout.root)).toEqual([
{
tabId: "draft-agent",
target: { kind: "agent", agentId: "agent-1" },
createdAt: 1,
},
]);
expect(findPaneById(layout.root, "main")?.focusedTabId).toBe("draft-agent");
});
it("reconcileTabs does not re-add locally hidden agent tabs", () => {
const workspaceKey = createWorkspaceKey();