-
- {description}
-
+
+ {description}
- ) : null}
+ ) : (
+
No description.
+ )}
-
-
-
- {normalizedIssue?.cycleName && (
-
- )}
-
-
-
-
-
-
-
-
- {normalizedIssue ? (
- <>
-
-
-
-
- >
- ) : null}
-
+
{normalizedIssue?.childIssues && normalizedIssue.childIssues.length > 0 ? (
@@ -1578,15 +1641,67 @@ function IssueLabels({ issue, normalizedIssue }: { issue: BrowserIssue; normaliz
);
}
-function InfoRow({ label, value }: { label: string; value: string }) {
+function PropRow({ label, value, children }: { label: string; value?: string; children?: React.ReactNode }) {
return (
-
-
{label}
-
{value}
+
+
{label}
+
+ {children ?? {value}}
+
);
}
+function IssueProperties({
+ issue,
+ normalizedIssue,
+ branchName,
+}: {
+ issue: BrowserIssue;
+ normalizedIssue: NormalizedLinearIssue | null;
+ branchName: string | null;
+}) {
+ return (
+
+
+
+
+ {issue.stateName}
+
+
+
+
+
+ {linearPriorityLabel(issue)}
+
+
+
+
+
+ {normalizedIssue?.cycleName ? : null}
+
+ {issue.estimate != null ? : null}
+ {issue.dueDate ? : null}
+
+
+ {normalizedIssue?.startedAt ? : null}
+ {normalizedIssue?.completedAt ? : null}
+ {normalizedIssue?.canceledAt ? : null}
+ {normalizedIssue?.hasOpenBlockers ? (
+
+ ) : null}
+ {branchName ? (
+
+
+
+ {branchName}
+
+
+ ) : null}
+
+ );
+}
+
function SubIssuesList({ issues }: { issues: NonNullable
}) {
const [expanded, setExpanded] = useState(false);
return (
diff --git a/apps/desktop/src/renderer/components/app/LinearPaneModal.tsx b/apps/desktop/src/renderer/components/app/LinearPaneModal.tsx
index 4c772d6a9..dbf96c607 100644
--- a/apps/desktop/src/renderer/components/app/LinearPaneModal.tsx
+++ b/apps/desktop/src/renderer/components/app/LinearPaneModal.tsx
@@ -3,6 +3,10 @@ import { createPortal } from "react-dom";
import { CircleNotch, X } from "@phosphor-icons/react";
import type { CtoLinearQuickView } from "../../../shared/types";
+import {
+ ADE_BROWSER_VIEW_OCCLUSION_END_EVENT,
+ ADE_BROWSER_VIEW_OCCLUSION_START_EVENT,
+} from "../../lib/workSidebarBrowserResize";
import { LinearMark, LINEAR_BRAND } from "../lanes/linearBrand";
/**
@@ -34,6 +38,14 @@ export function LinearPaneModal({
}) {
const popoverRef = useRef(null);
+ useEffect(() => {
+ if (!open || typeof window === "undefined") return undefined;
+ window.dispatchEvent(new Event(ADE_BROWSER_VIEW_OCCLUSION_START_EVENT));
+ return () => {
+ window.dispatchEvent(new Event(ADE_BROWSER_VIEW_OCCLUSION_END_EVENT));
+ };
+ }, [open]);
+
useEffect(() => {
if (!open) return;
const onDown = (event: MouseEvent) => {
@@ -73,7 +85,7 @@ export function LinearPaneModal({
role="dialog"
aria-modal="true"
aria-label={ariaLabel}
- className="fixed left-1/2 top-1/2 z-[9999] flex h-[min(900px,calc(100dvh-28px))] w-[min(1380px,calc(100vw-28px))] -translate-x-1/2 -translate-y-1/2 flex-col overflow-hidden rounded-xl border bg-[color:var(--ade-shell-surface,#121019)] text-fg shadow-2xl shadow-black/50"
+ className="fixed left-1/2 top-1/2 z-[9999] flex h-[min(940px,calc(100dvh-28px))] w-[min(1760px,calc(100vw-28px))] -translate-x-1/2 -translate-y-1/2 flex-col overflow-hidden rounded-xl border bg-[color:var(--ade-shell-surface,#121019)] text-fg shadow-2xl shadow-black/50"
style={{
borderColor: "rgba(123, 138, 240, 0.55)",
boxShadow: "0 24px 70px rgba(0, 0, 0, 0.58), 0 0 0 1px rgba(123, 138, 240, 0.18)",
diff --git a/apps/desktop/src/renderer/components/app/LinearQuickViewButton.tsx b/apps/desktop/src/renderer/components/app/LinearQuickViewButton.tsx
index 1624b3346..da5894b2f 100644
--- a/apps/desktop/src/renderer/components/app/LinearQuickViewButton.tsx
+++ b/apps/desktop/src/renderer/components/app/LinearQuickViewButton.tsx
@@ -13,6 +13,10 @@ import {
subscribeLinearIssueQuickViewRequests,
type LinearIssueQuickViewRequest,
} from "../../lib/linearIssueQuickViewNavigation";
+import {
+ ADE_BROWSER_VIEW_OCCLUSION_END_EVENT,
+ ADE_BROWSER_VIEW_OCCLUSION_START_EVENT,
+} from "../../lib/workSidebarBrowserResize";
import { cn } from "../ui/cn";
import { linearIssueLaneName } from "../../../shared/linearIssueBranch";
import { LinearMark, LINEAR_BRAND } from "../lanes/linearBrand";
@@ -123,6 +127,7 @@ export function LinearQuickViewButton({
const buttonRef = useRef(null);
const popoverRef = useRef(null);
const cachedQuickViewRef = useRef(null);
+ const occludesNativeBrowser = open || batchModalOpen;
// Remembers each issue's chosen config so "Retry failed" reuses the same model.
const batchConfigByIssueRef = useRef
- )
- }
- }), [
- tree, expanded, activeWorkspace, activeTabPath, activeContextDir, openTabs, activeTab, activeTabIsText,
- activeTabIsMarkdown, activeTabUsesCodeEditor, markdownPreviewActive, markdownPreviewEnabled,
- mode, canEdit, editorStatus, laneIdForDiff,
- searchQuery, inlineRenameRequest, selectedTreeNodePath, conflictHunks, editorTheme, editorModeHint,
- resolvedConflictKeys, createFileAt, createDirectoryAt, saveActive,
- closeTab, stagePath, unstagePath, discardPath, openFile, setShowQuickOpen, navigate,
- applyConflictResolution, setEditorHostRef, workspaceComparisonRoot, toggleDirectory, loadMoreChildren, renamePathTo,
- loadingDirectories,
- embedded
- ]);
-
- const renderPane = useCallback((paneId: keyof typeof paneConfigs) => {
- const config = paneConfigs[paneId];
- const Icon = config.icon;
- return (
-
-
-
- {Icon ? : null}
-
- {config.title.toUpperCase()}
-
- {config.meta ? (
-
- {config.meta}
-
- ) : null}
-
-
- {config.headerActions}
-
-
-
- {config.children}
-
-
- );
- }, [paneConfigs, embedded]);
-
- return (
-
- {/* Header bar — full Files tab only; Work sidebar uses the active session lane via preferredLaneId */}
- {!embedded ? (
-
- {/* Numbered title group */}
-
- 03
-
- FILES
- {workspaces.length} WS
-
-
- {/* Workspace selector */}
-
-
- <>
-
-
-
- >
-
-
- {/* Read-only badge */}
- {activeWorkspace?.isReadOnlyByDefault && !allowPrimaryEdit ? (
-
- READ-ONLY
-
- ) : null}
-
- {/* Trust / edit toggle */}
- {activeWorkspace?.isReadOnlyByDefault ? (
-
-
-
- ) : null}
-
- {/* Spacer */}
-
-
- <>
-
-
-
-
- {/* Open in external editor */}
-
-
-
-
- {openInMenuOpen ? (
-
- {(
- [
- { key: "default", label: "SYSTEM DEFAULT" },
- { key: "finder", label: revealLabel.toUpperCase() },
- { key: "vscode", label: "VS CODE" },
- { key: "cursor", label: "CURSOR" },
- { key: "zed", label: "ZED" },
- ] as const
- ).map((item) => (
-
- ))}
-
- ) : null}
-
-
- {/* File count stat */}
-
- {openTabs.length} OPEN
-
- >
-
- ) : null}
-
- {/* Warning banners */}
- {!embedded && ((activeWorkspace?.isReadOnlyByDefault && !allowPrimaryEdit) || (activeWorkspace?.kind === "primary" && suggestedLaneWorkspace)) ? (
-
-
- {activeWorkspace?.isReadOnlyByDefault && !allowPrimaryEdit ? (
-
- PRIMARY WORKSPACE IS READ-ONLY. USE "TRUST & EDIT" TO UNLOCK.
-
- ) : (
-
- EDITING DIRECTLY IN PRIMARY. LANE WORKSPACES ARE SAFER.
-
- )}
- {suggestedLaneWorkspace ? (
-
- ) : null}
-
- ) : null}
-
- {/* Error banner */}
- {error ? (
-
- {error}
-
-
- ) : null}
-
- {/* Static split layout for Files. This intentionally bypasses the shared
- tiling shell while Files-route crashes are under investigation. */}
-
-
-
- {renderPane("explorer")}
-
-
-
- {renderPane("editor")}
-
-
-
-
-
- {/* Context menu overlay */}
- {contextMenu ? (
-
e.stopPropagation()}
- >
- {contextMenu.nodeType === "file" ? (
- <>
-
FILE
- {[
- { label: "OPEN", action: async () => { await openFile(contextMenu.nodePath); }, color: COLORS.textSecondary },
- { label: "OPEN DIFF", action: async () => { await openFile(contextMenu.nodePath); setMode("diff"); }, color: COLORS.info },
- ].map((item) => (
-
- ))}
- {laneIdForWorkspace ? (
- <>
-
-
GIT
-
-
-
- >
- ) : null}
- >
- ) : null}
-
-
-
FILE OPS
-
-
-
-
-
-
-
-
- ) : null}
-
- {/* Quick Open overlay */}
- {showQuickOpen ? (
-
-
-
QUICK OPEN
-
-
- setQuickOpen(e.target.value)}
- placeholder={`Type to search files... (${modifierKeyLabel}+P)`}
- style={{
- height: 36, width: "100%", padding: "0 36px 0 32px",
- fontSize: 12, fontFamily: MONO_FONT, fontWeight: 500,
- background: COLORS.recessedBg, border: `1px solid ${COLORS.accent}`,
- borderRadius: 8, color: COLORS.textPrimary, outline: "none",
- letterSpacing: "0.3px",
- }}
- onKeyDown={(e) => { if (e.key === "Escape") setShowQuickOpen(false); }}
- />
-
-
-
- {quickOpenResults.map((item) => {
- const qoFileIcon = getFileIcon(item.path.split("/").pop() ?? "");
- const QoIcon = qoFileIcon.icon;
- return (
-
- );
- })}
- {!quickOpenResults.length ?
NO MATCHES
: null}
-
-
-
- ) : null}
-
- {/* Content Search overlay */}
- {showContentSearch ? (
-
-
-
CONTENT SEARCH
-
-
- setContentSearchQuery(e.target.value)}
- placeholder={`Search file contents... (${modifierKeyLabel}+Shift+F)`}
- style={{
- height: 36, width: "100%", padding: "0 36px 0 32px",
- fontSize: 12, fontFamily: MONO_FONT, fontWeight: 500,
- background: COLORS.recessedBg, border: `1px solid ${COLORS.accent}`,
- borderRadius: 8, color: COLORS.textPrimary, outline: "none",
- letterSpacing: "0.3px",
- }}
- onKeyDown={(e) => { if (e.key === "Escape") setShowContentSearch(false); }}
- />
-
-
-
- {contentSearchResults.map((item, idx) => {
- const srIcon = getFileIcon(item.path.split("/").pop() ?? "");
- const SrIcon = srIcon.icon;
- return (
-
- );
- })}
- {!contentSearchResults.length ? (
-
- {contentSearchQuery.trim() ? "NO MATCHES" : "TYPE TO SEARCH CONTENTS"}
-
- ) : null}
-
-
-
- ) : null}
-
- {/* Text prompt modal */}
- {textPrompt ? (
-
-
- {/* Modal header */}
-
- {textPrompt.title.toUpperCase()}
-
- {/* Modal body */}
-
- {textPrompt.message ? (
-
{textPrompt.message}
- ) : null}
-
{
- const nextValue = event.target.value;
- setTextPrompt((prev) => (prev ? { ...prev, value: nextValue } : prev));
- if (textPromptError) setTextPromptError(null);
- }}
- onKeyDown={(event) => {
- if (event.key === "Escape") { event.preventDefault(); cancelTextPrompt(); return; }
- if (event.key === "Enter") { event.preventDefault(); submitTextPrompt(); }
- }}
- placeholder={textPrompt.placeholder}
- style={{
- height: 36, width: "100%", padding: "0 12px",
- fontSize: 12, fontFamily: MONO_FONT, borderRadius: 8,
- background: COLORS.recessedBg, border: `1px solid ${COLORS.outlineBorder}`,
- color: COLORS.textPrimary, outline: "none",
- }}
- onFocus={(e) => { e.currentTarget.style.borderColor = COLORS.accent; }}
- onBlur={(e) => { e.currentTarget.style.borderColor = COLORS.outlineBorder; }}
- />
- {textPromptError ? (
-
{textPromptError}
- ) : null}
-
- {/* Modal footer */}
-
-
-
-
-
-
- ) : null}
-
- );
-}
-
-function FilesDiffPanel({
- active = true,
- laneId,
- path,
- theme,
- diffViewRef,
-}: {
- active?: boolean;
- laneId: string;
- path: string;
- theme: EditorThemeMode;
- diffViewRef?: React.MutableRefObject
;
-}) {
- const [mode, setMode] = useState<"unstaged" | "staged" | "commit">("unstaged");
- const [diff, setDiff] = useState(null);
- const [patch, setPatch] = useState(null);
- const [error, setError] = useState(null);
- const [commits, setCommits] = useState([]);
- const [compareRef, setCompareRef] = useState("");
-
- useEffect(() => {
- if (!active) return;
- let cancelled = false;
- setCompareRef("");
- window.ade.git.listRecentCommits({ laneId, limit: 30 })
- .then((rows) => {
- if (cancelled) return;
- setCommits(rows);
- setCompareRef(rows[0]?.sha ?? "");
- })
- .catch(() => {
- if (cancelled) return;
- setCommits([]);
- setCompareRef("");
- });
- return () => {
- cancelled = true;
- };
- }, [active, laneId]);
-
- useEffect(() => {
- if (!active) return;
- let cancelled = false;
- setError(null);
-
- const load = async () => {
- if (mode === "commit" && !compareRef.trim()) {
- setDiff(null);
- setPatch(null);
- return;
- }
-
- const args = {
- laneId,
- path,
- mode,
- compareRef: mode === "commit" ? compareRef : undefined
- } as const;
- const [nextDiff, nextPatch] = await Promise.allSettled([
- window.ade.diff.getFile(args),
- window.ade.diff.getFilePatch(args),
- ]);
- if (cancelled) return;
- const resolvedDiff = nextDiff.status === "fulfilled" ? nextDiff.value : null;
- const resolvedPatch = nextPatch.status === "fulfilled" && filePatchHasRenderableChanges(nextPatch.value) ? nextPatch.value : null;
- const hasRenderableDiff = resolvedDiff ? fileDiffHasRenderableChanges(resolvedDiff) : false;
- if (!resolvedPatch && !hasRenderableDiff) {
- if (nextDiff.status === "rejected") throw nextDiff.reason;
- if (nextPatch.status === "rejected") throw nextPatch.reason;
- }
- setDiff(resolvedDiff);
- setPatch(resolvedPatch);
- };
-
- load().catch((err) => {
- if (cancelled) return;
- setDiff(null);
- setPatch(null);
- setError(settledErrorMessage(err));
- });
-
- return () => {
- cancelled = true;
- };
- }, [active, laneId, path, mode, compareRef]);
-
- return (
-
-
- {/* Mode toggle group */}
-
- {(["unstaged", "staged", "commit"] as const).map((m) => {
- const label = m === "unstaged" ? "WORKING TREE" : m === "staged" ? "STAGED" : "COMMIT";
- const isActive = mode === m;
- return (
-
- );
- })}
-
-
- {mode === "commit" ? (
-
- ) : null}
-
-
{path}
-
-
- {error ?
{error}
: null}
-
- {patch || (diff && fileDiffHasRenderableChanges(diff)) ? (
-
- ) : diff ? (
-
- No changes for this file.
-
- ) : null}
-
-
- );
-}
diff --git a/apps/desktop/src/renderer/components/files/FilesTab.tsx b/apps/desktop/src/renderer/components/files/FilesTab.tsx
index d8dbb1853..9f108c682 100644
--- a/apps/desktop/src/renderer/components/files/FilesTab.tsx
+++ b/apps/desktop/src/renderer/components/files/FilesTab.tsx
@@ -1,6 +1,4 @@
import React from "react";
-import { isFilesWorkbenchV2Enabled } from "./filesWorkbenchFlag";
-import { FilesPage } from "./FilesPage";
import { FilesWorkbench } from "./v2/FilesWorkbench";
export type FilesTabProps = {
@@ -9,15 +7,6 @@ export type FilesTabProps = {
active?: boolean;
};
-/**
- * Entry point for the Files tab. Renders the v2 workbench when the
- * FILES_WORKBENCH_V2 flag is enabled, otherwise the current FilesPage. Branching
- * here keeps both the route (App.tsx) and the embedded sidebar (WorkSidebar) on
- * one switch so the default experience is unchanged until the flag flips.
- */
export function FilesTab(props: FilesTabProps) {
- if (isFilesWorkbenchV2Enabled()) {
- return ;
- }
- return ;
+ return ;
}
diff --git a/apps/desktop/src/renderer/components/files/filesWorkbenchFlag.ts b/apps/desktop/src/renderer/components/files/filesWorkbenchFlag.ts
deleted file mode 100644
index 1a5196602..000000000
--- a/apps/desktop/src/renderer/components/files/filesWorkbenchFlag.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-const STORAGE_KEY = "ade.files.workbenchV2";
-
-/**
- * Whether the v2 Files workbench (VSCode-like shell) is enabled. Defaults to OFF
- * so the shipped tab is unchanged; flip it per-machine from devtools with:
- * localStorage.setItem("ade.files.workbenchV2", "1"); location.reload();
- * Phase 9 of the rollout flips the default to ON after the parity checklist passes.
- */
-export function isFilesWorkbenchV2Enabled(): boolean {
- try {
- if (typeof localStorage !== "undefined" && localStorage.getItem(STORAGE_KEY) === "1") {
- return true;
- }
- } catch {
- // localStorage unavailable (SSR/sandbox) — fall through to default
- }
- return false;
-}
-
-export function setFilesWorkbenchV2Enabled(enabled: boolean): void {
- try {
- if (enabled) localStorage.setItem(STORAGE_KEY, "1");
- else localStorage.removeItem(STORAGE_KEY);
- } catch {
- // ignore
- }
-}
diff --git a/apps/desktop/src/renderer/components/files/monacoModelRegistry.ts b/apps/desktop/src/renderer/components/files/monacoModelRegistry.ts
index 6134701c5..1fb3f5851 100644
--- a/apps/desktop/src/renderer/components/files/monacoModelRegistry.ts
+++ b/apps/desktop/src/renderer/components/files/monacoModelRegistry.ts
@@ -8,7 +8,7 @@ type Entry = {
};
/**
- * Per-FilesPage cache of Monaco text models keyed by workspace-relative path.
+ * Per-workbench cache of Monaco text models keyed by workspace-relative path.
*
* A model is created once per file and reused across tab switches, so switching
* tabs is `editor.setModel(existing)` instead of dispose → recreate → re-tokenize.
diff --git a/apps/desktop/src/renderer/components/files/v2/FilesWorkbench.tsx b/apps/desktop/src/renderer/components/files/v2/FilesWorkbench.tsx
index e86ce8fb8..39a076d6a 100644
--- a/apps/desktop/src/renderer/components/files/v2/FilesWorkbench.tsx
+++ b/apps/desktop/src/renderer/components/files/v2/FilesWorkbench.tsx
@@ -54,9 +54,9 @@ const readCachedWorkspaces = (projectRoot: string): FilesWorkspace[] => workspac
const rootTreeCacheKey = (projectRoot: string, workspaceId: string): string => `${projectRoot}::${workspaceId}`;
/**
- * v2 Files workbench: the VSCode-like shell rendered behind the
- * FILES_WORKBENCH_V2 flag. Reuses the proven IPC + FilesExplorer + Monaco model
- * registry, the new streaming/decoration backend, and the editor-groups store.
+ * Files workbench: the VS Code-like shell for the main Files route and the
+ * embedded Work sidebar. Reuses the proven IPC + FilesExplorer + Monaco model
+ * registry, the streaming/decoration backend, and the editor-groups store.
*/
export function FilesWorkbench({
preferredLaneId,
diff --git a/apps/desktop/src/renderer/components/files/v2/ViewerHost.tsx b/apps/desktop/src/renderer/components/files/v2/ViewerHost.tsx
index 3906bb15c..fcfad49af 100644
--- a/apps/desktop/src/renderer/components/files/v2/ViewerHost.tsx
+++ b/apps/desktop/src/renderer/components/files/v2/ViewerHost.tsx
@@ -27,7 +27,7 @@ export type ViewerHostProps = {
/**
* Resolve the active tab's viewer from its kind and render it with the loaded
- * file content. Replaces FilesPage's hardcoded if/else preview surface.
+ * file content for the active workbench tab.
*/
export function ViewerHost(props: ViewerHostProps) {
const { workspaceId, tab, reloadToken = 0 } = props;
diff --git a/apps/desktop/src/renderer/components/terminals/WorkSidebar.test.tsx b/apps/desktop/src/renderer/components/terminals/WorkSidebar.test.tsx
index 7c9f248ab..6854e51ab 100644
--- a/apps/desktop/src/renderer/components/terminals/WorkSidebar.test.tsx
+++ b/apps/desktop/src/renderer/components/terminals/WorkSidebar.test.tsx
@@ -115,9 +115,9 @@ vi.mock("../chat/ChatBuiltInBrowserPanel", async () => {
};
});
-vi.mock("../files/FilesPage", async () => {
+vi.mock("../files/FilesTab", async () => {
const React = await import("react");
- return { FilesPage: () => React.createElement("div", null, "Files") };
+ return { FilesTab: () => React.createElement("div", null, "Files") };
});
vi.mock("../lanes/LaneDiffPane", async () => {
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
index bc6a9ed14..d924e2054 100644
--- a/docs/ARCHITECTURE.md
+++ b/docs/ARCHITECTURE.md
@@ -600,7 +600,7 @@ Design tokens have been intentionally trimmed. The CTO design tokens at `apps/de
- Project tab hosting: `App.tsx`'s `ProjectTabHost` mounts one persistent `ProjectSurface` per open project tab inside a single window. Each `ProjectSurface` owns its own zustand store instance (`createProjectAppStore(project)`), pre-hydrated with the project binding plus a copy of root-store user preferences (theme, terminal preferences, chat font, sound, density, etc.). User-preference setters point at the **root** store, so changes flow to one place and are then mirrored into every project store on the next `rootPrefs` change. A LRU sorts mounted surfaces and caps the warm-mounted set at `WARM_PROJECT_SURFACE_LIMIT = 8`; surfaces beyond that limit are dropped from the React tree (their store entry is GC'd) but the persisted lane/chat caches in the root store keep their data live so a re-mount is cheap.
- Per-project routing: each surface remembers its own route (`/work`, `/lanes`, `/files`, `/prs`, `/cto`, `/automations`, `/settings`, …) under `ade:project-route:` in `localStorage`. `ProjectTabHost` swaps which surface is `active` based on the foreground project tab, stashing the outgoing route and replaying the incoming surface's last route via `navigate(..., { replace: true })`. Inactive surfaces stay in the tree (`aria-hidden`, `inert`, absolutely positioned at `z-index: -1`, opacity 0, pointer-events none) so chats / terminals / live polling don't tear down on tab swap.
- Work-surface reveal: `ProjectRouteContent` keeps the `/work` route mounted lazily inside each project surface. When the surface itself becomes active **and** the route is a work route, it dispatches the `WORK_SURFACE_REVEALED_EVENT` window event so terminal tiles can clear their texture atlas, force-fit, and refocus.
-- Page-level active gating: lazy feature pages (`LanesPage`, `FilesPage`, `WorkspaceGraphPage`, `PRsPage`, `ReviewPage`, `HistoryPage`, `AutomationsPage`, `AutomationsTemplatesPage`, `CtoPage`, `SettingsPage`) accept an `active?: boolean` prop and gate every `useEffect` that fires IPC polling, event subscriptions, or initial data fetches behind it. Inactive surfaces in background project tabs render their last state but don't poll — the project's runtime is still alive, so the freshness is restored on the next refresh when the user returns.
+- Page-level active gating: lazy feature pages (`LanesPage`, `FilesTab`, `WorkspaceGraphPage`, `PRsPage`, `ReviewPage`, `HistoryPage`, `AutomationsPage`, `AutomationsTemplatesPage`, `CtoPage`, `SettingsPage`) accept an `active?: boolean` prop and gate every `useEffect` that fires IPC polling, event subscriptions, or initial data fetches behind it. Inactive surfaces in background project tabs render their last state but don't poll — the project's runtime is still alive, so the freshness is restored on the next refresh when the user returns.
- The desktop TopBar project tab strip resolves a per-project favicon via `window.ade.project.resolveIcon(rootPath)` and caches the result in a module-local `Map`. Tabs without an icon (or a missing project root) fall back to the `Folder` Phosphor glyph; the same component drives the loading-pulse animation when a tab is being switched into or closed.
- Layout state persists to SQLite (`layout`, `tilingTree`, `graphState` domains via the `kv` table).
diff --git a/docs/features/files-and-editor/README.md b/docs/features/files-and-editor/README.md
index 353b75ff3..c095e4f0e 100644
--- a/docs/features/files-and-editor/README.md
+++ b/docs/features/files-and-editor/README.md
@@ -96,17 +96,13 @@ Preload bridge:
Renderer:
- `apps/desktop/src/renderer/components/files/FilesTab.tsx` — shared
- route/sidebar entry point. It renders the v2 workbench only when
- `localStorage["ade.files.workbenchV2"] === "1"`; the default remains
- the legacy `FilesPage`.
-- `apps/desktop/src/renderer/components/files/FilesPage.tsx` — Files
- tab shell (~2,840 lines): workspace chrome, tab bar, Monaco edit host,
- diff and conflict modes, quick open, text search, trust warnings. It
- composes the virtualized tree below and mounts `AdeDiffViewer` for diff
- tabs. Accepts optional `preferredLaneId` and `embedded` props so the
- same component can mount inside the Work right-edge sidebar; in
- `embedded` mode the header is compact (workspace selector and
- read-only badge only).
+ route/sidebar entry point. It always renders the workbench.
+- `apps/desktop/src/renderer/components/files/v2/FilesWorkbench.tsx` —
+ Files tab shell: workspace chrome, activity bar, explorer, editor
+ groups, Monaco edit host, diff/conflict surfaces, quick open, text
+ search, trust warnings, and file-type viewers. Accepts optional
+ `preferredLaneId` and `embedded` props so the same component can mount
+ inside the Work right-edge sidebar.
- `apps/desktop/src/renderer/components/files/FilesExplorer.tsx` —
virtualized file tree (`@tanstack/react-virtual`), inline rename/create,
explorer search, and context-menu wiring; git status coloring uses
@@ -115,20 +111,20 @@ Renderer:
file-type icons and `changeStatus*` helpers shared with the explorer.
- `apps/desktop/src/renderer/components/files/monacoModelRegistry.ts`
and `treeHelpers.ts` — reusable Monaco model lifetime tracking and
- tree/decorations helpers used by both FilesPage and the v2 workbench.
-- `apps/desktop/src/renderer/components/files/v2/` — flagged VS
- Code-style workbench shell: editor groups, preview/pinned tabs,
- split/move support, warm empty state, search/create overlays, and
+ tree/decorations helpers used by the workbench.
+- `apps/desktop/src/renderer/components/files/v2/` — VS Code-style
+ workbench shell: editor groups, preview/pinned tabs, split/move
+ support, warm empty state, search/create overlays, and
viewers for code, markdown, image, CSV/TSV, PDF, large text, binary,
- and diffs. The workbench is off by default until the parity checklist
- intentionally flips it.
+ and diffs.
- `apps/desktop/src/renderer/components/shared/AdeDiffViewer.tsx` —
shared read-only diff chrome (`@pierre/diffs` `MultiFileDiff` /
`PatchDiff` with split/unified, wrap, line numbers); editable working-tree
diffs delegate to `MonacoDiffView`. Also used from `LaneDiffPane`,
`ChatFileChangesPanel`, and `PrDetailPane`.
-- `apps/desktop/src/renderer/components/files/FilesPage.test.tsx` —
- renderer tests.
+- `apps/desktop/src/renderer/components/files/v2/*.test.ts` and
+ `apps/desktop/src/renderer/components/files/monacoModelRegistry.test.ts`
+ — renderer workbench state and model-lifetime tests.
- `apps/ios/ADE/Views/Files/FilesRootScreen.swift` — mobile Files
root with workspace picker, quick-open and text-search cards, capped
visible result lists (first 40) with refine-search copy when more
diff --git a/docs/features/files-and-editor/editor-surfaces.md b/docs/features/files-and-editor/editor-surfaces.md
index 4ad07a03f..ecb40c044 100644
--- a/docs/features/files-and-editor/editor-surfaces.md
+++ b/docs/features/files-and-editor/editor-surfaces.md
@@ -1,300 +1,161 @@
# Editor Surfaces
-Renderer surfaces that present the Files tab and embed Monaco editors
-for edit, diff, and conflict modes.
+Renderer surfaces that present the Files tab and embed Monaco editors for
+edit, diff, and conflict-oriented file workflows.
-## Main entry: `FilesTab.tsx`
+## Main Entry: `FilesTab.tsx`
Path: `apps/desktop/src/renderer/components/files/FilesTab.tsx`
-`FilesTab` is the shared entry point for the standalone Files route and
-the embedded Work sidebar. It renders `FilesPage` by default. The v2
-workbench renders only when `localStorage["ade.files.workbenchV2"] ===
-"1"` so the shipped experience stays on the legacy page until the
-parity checklist intentionally flips the flag.
+`FilesTab` is the shared entry point for the standalone Files route and the
+embedded Work sidebar. It renders `FilesWorkbench` unconditionally.
-## Legacy entry: `FilesPage.tsx`
+## Workbench Shell: `FilesWorkbench.tsx`
-Path: `apps/desktop/src/renderer/components/files/FilesPage.tsx`
+Path: `apps/desktop/src/renderer/components/files/v2/FilesWorkbench.tsx`
-A single large component (~2,840 lines), parameterized by optional
-`preferredLaneId` (selects a lane worktree as the default workspace)
-and `embedded` (compact chrome for the Work sidebar mount). It owns:
+`FilesWorkbench` owns the VS Code-style shell:
-- workspace selection (dropdown synced to `laneService` workspaces)
-- file explorer via `FilesExplorer.tsx` (virtualized rows, lazy-loaded
- directories, context menu, drag/drop placeholder) with icons and git
- status labels from `filePresentation.tsx`
-- tab bar with reorderable tabs, dirty indicators, middle-click close
-- file path breadcrumb under the tab bar
-- Monaco host for edit mode
-- diff mode via `AdeDiffViewer` (read-only: `@pierre/diffs`; editable
- right pane: `MonacoDiffView` inside `AdeDiffViewer`)
-- 3-way merge layout for conflict mode
-- quick open modal (Cmd+P)
-- cross-file search panel (Cmd+Shift+F)
-- protected-branch warning banner
-- external change notification ("file modified on disk")
+- workspace resolution from `files.listWorkspaces()`
+- cached root tree loading and git decorations
+- the reusable `FilesExplorer` tree
+- editor groups with preview and pinned tabs
+- split editor groups and tab move/drop behavior
+- unified quick-open and content-search overlay
+- file and directory creation prompts
+- context-menu actions for open, rename, delete, copy path, and reveal
+- status bar with branch, group, open-tab, and dirty counts
-Per-tab state (kept in renderer memory, not persisted):
+The component accepts `preferredLaneId`, `embedded`, and `active`. The
+`active` prop gates IPC loading and keybindings for inactive project tabs.
+`preferredLaneId` selects a lane workspace when Files is mounted from Work.
+`embedded` compacts the Work-sidebar mount without introducing a separate
+implementation path.
-- relative path, workspace ID
-- file content, dirty state, and the active Monaco model key
-- dirty flag (`isDirty`)
-- external change indicator (`externallyChangedAt`)
-- mode (`edit` | `diff` | `conflict`)
-- diff or conflict payload (diff sources, merge state)
+Module-level caches keep workspaces and root trees warm across route
+remounts:
-Per-page session state (kept in `FilesPage`'s per-session snapshot map,
-keyed by the page session id):
+- `workspacesCacheByProject` keyed by project root
+- `rootTreeCacheByKey` keyed by `projectRoot::workspaceId`
-- selected node path, expanded directories, open tabs, active tab path
-- editor mode (`edit` / `diff` / `conflict`)
-- markdown preview enabled flag — when toggled on for a markdown tab,
- the editor is replaced with the lazy `PlanMarkdown` renderer
-- search query, editor theme
+Editor state is kept per `filesSessionKey(projectRoot, laneId)` through
+`useEditorGroupsStore`.
-### Lifecycle
+## Workspace Selector
-The page subscribes on mount to:
+The standalone route renders `WorkspacePicker`; embedded Work Files omits the
+picker chrome and preselects the active lane worktree. Switching workspaces:
-- `ade.files.change` — applies external-change sync per tab
-- `ade.lanes.changed` — refreshes the workspace dropdown when lanes
- come/go
-- `ade.sessions.changed` — not directly, but used to show dirty
- overlays when a session creates new files
+1. Resolves the new workspace id.
+2. Loads the root tree and git decorations.
+3. Switches the editor-group session key.
+4. Leaves file service and preload contracts unchanged.
-On unmount, the page calls `files.stopWatching` for every active
-watcher subscription, matching the mode it started with.
+The main-process file service remains the source of truth for read-only
+policy, trust checks, and workspace roots.
-## Workspace selector
+## File Explorer Tree
-Renders a dropdown populated from `files.listWorkspaces()`. Primary
-workspace is pinned first. Switching workspaces:
+Implementation: `FilesExplorer.tsx` over `FileTreeNode[]` from
+`files.listTree`.
-1. Prompts for save on any dirty tab (per-tab confirmation).
-2. Closes all open tabs.
-3. Unsubscribes the old workspace watcher.
-4. Re-lists the tree for the new workspace.
-5. Subscribes a new watcher.
+Lazy loading uses `files.listTreeChildren` when a directory is expanded,
+following `nextOffset` until all children are loaded or the renderer hits its
+safety cap. `listTree` and `listTreeChildren` share filtering and ordering:
+volatile `.ade` runtime paths and `.git` are hidden, ignored files respect
+`includeIgnored`, and directories sort before files.
-## File explorer tree
+Visual indicators per node:
-Implementation: `FilesExplorer.tsx` over `FileTreeNode[]` from
-`files.listTree`. Lazy loading uses `files.listTreeChildren` when a
-directory is expanded, following `nextOffset` until all children are
-loaded or the renderer hits its safety cap. `listTree` and
-`listTreeChildren` intentionally share the same filtering and ordering:
-volatile `.ade` runtime paths and `.git` are hidden, ignored files
-respect `includeIgnored`, and directories sort before files.
+- file icons by extension via `filePresentation.tsx`
+- change status coloring from git decorations
+- directory change dots for descendants with changes
-Visual indicators per node:
+Context-menu actions are built in `FilesWorkbench` and rendered by
+`v2/ContextMenu.tsx`. The menu clamps to the viewport before opening.
-- file icons by extension via `filePresentation.tsx` (Phosphor)
-- change status badge (modified / added / deleted coloring from the same
- helpers)
-- "has changes" dot on directories that contain any changed descendant
+## Editor Groups
-Context menu (right-click):
+Implementation:
-| Action | Target | Notes |
-|---|---|---|
-| Open | file | same as click |
-| Open to the side | file | opens in a new tab without closing current |
-| Diff | file | switches tab to diff mode (staged vs unstaged) |
-| Stage | file | git add |
-| Unstage | file | git reset HEAD |
-| Discard | file | git checkout -- |
-| Copy path | file/dir | absolute host path |
-| Copy relative path | file/dir | relative to workspace root |
-| Reveal in Finder | file/dir | uses `shell.showItemInFolder` |
-| New File | dir | inline input |
-| New Folder | dir | inline input |
-| Rename | file/dir | inline rename |
-| Delete | file/dir | confirm dialog then `files.delete` |
+- `v2/editorGroupsStore.ts` for immutable group/tab operations
+- `v2/EditorGroups.tsx` for the group grid
+- `v2/EditorGroup.tsx` for one group, tab strip, diff toggle, and active tab
+- `v2/ViewerHost.tsx` for resolving and rendering the active viewer
-Stage/Unstage/Discard go through the git service, not the files
-service — they rely on `git` commands against the workspace root.
+Tabs can be preview or pinned. Opening a file from single click creates or
+reuses a preview tab; activation/edit/save promotes it. Tabs can be closed,
+closed-other-tabs, split into a new group, or moved between groups by drag.
-## Tab bar and breadcrumb
+The active group's active tab is the status-bar source for path, language,
+branch, and dirty state.
-Tabs are draggable (reorderable) and show:
+## Monaco Model Lifecycle
-- file icon
-- file name
-- dirty dot (unsaved changes)
-- external change indicator (reload prompt)
-- close button (or middle-click to close)
+`monacoModelRegistry.ts` keeps one Monaco text model per
+workspace-relative path. Switching tabs calls `editor.setModel(existing)`
+instead of dispose/recreate, preserving tokenization and undo stacks.
-Below the tabs, a breadcrumb trail (`src > components > App.tsx`) is
-clickable per segment — clicking navigates the file tree to that
-directory.
+Callers dispose models on tab close, rename/delete cleanup, workspace switch,
+and unmount. Do not dispose models on tab switch, theme change, read-only
+toggle, or group move.
-## Edit mode
+Dirty tracking is based on Monaco alternative version ids. A save writes
+through `files.writeTextAtomic`, updates the model baseline, invalidates the
+content cache, and refreshes git decorations.
-Monaco Editor mounted in the tab's Monaco host. Key bindings:
+## Viewers
-| Shortcut | Action |
-|---|---|
-| `Cmd+S` | save (calls `files.writeTextAtomic`) |
-| `Cmd+Z` / `Cmd+Shift+Z` | undo / redo (Monaco native) |
-| `Cmd+F` | in-file find (Monaco native) |
-| `Cmd+Shift+P` | Monaco command palette |
-
-Save flow:
-
-1. Read model value.
-2. Call `files.writeTextAtomic({ workspaceId, path, text })`.
-3. Mark the tab clean on resolve.
-4. If the watcher's ref count is active, the subsequent
- `modified` event is suppressed for this tab (already in sync).
-
-Protection rails:
-
-- Writing to the primary workspace while active lanes exist shows a
- banner above the editor: "You have active lanes. Saving here writes
- to main." The user must click "I understand" to dismiss for the
- session.
-- Saving a file marked read-only fails at the runtime boundary (or
- the main-process boundary on the fallback path) and the tab displays
- the error.
-
-The page keeps one Monaco editor instance alive while the edit host is
-mounted. Opening another text file swaps the editor to a cached Monaco
-model for that path/language through `monacoModelRegistry`; it does not
-recreate the whole editor or dispose the model on every file switch.
-Re-opening a path that is already in the tab bar (via the explorer or
-quick open) reselects the existing tab instead of issuing another
-`files.readFile` — the read-side guard short-circuits when the requested
-path matches a known open tab, preserving any in-flight editor state.
-
-### Markdown preview
-
-For tabs whose `languageId === "markdown"` or whose path ends in `.md`
-/ `.mdx`, the tab toolbar adds an Eye toggle. Toggling it on swaps the
-Monaco host for a `React.Suspense`-wrapped lazy import of
-`PlanMarkdown` (the same renderer the orchestration plan view uses).
-Toggling it off returns to the code editor with the model intact. The
-preference is per tab and lives in the per-session snapshot
-(`markdownPreviewEnabled`) so it survives tab/workspace switches
-within the page lifetime and is restored when the page re-mounts. The
-status hint under the tab strip ("Markdown view: rendered preview for
-the current file.") reflects the active mode so it is obvious that
-saves and shortcuts target the markdown source even while the preview
-is showing.
-
-## Diff mode
-
-`FilesPage` mounts `AdeDiffViewer` for diff tabs. Payloads come from
-`diffService` via `window.ade.diff` (`getFile` / `getFilePatch` as
-appropriate for the tab’s comparison mode).
-
-- **Read-only** — `@pierre/diffs` renders `MultiFileDiff` (old/new text)
- or `PatchDiff` (unified patch text) with a small toolbar: split vs
- unified layout, wrap vs scroll overflow, line numbers, copy path.
-- **Editable working-tree** — when the user enables editing on the
- modified side, `AdeDiffViewer` switches to `MonacoDiffView` so changes
- save through the same Monaco path as before.
-
-Comparison sources (staged vs unstaged, HEAD vs working tree,
-commit-to-commit) are unchanged at the service layer.
-
-Save behavior in diff mode writes only when the modified side is
-editable; content is written atomically via `files.writeTextAtomic`.
-
-## Conflict mode
-
-3-way merge view with four regions:
-
-- top-left: Base (common ancestor)
-- bottom-left: Ours (current branch)
-- bottom-right: Theirs (incoming branch)
-- right: Result (working copy with conflict markers)
-
-Per-conflict controls (rendered as inline overlays):
-
-- Accept Ours
-- Accept Theirs
-- Accept Both
-- manual edit in the Result pane
-
-Behind this, `conflictService.ts` parses the conflict markers and
-tracks remaining conflicts. "Mark as Resolved" becomes enabled when
-no `<<<<<<<`, `=======`, `>>>>>>>` remain in Result. Saving writes
-via `files.writeTextAtomic`.
-
-## Quick open (Cmd+P)
-
-Modal overlay:
-
-- input box routes to `files.quickOpen({ workspaceId, query, limit:
- 80, includeIgnored: true })`
-- results are `{ path, score }[]`, rendered with file icon and relative
- path
-- `Enter` opens the selected file; `Shift+Enter` opens in a new tab;
- `Ctrl+Enter` opens to the side
-- the modal uses its own cache so repeated queries do not re-hit IPC
-
-## Cross-file search (Cmd+Shift+F)
-
-Panel overlay with:
-
-- query input
-- results grouped by file path (collapsible), showing `{ line, column,
- preview }`
-- clicking a result opens the file and navigates to the line
-- the search uses `files.searchText({ workspaceId, query, limit: 500,
- includeIgnored: false })`
-
-## Embedded Files in the Work sidebar
-
-`FilesPage` accepts `preferredLaneId` and `embedded` props so it can be
-mounted as the `files` tab of the Work right-edge sidebar
-(`apps/desktop/src/renderer/components/terminals/WorkSidebar.tsx`).
-When `preferredLaneId` is set, the workspace selector pre-selects the
-matching lane worktree on first load and on every change to the prop;
-when `embedded` is true the header collapses to just the workspace
-selector and read-only badge (the title block, `View lane` button,
-editor theme toggle, `Open In` menu, and file count are hidden) so the
-tree, tab bar, and editor pack into a narrow column. All IPC calls and
-modes are identical to the standalone Files tab — there is no separate
-code path for the embedded view.
-
-The embedded mount is responsive to the Work sidebar width. At narrow
-sizes the explorer/editor split stacks vertically instead of preserving
-the standalone Files tab's fixed explorer column, so editor controls
-remain reachable. File-tree context menus measure their rendered size
-and clamp to the renderer viewport on both axes before opening; do the
-same for any new Files context menu surface.
-
-## V2 workbench
-
-The flagged v2 workbench lives under
-`apps/desktop/src/renderer/components/files/v2/`. It keeps editor-group
-state per `projectRoot::laneId`, reuses the same `FilesExplorer`,
-`monacoModelRegistry`, preload API, and file service contracts, and adds:
-
-- preview tabs that promote to pinned tabs on edit/save
-- split editor groups and drag-to-move or drag-to-split tab behavior
-- per-path dirty tracking backed by Monaco alternative version ids
-- workbench-level search/create overlays
-- viewers for code, markdown, image, CSV/TSV, PDF, large text, binary,
- and diffs
+Viewer selection lives in `v2/viewerRegistry.ts`. The shell can force special
+viewer kinds such as `diff` or `conflict`; normal file viewers are selected
+from path extension and file metadata.
+
+Supported viewers:
+
+- `CodeViewer` for editable text and Monaco-backed source files
+- `MarkdownViewer`
+- `ImageViewer`
+- `CsvViewer`
+- `PdfViewer`
+- `LargeTextViewer` for streamed large text
+- `BinaryViewer`
+- `DiffViewer`, backed by `window.ade.diff` and `AdeDiffViewer`
+
+Large text uses `readFileRange` for follow-up chunks. Unsupported binary
+content remains non-editable.
+
+## Search And Create Overlays
+
+`v2/overlays.tsx` provides:
+
+- `SearchOverlay`, a unified quick-open plus text-search surface
+- `CreatePromptModal` for new file and new directory prompts
+
+`Cmd+P` / `Ctrl+P` and `Cmd+Shift+F` / `Ctrl+Shift+F` both open the search
+overlay. File hits open directly; content hits call `setPendingReveal` so the
+next `CodeViewer` mount jumps to the matching line.
+
+## Embedded Files In Work
+
+`WorkSidebar` mounts `FilesTab` with `preferredLaneId={laneId}` and
+`embedded={true}`. The embedded layout keeps the same service calls, editor
+groups, viewers, and tree behavior as the standalone route, but uses a
+narrower explorer column and compact explorer controls.
-Do not change the default flag state as a side effect of renderer or
-service refactors; the v2 shell is intentionally opt-in until the
-parity rollout flips it.
+The embedded mount shares Work's right-edge sidebar. Keep context menus,
+overlays, and editor controls clamped to the renderer viewport so they remain
+usable in the narrower column.
-## Keyboard shortcuts
+## Keyboard Shortcuts
Registered through the global keybinding service
-(`apps/desktop/src/main/services/keybindings/`):
+(`apps/desktop/src/main/services/keybindings/`) and Files-local handlers:
| Shortcut | Action |
|---|---|
| `Cmd+S` / `Ctrl+S` | save |
-| `Cmd+P` / `Ctrl+P` | quick open |
-| `Cmd+Shift+F` / `Ctrl+Shift+F` | search |
+| `Cmd+P` / `Ctrl+P` | search/open |
+| `Cmd+Shift+F` / `Ctrl+Shift+F` | search/open |
| `Cmd+W` / `Ctrl+W` | close current tab |
| `Cmd+Tab` / `Ctrl+Tab` | next tab |
| `Cmd+\` / `Ctrl+\` | toggle file explorer |
@@ -303,27 +164,22 @@ Registered through the global keybinding service
## Gotchas
-- **Monaco model lifecycle.** Monaco models are reused per path and
- disposed on tab close, rename/delete cleanup, workspace switch, or
- unmount. Do not dispose them on tab switch, theme change, read-only
- toggle, or v2 group move.
-- **External change + dirty tab.** A file modified on disk with
- unsaved edits surfaces a "file changed on disk" banner. The user
- must explicitly choose "Reload" (discards edits) or "Keep editing"
- (leaves the warning up). The model is never overwritten silently.
-- **Large files.** Oversized text opens as a read-only streamed view:
- `readFile` returns a UTF-8-safe first chunk and viewers request
- follow-up ranges via `readFileRange`. Oversized images and unsupported
- binaries still render non-editable fallback views.
-- **Breadcrumb on root.** Files in the workspace root show only the
- filename in the breadcrumb; clicking it has no effect.
-- **Tab ordering.** The tab order is stored in renderer memory and
- does not survive a full reload unless persisted by the future
- `editor-state.json` work (not yet implemented).
-
-## Cross-links
-
-- Main-process services and watcher: [file-watcher-and-trust.md](./file-watcher-and-trust.md)
+- **External change plus dirty tab.** File watcher events must not overwrite
+ unsaved Monaco models. Surface the external change and require an explicit
+ user choice.
+- **Primary checkout writes.** Read-only and primary-workspace policy is
+ enforced by the file service and preload boundary; renderer affordances are
+ only presentation.
+- **Large files.** Oversized text opens as read-only streamed content. Do not
+ force large files through the editable Monaco viewer.
+- **Tab ordering.** Editor group order lives in renderer memory for the
+ session key. Persisting it across full reloads belongs to future
+ editor-state work.
+
+## Cross-Links
+
+- Main-process services and watcher:
+ [file-watcher-and-trust.md](./file-watcher-and-trust.md)
- Files tab entry from the app shell:
`apps/desktop/src/renderer/components/app/App.tsx`
- Conflict resolution data: `apps/desktop/src/main/services/conflicts/`
diff --git a/docs/features/files-and-editor/file-watcher-and-trust.md b/docs/features/files-and-editor/file-watcher-and-trust.md
index ec270bfd9..29908f740 100644
--- a/docs/features/files-and-editor/file-watcher-and-trust.md
+++ b/docs/features/files-and-editor/file-watcher-and-trust.md
@@ -192,7 +192,7 @@ matches.
## External change sync
-The renderer's `FilesPage.tsx` subscribes to `ade.files.change` and
+The renderer's `FilesWorkbench.tsx` subscribes to `ade.files.change` and
handles events like this:
1. **Lookup open tabs by path.** Each open Monaco editor tracks its
diff --git a/docs/features/terminals-and-sessions/README.md b/docs/features/terminals-and-sessions/README.md
index 05e9cae17..929ae209b 100644
--- a/docs/features/terminals-and-sessions/README.md
+++ b/docs/features/terminals-and-sessions/README.md
@@ -193,7 +193,7 @@ Renderer surfaces:
- `apps/desktop/src/renderer/components/terminals/WorkSidebar.tsx` —
right-edge sidebar tied to the active lane (and active Work session
when present). Tabbed into `git` (lane git actions + selection-driven
- diff), `files` (mounts `FilesPage` in `embedded` mode with the lane
+ diff), `files` (mounts `FilesTab` in `embedded` mode with the lane
worktree pre-selected), `ios` (mounts `ChatIosSimulatorPanel` against
the active lane), `app-control` (mounts `ChatAppControlPanel`), and
`browser` (mounts `ChatBuiltInBrowserPanel` over the current ADE
diff --git a/docs/features/terminals-and-sessions/ui-surfaces.md b/docs/features/terminals-and-sessions/ui-surfaces.md
index 03b4ff1fb..a9c0ed0d2 100644
--- a/docs/features/terminals-and-sessions/ui-surfaces.md
+++ b/docs/features/terminals-and-sessions/ui-surfaces.md
@@ -307,7 +307,7 @@ Tabs:
- `git` — `LaneGitActionsPane` on top, `LaneDiffPane` underneath
whenever a file or commit is selected. The two share the row via
the same min-height-aware flex layout as the lane detail view.
-- `files` — `FilesPage` mounted with `preferredLaneId={laneId}` and
+- `files` — `FilesTab` mounted with `preferredLaneId={laneId}` and
`embedded={true}`. The `embedded` prop drops the desktop title block,
the `View lane` button, the editor theme toggle, the `Open In` menu,
and the file count, and shrinks the workspace selector so the file
diff --git a/docs/perf/work-tab-action-inventory.md b/docs/perf/work-tab-action-inventory.md
index 764e013f7..99e51a3c3 100644
--- a/docs/perf/work-tab-action-inventory.md
+++ b/docs/perf/work-tab-action-inventory.md
@@ -292,24 +292,24 @@ Coverage states:
| id | action | state | source |
| --- | --- | --- | --- |
-| work.files.mount | Mount Files tab | measured | `FilesPage.tsx` |
-| work.files.workspace | Change workspace selector | measured | `FilesPage.tsx` |
-| work.files.view-lane | Navigate to lane from Files header/banner | external-skip | `FilesPage.tsx` |
-| work.files.primary-edit | Toggle primary edit allowance | measured | `FilesPage.tsx` |
-| work.files.trust-edit | Trust and edit primary workspace | prompt-only | `FilesPage.tsx` |
-| work.files.theme | Toggle editor light/dark theme | measured | `FilesPage.tsx` |
-| work.files.open-in.menu | Open external app menu | external-skip | `FilesPage.tsx` |
-| work.files.open-in.item | Open file in external app | external-skip | `FilesPage.tsx` |
-| work.files.suggested-lane | Switch to suggested lane workspace | measured | `FilesPage.tsx` |
-| work.files.error.dismiss | Dismiss error banner | measured | `FilesPage.tsx` |
+| work.files.mount | Mount Files tab | measured | `FilesWorkbench.tsx` |
+| work.files.workspace | Change workspace selector | measured | `FilesWorkbench.tsx` |
+| work.files.view-lane | Navigate to lane from Files header/banner | external-skip | `FilesWorkbench.tsx` |
+| work.files.primary-edit | Toggle primary edit allowance | measured | `FilesWorkbench.tsx` |
+| work.files.trust-edit | Trust and edit primary workspace | prompt-only | `FilesWorkbench.tsx` |
+| work.files.theme | Toggle editor light/dark theme | measured | `FilesWorkbench.tsx` |
+| work.files.open-in.menu | Open external app menu | external-skip | `FilesWorkbench.tsx` |
+| work.files.open-in.item | Open file in external app | external-skip | `FilesWorkbench.tsx` |
+| work.files.suggested-lane | Switch to suggested lane workspace | measured | `FilesWorkbench.tsx` |
+| work.files.error.dismiss | Dismiss error banner | measured | `FilesWorkbench.tsx` |
| work.files.filter | Type path filter | measured | `FilesExplorer.tsx` |
| work.files.filter.clear | Clear path filter | measured | `FilesExplorer.tsx` |
| work.files.content.open | Open content search overlay | measured | `FilesExplorer.tsx` |
-| work.files.content.search | Search file contents | measured | `FilesPage.tsx` |
-| work.files.content.result | Open content search result | measured | `FilesPage.tsx` |
+| work.files.content.search | Search file contents | measured | `FilesWorkbench.tsx` |
+| work.files.content.result | Open content search result | measured | `FilesWorkbench.tsx` |
| work.files.quick.open | Open quick open overlay | measured | `FilesExplorer.tsx` |
-| work.files.quick.search | Search quick open | measured | `FilesPage.tsx` |
-| work.files.quick.result | Open quick open result | measured | `FilesPage.tsx` |
+| work.files.quick.search | Search quick open | measured | `FilesWorkbench.tsx` |
+| work.files.quick.result | Open quick open result | measured | `FilesWorkbench.tsx` |
| work.files.new-file | New file prompt | measured | `FilesExplorer.tsx` |
| work.files.new-folder | New folder prompt | measured | `FilesExplorer.tsx` |
| work.files.tree.expand | Expand directory | measured | `FilesExplorer.tsx` |
@@ -317,31 +317,31 @@ Coverage states:
| work.files.tree.open | Open file | measured | `FilesExplorer.tsx` |
| work.files.tree.context | Open file/folder context menu | measured | `FilesExplorer.tsx` |
| work.files.tree.inline-rename | Inline rename path | measured | `FilesExplorer.tsx` |
-| work.files.tab.switch | Switch open file tab | measured | `FilesPage.tsx` |
-| work.files.tab.close | Close file tab | measured | `FilesPage.tsx` |
-| work.files.mode.code | Switch editor to CODE | measured | `FilesPage.tsx` |
-| work.files.mode.changes | Switch editor to CHANGES | measured | `FilesPage.tsx` |
-| work.files.mode.merge | Switch editor to MERGE | measured | `FilesPage.tsx` |
-| work.files.save | Save edited file | measured | `FilesPage.tsx` |
-| work.files.conflict.ours | Resolve conflict as ours | sandbox-only | `FilesPage.tsx` |
-| work.files.conflict.theirs | Resolve conflict as theirs | sandbox-only | `FilesPage.tsx` |
-| work.files.conflict.both | Resolve conflict as both | sandbox-only | `FilesPage.tsx` |
-| work.files.context.open | Context menu OPEN | measured | `FilesPage.tsx` |
-| work.small.files-context-menu-overflow | Verify Files context menu stays contained near viewport edges | measured | `FilesPage.tsx` |
-| work.small.files-embedded-overflow | Verify embedded Files explorer/editor stay contained in the Work tools pane | measured | `FilesPage.tsx` |
-| work.files.context.open-diff | Context menu OPEN DIFF | measured | `FilesPage.tsx` |
-| work.files.context.stage | Context menu STAGE | measured | `FilesPage.tsx` |
-| work.files.context.unstage | Context menu UNSTAGE | measured | `FilesPage.tsx` |
-| work.files.context.discard | Context menu DISCARD | prompt-only | `FilesPage.tsx` |
-| work.files.context.copy-path | Context menu COPY PATH | measured | `FilesPage.tsx` |
-| work.files.context.reveal | Context menu reveal in Finder | external-skip | `FilesPage.tsx` |
-| work.files.context.new-file | Context menu NEW FILE | measured | `FilesPage.tsx` |
-| work.files.context.new-folder | Context menu NEW FOLDER | measured | `FilesPage.tsx` |
-| work.files.context.rename | Context menu RENAME | measured | `FilesPage.tsx` |
-| work.files.context.delete | Context menu DELETE | prompt-only | `FilesPage.tsx` |
-| work.files.diff.mode-working | Files diff: working tree mode | measured | `FilesPage.tsx` |
-| work.files.diff.mode-staged | Files diff: staged mode | measured | `FilesPage.tsx` |
-| work.files.diff.mode-commit | Files diff: commit mode and compare ref select | measured | `FilesPage.tsx` |
+| work.files.tab.switch | Switch open file tab | measured | `FilesWorkbench.tsx` |
+| work.files.tab.close | Close file tab | measured | `FilesWorkbench.tsx` |
+| work.files.mode.code | Switch editor to CODE | measured | `FilesWorkbench.tsx` |
+| work.files.mode.changes | Switch editor to CHANGES | measured | `FilesWorkbench.tsx` |
+| work.files.mode.merge | Switch editor to MERGE | measured | `FilesWorkbench.tsx` |
+| work.files.save | Save edited file | measured | `FilesWorkbench.tsx` |
+| work.files.conflict.ours | Resolve conflict as ours | sandbox-only | `FilesWorkbench.tsx` |
+| work.files.conflict.theirs | Resolve conflict as theirs | sandbox-only | `FilesWorkbench.tsx` |
+| work.files.conflict.both | Resolve conflict as both | sandbox-only | `FilesWorkbench.tsx` |
+| work.files.context.open | Context menu OPEN | measured | `FilesWorkbench.tsx` |
+| work.small.files-context-menu-overflow | Verify Files context menu stays contained near viewport edges | measured | `FilesWorkbench.tsx` |
+| work.small.files-embedded-overflow | Verify embedded Files explorer/editor stay contained in the Work tools pane | measured | `FilesWorkbench.tsx` |
+| work.files.context.open-diff | Context menu OPEN DIFF | measured | `FilesWorkbench.tsx` |
+| work.files.context.stage | Context menu STAGE | measured | `FilesWorkbench.tsx` |
+| work.files.context.unstage | Context menu UNSTAGE | measured | `FilesWorkbench.tsx` |
+| work.files.context.discard | Context menu DISCARD | prompt-only | `FilesWorkbench.tsx` |
+| work.files.context.copy-path | Context menu COPY PATH | measured | `FilesWorkbench.tsx` |
+| work.files.context.reveal | Context menu reveal in Finder | external-skip | `FilesWorkbench.tsx` |
+| work.files.context.new-file | Context menu NEW FILE | measured | `FilesWorkbench.tsx` |
+| work.files.context.new-folder | Context menu NEW FOLDER | measured | `FilesWorkbench.tsx` |
+| work.files.context.rename | Context menu RENAME | measured | `FilesWorkbench.tsx` |
+| work.files.context.delete | Context menu DELETE | prompt-only | `FilesWorkbench.tsx` |
+| work.files.diff.mode-working | Files diff: working tree mode | measured | `FilesWorkbench.tsx` |
+| work.files.diff.mode-staged | Files diff: staged mode | measured | `FilesWorkbench.tsx` |
+| work.files.diff.mode-commit | Files diff: commit mode and compare ref select | measured | `FilesWorkbench.tsx` |
## Browser tools
@@ -1116,9 +1116,11 @@ Files context-menu UX fix:
- After clamping to the measured menu size with an `8px` viewport inset, the
same right-edge probe placed the menu at `x=956.0px`, ending at
`right=1156.0px` and `bottom=737.0px` in the `1164x745` viewport.
-- Validation: `npm --prefix apps/desktop run test --
- src/renderer/components/files/FilesPage.test.tsx` passed (`20` tests), and
- `npm --prefix apps/desktop run typecheck` passed.
+- Validation now covers the permanent workbench with
+ `src/renderer/components/files/v2/editorGroupsStore.test.ts`,
+ `src/renderer/components/files/v2/viewerRegistry.test.ts`, and
+ `src/renderer/components/files/monacoModelRegistry.test.ts`; also run
+ `npm --prefix apps/desktop run typecheck`.
Embedded Files layout UX fix:
@@ -1127,12 +1129,11 @@ Embedded Files layout UX fix:
ended at `right=1170.8px`, the editor was squeezed to `1.8px`, and the
`CODE` button ended at `right=1256.2px`, overflowing the viewport by
`92.2px`.
-- After stacking explorer over editor only when `FilesPage` is embedded, the
+- After making the embedded workbench layout responsive, the
explorer and editor both fit at `right=1151.6px`; the `CODE` button ended at
`right=953.8px`, with `0px` right overflow.
-- Validation after the embedded layout fix: `npm --prefix apps/desktop run test
- -- src/renderer/components/files/FilesPage.test.tsx` passed (`20` tests),
- and `npm --prefix apps/desktop run typecheck` passed.
+- Validation after the embedded layout fix now uses the permanent workbench
+ tests plus `npm --prefix apps/desktop run typecheck`.
Browser coverage:
@@ -1514,10 +1515,9 @@ Rows this run left for later focused evidence:
inserting a chat draft. Later focused iOS panel evidence covers the
chat-draft-capable callbacks.
- `work.files.primary-edit`: with `README.md` open in the embedded Work Files
- pane, the primary edit toggle did not render. The retained evidence is the
- non-embedded Files chrome fixture below, because the exact `TRUST & EDIT`
- control belongs to `FilesPage.tsx` but is intentionally hidden when embedded
- in the Work tools pane.
+ pane, primary-workspace edit controls are intentionally not part of the
+ compact embedded chrome. Keep primary-workspace policy evidence on the
+ standalone Files route or at the service boundary.
Invalid / setup markers:
@@ -1702,14 +1702,13 @@ npm --prefix apps/desktop run test -- src/renderer/components/chat/AgentChatComp
npm --prefix apps/desktop run test -- src/renderer/components/chat/AgentChatComposer.test.tsx -t "edits a queued steer message|removes a queued steer message"
npm --prefix apps/desktop run test -- src/renderer/components/chat/AgentChatComposer.test.tsx -t "accepts the prompt suggestion with Tab"
npm --prefix apps/desktop run test -- src/renderer/components/chat/AgentChatComposer.test.tsx
-npm --prefix apps/desktop run test -- src/renderer/components/files/FilesPage.test.tsx -t "toggles editor theme from main Files header and persists"
-npm --prefix apps/desktop run test -- src/renderer/components/files/FilesPage.test.tsx -t "copies a file path from the tree context menu"
-npm --prefix apps/desktop run test -- src/renderer/components/files/FilesPage.test.tsx -t "primary workspace edit allowance"
-npm --prefix apps/desktop run test -- src/renderer/components/files/FilesPage.test.tsx
+npm --prefix apps/desktop run test -- src/renderer/components/files/v2/editorGroupsStore.test.ts
+npm --prefix apps/desktop run test -- src/renderer/components/files/v2/viewerRegistry.test.ts
+npm --prefix apps/desktop run test -- src/renderer/components/files/monacoModelRegistry.test.ts
```
Results: passed (`1`, `2`, `1`, full composer file with `41` tests, `1`,
-`1`, `1` focused tests, and full FilesPage file with `22` tests respectively).
+`1`, and `1` Files workbench-focused test files respectively).
Rows promoted to `measured`:
@@ -1733,16 +1732,11 @@ Rows promoted to `measured`:
- `work.chat.dismiss.error`: the full composer test selected an oversized file
through the hidden upload input, verified the attach error rendered, clicked
`Dismiss error`, and verified the error cleared.
-- `work.files.theme`: the FilesPage test opened `src/index.ts`, clicked
- `files-editor-theme-toggle`, verified localStorage and Monaco switched to
- `vs`, clicked it again, and verified the state returned to `vs-dark`.
-- `work.files.context.copy-path`: the FilesPage context-menu test opened
- `src/index.ts`, expanded `src`, right-clicked `src/index.ts`, clicked
- `COPY PATH`, and verified `writeClipboardText("src/index.ts")`.
-- `work.files.primary-edit`: the non-embedded FilesPage test loaded a
- read-only primary workspace, verified `READ-ONLY`, clicked `TRUST & EDIT`,
- verified `DISABLE EDITS` plus Monaco `readOnly: false`, then clicked
- `DISABLE EDITS` and verified the read-only state returned.
+- `work.files.context.copy-path`: keep coverage on the permanent workbench
+ context menu and clipboard path action.
+- `work.files.primary-edit`: keep coverage on service-level read-only policy
+ and the standalone Files route, since the embedded Work pane intentionally
+ keeps primary-workspace edit chrome out of the compact surface.
### Focused fixture evidence: Work grid and session-list tests