diff --git a/packages/app/e2e/helpers/new-workspace.ts b/packages/app/e2e/helpers/new-workspace.ts index 24fce38ea..a1530dc22 100644 --- a/packages/app/e2e/helpers/new-workspace.ts +++ b/packages/app/e2e/helpers/new-workspace.ts @@ -238,7 +238,13 @@ export async function expectComposerGithubAttachmentPill( export async function assertNewWorkspaceSidebarAndHeader( page: Page, - input: { serverId: string; previousWorkspaceId: string; projectDisplayName: string }, + input: { + serverId: string; + previousWorkspaceId: string; + projectDisplayName: string; + assertSidebarRow?: boolean; + assertHeader?: boolean; + }, ): Promise<{ workspaceId: string }> { // Wait for URL to redirect to the newly created workspace. // Uses URL as source of truth to avoid picking up sidebar rows from concurrent tests. @@ -256,15 +262,19 @@ export async function assertNewWorkspaceSidebarAndHeader( throw new Error(`Expected URL to redirect to a new workspace.\nCurrent URL: ${page.url()}`); } - const createdWorkspaceRow = page.getByTestId( - `sidebar-workspace-row-${input.serverId}:${workspaceId}`, - ); - await expect(createdWorkspaceRow.first()).toBeVisible({ timeout: 30_000 }); + if (input.assertSidebarRow !== false) { + const createdWorkspaceRow = page.getByTestId( + `sidebar-workspace-row-${input.serverId}:${workspaceId}`, + ); + await expect(createdWorkspaceRow.first()).toBeVisible({ timeout: 30_000 }); + } - await expectWorkspaceHeader(page, { - title: workspaceLabelFromPath(workspaceId), - subtitle: input.projectDisplayName, - }); + if (input.assertHeader !== false) { + await expectWorkspaceHeader(page, { + title: workspaceLabelFromPath(workspaceId), + subtitle: input.projectDisplayName, + }); + } return { workspaceId }; } diff --git a/packages/app/e2e/new-workspace.spec.ts b/packages/app/e2e/new-workspace.spec.ts index f5388a3be..0eccfc232 100644 --- a/packages/app/e2e/new-workspace.spec.ts +++ b/packages/app/e2e/new-workspace.spec.ts @@ -38,6 +38,147 @@ import { workspaceLabelFromPath, } from "./helpers/workspace-ui"; +interface WorkspaceStatusGroupEvent { + rowTestId: string; + bucket: string; + indicatorTestId: string | null; + label: string; + at: number; +} + +async function switchSidebarToStatusGrouping(page: import("@playwright/test").Page) { + await page.getByTestId("sidebar-grouping-selector").click(); + await page.getByTestId("sidebar-grouping-status").click(); + await expect(page.getByTestId("sidebar-status-group-done")).toBeVisible({ timeout: 30_000 }); +} + +async function startTrackingSidebarStatusGroups(page: import("@playwright/test").Page) { + await page.evaluate(() => { + interface StatusGroupEvent { + rowTestId: string; + bucket: string; + indicatorTestId: string | null; + label: string; + at: number; + } + const win = window as typeof window & { + __workspaceStatusGroupEvents?: StatusGroupEvent[]; + __workspaceStatusGroupObserver?: MutationObserver; + }; + win.__workspaceStatusGroupEvents = []; + win.__workspaceStatusGroupObserver?.disconnect(); + + const capture = () => { + const events = win.__workspaceStatusGroupEvents; + if (!events) return; + const groups = document.querySelectorAll( + '[data-testid^="sidebar-status-group-"]', + ); + for (const group of groups) { + const groupTestId = group.getAttribute("data-testid") ?? ""; + const bucket = groupTestId.replace("sidebar-status-group-", ""); + const label = group.textContent ?? ""; + const block = group.parentElement?.parentElement; + if (!block) continue; + const rows = block.querySelectorAll('[data-testid^="sidebar-workspace-row-"]'); + for (const row of rows) { + const rowTestId = row.getAttribute("data-testid"); + if (!rowTestId) continue; + const indicatorTestId = + row + .querySelector('[data-testid^="workspace-status-indicator-"]') + ?.getAttribute("data-testid") ?? null; + const last = events.at(-1); + if ( + last?.rowTestId === rowTestId && + last.bucket === bucket && + last.indicatorTestId === indicatorTestId + ) { + continue; + } + events.push({ rowTestId, bucket, indicatorTestId, label, at: performance.now() }); + } + } + }; + + capture(); + const observer = new MutationObserver(capture); + observer.observe(document.body, { childList: true, subtree: true, characterData: true }); + win.__workspaceStatusGroupObserver = observer; + }); +} + +async function getTrackedSidebarStatusGroups( + page: import("@playwright/test").Page, +): Promise { + return page.evaluate(() => { + const win = window as typeof window & { + __workspaceStatusGroupEvents?: WorkspaceStatusGroupEvent[]; + }; + return win.__workspaceStatusGroupEvents ?? []; + }); +} + +async function waitForWorkspaceStatusGroupEvent(input: { + page: import("@playwright/test").Page; + rowTestId: string; + bucket: string; +}) { + await input.page.waitForFunction( + ({ expectedRowTestId, expectedBucket }) => { + const win = window as typeof window & { + __workspaceStatusGroupEvents?: WorkspaceStatusGroupEvent[]; + }; + for (const event of win.__workspaceStatusGroupEvents ?? []) { + if (event.rowTestId === expectedRowTestId && event.bucket === expectedBucket) { + return true; + } + } + return false; + }, + { expectedRowTestId: input.rowTestId, expectedBucket: input.bucket }, + { timeout: 30_000 }, + ); +} + +async function expectWorkspaceStatusGroupEvents(input: { + page: import("@playwright/test").Page; + rowTestId: string; + includes: string; + excludes: string; + includesIndicator?: string; + excludesIndicator?: string; +}) { + await waitForWorkspaceStatusGroupEvent({ + page: input.page, + rowTestId: input.rowTestId, + bucket: input.includes, + }); + const createdWorkspaceEvents = (await getTrackedSidebarStatusGroups(input.page)).filter( + (event) => event.rowTestId === input.rowTestId, + ); + expect(createdWorkspaceEvents.map((event) => event.bucket)).toContain(input.includes); + expect(createdWorkspaceEvents.filter((event) => event.bucket === input.excludes)).toEqual([]); + if (input.includesIndicator) { + expect(createdWorkspaceEvents.map((event) => event.indicatorTestId)).toContain( + input.includesIndicator, + ); + } + if (input.excludesIndicator) { + expect( + createdWorkspaceEvents.filter((event) => event.indicatorTestId === input.excludesIndicator), + ).toEqual([]); + } +} + +async function submitNewWorkspaceWithoutPrompt(page: import("@playwright/test").Page) { + const createButton = page + .getByTestId("message-input-root") + .getByRole("button", { name: "Create" }); + await expect(createButton).toBeVisible({ timeout: 30_000 }); + await createButton.click(); +} + test.describe("New workspace flow", () => { let client: Awaited>; const localWorkspaceIds = new Set(); @@ -227,6 +368,8 @@ test.describe("New workspace flow", () => { serverId, previousWorkspaceId: openedProject.workspaceId, projectDisplayName: openedProject.projectDisplayName, + assertSidebarRow: false, + assertHeader: false, }); createdWorktreeIds.add(createdWorkspace.workspaceId); @@ -313,6 +456,8 @@ test.describe("New workspace flow", () => { serverId, previousWorkspaceId: openedProject.workspaceId, projectDisplayName: openedProject.projectDisplayName, + assertSidebarRow: false, + assertHeader: false, }); createdWorktreeIds.add(createdWorkspace.workspaceId); @@ -344,6 +489,117 @@ test.describe("New workspace flow", () => { } }); + test("new workspace with initial agent never appears in the Done status group", async ({ + page, + }) => { + const serverId = getServerId(); + + const tempRepo = await createTempGitRepo("new-workspace-status-optimistic-"); + + try { + const openedProject = await openProjectViaDaemon(client, tempRepo.path); + localWorkspaceIds.add(openedProject.workspaceId); + + await gotoAppShell(page); + await waitForSidebarHydration(page); + + await switchWorkspaceViaSidebar({ + page, + serverId, + targetWorkspacePath: openedProject.workspaceId, + }); + await expectWorkspaceHeader(page, { + title: openedProject.workspaceName, + subtitle: openedProject.projectDisplayName, + }); + + await switchSidebarToStatusGrouping(page); + await startTrackingSidebarStatusGroups(page); + + await openGlobalNewWorkspaceComposer(page); + await expectNewWorkspaceProjectSelected(page, openedProject.projectDisplayName); + await submitNewWorkspacePrompt(page); + + const createdWorkspace = await assertNewWorkspaceSidebarAndHeader(page, { + serverId, + previousWorkspaceId: openedProject.workspaceId, + projectDisplayName: openedProject.projectDisplayName, + assertSidebarRow: false, + assertHeader: false, + }); + createdWorktreeIds.add(createdWorkspace.workspaceId); + + const rowTestId = `sidebar-workspace-row-${serverId}:${createdWorkspace.workspaceId}`; + await expectWorkspaceStatusGroupEvents({ + page, + rowTestId, + includes: "running", + excludes: "done", + includesIndicator: "workspace-status-indicator-running", + }); + } finally { + await tempRepo.cleanup(); + } + }); + + test("new workspace without an initial agent appears in the Done status group", async ({ + page, + }) => { + const serverId = getServerId(); + + const tempRepo = await createTempGitRepo("new-workspace-status-empty-"); + + try { + const openedProject = await openProjectViaDaemon(client, tempRepo.path); + localWorkspaceIds.add(openedProject.workspaceId); + + await gotoAppShell(page); + await waitForSidebarHydration(page); + + await switchWorkspaceViaSidebar({ + page, + serverId, + targetWorkspacePath: openedProject.workspaceId, + }); + await expectWorkspaceHeader(page, { + title: openedProject.workspaceName, + subtitle: openedProject.projectDisplayName, + }); + + await switchSidebarToStatusGrouping(page); + await startTrackingSidebarStatusGroups(page); + + await openGlobalNewWorkspaceComposer(page); + await expectNewWorkspaceProjectSelected(page, openedProject.projectDisplayName); + await submitNewWorkspaceWithoutPrompt(page); + + const createdWorkspace = await assertNewWorkspaceSidebarAndHeader(page, { + serverId, + previousWorkspaceId: openedProject.workspaceId, + projectDisplayName: openedProject.projectDisplayName, + }); + createdWorktreeIds.add(createdWorkspace.workspaceId); + + const rowTestId = `sidebar-workspace-row-${serverId}:${createdWorkspace.workspaceId}`; + await expectWorkspaceStatusGroupEvents({ + page, + rowTestId, + includes: "done", + excludes: "running", + excludesIndicator: "workspace-status-indicator-loading", + }); + await expectWorkspaceStatusGroupEvents({ + page, + rowTestId, + includes: "done", + excludes: "running", + excludesIndicator: "workspace-status-indicator-running", + }); + } finally { + await tempRepo.cleanup(); + } + }); + test("selected branch becomes the base of a new workspace worktree", async ({ page }) => { const serverId = getServerId(); diff --git a/packages/app/src/components/sidebar-workspace-list.tsx b/packages/app/src/components/sidebar-workspace-list.tsx index 7fa9429cf..97dabea00 100644 --- a/packages/app/src/components/sidebar-workspace-list.tsx +++ b/packages/app/src/components/sidebar-workspace-list.tsx @@ -66,7 +66,7 @@ import { parseHostWorkspaceRouteFromPathname, } from "@/utils/host-routes"; import { - createSidebarWorkspaceEntry, + useSidebarWorkspaceEntry, type SidebarProjectEntry, type SidebarWorkspaceEntry, } from "@/hooks/use-sidebar-workspaces-list"; @@ -105,8 +105,8 @@ import { SidebarWorkspaceTrailingActionSlot, } from "@/components/sidebar/sidebar-workspace-row-content"; import { - useHydratedWorkspaceEntries, useProjectNamesMap, + useStatusModeWorkspaceEntries, } from "@/hooks/use-status-mode-workspaces"; import { Button } from "@/components/ui/button"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; @@ -117,8 +117,7 @@ import { useKeyboardActionHandler } from "@/hooks/use-keyboard-action-handler"; import { useClearWorkspaceAttention } from "@/hooks/use-clear-workspace-attention"; import type { PrHint } from "@/git/use-pr-status-query"; import { buildSidebarProjectRowModel } from "@/utils/sidebar-project-row-model"; -import { useSessionStore, type WorkspaceDescriptor } from "@/stores/session-store"; -import { useWorkspaceFields } from "@/stores/session-store-hooks"; +import { useSessionStore } from "@/stores/session-store"; import { redirectIfArchivingActiveWorkspace } from "@/utils/sidebar-workspace-archive-redirect"; import { openExternalUrl } from "@/utils/open-external-url"; import { @@ -293,19 +292,6 @@ function getWorkspaceArchiveStatus( return "idle"; } -export function useSidebarWorkspaceEntry( - serverId: string | null, - workspaceId: string | null, -): SidebarWorkspaceEntry | null { - const projectWorkspaceEntry = useCallback( - (workspace: WorkspaceDescriptor): SidebarWorkspaceEntry => - createSidebarWorkspaceEntry({ serverId: serverId ?? "", workspace }), - [serverId], - ); - - return useWorkspaceFields(serverId, workspaceId, projectWorkspaceEntry); -} - export function PrBadge({ hint }: { hint: PrHint }) { const [isHovered, setIsHovered] = useState(false); @@ -2382,6 +2368,7 @@ export function SidebarWorkspaceList({ return ( @@ -2406,14 +2393,16 @@ export function SidebarWorkspaceList({ function SidebarStatusModeWrapper({ serverId, + projects, shortcutIndexByWorkspaceKey: _projectShortcutIndex, onWorkspacePress, }: { serverId: string | null; + projects: SidebarProjectEntry[]; shortcutIndexByWorkspaceKey: Map; onWorkspacePress?: () => void; }) { - const hydratedWorkspaces = useHydratedWorkspaceEntries(serverId); + const hydratedWorkspaces = useStatusModeWorkspaceEntries({ serverId, projects }); const projectNamesByKey = useProjectNamesMap(serverId); const showShortcutBadges = useShowShortcutBadges(); diff --git a/packages/app/src/components/sidebar/sidebar-status-list.tsx b/packages/app/src/components/sidebar/sidebar-status-list.tsx index 3ebb514a6..dc092304f 100644 --- a/packages/app/src/components/sidebar/sidebar-status-list.tsx +++ b/packages/app/src/components/sidebar/sidebar-status-list.tsx @@ -26,7 +26,7 @@ import { Pencil, } from "lucide-react-native"; import { DiffStat } from "@/components/diff-stat"; -import { useSidebarWorkspaceEntry } from "../sidebar-workspace-list"; +import { useSidebarWorkspaceEntry } from "@/hooks/use-sidebar-workspaces-list"; import { DropdownMenu, DropdownMenuTrigger, diff --git a/packages/app/src/components/sidebar/sidebar-workspace-row-content.tsx b/packages/app/src/components/sidebar/sidebar-workspace-row-content.tsx index 5d06b740e..facd12af3 100644 --- a/packages/app/src/components/sidebar/sidebar-workspace-row-content.tsx +++ b/packages/app/src/components/sidebar/sidebar-workspace-row-content.tsx @@ -186,7 +186,7 @@ function WorkspaceStatusIndicator({ if (loading) { return ( - + ); @@ -194,7 +194,7 @@ function WorkspaceStatusIndicator({ if (shouldShowSyncedLoader) { return ( - + ); diff --git a/packages/app/src/components/workspace-shortcut-targets-subscriber.tsx b/packages/app/src/components/workspace-shortcut-targets-subscriber.tsx index f5762cc42..935dbe57c 100644 --- a/packages/app/src/components/workspace-shortcut-targets-subscriber.tsx +++ b/packages/app/src/components/workspace-shortcut-targets-subscriber.tsx @@ -1,7 +1,7 @@ import { useEffect, useMemo } from "react"; import { - useHydratedWorkspaceEntries, useProjectNamesMap, + useStatusModeWorkspaceEntries, } from "@/hooks/use-status-mode-workspaces"; import { useSidebarWorkspacesList } from "@/hooks/use-sidebar-workspaces-list"; import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store"; @@ -20,7 +20,10 @@ export function WorkspaceShortcutTargetsSubscriber({ serverId: string | null; }) { const { projects } = useSidebarWorkspacesList({ serverId, enabled }); - const statusWorkspaces = useHydratedWorkspaceEntries(enabled ? serverId : null); + const statusWorkspaces = useStatusModeWorkspaceEntries({ + serverId: enabled ? serverId : null, + projects, + }); const projectNamesByKey = useProjectNamesMap(enabled ? serverId : null); const groupMode = useSidebarViewStore((state) => enabled && serverId ? state.getGroupMode(serverId) : "project", diff --git a/packages/app/src/hooks/use-sidebar-workspaces-list.ts b/packages/app/src/hooks/use-sidebar-workspaces-list.ts index 43b1274fb..25be332ef 100644 --- a/packages/app/src/hooks/use-sidebar-workspaces-list.ts +++ b/packages/app/src/hooks/use-sidebar-workspaces-list.ts @@ -1,5 +1,9 @@ import { useCallback, useEffect, useMemo, useSyncExternalStore } from "react"; -import { useSessionStore, type WorkspaceDescriptor } from "@/stores/session-store"; +import { useCreateFlowStore, type PendingCreateAttempt } from "@/stores/create-flow-store"; +import { useSessionStore, type Agent, type WorkspaceDescriptor } from "@/stores/session-store"; +import { useWorkspaceFields } from "@/stores/session-store-hooks"; +import { deriveSidebarStateBucket } from "@/utils/sidebar-agent-state"; +import { normalizeWorkspacePath } from "@/utils/workspace-identity"; import { selectPrHintFromStatus } from "@/git/use-pr-status-query"; import { useHostProjects } from "@/projects/host-projects"; import { fetchAllWorkspaceDescriptors } from "@/projects/workspace-fetching"; @@ -31,7 +35,10 @@ export { export function createSidebarWorkspaceEntry(input: { serverId: string; workspace: WorkspaceDescriptor; + pendingCreateAttempts?: Record; + agents?: Map; }): SidebarWorkspaceEntry { + const effectiveStatus = deriveEffectiveWorkspaceStatus(input); return { workspaceKey: `${input.serverId}:${input.workspace.id}`, serverId: input.serverId, @@ -42,8 +49,8 @@ export function createSidebarWorkspaceEntry(input: { projectKind: input.workspace.projectKind, workspaceKind: input.workspace.workspaceKind, name: input.workspace.name, - statusBucket: input.workspace.status, - statusEnteredAt: input.workspace.statusEnteredAt, + statusBucket: effectiveStatus.status, + statusEnteredAt: effectiveStatus.enteredAt, archivingAt: input.workspace.archivingAt, diffStat: input.workspace.diffStat, prHint: selectPrHintFromStatus(input.workspace.githubRuntime?.pullRequest), @@ -54,6 +61,110 @@ export function createSidebarWorkspaceEntry(input: { }; } +interface EffectiveWorkspaceStatus { + status: WorkspaceDescriptor["status"]; + enteredAt: Date | null; +} + +interface WorkspaceAgentActivity extends EffectiveWorkspaceStatus {} + +function deriveEffectiveWorkspaceStatus(input: { + serverId: string; + workspace: WorkspaceDescriptor; + pendingCreateAttempts?: Record; + agents?: Map; +}): EffectiveWorkspaceStatus { + if (input.workspace.status !== "done") { + return { status: input.workspace.status, enteredAt: input.workspace.statusEnteredAt }; + } + + const pendingStartedAt = getPendingInitialAgentCreateStartedAt({ + serverId: input.serverId, + workspaceId: input.workspace.id, + pendingCreateAttempts: input.pendingCreateAttempts, + }); + if (pendingStartedAt) { + return { status: "running", enteredAt: pendingStartedAt }; + } + + const rootAgentActivity = getRootAgentWorkspaceActivity({ + workspace: input.workspace, + agents: input.agents, + }); + if (rootAgentActivity && rootAgentActivity.status !== "done") { + return rootAgentActivity; + } + + return { status: input.workspace.status, enteredAt: input.workspace.statusEnteredAt }; +} + +function getPendingInitialAgentCreateStartedAt(input: { + serverId: string; + workspaceId: string; + pendingCreateAttempts: Record | undefined; +}): Date | null { + let latestStartedAt: Date | null = null; + for (const pending of Object.values(input.pendingCreateAttempts ?? {})) { + if (pending.serverId !== input.serverId) continue; + if (pending.workspaceId !== input.workspaceId) continue; + if (pending.lifecycle === "abandoned") continue; + const startedAt = new Date(pending.timestamp); + if (!latestStartedAt || startedAt > latestStartedAt) { + latestStartedAt = startedAt; + } + } + return latestStartedAt; +} + +function getRootAgentWorkspaceActivity(input: { + workspace: WorkspaceDescriptor; + agents: Map | undefined; +}): WorkspaceAgentActivity | null { + const workspaceDirectory = normalizeWorkspacePath(input.workspace.workspaceDirectory); + if (!workspaceDirectory) { + return null; + } + + let latest: WorkspaceAgentActivity | null = null; + for (const agent of input.agents?.values() ?? []) { + if (agent.archivedAt || agent.parentAgentId) continue; + if (normalizeWorkspacePath(agent.cwd) !== workspaceDirectory) continue; + const status = deriveSidebarStateBucket({ + status: agent.status, + pendingPermissionCount: agent.pendingPermissions.length, + requiresAttention: agent.requiresAttention, + attentionReason: agent.attentionReason, + }); + const enteredAt = agent.attentionTimestamp ?? agent.updatedAt; + if (!latest || enteredAt > (latest.enteredAt ?? new Date(0))) { + latest = { status, enteredAt }; + } + } + return latest; +} + +export function useSidebarWorkspaceEntry( + serverId: string | null, + workspaceId: string | null, +): SidebarWorkspaceEntry | null { + const pendingCreateAttempts = useCreateFlowStore((state) => state.pendingByDraftId); + const agents = useSessionStore((state) => + serverId ? state.sessions[serverId]?.agents : undefined, + ); + const projectWorkspaceEntry = useCallback( + (workspace: WorkspaceDescriptor): SidebarWorkspaceEntry => + createSidebarWorkspaceEntry({ + serverId: serverId ?? "", + workspace, + pendingCreateAttempts, + agents, + }), + [agents, pendingCreateAttempts, serverId], + ); + + return useWorkspaceFields(serverId, workspaceId, projectWorkspaceEntry); +} + const EMPTY_ORDER: string[] = []; const EMPTY_PROJECTS: SidebarProjectEntry[] = []; diff --git a/packages/app/src/hooks/use-status-mode-workspaces.ts b/packages/app/src/hooks/use-status-mode-workspaces.ts index 55cb1d1ab..5aaf51294 100644 --- a/packages/app/src/hooks/use-status-mode-workspaces.ts +++ b/packages/app/src/hooks/use-status-mode-workspaces.ts @@ -1,21 +1,47 @@ import { useMemo } from "react"; -import { useSessionStore, type WorkspaceDescriptor } from "@/stores/session-store"; +import { useCreateFlowStore } from "@/stores/create-flow-store"; +import { useSessionStore } from "@/stores/session-store"; import { createSidebarWorkspaceEntry, + type SidebarProjectEntry, type SidebarWorkspaceEntry, } from "./use-sidebar-workspaces-list"; -export function useHydratedWorkspaceEntries(serverId: string | null): SidebarWorkspaceEntry[] { +const EMPTY_WORKSPACES: SidebarWorkspaceEntry[] = []; + +export function useStatusModeWorkspaceEntries(input: { + serverId: string | null; + projects: SidebarProjectEntry[]; +}): SidebarWorkspaceEntry[] { const workspaces = useSessionStore((state) => - serverId ? state.sessions[serverId]?.workspaces : undefined, + input.serverId ? state.sessions[input.serverId]?.workspaces : undefined, ); + const agents = useSessionStore((state) => + input.serverId ? state.sessions[input.serverId]?.agents : undefined, + ); + const pendingCreateAttempts = useCreateFlowStore((state) => state.pendingByDraftId); return useMemo(() => { - if (!serverId || !workspaces || workspaces.size === 0) return []; - return Array.from(workspaces.values()).map((workspace: WorkspaceDescriptor) => - createSidebarWorkspaceEntry({ serverId, workspace }), - ); - }, [serverId, workspaces]); + if (!input.serverId || input.projects.length === 0 || !workspaces) { + return EMPTY_WORKSPACES; + } + + const entries: SidebarWorkspaceEntry[] = []; + for (const placedWorkspace of input.projects.flatMap((project) => project.workspaces)) { + const workspace = workspaces.get(placedWorkspace.workspaceId); + entries.push( + workspace + ? createSidebarWorkspaceEntry({ + serverId: input.serverId, + workspace, + pendingCreateAttempts, + agents, + }) + : placedWorkspace, + ); + } + return entries; + }, [agents, input.projects, input.serverId, pendingCreateAttempts, workspaces]); } export function useProjectNamesMap(serverId: string | null): Map { diff --git a/packages/app/src/screens/new-workspace-screen.tsx b/packages/app/src/screens/new-workspace-screen.tsx index dc28c2823..0c99b729a 100644 --- a/packages/app/src/screens/new-workspace-screen.tsx +++ b/packages/app/src/screens/new-workspace-screen.tsx @@ -591,7 +591,10 @@ async function createAndMergeWorkspace(input: { throw new Error(payload.error ?? "Failed to create worktree"); } const normalizedWorkspace = normalizeWorkspaceDescriptor(payload.workspace); - input.mergeWorkspaces(input.serverId, [normalizedWorkspace]); + const workspaceForInitialMerge = input.createInput.firstAgentContext + ? { ...normalizedWorkspace, status: "running" as const, statusEnteredAt: new Date() } + : normalizedWorkspace; + input.mergeWorkspaces(input.serverId, [workspaceForInitialMerge]); return normalizedWorkspace; } @@ -726,6 +729,7 @@ function submitWorkspaceDraft(input: SubmitDraftInput): void { useCreateFlowStore.getState().setPending({ serverId, draftId, + workspaceId, agentId: null, clientMessageId, text: text.trim(), diff --git a/packages/app/src/stores/create-flow-store.ts b/packages/app/src/stores/create-flow-store.ts index 296791553..5f4cfea5d 100644 --- a/packages/app/src/stores/create-flow-store.ts +++ b/packages/app/src/stores/create-flow-store.ts @@ -7,6 +7,7 @@ export type CreateFlowLifecycleState = "active" | "abandoned" | "sent"; export interface PendingCreateAttempt { draftId: string; serverId: string; + workspaceId?: string; agentId: string | null; clientMessageId: string; text: string;