diff --git a/.changeset/workspace-dashboard-floor.md b/.changeset/workspace-dashboard-floor.md
new file mode 100644
index 0000000000..47db598dea
--- /dev/null
+++ b/.changeset/workspace-dashboard-floor.md
@@ -0,0 +1,8 @@
+---
+"@runfusion/fusion": minor
+---
+
+Workspace tasks no longer render blank in the dashboard. Task cards and the task
+detail view now surface a workspace task's acquired per-sub-repo worktrees as a
+read-only "N repos acquired" placeholder and flat repo → worktree/branch list,
+instead of an empty branch area (no `task.worktree`/`task.branch`).
diff --git a/CONCEPTS.md b/CONCEPTS.md
index 1fccc91371..e80ef40df8 100644
--- a/CONCEPTS.md
+++ b/CONCEPTS.md
@@ -61,6 +61,10 @@ sub-directories. Fusion discovers sub-repos at init time and records them in
single root-level worktree; instead, the agent acquires per-repo worktrees
on demand via `fn_acquire_repo_worktree`.
+Workspace-task merges are **non-atomic**: each sub-repo lands on its own local
+integration ref independently, so a partial-land window (some sub-repos merged,
+others not) is possible mid-task — this state is local and operator-resettable.
+
### Project Identity
The durable identity a registered Project carries locally so it can be reattached to the central registry after central state is lost or rebuilt, preserving rows keyed by the same project id instead of minting a replacement.
diff --git a/packages/dashboard/app/components/TaskCard.tsx b/packages/dashboard/app/components/TaskCard.tsx
index 3e5898ec52..46748eb497 100644
--- a/packages/dashboard/app/components/TaskCard.tsx
+++ b/packages/dashboard/app/components/TaskCard.tsx
@@ -35,6 +35,7 @@ import { extractDependencyDeleteConflict, extractLineageDeleteConflict } from ".
import { MAX_AUTO_MERGE_RETRIES, type BlockerFanoutEntry } from "../hooks/useBlockerFanout";
import { useRetryWarning } from "../context/RetryWarningContext";
import { useColumnLabel } from "../i18n/labels";
+import { WorkspaceWorktreesSummary, isWorkspaceTask } from "./WorkspaceWorktreesSummary";
/** Per-branch progress snapshot (U13). Surfaced as an optional additive field
* on the task payload for the parallel-window badge (U9). */
@@ -625,6 +626,10 @@ function areTaskCardPropsEqual(previous: TaskCardProps, next: TaskCardProps): bo
previousTask.blockedBy === nextTask.blockedBy &&
previousTask.overlapBlockedBy === nextTask.overlapBlockedBy &&
previousTask.worktree === nextTask.worktree &&
+ // FNXC:Workspace 2026-06-21-00:00: re-render the card when a workspace task acquires/
+ // releases sub-repo worktrees so the "N repos acquired" placeholder stays current (U3).
+ Object.keys(previousTask.workspaceWorktrees ?? {}).length ===
+ Object.keys(nextTask.workspaceWorktrees ?? {}).length &&
previousTask.branch === nextTask.branch &&
previousTask.baseBranch === nextTask.baseBranch &&
previousTask.breakIntoSubtasks === nextTask.breakIntoSubtasks &&
@@ -2186,6 +2191,10 @@ function TaskCardComponent({
);
})()}
+ {/* FNXC:Workspace 2026-06-21-00:00: workspace tasks have no singular task.branch,
+ so the branch-metadata row below renders nothing. Surface the acquired sub-repos
+ as a compact "N repos acquired" placeholder so the card isn't blank (U3/KTD5). */}
+ {isWorkspaceTask(task) && }
{hasBranchMetadata && (
{branchMetadata.branch && (
diff --git a/packages/dashboard/app/components/TaskDetailModal.css b/packages/dashboard/app/components/TaskDetailModal.css
index 18c1cf0968..2b372606da 100644
--- a/packages/dashboard/app/components/TaskDetailModal.css
+++ b/packages/dashboard/app/components/TaskDetailModal.css
@@ -2209,3 +2209,39 @@ FN-6500 fixes a tablet regression from FN-5599: the task-detail overlay offset a
color: var(--color-error);
font-size: 0.75rem;
}
+
+/*
+FNXC:Workspace 2026-06-21-00:00:
+Flat read-only per-sub-repo worktree list for a workspace task (U3/KTD5 dashboard floor).
+Read-only list/placeholder only — not the deferred rich per-repo-status component.
+*/
+.workspace-worktrees-summary {
+ margin: var(--space-sm) 0 0;
+}
+.workspace-worktrees-placeholder {
+ font-size: 0.75rem;
+ font-weight: 600;
+ color: var(--color-text-secondary, inherit);
+ margin-bottom: var(--space-xs);
+}
+.workspace-worktrees-list {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-xs);
+}
+.workspace-worktrees-item {
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--space-xs) var(--space-sm);
+ font-size: 0.75rem;
+ font-family: var(--font-mono, monospace);
+}
+.workspace-worktrees-repo {
+ font-weight: 600;
+}
+.workspace-worktrees-branch {
+ color: var(--color-text-secondary, inherit);
+}
diff --git a/packages/dashboard/app/components/TaskDetailModal.tsx b/packages/dashboard/app/components/TaskDetailModal.tsx
index 90b7546338..6432ed838f 100644
--- a/packages/dashboard/app/components/TaskDetailModal.tsx
+++ b/packages/dashboard/app/components/TaskDetailModal.tsx
@@ -39,6 +39,7 @@ import { TaskChatTab } from "./TaskChatTab";
import { TaskReviewTab } from "./TaskReviewTab";
import { MergeDetails } from "./MergeDetails";
import { TaskChangesTab } from "./TaskChangesTab";
+import { WorkspaceWorktreesSummary, isWorkspaceTask } from "./WorkspaceWorktreesSummary";
import { TaskForm, type PendingImage } from "./TaskForm";
import { useNodes } from "../hooks/useNodes";
import { WorkflowResultsTab } from "./WorkflowResultsTab";
@@ -3065,6 +3066,10 @@ export function TaskDetailContent({
{task.branchContext?.groupId && (
)}
+ {/* FNXC:Workspace 2026-06-21-00:00: workspace tasks have no singular
+ task.worktree/task.branch; surface their acquired per-sub-repo worktrees
+ as a flat read-only list so the detail view isn't blank (U3/KTD5). */}
+ {isWorkspaceTask(task) &&
}
>
)}
{task.status === "failed" && task.error && (
diff --git a/packages/dashboard/app/components/WorkspaceWorktreesSummary.tsx b/packages/dashboard/app/components/WorkspaceWorktreesSummary.tsx
new file mode 100644
index 0000000000..90625a96eb
--- /dev/null
+++ b/packages/dashboard/app/components/WorkspaceWorktreesSummary.tsx
@@ -0,0 +1,92 @@
+import { useTranslation } from "react-i18next";
+import type { Task } from "@fusion/core";
+
+/*
+FNXC:Workspace 2026-06-21-00:00:
+Dashboard "doesn't look broken" floor (Phase A U3 / master U10, KTD5).
+A workspace-mode task has NO singular `task.worktree`/`task.branch`; instead it carries
+`task.workspaceWorktrees` — one acquired git worktree per sub-repo, keyed by repo path
+relative to the workspace root. Existing display surfaces (TaskCard branch row, TaskDetail
+metadata) key off the singular `task.branch`, so a workspace task would render an EMPTY
+branch area — looking broken. This guard renders a static placeholder ("N repos acquired")
+plus a flat read-only per-repo path/branch list so the task is observable, never crashing
+and never blank.
+
+Scope ceiling: flat read-only list / placeholder ONLY. A rich per-repo-status component
+(live diff/lease/merge state per repo) is the deferred registration UI — out of scope here.
+Single-repo rendering is untouched: callers only mount this when `isWorkspaceTask(task)`.
+*/
+
+/**
+ * True when the task is a workspace-mode task: no singular `worktree` recorded
+ * and at least one acquired per-sub-repo worktree in `workspaceWorktrees`.
+ * Single-repo tasks (populated `worktree`, no `workspaceWorktrees`) return false,
+ * keeping their existing rendering byte-for-byte unchanged.
+ */
+export function isWorkspaceTask(task: Pick
): boolean {
+ if (task.worktree) return false;
+ const entries = task.workspaceWorktrees;
+ return Boolean(entries && Object.keys(entries).length > 0);
+}
+
+interface WorkspaceWorktreesSummaryProps {
+ task: Pick;
+ /** Compact variant for the dense TaskCard surface (placeholder only). */
+ compact?: boolean;
+}
+
+/**
+ * Read-only summary of a workspace task's acquired sub-repo worktrees.
+ *
+ * - `compact` (TaskCard): renders just the "N repos acquired" placeholder chip.
+ * - default (TaskDetail): renders the placeholder plus a flat per-repo list of
+ * `repo → worktreePath (branch)`.
+ *
+ * Renders nothing for non-workspace tasks; mount only behind `isWorkspaceTask`.
+ */
+export function WorkspaceWorktreesSummary({ task, compact = false }: WorkspaceWorktreesSummaryProps) {
+ const { t } = useTranslation("app");
+ const entries = task.workspaceWorktrees;
+ if (!isWorkspaceTask(task) || !entries) return null;
+
+ const repos = Object.entries(entries);
+ const placeholder = t("tasks.workspaceReposAcquired", "{{count}} repos acquired", { count: repos.length });
+
+ if (compact) {
+ return (
+
+
+ {t("tasks.workspace", "Workspace")}
+ {placeholder}
+
+
+ );
+ }
+
+ return (
+
+
+ {placeholder}
+
+
+ {repos.map(([repoRelPath, info]) => (
+ -
+
+ {repoRelPath}
+
+
+ {info.worktreePath}
+
+
+ {info.branch}
+
+
+ ))}
+
+
+ );
+}
diff --git a/packages/dashboard/app/components/__tests__/WorkspaceWorktreesSummary.test.tsx b/packages/dashboard/app/components/__tests__/WorkspaceWorktreesSummary.test.tsx
new file mode 100644
index 0000000000..dfd23f3875
--- /dev/null
+++ b/packages/dashboard/app/components/__tests__/WorkspaceWorktreesSummary.test.tsx
@@ -0,0 +1,89 @@
+import { describe, it, expect } from "vitest";
+import { render, screen } from "@testing-library/react";
+import { WorkspaceWorktreesSummary, isWorkspaceTask } from "../WorkspaceWorktreesSummary";
+
+/*
+FNXC:Workspace 2026-06-21-00:00:
+U3/KTD5 dashboard "doesn't look broken" floor. Asserts the invariant across both surfaces
+the summary serves (FN-5893):
+- happy path: workspace task (no task.worktree, two workspaceWorktrees entries) renders a
+ flat per-repo list + "N repos acquired" placeholder — no crash, not blank.
+- regression: single-repo task (task.worktree set, no workspaceWorktrees) renders nothing
+ from this guard, so its existing rendering stays unchanged.
+Narrow seam: tests the presentational component directly, no API / SSE / timers (FN-5048).
+*/
+
+const workspaceTask = {
+ worktree: undefined,
+ workspaceWorktrees: {
+ "repo-a": { worktreePath: "/wt/repo-a", branch: "fusion/fn-1-a" },
+ "repo-b": { worktreePath: "/wt/repo-b", branch: "fusion/fn-1-b" },
+ },
+} as const;
+
+const singleRepoTask = {
+ worktree: "/wt/single",
+ workspaceWorktrees: undefined,
+} as const;
+
+describe("isWorkspaceTask", () => {
+ it("is true when worktree is absent and workspaceWorktrees has entries", () => {
+ expect(isWorkspaceTask(workspaceTask)).toBe(true);
+ });
+
+ it("is false for a single-repo task (worktree set)", () => {
+ expect(isWorkspaceTask(singleRepoTask)).toBe(false);
+ });
+
+ it("is false when workspaceWorktrees is an empty record", () => {
+ expect(isWorkspaceTask({ worktree: undefined, workspaceWorktrees: {} })).toBe(false);
+ });
+
+ it("prefers the singular worktree even if workspaceWorktrees is populated", () => {
+ expect(
+ isWorkspaceTask({ worktree: "/wt/x", workspaceWorktrees: workspaceTask.workspaceWorktrees }),
+ ).toBe(false);
+ });
+});
+
+describe("WorkspaceWorktreesSummary", () => {
+ it("renders a flat per-repo list and placeholder for a two-repo workspace task (no crash, not empty)", () => {
+ render();
+
+ // Placeholder reflects the repo count.
+ expect(screen.getByTestId("workspace-worktrees-placeholder").textContent).toContain("2");
+ expect(screen.getByText(/2 repos acquired/i)).toBeTruthy();
+
+ // Flat per-repo list: each repo path, worktree path, and branch is shown.
+ const summary = screen.getByTestId("workspace-worktrees-summary");
+ expect(summary).toBeTruthy();
+ expect(screen.getByText("repo-a")).toBeTruthy();
+ expect(screen.getByText("repo-b")).toBeTruthy();
+ expect(screen.getByText("/wt/repo-a")).toBeTruthy();
+ expect(screen.getByText("/wt/repo-b")).toBeTruthy();
+ expect(screen.getByText("fusion/fn-1-a")).toBeTruthy();
+ expect(screen.getByText("fusion/fn-1-b")).toBeTruthy();
+ });
+
+ it("renders only the compact placeholder in compact mode", () => {
+ render();
+ expect(screen.getByTestId("workspace-worktrees-placeholder").textContent).toContain("2 repos");
+ // Compact variant omits the full per-repo list.
+ expect(screen.queryByTestId("workspace-worktrees-summary")).toBeNull();
+ expect(screen.queryByText("/wt/repo-a")).toBeNull();
+ });
+
+ it("renders nothing for a single-repo task, leaving existing rendering unchanged", () => {
+ const { container } = render();
+ expect(container.firstChild).toBeNull();
+ expect(screen.queryByTestId("workspace-worktrees-summary")).toBeNull();
+ expect(screen.queryByTestId("workspace-worktrees-placeholder")).toBeNull();
+ });
+
+ it("renders nothing when workspaceWorktrees is empty", () => {
+ const { container } = render(
+ ,
+ );
+ expect(container.firstChild).toBeNull();
+ });
+});