From dfd430f27968cf6d74f94c88054c7198e34f4d92 Mon Sep 17 00:00:00 2001 From: idevlab Date: Mon, 28 Sep 2026 15:16:30 +0800 Subject: [PATCH 1/3] refine desktop workspace layout and dock chrome --- Design.md | 27 +- apps/desktop/src/App.tsx | 265 +++--- apps/desktop/src/components/ui/button.tsx | 8 +- apps/desktop/src/design/tokens.css | 8 +- apps/desktop/src/design/ui-lab/UiLab.tsx | 89 +- apps/desktop/src/dock/Dock.tsx | 168 ++-- apps/desktop/src/files/FileDockContent.tsx | 109 +-- apps/desktop/src/files/FilePanel.tsx | 15 +- apps/desktop/src/i18n/strings.ts | 10 +- apps/desktop/src/plugins/PluginUiSlot.tsx | 96 +- apps/desktop/src/session/Composer.tsx | 30 +- .../src/session/SessionHeaderActions.tsx | 11 +- apps/desktop/src/sidebar/SessionRail.tsx | 852 +++++++++--------- apps/desktop/src/styles.css | 88 +- .../tests/checkoutPickerRendered.test.tsx | 4 +- .../tests/composerGeometryContract.test.ts | 6 +- .../tests/dockPluginGateRendered.test.tsx | 83 +- .../tests/fileDockContentRendered.test.tsx | 77 ++ .../tests/pluginManagerRendered.test.tsx | 5 +- .../sessionHeaderActionsRendered.test.tsx | 7 +- .../tests/sessionRailRendered.test.tsx | 152 +--- .../tests/windowChromeContract.test.ts | 58 +- docs/design/system.md | 14 +- .../2026-09-28-codex-layout-reset/intent.md | 45 + .../2026-09-28-codex-layout-reset/plan.md | 48 + .../2026-09-28-codex-layout-reset/spec.md | 50 + .../verification.md | 74 ++ 27 files changed, 1338 insertions(+), 1061 deletions(-) create mode 100644 apps/desktop/tests/fileDockContentRendered.test.tsx create mode 100644 docs/sdlc/changes/2026-09-28-codex-layout-reset/intent.md create mode 100644 docs/sdlc/changes/2026-09-28-codex-layout-reset/plan.md create mode 100644 docs/sdlc/changes/2026-09-28-codex-layout-reset/spec.md create mode 100644 docs/sdlc/changes/2026-09-28-codex-layout-reset/verification.md diff --git a/Design.md b/Design.md index c793c993..6aa8681a 100644 --- a/Design.md +++ b/Design.md @@ -8,8 +8,8 @@ Make controls compact, reading comfortable, navigation identifiable and repeated ## Shape and hierarchy -- Minimum control/menu-item radius: **12px**, explicitly confirmed by the user. Cards and dialogs: **16px**. Main composer: **24px**. Circular controls remain circular; small marks naturally clamp their radius. -- Persistent content surfaces are flat. Menus and dialogs may use the shared elevation/material. Hover and press never translate, scale, glow or increase shadow. +- Minimum control/menu-item radius: **12px**, explicitly confirmed by the user. Cards and dialogs: **16px**. Main composer: **24px**. Standalone icon-only buttons are fully circular at every size; text buttons keep the control radius. Responsive toolbar buttons become circular when their label is hidden; joined split-button segments retain their shared outline. Small marks naturally clamp their radius. +- Persistent content surfaces are flat, except the floating main workspace, composer and right tool panel, which use the shared raised elevation. Menus and dialogs may use the shared elevation/material. Hover and press never translate, scale, glow or increase shadow. - Keep one primary action per local group. Use a neutral surface for ordinary controls and C2 blue for primary actions and links. Success, warning and destructive colors convey actual status. - Page title, explanatory copy, content and action appear in that order. Headings and actions use the same content alignment grid. @@ -63,8 +63,31 @@ Use the existing 2/4/6/8/12/16/24/32px scale: icon/text 6–8px, related control - Compact composition remains a bounded input card. Expansion must reuse the same editor tree and preserve its draft. - Settings use a consistent 24px page inset, aligned trailing controls and section grouping. Narrow content columns stack controls below labels. - Session names lead the rail. Supporting text is bounded to one line; status, age and provider remain aligned. Routine row actions appear on hover/focus. External resource groups default collapsed only when no saved preference exists. +- The Files surface shows document tabs only when documents are open or an editor needs a route back to the tree. Its search/action toolbar uses 32px controls with 8px padding and no extra folder-only navigation row. - File/diff/terminal panels retain task-appropriate density; shared chrome and controls follow this standard without constraining code to a prose column. +## Workspace layout + +The main workspace has a 32px window drag strip with the focused task title above three independent regions: a persistent +48px icon navigation, the resizable task sidebar, and the content workspace with its optional dock. +The window strip owns search and the sidebar toggle; no product wordmark is displayed there or in the task list. The sidebar starts with new task and quick chat, followed by project/task organization. Global destinations +and utilities remain reachable when the task list is collapsed. Narrow windows show the task list +over the workspace below the window strip; dock width calculations reserve the icon navigation. +Workspace pane headers share a 40px height. Right-panel tabs and the close control live in the global 32px window strip, aligned to the panel width, leaving no inner dock header. Both use a 28px control height with 2px vertical clearance. Toolbar labels collapse to named icons below 48rem so task titles remain readable; the selected tool tab retains its text at narrow widths. Native window controls retain their existing placement. Settings and Scene Studio retain their existing dedicated page layouts. + +New installations start in compact mode; saved document-mode preferences remain unchanged. The +checkout context sits above the compact input. An empty compact workspace centers its greeting in the space above the bottom composer. The same +composer/editor tree serves compact and document modes so layout changes preserve the draft. Short +windows scroll without clipping the input; conversations retain their transcript scroll behavior. + +The workspace chrome stays quiet: toolbar actions use shared Ghost states, the composer keeps a +constant low elevation when focused, and an unopened right panel offers a centered single-column +list with descriptions available on hover or focus. Keep normal text contrast, persistent selected +states and visible keyboard focus; muted styling does not mean dimming usable controls. Preserve +the established shell regions and reserve decorative color for meaningful state. New-task panes omit a duplicate local title. Plugin actions above the composer or transcript are flat named Ghost controls with descriptions on hover or keyboard focus, rather than nested cards. + +The compact composer and document-mode control bar use a constant raised surface. The right tool panel floats within an 8px gutter using the shared raised Card, retaining resize and collapse behavior. Gutters are included in its reserved width and disappear when closed. The main workspace uses the same 16px rounded, clipped boundary and 8px outer inset. Adjacent workspace and dock surfaces share one 8px gap instead of summing their margins. Checkout context has an 8px inset within the input width; related plugin/context rows use 8px vertical spacing. The main workspace, right tool panel and composer share the same constant raised shadow token: 0 2px 8px, 4% opacity in light mode and 12% in dark mode. Sidebars retain flat elevation. + ## Application rules Update tokens/resolver and shared primitives before pages. The shared Button, Input, Textarea, Select, menus, Command, Tabs and navigation rows own interaction visuals. Feature components own content and layout only. Existing callers using a legacy outline Button are treated as neutral secondary controls until API migration; they do not introduce an outlined visual system. diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index 400e313e..a3c2050b 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -35,6 +35,7 @@ import { Folder, FolderPlus, PanelLeft, + Search, SquareKanban, } from "@/components/ui/icons"; import { Input } from "@/components/ui/input"; @@ -453,7 +454,7 @@ import { SettingsPage } from "./settings/SettingsPage"; import type { SettingsTab } from "./settings/SettingsPage"; import { needsMeCount } from "./sidebar/missionControl.ts"; import { MissionControlDialog } from "./sidebar/MissionControl.tsx"; -import { SessionRail } from "./sidebar/SessionRail"; +import { APP_NAV_WIDTH, SessionRail } from "./sidebar/SessionRail"; import type { CanvasBlockRuntime } from "./skillInline"; import { associateTaskSession, @@ -701,7 +702,7 @@ function slug(name: string): string { .replaceAll(/^-|-$/gu, ""); } -/** A header icon with a tooltip — the always-visible way into a dock surface. */ +/** A window-strip action with its accessible name and current shortcut. */ function IconAction({ icon: Icon, label, @@ -1511,6 +1512,8 @@ export default function App() { const [activeFile, setActiveFile] = useState(null); const [fileReveal, setFileReveal] = useState(null); const fileRevealRequestRef = useRef(0); + const [dockTitlebarHost, setDockTitlebarHost] = + useState(null); const [dockWidth, setDockWidth] = usePersistedNumber( "codetwo.dockWidth", 440 @@ -1528,7 +1531,7 @@ export default function App() { const appliedRailWidth = Math.min(420, Math.max(220, railWidth)); const [viewportWidth, setViewportWidth] = useState(() => window.innerWidth); const narrowLayout = shouldOverlayRailForWorkspace( - viewportWidth, + viewportWidth - APP_NAV_WIDTH, appliedRailWidth ); const [narrowRailOpen, setNarrowRailOpen] = useState(false); @@ -1546,7 +1549,7 @@ export default function App() { }, [narrowLayout]); const dockForcesRailOverlay = dockTab !== null && - shouldOverlayRailForDock(viewportWidth, appliedRailWidth); + shouldOverlayRailForDock(viewportWidth - APP_NAV_WIDTH, appliedRailWidth); const railOverlay = narrowLayout || dockForcesRailOverlay; const wasDockRailOverlayRef = useRef(dockForcesRailOverlay); useLayoutEffect(() => { @@ -1570,13 +1573,9 @@ export default function App() { setShowFeishu(false); setShowTaskBoard(true); if (railOverlay) setNarrowRailOpen(false); - else if (railCollapsed) setRailCollapsedRaw(0); }; - // Full-page document is *the* mode of this app, not a temporary state it visits — it's what - // sets a document-first tool apart from a chat box, so it is also the default. Nothing takes it - // away on your behalf; the composer's ⤢ button, the grip double-click and Mod+Shift+E change it, - // and the choice persists. - const [docModeRaw, setDocModeRaw] = usePersistedNumber("codetwo.docMode", 1); + // New installs start with the bottom composer; the document toggle preserves the user's choice. + const [docModeRaw, setDocModeRaw] = usePersistedNumber("codetwo.docMode", 0); const docMode = docModeRaw !== 0; const setDocMode = (v: boolean) => setDocModeRaw(v ? 1 : 0); const mainRef = useRef(null); @@ -5889,7 +5888,6 @@ export default function App() { setShowFeishu(false); setShowPluginManager(true); if (railOverlay) setNarrowRailOpen(false); - else if (railCollapsed) setRailCollapsedRaw(0); }; const openPluginManager = () => { openPluginManagerFor(null); @@ -5981,7 +5979,6 @@ export default function App() { setShowFeishu(false); setShowAutomations(true); if (railOverlay) setNarrowRailOpen(false); - else if (railCollapsed) setRailCollapsedRaw(0); }; const openPullRequests = () => { @@ -5997,7 +5994,6 @@ export default function App() { readPullRequestTasks(); setShowPullRequests(true); if (railOverlay) setNarrowRailOpen(false); - else if (railCollapsed) setRailCollapsedRaw(0); }; const openDocker = () => { @@ -6008,7 +6004,6 @@ export default function App() { setShowFeishu(false); setShowDocker(true); if (railOverlay) setNarrowRailOpen(false); - else if (railCollapsed) setRailCollapsedRaw(0); }; const openFeishu = () => { @@ -6019,7 +6014,6 @@ export default function App() { setShowDocker(false); setShowFeishu(true); if (railOverlay) setNarrowRailOpen(false); - else if (railCollapsed) setRailCollapsedRaw(0); }; const openSourceControl = () => { @@ -7725,20 +7719,55 @@ export default function App() { setSceneEditorRequest(null); }; - const railExpandAction = displayedRailCollapsed ? ( - - ) : undefined; - return (
+ {!showSettings && !(showSceneStudio && scenesSurfaceEnabled) && ( +
+
+ setShowPalette(true)} + /> + +
+
+ + {showTaskBoard || + showPluginManager || + showAutomations || + showPullRequests || + showDocker || + showFeishu + ? null + : activeTitle} + +
+
+
+ )} {/* Settings takes the whole window — its own nav rail replaces the session rail, and the Back row at its foot is the way home. */} {showSettings ? ( @@ -7805,7 +7834,7 @@ export default function App() { ) : ( // page-in makes the return from settings (which remounts this whole subtree) a transition // rather than a cut, and doubles as the app's own opening animation. -
+
{/* ---------------- sessions rail ---------------- */} {railOverlay && narrowRailOpen && ( - } - /> - - {projects.length > 0 && ( - <> - - {projects.map((project) => ( - - selectProject(project.path) +
+

+ {activeProjectName == null || + activeProjectName === "" ? ( + t("transcript.greeting") + ) : ( + <> + {t("transcript.greetingIn")}{" "} + + - - - {project.name} - - {project.path === - activeProject && } - - ))} - - - - )} - void addProjectFolder()} - > - - {t("rail.addProject")} - - - {" "} - {t("transcript.greetingEnd")} -

+ {activeProjectName ?? + t("rail.noProject")} + + } + /> + + {projects.length > 0 && ( + <> + + {projects.map((project) => ( + + selectProject( + project.path + ) + } + > + + + {project.name} + + {project.path === + activeProject && ( + + )} + + ))} + + + + )} + + void addProjectFolder() + } + > + + {t("rail.addProject")} + + + {" "} + {t("transcript.greetingEnd")} + + )} + +
)} {/* An archived chat reads, but doesn't run: the composer yields its slot to this notice until the session is restored. The composer stays mounted (hidden) — unmounting @@ -8880,7 +8935,6 @@ export default function App() { >
diff --git a/apps/desktop/src/components/ui/button.tsx b/apps/desktop/src/components/ui/button.tsx index c036e170..e198a2a3 100644 --- a/apps/desktop/src/components/ui/button.tsx +++ b/apps/desktop/src/components/ui/button.tsx @@ -33,11 +33,11 @@ const buttonVariants = cva( xs: "h-control-mini rounded-control text-metadata gap-1 px-2 has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3", sm: "h-control-mini gap-inline px-surface-inset has-[>svg]:px-module-inset", lg: "h-control-field rounded-control px-6 has-[>svg]:px-4", - icon: "size-control", + icon: "size-control rounded-full", "icon-xs": - "size-control-mini rounded-control [&_svg:not([class*='size-'])]:size-3", - "icon-sm": "size-control", - "icon-lg": "size-control-field", + "size-control-mini rounded-full [&_svg:not([class*='size-'])]:size-3", + "icon-sm": "size-control rounded-full", + "icon-lg": "size-control-field rounded-full", row: "min-h-navigation-row gap-module-inset px-module-inset py-control-group [&_[data-slot=navigation-row-meta]]:text-callout [&_[data-slot=navigation-row-meta]]:text-muted-foreground [&_[data-slot=selectable-row-description]]:text-callout [&_[data-slot=selectable-row-description]]:text-muted-foreground [&_[data-slot=selectable-row-meta]]:text-callout [&_[data-slot=selectable-row-meta]]:text-muted-foreground w-full justify-start text-start font-normal whitespace-normal", }, focusStyle: { diff --git a/apps/desktop/src/design/tokens.css b/apps/desktop/src/design/tokens.css index e47cf1b9..1d652da4 100644 --- a/apps/desktop/src/design/tokens.css +++ b/apps/desktop/src/design/tokens.css @@ -226,6 +226,8 @@ --ds-control-field: var(--ds-foundation-control-36); --ds-navigation-row-height: var(--ds-foundation-control-32); --ds-layout-titlebar-height: var(--ds-foundation-chrome-46); + --ds-workspace-windowbar-height: var(--ds-foundation-control-32); + --ds-workspace-header-height: var(--ds-foundation-chrome-40); /* Semantic motion. */ --ds-motion-feedback: var(--ds-foundation-motion-120); @@ -302,7 +304,7 @@ --ds-focus-width: var(--ds-foundation-space-2); --ds-focus-offset: var(--ds-foundation-space-2); --ds-elevation-surface: none; - --ds-elevation-raised: 0 4px 12px rgb(16 24 40 / 8%); + --ds-elevation-raised: 0 2px 8px rgb(16 24 40 / 4%); --ds-elevation-modal: 0 12px 36px rgb(16 24 40 / 12%); /* Shared material for menu-like transient layers. It remains mostly opaque so text contrast is @@ -396,7 +398,7 @@ --ds-color-chart-5: var(--ds-foundation-light-chart-5); --ds-color-chart-6: var(--ds-foundation-light-chart-6); --ds-elevation-surface: none; - --ds-elevation-raised: 0 4px 12px rgb(16 24 40 / 8%); + --ds-elevation-raised: 0 2px 8px rgb(16 24 40 / 4%); --ds-elevation-modal: 0 12px 36px rgb(16 24 40 / 12%); --ds-material-raised-background: color-mix( in oklch, @@ -465,7 +467,7 @@ --ds-color-chart-5: var(--ds-foundation-dark-chart-5); --ds-color-chart-6: var(--ds-foundation-dark-chart-6); --ds-elevation-surface: none; - --ds-elevation-raised: 0 4px 14px rgb(0 0 0 / 22%); + --ds-elevation-raised: 0 2px 8px rgb(0 0 0 / 12%); --ds-elevation-modal: 0 16px 40px rgb(0 0 0 / 28%); --ds-material-raised-background: color-mix( in oklch, diff --git a/apps/desktop/src/design/ui-lab/UiLab.tsx b/apps/desktop/src/design/ui-lab/UiLab.tsx index 3a7664ed..79df7f07 100644 --- a/apps/desktop/src/design/ui-lab/UiLab.tsx +++ b/apps/desktop/src/design/ui-lab/UiLab.tsx @@ -316,51 +316,56 @@ function ConversationFixture() { } function PullRequestDockScenario() { + const [titlebarHost, setTitlebarHost] = useState(null); const [tab, setTab] = useState("pull-request"); return ( -
- - Execution trajectory fixture

, - browser:

Browser fixture

, - terminal:

Terminal fixture

, - "side-chat":

Side chat fixture

, - files:

Files fixture

, - git:

Git fixture

, - "pull-request": ( - -
- -
-
- ), - }} - onClose={() => { - /* empty */ - }} - onTab={setTab} - onWidth={() => { - /* empty */ - }} - open - tab={tab} - width={420} - /> +
+
+
+ + Execution trajectory fixture

, + browser:

Browser fixture

, + terminal:

Terminal fixture

, + "side-chat":

Side chat fixture

, + files:

Files fixture

, + git:

Git fixture

, + "pull-request": ( + +
+ +
+
+ ), + }} + onClose={() => { + /* empty */ + }} + onTab={setTab} + onWidth={() => { + /* empty */ + }} + open + tab={tab} + width={420} + /> +
); diff --git a/apps/desktop/src/dock/Dock.tsx b/apps/desktop/src/dock/Dock.tsx index 69be7ef5..4771eef6 100644 --- a/apps/desktop/src/dock/Dock.tsx +++ b/apps/desktop/src/dock/Dock.tsx @@ -1,7 +1,10 @@ import { useEffect, useRef, useState, useCallback } from "react"; import type { ReactNode } from "react"; +import { createPortal } from "react-dom"; +import { NavigationRow } from "@/components/business/navigation-row"; import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; import { Activity, FolderTree, @@ -42,7 +45,7 @@ const DOCK_SURFACES = [ "pull-request", ] as const satisfies readonly DockSurface[]; -/** The picker's cards, in the order a coding session tends to want them. */ +/** Panel choices share one list order across picker and tabs. */ interface DockSurfaceDefinition { id: DockSurface; icon: typeof Globe; @@ -53,6 +56,8 @@ interface DockSurfaceDefinition { interface DockProps { /** Whether the dock is expanded. It stays mounted while closed so shells survive. */ open: boolean; + /** Shell-owned titlebar destination; null until the window strip mounts. */ + titlebarHost: HTMLElement | null; /** null while closed; the last surface stays rendered underneath the collapse animation. */ tab: DockTab | null; onTab: (surface: DockSurface) => void; @@ -135,6 +140,7 @@ export function shouldOverlayRailForDock( /** Right-side container for navigation, sizing, animation, and caller-supplied surface content. */ export function Dock({ open, + titlebarHost, tab, onTab, onClose, @@ -221,32 +227,75 @@ export function Dock({ }, }); - const renderSurfaceCard = ({ + const renderSurfaceRow = ({ id, icon: Icon, titleKey, descKey, }: DockSurfaceDefinition) => ( - + label={t(titleKey)} + leading={} + tooltip={t(descKey)} + onSelect={() => onTab(id)} + /> ); + const header = open ? ( +
+
+ {shown === "home" ? ( +
+ ) : ( + + {visibleSurfaces.map(({ id, icon: Icon, titleKey }) => ( + + + {t(titleKey)} + {autoTab === id && ( + + )} + + ))} + + )} + +
+
+ ) : null; + const titlebar = titlebarHost ? createPortal(header, titlebarHost) : null; + return ( ); } diff --git a/apps/desktop/src/files/FileDockContent.tsx b/apps/desktop/src/files/FileDockContent.tsx index d2c281da..40cb1597 100644 --- a/apps/desktop/src/files/FileDockContent.tsx +++ b/apps/desktop/src/files/FileDockContent.tsx @@ -48,60 +48,65 @@ export function FileDockContent({ data-file-dock-view={showTree ? "browser" : "editor"} >
-
- setBrowsing((current) => !current)} + {(openFiles.length > 0 || !showTree) && ( +
- - - {openFiles.map((path) => { - const name = path.split("/").pop() ?? path; - const active = path === activeFile; - return ( - - ); - })} -
+ title={path} + className={cn( + "group px-module-inset text-metadata h-control relative max-w-48 shrink-0 gap-1.5", + active + ? "text-foreground" + : "text-muted-foreground hover:text-foreground" + )} + > + + {name} + {cwd != null && + cwd !== "" && + dirtyPaths.has(dirtyKey(cwd, path)) && ( + + )} + { + event.stopPropagation(); + onCloseFile(path); + }} + /> + {active && ( + + )} + + ); + })} +
+ )} {showTree ? ( - {/* The shared panel strip matches the viewer's file tabs across one continuous separator. */} -
+
setFilter(e.target.value)} @@ -476,7 +476,7 @@ export function FilePanel({ label={t("files.newFile")} variant="ghost" size="icon" - className="size-7 shrink-0" + className="shrink-0" disabled={cwd == null || cwd === ""} onClick={() => setDraft({ kind: "new-file", parent: "", value: "" })} > @@ -486,7 +486,7 @@ export function FilePanel({ label={t("files.newFolder")} variant="ghost" size="icon" - className="size-7 shrink-0" + className="shrink-0" disabled={cwd == null || cwd === ""} onClick={() => setDraft({ kind: "new-folder", parent: "", value: "" }) @@ -498,13 +498,12 @@ export function FilePanel({ label={t("files.refresh")} variant="ghost" size="icon" - className="size-7 shrink-0" + className="shrink-0" onClick={() => void reload()} >
-
diff --git a/apps/desktop/src/i18n/strings.ts b/apps/desktop/src/i18n/strings.ts index e4dd4599..545df7a6 100644 --- a/apps/desktop/src/i18n/strings.ts +++ b/apps/desktop/src/i18n/strings.ts @@ -73,8 +73,6 @@ export const en = { // rail — projects "rail.noProject": "No project selected", - "rail.projectsEmpty": - "No projects yet. Add a directory to work in — sessions and git status follow it.", "rail.addProject": "Add a project…", "rail.renameProject": "Rename", "rail.removeProject": "Remove from list (chats stay)", @@ -1954,8 +1952,7 @@ export const en = { "dock.reviewCommit": "Review & commit", "dock.workingTree": "Working tree", "dock.files": "Files", - "dock.openSurface": "Panel modules", - "dock.openSurfaceHint": "Choose a module for the right panel.", + "dock.openSurface": "Open a panel", "dock.terminalDesc": "Start a shell in this workspace.", "dock.browserDesc": "Open a local app or URL.", "dock.filesDesc": "Browse and edit workspace files.", @@ -3079,8 +3076,6 @@ export const zhCN: Record = { "app.name": "C2", "rail.noProject": "未选择项目", - "rail.projectsEmpty": - "还没有项目。添加一个工作目录——会话和 Git 状态都会跟着它走。", "rail.addProject": "添加项目…", "rail.renameProject": "重命名", "rail.removeProject": "从列表移除(聊天记录会保留)", @@ -4808,8 +4803,7 @@ export const zhCN: Record = { "dock.reviewCommit": "审查并提交", "dock.workingTree": "工作区改动", "dock.files": "文件", - "dock.openSurface": "工作模块", - "dock.openSurfaceHint": "选择要在右侧面板中打开的模块。", + "dock.openSurface": "打开面板", "dock.terminalDesc": "在这个工作区启动一个 shell。", "dock.browserDesc": "打开本地应用或网址。", "dock.filesDesc": "浏览、编辑工作区的文件。", diff --git a/apps/desktop/src/plugins/PluginUiSlot.tsx b/apps/desktop/src/plugins/PluginUiSlot.tsx index c094071a..f4f0de31 100644 --- a/apps/desktop/src/plugins/PluginUiSlot.tsx +++ b/apps/desktop/src/plugins/PluginUiSlot.tsx @@ -126,37 +126,42 @@ export function PluginUiSlot({ ); } - if (slot === "transcript.before") { + if (slot === "transcript.before" || slot === "composer.above") { return (
-
+
{contributions.map((contribution) => { const key = `${contribution.pluginId}:${contribution.id}`; return ( - + + void invoke(contribution)} + > + {busy === key ? ( + + ) : ( + + )} + {contribution.label} + + } + /> + + {contribution.description || contribution.label} + + ); })}
@@ -206,51 +211,6 @@ export function PluginUiSlot({ ); } - if (slot === "composer.above") { - return ( -
-
- {contributions.map((contribution) => { - const key = `${contribution.pluginId}:${contribution.id}`; - return ( -
-
- ); - })} -
-
- ); - } - const unhandledSlot: never = slot; return unhandledSlot; } diff --git a/apps/desktop/src/session/Composer.tsx b/apps/desktop/src/session/Composer.tsx index 9b1f2065..709152d4 100644 --- a/apps/desktop/src/session/Composer.tsx +++ b/apps/desktop/src/session/Composer.tsx @@ -93,8 +93,6 @@ interface ComposerProps { dirty: number; onOpen: () => void; } | null; - /** Empty-thread centre stage: the card narrows to the reference's hero measure. */ - hero?: boolean; /** Full-page authoring: the document takes the whole column and the transcript steps aside. */ docMode: boolean; onDocMode: (v: boolean) => void; @@ -237,7 +235,7 @@ export function CheckoutBar({ return (
void; modelChangeDisabled?: boolean; - /** The checkout bar already owns this choice when it is rendered below the composer. */ + /** The checkout bar already owns this choice when it is rendered above the composer. */ showWorktreePicker?: boolean; }) { const t = useT(); @@ -1495,7 +1493,6 @@ export function Composer({ children, config, checkout, - hero, docMode, onDocMode, boundsRef, @@ -1894,7 +1891,7 @@ export function Composer({ "composer-mode-transition flex flex-col", // min-w-0: in document mode the composer sits in a row beside the transcript panel and // must be able to shrink, or the panel gets pushed off the module's edge. - docMode ? "min-h-0 min-w-0 flex-1" : "shrink-0 px-6 pt-3 pb-6" + docMode ? "min-h-0 min-w-0 flex-1" : "shrink-0 px-6 pt-2 pb-6" )} >
+ {/* Execution location and source control are adjacent but distinct: changing where a fresh + session runs must never be confused with inspecting the current branch. */} + {!docMode && checkout && ( + + )} {/* No `overflow-hidden`: BlockNote's drag/insert handles render just outside the text column, and clipping them takes the block gutter away. The compact card paints its own surface so its background can never drift away from the editor in WebKit. */} @@ -1916,17 +1918,13 @@ export function Composer({ ? // Expanded, the composer *is* the page: no card, no border, the app's own surface. // `relative` anchors the floating control bar below. "min-h-0 flex-1" - : "rounded-composer bg-card shadow-surface duration-feedback ease-enter focus-within:shadow-raised transition-shadow" + : "rounded-composer bg-raised shadow-raised" )} >
@@ -2038,7 +2036,7 @@ export function Composer({ className={cn( "flex min-w-0 items-center gap-0.5", docMode - ? "rounded-composer bg-surface shadow-surface pointer-events-auto mx-auto w-full max-w-3xl p-2" + ? "rounded-composer bg-raised shadow-raised pointer-events-auto mx-auto w-full max-w-3xl p-2" : // Keep every outer edge 8px from the controls. The 24px surface radius then // shares its bottom-right centre with the circular send/stop control. "p-2" @@ -2048,12 +2046,6 @@ export function Composer({
- - {/* Execution location and source control are adjacent but distinct: changing where a fresh - session runs must never be confused with inspecting the current branch. */} - {!docMode && checkout && ( - - )}
); diff --git a/apps/desktop/src/session/SessionHeaderActions.tsx b/apps/desktop/src/session/SessionHeaderActions.tsx index 8cf4ce99..9dfdc2e2 100644 --- a/apps/desktop/src/session/SessionHeaderActions.tsx +++ b/apps/desktop/src/session/SessionHeaderActions.tsx @@ -128,8 +128,9 @@ export function SessionHeaderActions({ type="button" variant="ghost" size="compact" - className="session-header-action-main bg-fill-rest text-foreground hover:bg-fill-hover hover:text-foreground" + className="session-header-action-main" aria-label={t("header.addAction")} + title={t("header.addAction")} onClick={onAddAction} > onRunAction?.(action)} @@ -216,8 +217,9 @@ export function SessionHeaderActions({ type="button" variant="ghost" size="compact" - className="session-header-action-main bg-fill-rest text-foreground hover:bg-fill-hover hover:text-foreground" + className="session-header-action-main" aria-label={t("header.open")} + title={t("header.open")} >
); diff --git a/apps/desktop/src/sidebar/SessionRail.tsx b/apps/desktop/src/sidebar/SessionRail.tsx index 54b49ade..64426e04 100644 --- a/apps/desktop/src/sidebar/SessionRail.tsx +++ b/apps/desktop/src/sidebar/SessionRail.tsx @@ -9,7 +9,6 @@ import { } from "react"; import type { HTMLAttributes, ReactElement, ReactNode } from "react"; -import { NavigationRow } from "@/components/business/navigation-row"; import { ActivityOrb } from "@/components/ui/activity-orb"; import { Button } from "@/components/ui/button"; import { @@ -68,13 +67,12 @@ import { GitPullRequest, GripVertical, Hash, + MessageSquare, MessageSquarePlus, MoreHorizontal, - PanelLeft, Pencil, Pin, Plus, - Search, Settings, SquareKanban, SquarePen, @@ -178,6 +176,8 @@ const projectRowSensors = [ KeyboardSensor, ]; +export const APP_NAV_WIDTH = 48; + function RailUtilityButton({ label, selected = false, @@ -197,25 +197,21 @@ function RailUtilityButton({ render={ } /> - {label} + {label} ); } @@ -265,6 +261,7 @@ export function SessionRail({ runningSessions, onSelect, onNew, + onOpenTasks, quickChatOpen, onToggleQuickChat, onRename, @@ -280,12 +277,9 @@ export function SessionRail({ deviceConnectionsOpen, onOpenDeviceConnections, newHint, - searchHint, - onOpenSearch, onOpenSettings, collapsed, overlay, - onToggleCollapse, width, onWidth, taskBoardOpen, @@ -318,6 +312,7 @@ export function SessionRail({ onSelect: (id: string) => void; /** Opens the default Task-owned draft. */ onNew: () => void; + onOpenTasks: () => void; /** App-lifetime quick chat that stays outside the tracked task list. */ quickChatOpen: boolean; onToggleQuickChat: () => void; @@ -341,15 +336,12 @@ export function SessionRail({ deviceConnectionsOpen: boolean; onOpenDeviceConnections: () => void; newHint: string; - /** The palette's shortcut, shown in the search box. */ - searchHint: string; - onOpenSearch: () => void; + /** The palette's shortcut, included in the search tooltip. */ onOpenSettings: () => void; /** Collapsed: the rail animates to zero width; the main header grows an expand button. */ collapsed: boolean; /** Narrow layouts take the rail out of the flex row and show it above the session column. */ overlay: boolean; - onToggleCollapse: () => void; /** Rail width in px — dragged by the right-edge grip, persisted by the caller. */ width: number; onWidth: (n: number) => void; @@ -2136,459 +2128,447 @@ export function SessionRail({ }; return ( -
- - + + + ); } diff --git a/apps/desktop/src/styles.css b/apps/desktop/src/styles.css index 06db3cb5..5627b461 100644 --- a/apps/desktop/src/styles.css +++ b/apps/desktop/src/styles.css @@ -525,7 +525,24 @@ html[data-appearance-pointer-cursors="false"] ); box-shadow: inset calc(-1 * var(--hairline-width)) 0 0 var(--sidebar-border); } - .session-rail [data-rail-header] { + /* Keep native controls in their own strip; compact only the workspace headers. */ + .workspace-titlebar { + height: var(--ds-workspace-windowbar-height); + } + .workspace-shell { + --ds-titlebar-height: var(--ds-workspace-header-height); + } + /* Native controls occupy the window strip, independently of the task sidebar. */ + html[data-platform="macos"] .workspace-shell .window-controls-safe-main, + html[data-platform="macos"] + .workspace-shell + .window-controls-safe-compact-main { + padding-left: 1rem; + } + .workspace-shell .session-rail[data-overlay] { + background: var(--sidebar); + } + .workspace-shell .glass-rail { box-shadow: none; } .session-rail:not([data-dragging]) { @@ -586,10 +603,23 @@ html[data-appearance-pointer-cursors="false"] .dock-content-split { box-shadow: inset 1px 0 var(--border); } + .dock-tabs { + scrollbar-width: none; + } + .dock-tabs::-webkit-scrollbar { + display: none; + } @container dock (max-width: 359px) { .dock-tab-label { display: none; } + [data-slot="tabs-trigger"][data-active] .dock-tab-label { + display: inline; + } + .dock-tabs [data-slot="tabs-trigger"]:not([data-active]) { + width: var(--ds-control-mini); + padding-inline: 0; + } } /* One managed material for menu-like transient layers. Shared primitives and the few external positioning adapters use this class; feature code never chooses opacity or blur values. */ @@ -1708,41 +1738,6 @@ pre { } } -/* The dock picker changes anatomy at its real container width, independently of the window. */ -.dock-surface-picker { - padding: 18vh 1.5rem 1.5rem; -} - -.dock-surface-grid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 0.75rem; - margin-top: 1.5rem; -} - -.dock-surface-card { - min-width: 0; - flex-direction: column; -} - -.dock-surface-card > span { - min-width: 0; -} - -@container dock (max-width: 359px) { - .dock-surface-picker { - padding-top: 1.5rem; - } - - .dock-surface-grid { - grid-template-columns: minmax(0, 1fr); - } - - .dock-surface-card { - flex-direction: row; - } -} - /* Header actions retain their accessible names while shedding labels before the title collapses. */ .session-header { container: session-header / inline-size; @@ -1783,6 +1778,8 @@ pre { .session-header-context-main, .session-header-plugin-action, .session-header-layout-main { + height: var(--ds-control-normal); + border-radius: 50%; flex: 0 0 var(--ds-control-normal); width: var(--ds-control-normal); min-width: var(--ds-control-normal); @@ -1792,12 +1789,13 @@ pre { padding-left: 0; } -@container session-header (max-width: 36rem) { +@container session-header (max-width: 48rem) { .session-header-action-label { display: none; } .session-header-action-main { + height: var(--ds-control-normal); flex: 0 0 var(--ds-control-normal); width: var(--ds-control-normal); min-width: var(--ds-control-normal); @@ -1807,6 +1805,10 @@ pre { padding-left: 0; background: transparent; } + + .session-header-action-main:not(.rounded-r-none) { + border-radius: 50%; + } } @container session-header (max-width: 28rem) { @@ -1830,3 +1832,15 @@ pre { overflow: auto; } } + +/* Inactive dock tabs become icon-only below this width; match their circular hit surface. */ +@container dock (max-width: 359px) { + .dock-tabs [data-slot="tabs-trigger"]:not([data-active]) { + border-radius: 50%; + } +} + +/* Tabs share the window-strip icon baseline, leaving 2px above and below. */ +[data-dock-titlebar] .dock-tabs { + height: var(--ds-control-mini); +} diff --git a/apps/desktop/tests/checkoutPickerRendered.test.tsx b/apps/desktop/tests/checkoutPickerRendered.test.tsx index e0e7adb9..0b7d9d0f 100644 --- a/apps/desktop/tests/checkoutPickerRendered.test.tsx +++ b/apps/desktop/tests/checkoutPickerRendered.test.tsx @@ -138,8 +138,8 @@ describe("CheckoutBar", () => { const checkoutBar = rendered.container.querySelector( "[data-checkout-bar]" ); - expect(checkoutBar?.className).toContain("mx-page"); - expect(checkoutBar?.className).toContain("mt-2"); + expect(checkoutBar?.className).toContain("mx-2"); + expect(checkoutBar?.className).toContain("mb-2"); expect(checkoutBar?.className).not.toContain("-mt-"); const sourceControl = button( diff --git a/apps/desktop/tests/composerGeometryContract.test.ts b/apps/desktop/tests/composerGeometryContract.test.ts index 98c5ce8c..f932ef19 100644 --- a/apps/desktop/tests/composerGeometryContract.test.ts +++ b/apps/desktop/tests/composerGeometryContract.test.ts @@ -25,9 +25,7 @@ describe("composer geometry contract", () => { test("paints the compact composer on the same DOM card as its interactive content", () => { expect(composer).not.toContain("function ComposerLiquidSurface"); expect(composer).not.toContain("data-gooey-composer"); - expect(composer).toContain( - '"rounded-composer bg-card shadow-surface duration-feedback ease-enter focus-within:shadow-raised transition-shadow"' - ); + expect(composer).toContain('"rounded-composer bg-raised shadow-raised"'); }); test("keeps the circular submit control concentric with the composer corner", () => { @@ -40,7 +38,7 @@ describe("composer geometry contract", () => { expect(composer).toContain('"composer-mode-transition flex flex-col"'); expect(composer).not.toContain("reducedMotion"); expect(composer).toContain( - '"rounded-composer bg-surface shadow-surface pointer-events-auto mx-auto w-full max-w-3xl p-2"' + '"rounded-composer bg-raised shadow-raised pointer-events-auto mx-auto w-full max-w-3xl p-2"' ); expect(composer.match(/"size-8 shrink-0 rounded-full"/gu)).toHaveLength(2); expect( diff --git a/apps/desktop/tests/dockPluginGateRendered.test.tsx b/apps/desktop/tests/dockPluginGateRendered.test.tsx index 3fbed3b0..175b9150 100644 --- a/apps/desktop/tests/dockPluginGateRendered.test.tsx +++ b/apps/desktop/tests/dockPluginGateRendered.test.tsx @@ -34,6 +34,7 @@ function renderDock( { + test("portals tab controls into the shell while retaining selection and close behavior", async () => { + activateDom(); + const host = dom.document.createElement("div"); + dom.document.body.append(host); + const selected = []; + let closed = 0; + const render = (open, tab) => ( + + {}} + availableSurfaces={["trajectory", "files"]} + onTab={(value) => selected.push(value)} + onClose={() => { + closed += 1; + }} + content={{ + trajectory:
Timeline content
, + files:
File content
, + }} + /> +
+ ); + const view = mount(render(true, "trajectory")); + await flush(); + expect(view.container.querySelector("[data-dock-titlebar]")).toBeNull(); + expect(host.querySelectorAll('[role="tab"]')).toHaveLength(2); + const files = [...host.querySelectorAll('[role="tab"]')].find( + (tab) => tab.textContent === "Files" + ); + click(files); + await flush(); + expect(selected).toEqual(["files"]); + view.rerender(render(true, "files")); + await flush(); + expect(files.getAttribute("aria-selected")).toBe("true"); + expect(view.container.textContent).toContain("File content"); + expect(host.textContent).not.toContain("File content"); + click(button(host, "Close panel")); + expect(closed).toBe(1); + view.rerender(render(false, null)); + await flush(); + expect(host.querySelector("button")).toBeNull(); + view.rerender(render(true, "home")); + await flush(); + expect(host.querySelectorAll("button")).toHaveLength(1); + expect(view.container.textContent).toContain("Open a panel"); + view.unmount(); + host.remove(); + }); + test("preserves the document measure after accounting for an inline rail", () => { expect(dockMaxWidth(1280, 288)).toBe(372); expect(dockMaxWidth(800)).toBe(300); @@ -99,18 +154,22 @@ describe("Dock plugin component gate", () => { await flush(); const cards = [ - ...view.container.querySelectorAll(".dock-surface-grid > button"), + ...view.container.querySelectorAll( + '.dock-surface-list [data-slot="navigation-row"]' + ), ]; expect(cards[2]?.textContent).toContain("Terminal"); - expect(cards[3]?.getAttribute("aria-label")).toBe("Side chat"); - expect(cards[6]?.getAttribute("aria-label")).toBe("PR"); - expect( - cards.every((card) => card.classList.contains("dock-surface-card")) - ).toBe(true); - expect(cards.every((card) => card.classList.contains("bg-card"))).toBe( + expect(cards[3]?.textContent).toBe("Side chat"); + expect(cards[6]?.textContent).toBe("PR"); + expect(cards.every((card) => card.dataset.slot === "navigation-row")).toBe( true ); - expect(cards.every((card) => card.classList.contains("p-3"))).toBe(true); + expect(cards.every((card) => !card.classList.contains("bg-card"))).toBe( + true + ); + expect( + cards.every((card) => card.classList.contains("min-h-navigation-row")) + ).toBe(true); expect( cards.every((card) => !card.className.includes("ring-foreground")) ).toBe(true); @@ -149,7 +208,10 @@ describe("Dock plugin component gate", () => { const panel = view.container.querySelector('[data-dock-placement="right"]'); expect(panel).not.toBeNull(); expect(panel?.classList.contains("dock-panel-side")).toBe(true); - expect(panel?.classList.contains("border-l")).toBe(true); + expect(panel?.classList.contains("border-l")).toBe(false); + const surface = panel?.querySelector('[data-slot="card"]'); + expect(surface?.getAttribute("data-variant")).toBe("raised"); + expect(surface?.classList.contains("m-2")).toBe(true); expect(panel?.getAttribute("style")).toMatch(/^width: \d+px;$/u); expect(panel?.getAttribute("style")).not.toContain("height"); expect( @@ -184,7 +246,8 @@ describe("Dock plugin component gate", () => { await flush(); expect(home.container.textContent).toContain("Execution trajectory"); - expect(home.container.textContent).toContain( + expect(home.container.textContent).toContain("Open a panel"); + expect(home.container.textContent).not.toContain( "Inspect the session timeline" ); home.unmount(); diff --git a/apps/desktop/tests/fileDockContentRendered.test.tsx b/apps/desktop/tests/fileDockContentRendered.test.tsx new file mode 100644 index 00000000..36b6dcca --- /dev/null +++ b/apps/desktop/tests/fileDockContentRendered.test.tsx @@ -0,0 +1,77 @@ +// @ts-nocheck +import { afterEach, expect, test } from "bun:test"; + +import { + activateDom, + button, + click, + dom, + flush, + mount, +} from "./domTestHarness"; + +activateDom(); +const { I18nProvider } = await import("../src/i18n"); +const { FileDockContent } = await import("../src/files/FileDockContent"); + +afterEach(() => dom.document.body.replaceChildren()); + +function renderContent( + openFiles = [], + activeFile = null, + onActiveFile = () => {} +) { + return ( + + {}} + onInsertFile={() => {}} + onOpenFile={() => {}} + onSendText={() => {}} + /> + + ); +} + +test("empty file browser has one toolbar without a redundant navigation row", async () => { + const view = mount(renderContent()); + await flush(); + expect(view.container.querySelector("[data-file-tabs]")).toBeNull(); + expect(view.container.querySelectorAll("[data-file-toolbar]")).toHaveLength( + 1 + ); + expect( + view.container.querySelector('input[aria-label="Search files"]') + ).not.toBeNull(); + expect(button(view.container, "New file").disabled).toBe(true); + expect(button(view.container, "New folder").disabled).toBe(true); + expect(button(view.container, "Rescan the workspace")).toBeTruthy(); + view.unmount(); +}); + +test("open documents retain tab selection and a path back to browsing", async () => { + const selected = []; + const view = mount( + renderContent(["README.md"], "README.md", (path) => selected.push(path)) + ); + await flush(); + expect(view.container.querySelector("[data-file-tabs]")).not.toBeNull(); + expect(view.container.querySelector("[data-file-toolbar]")).toBeNull(); + click(button(view.container, "Browse workspace files")); + await flush(); + expect(view.container.querySelector("[data-file-toolbar]")).not.toBeNull(); + click(button(view.container, "README.md")); + await flush(); + expect(selected).toEqual(["README.md"]); + expect(view.container.querySelector("[data-file-toolbar]")).toBeNull(); + view.rerender(renderContent()); + await flush(); + expect(view.container.querySelector("[data-file-tabs]")).toBeNull(); + expect(view.container.querySelector("[data-file-toolbar]")).not.toBeNull(); + view.unmount(); +}); diff --git a/apps/desktop/tests/pluginManagerRendered.test.tsx b/apps/desktop/tests/pluginManagerRendered.test.tsx index 0ad4a3ed..abc939a0 100644 --- a/apps/desktop/tests/pluginManagerRendered.test.tsx +++ b/apps/desktop/tests/pluginManagerRendered.test.tsx @@ -416,7 +416,8 @@ describe("PluginManagerPage", () => { ?.getAttribute("aria-label") ).toBe("Plugin actions"); expect(view.container.textContent).toContain("Review workspace"); - click(button(view.container, "Run")); + expect(view.container.textContent).not.toContain(contribution.description); + click(button(view.container, "Review Tools: Review workspace")); await flush(); expect(invoked).toEqual([contribution]); @@ -466,7 +467,7 @@ describe("PluginManagerPage", () => { const action = region.querySelector( `button[aria-label="Review Tools: Action ${order}"]` - ) ?? button(region, order === 3 ? "Run" : `Action ${order}`); + ) ?? button(region, `Action ${order}`); expect(action).not.toBeNull(); if (slot === "session.header") { expect(action.dataset.variant).toBe("ghost"); diff --git a/apps/desktop/tests/sessionHeaderActionsRendered.test.tsx b/apps/desktop/tests/sessionHeaderActionsRendered.test.tsx index 7dc4a2ae..6072ee51 100644 --- a/apps/desktop/tests/sessionHeaderActionsRendered.test.tsx +++ b/apps/desktop/tests/sessionHeaderActionsRendered.test.tsx @@ -107,7 +107,7 @@ describe("SessionHeaderActions", () => { view.unmount(); }); - test("renders independent filled icon-and-label primary actions", () => { + test("renders quiet icon-and-label actions with shared interaction states", () => { activateDom(); const { view } = renderActions(); const group = view.container.querySelector(".session-header-actions"); @@ -122,9 +122,8 @@ describe("SessionHeaderActions", () => { for (const label of ["Add action", "Open", "Review changes"]) { const action = button(view.container, label); expect(action.dataset.variant).toBe("ghost"); - expect(action.classList.contains("bg-fill-rest")).toBe(true); + expect(action.classList.contains("bg-fill-rest")).toBe(false); expect(action.classList.contains("hover:bg-fill-hover")).toBe(true); - expect(action.classList.contains("text-foreground")).toBe(true); expect( action.querySelector(".session-header-action-icon") ).not.toBeNull(); @@ -217,7 +216,7 @@ describe("SessionHeaderActions", () => { ); expect( button(view.container, "Source control unavailable").classList.contains( - "disabled:opacity-60" + "disabled:opacity-50" ) ).toBe(true); expect( diff --git a/apps/desktop/tests/sessionRailRendered.test.tsx b/apps/desktop/tests/sessionRailRendered.test.tsx index 1f38fa67..d21c0767 100644 --- a/apps/desktop/tests/sessionRailRendered.test.tsx +++ b/apps/desktop/tests/sessionRailRendered.test.tsx @@ -102,12 +102,10 @@ function renderRail(overrides = {}) { deviceConnectionsOpen={false} onOpenDeviceConnections={() => {}} newHint="⌘N" - searchHint="⌘K" - onOpenSearch={() => {}} + onOpenTasks={() => {}} onOpenSettings={() => {}} collapsed={false} overlay={false} - onToggleCollapse={() => {}} width={320} onWidth={() => {}} taskBoardOpen={false} @@ -568,49 +566,22 @@ describe("SessionRail row layout", () => { view.unmount(); }); - test("keeps collapse aligned in the title row and exposes search as a labeled launcher", () => { - activateDom(); - const opened = []; - const view = renderRail({ onOpenSearch: () => opened.push("search") }); - const header = view.container.querySelector("[data-rail-header]"); - const collapse = header?.querySelector( - 'button[aria-label="Collapse the sidebar"]' - ); - const search = view.container.querySelector("[data-rail-search]"); - - expect(view.container.textContent).not.toContain("C2"); - expect(search).toBeTruthy(); - expect(header?.querySelector("[data-rail-search]")).toBeNull(); - expect(collapse).toBeTruthy(); - expect(collapse?.classList.contains("mr-2")).toBe(true); - expect(search?.textContent).toContain("Search chats"); - expect(search?.querySelector("kbd")?.textContent).toBe("⌘K"); - - click(search); - expect(opened).toEqual(["search"]); - - view.unmount(); - }); - - test("lets the search launcher stretch between equal rail insets without overflowing", () => { + test("starts the task list without a duplicate window header", () => { activateDom(); const view = renderRail(); - const search = view.container.querySelector("[data-rail-search]"); - - expect(search?.classList.contains("mx-2")).toBe(true); - expect(search?.classList.contains("w-auto")).toBe(true); - expect(search?.classList.contains("w-full")).toBe(false); - + expect(view.container.querySelector("[data-rail-header]")).toBeNull(); + expect(view.container.textContent).not.toContain("CodeTwo"); + expect(view.container.querySelector("[data-task-actions]")).not.toBeNull(); view.unmount(); }); - test("groups primary features into Codex-aligned labeled navigation rows", () => { + test("keeps labeled global navigation and utilities outside the collapsed task list", () => { activateDom(); const opened = []; const view = renderRail({ + collapsed: true, taskBoardOpen: true, - automationsOpen: true, - onNew: () => opened.push("new"), + onOpenTasks: () => opened.push("chats"), onOpenPullRequests: () => opened.push("pull-requests"), onOpenTaskBoard: () => opened.push("tasks"), onOpenAutomations: () => opened.push("scheduled"), @@ -618,77 +589,27 @@ describe("SessionRail row layout", () => { onOpenUsage: () => opened.push("usage"), onOpenSettings: () => opened.push("settings"), }); - const features = view.container.querySelector("[data-rail-features]"); - const rows = [ - ...(features?.querySelectorAll( - ':scope > [data-rail-feature="new-task"] > button:first-child, :scope > [data-rail-feature]:not([data-rail-feature="new-task"]) > [data-slot="navigation-row"]' - ) ?? []), - ]; - const sessionScroll = view.container.querySelector( - "[data-rail-session-scroll]" - ); - const utilities = view.container.querySelector("[data-rail-utilities]"); - const utilityButtons = [ - ...(utilities?.querySelectorAll( - ':scope > [data-rail-feature] > [data-slot="rail-utility-button"]' - ) ?? []), - ]; - - expect( - rows.map((row) => { - const copy = row.cloneNode(true) as HTMLElement; - copy - .querySelectorAll('[role="progressbar"] [role="presentation"]') - .forEach((node) => node.remove()); - return copy.textContent?.replaceAll(/\s+/g, " ").trim(); - }) - ).toEqual([ - "New task", + const navigation = view.container.querySelector("[data-app-navigation]"); + const sidebar = view.container.querySelector("aside"); + expect(sidebar?.getAttribute("aria-hidden")).toBe("true"); + expect(sidebar?.hasAttribute("inert")).toBe(true); + expect(sidebar?.contains(navigation)).toBe(false); + const buttons = [...navigation.querySelectorAll("button")]; + expect(buttons.map((button) => button.getAttribute("aria-label"))).toEqual([ + "Chats", "Pull requests", "Task board", "Scheduled tasks", "Plugins", - ]); - expect( - utilityButtons.map((button) => button.getAttribute("aria-label")) - ).toEqual([ "Settings", "Codex · Weekly limit · 42% left · Open Usage settings", ]); - expect(utilities?.dataset.layout).toBe("icon-toolbar"); - expect(sessionScroll?.nextElementSibling).toBe(utilities); - expect( - features - ?.querySelector( - '[data-rail-feature="task-board"] [data-slot="navigation-row"]' - ) - ?.getAttribute("aria-current") - ).toBe("page"); - expect( - features - ?.querySelector( - '[data-rail-feature="scheduled-tasks"] [data-slot="navigation-row"]' - ) - ?.getAttribute("aria-current") - ).toBe("page"); expect( - features - ?.querySelector( - '[data-rail-feature="task-board"] [data-slot="navigation-row-leading"]' - ) - ?.getAttribute("class") - ).toContain("text-current"); - expect( - features - ?.querySelector( - '[data-rail-feature="pull-requests"] [data-slot="navigation-row-leading"]' - ) - ?.getAttribute("class") - ).toContain("text-muted-foreground"); - expect(view.container.textContent).not.toContain("gpt-5.6-sol"); - for (const row of [...rows, ...utilityButtons]) click(row); + buttons.filter((button) => button.getAttribute("aria-current") === "page") + ).toEqual([buttons[2]]); + for (const button of buttons) click(button); expect(opened).toEqual([ - "new", + "chats", "pull-requests", "tasks", "scheduled", @@ -697,31 +618,14 @@ describe("SessionRail row layout", () => { "usage", ]); expect( - features?.querySelector('[data-rail-feature="mission-control"]') - ).toBeNull(); - const quotaButton = utilities?.querySelector( - '[data-rail-feature="usage"] [data-slot="rail-utility-button"]' - ); - expect(quotaButton?.getAttribute("aria-label")).toBe( - "Codex · Weekly limit · 42% left · Open Usage settings" - ); - expect(quotaButton?.querySelector('[role="progressbar"]')).toBeNull(); + view.container.querySelector("[data-rail-utilities]")?.dataset.layout + ).toBe("icon-column"); expect( - utilities?.querySelector( - '[data-rail-feature="usage"] [data-quota-provider]' - )?.dataset.quotaProvider - ).toBe("codex"); - for (const row of rows.slice(1)) { - expect(row.dataset.slot).toBe("navigation-row"); - expect(row.className).toContain("min-h-navigation-row"); - expect(row.className).toContain("rounded-control"); - } - for (const button of utilityButtons) { - expect(button.className).toContain("size-control"); - expect(button.className).toContain("rounded-full"); - expect(button.textContent?.trim()).toBe(""); - } - + sidebar?.querySelector('[data-rail-feature="new-task"]') + ).toBeTruthy(); + expect( + navigation?.querySelector('[data-rail-feature="new-task"]') + ).toBeNull(); view.unmount(); }); @@ -1507,7 +1411,7 @@ describe("SessionRail empty projects", () => { }, }); - expect(view.container.textContent).toContain("No projects yet"); + expect(view.container.textContent).not.toContain("No projects yet"); const addButton = [...view.container.querySelectorAll("button")].find( (item) => item.textContent?.trim() === "Add a project…" ); diff --git a/apps/desktop/tests/windowChromeContract.test.ts b/apps/desktop/tests/windowChromeContract.test.ts index 2a878a29..3386b471 100644 --- a/apps/desktop/tests/windowChromeContract.test.ts +++ b/apps/desktop/tests/windowChromeContract.test.ts @@ -84,7 +84,8 @@ describe("macOS window chrome contract", () => { expect(appSource).toContain("displayedRailCollapsed"); expect(appSource).toContain('"window-controls-safe-main"'); expect(appSource).toContain(': "pl-4"'); - expect(railSource).toContain("window-controls-safe-rail"); + expect(railSource).not.toContain("window-controls-safe-rail"); + expect(appSource).toContain("data-workspace-titlebar"); expect(sceneStudioSource).toContain("window-controls-safe-scene"); expect(styles).toMatch( /html\[data-platform="macos"\] \.window-controls-safe-main\s*\{[^}]*padding-left:\s*6rem/su @@ -108,7 +109,7 @@ describe("macOS window chrome contract", () => { /\.macos-window-glass \.glass-rail\s*\{[^}]*--appearance-macos-panel-tint-opacity/su ); expect(appSource).toContain( - 'className="bg-background @container/workspace flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden"' + '"bg-background rounded-card shadow-raised @container/workspace m-2 flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden"' ); }); @@ -170,12 +171,18 @@ describe("macOS window chrome contract", () => { ); }); - test("keeps the empty-session hero safely centered in constrained window heights", () => { + test("gives the empty greeting spare space above the bottom composer", () => { expect(appSource).toContain( - '"hero-scroll-shell pb-page-end order-2 min-h-0 flex-1 flex-col justify-center-safe overflow-y-auto pt-6"' + '"hero-scroll-shell order-2 min-h-0 flex-1 flex-col overflow-y-auto"' ); + expect(appSource).toContain("data-workspace-greeting"); expect(appSource).not.toContain( - '"order-2 min-h-0 flex-1 flex-col justify-center pb-page-end"' + "else if (railCollapsed) setRailCollapsedRaw(0)" + ); + expect(appSource).toContain('usePersistedNumber("codetwo.docMode", 0)'); + expect(appSource).toContain("viewportWidth - APP_NAV_WIDTH"); + expect(appSource).toContain( + "reservedWidth={railInlineWidth + APP_NAV_WIDTH}" ); }); @@ -188,12 +195,7 @@ describe("macOS window chrome contract", () => { ); }); - test("keeps the rail, workspace, and both dock states on one shared titlebar baseline", () => { - const titlebarClasses = Array.from( - dockSource.matchAll(/data-dock-titlebar[\s\S]*?className="([^"]+)"/gu), - (match) => match[1].split(/\s+/u) - ); - + test("keeps dock controls in the window strip and workspace actions in the pane header", () => { expect(styles).toMatch( /\.window-titlebar\s*\{[^}]*height:\s*var\(--ds-titlebar-height\);/su ); @@ -209,9 +211,7 @@ describe("macOS window chrome contract", () => { expect(sessionHeaderActionsSource).toContain( 'className="session-header-actions gap-inline flex shrink-0 items-center"' ); - expect(sessionHeaderActionsSource).toContain( - "session-header-action-main bg-fill-rest text-foreground hover:bg-fill-hover hover:text-foreground" - ); + expect(sessionHeaderActionsSource).toContain("session-header-action-main"); expect(styles).not.toMatch( /\.session-header-actions\s*\{[^}]*box-shadow:\s*inset 0 0 0 var\(--hairline-width\) var\(--border\);/su ); @@ -225,33 +225,25 @@ describe("macOS window chrome contract", () => { /\.session-header-context-main,[\s\S]*?\.session-header-plugin-action,[\s\S]*?\.session-header-layout-main\s*\{[^}]*width:\s*var\(--ds-control-normal\);[^}]*justify-content:\s*center;/su ); expect(styles).toMatch( - /@container session-header \(max-width: 36rem\)[\s\S]*?\.session-header-action-label\s*\{\s*display:\s*none;/su + /@container session-header \(max-width: 48rem\)[\s\S]*?\.session-header-action-label\s*\{\s*display:\s*none;/su ); expect(appSource).toMatch( / classes.includes("window-titlebar")) - ).toBe(true); - expect( - titlebarClasses.every((classes) => !classes.includes("py-2.5")) - ).toBe(true); - expect( - titlebarClasses.every((classes) => !classes.includes("border-b")) - ).toBe(true); - expect(dockSource).toContain('size="compact"'); - expect(dockSource).toContain('className="w-(--ds-control-normal) px-0"'); + expect(appSource).toContain("data-dock-titlebar-host"); + expect(appSource).toContain("titlebarHost={dockTitlebarHost}"); + expect(dockSource).toContain("createPortal(header, titlebarHost)"); + expect(dockSource).toContain( + 'height: "var(--ds-workspace-windowbar-height)"' + ); + expect(dockSource).toContain('size="icon-xs"'); expect(dockSource).toContain("onClick={onClose}"); }); diff --git a/docs/design/system.md b/docs/design/system.md index b4be9cdf..bafd50a0 100644 --- a/docs/design/system.md +++ b/docs/design/system.md @@ -182,7 +182,7 @@ The shared business set is: | `SearchField` | labelled search input, icon geometry, optional accessible clear action | Automations, Docker, Task Board, Plugin Manager, Memory, Trajectory | | `Empty` primitive | empty-state hierarchy, media, description, and action composition | Automations, Pull Requests | | `SelectableRow` | compact picker choice, visible selection mark, accessible selected/disabled state, description and metadata layout | Composer mode, memory, collaboration, worktree, provider, and model pickers; Scene picker; Checkout picker | -| `NavigationRow` | navigation/current-page state, inset focus, truncation, and optional themed tooltip | application rail and Settings navigation | +| `NavigationRow` | navigation/current-page state, inset focus, truncation, and optional themed tooltip | Settings navigation and right-panel picker | | `CompositeActionRow` | semantic full-row primary action plus independent trailing controls | Mission Control, Browser tabs, File tree | | `ChoiceRow` | labelled radio or checkbox choice, description/preview layout, selected and disabled presentation | Agent question dialog, design-system preview | | `MasterDetailRow` | selected master-list row, leading mark, description, and metadata layout | Automations, Pull Requests | @@ -247,7 +247,7 @@ Filled actions always consume an inseparable semantic pair: Primary uses `primar must reach 4.5:1 in light and dark. A page or preview shell may inherit the UI font family, but must not reset a shared control's color, font size, line height, or state styling. -Dark mode separates persistent planes with lightness, not shadow. Do not add a white hairline, +Dark mode separates persistent planes primarily with lightness; floating workspace surfaces may use the shared raised elevation. Do not add a white hairline, inner glow, or feature-local translucent glass to recover separation. ## Typography @@ -317,6 +317,8 @@ rows, and 36px for inputs, selects, and important controls. The main titlebar is strips are 40px. A taller element is content input such as Composer, not a generic large-button size. +Workspace window chrome uses a 32px drag strip and scoped 40px workspace pane headers. Right-panel tabs and its close control share the window strip through the shell titlebar host, track the dock width, and leave no inner dock header. The focused task title reuses the existing title projection in the window strip; feature pages leave that title empty. Search and the sidebar toggle live in the window strip, aligned with the expanded sidebar and reserving at least 96px before controls for native window buttons. The task list has no duplicate header or product wordmark. Below 48rem, toolbar actions retain accessible names while dropping visible labels; the selected dock tab keeps its label even when inactive tabs become icons. Settings and Scene Studio retain their own header geometry. + Session titlebar tools form one quiet monochrome toolbar. Icons and labels keep the muted foreground across rest, hover, open, and pressed states; hover may add a neutral surface and keyboard focus adds the standard ring. A real open or pressed state may use only a neutral fill. Titlebar actions @@ -334,11 +336,11 @@ Elevation communicates a real layer and never changes on hover: | role | use | | --- | --- | | `elevation-surface` | no shadow; compatibility role for cards, inputs, and persistent panels | -| `elevation-raised` | one restrained short shadow for menu, popover, tooltip | +| `elevation-raised` | 0 2px 8px, 4% light / 12% dark; one restrained short shadow for menu, popover, tooltip, floating main workspace, composer and right tool panel | | `elevation-modal` | one restrained wider shadow for dialog, blocking overlay | -Persistent titlebars, sidebars, cards, inputs, and panels are flat. They use tonal planes, spacing, -and contextual structural separators instead of floating-card shadows. Settings pages separate major groups with page-section spacing. Related rows share one tonal module +Persistent titlebars, sidebars, cards, inputs, and panels are flat by default. The workspace composer (including its document-mode control bar) and right tool panel are explicit floating surfaces: use the existing raised background and elevation, constant across focus and hover. The right panel uses the shared raised Card with an 8px outer gutter included in its reserved width; closing it leaves no gutter. The main workspace shares its 16px rounded, clipped boundary and 8px inset, using the same raised shadow as the right panel and composer. Keep its reading background unchanged. Other surfaces use tonal planes, spacing, +and contextual structural separators. Settings pages separate major groups with page-section spacing. Related rows share one tonal module with internal hairlines; they do not become a stack of individually elevated cards. Choice tiles such as appearance schemes and themes use tonal hover and a semantic selected ring without surface elevation. Popovers and dialogs retain only the restrained elevation assigned to their real layer. @@ -364,7 +366,7 @@ selected-hover and invalid are added where meaningful. The state ladder is owned - **Loading:** stable dimensions with a 14px spinner; do not swap to a differently sized control. Button variants are Primary, Secondary, Ghost, and Destructive. Secondary is a neutral tonal -surface, Ghost has no shadow, and Destructive is red only for a destructive action. Legacy Outline callers share the Secondary neutral visual treatment; no outlined visual system is added. Default and icon buttons are 32px, compact and mini buttons are 28px, and the 36px field +surface, Ghost has no shadow, and Destructive is red only for a destructive action. Legacy Outline callers share the Secondary neutral visual treatment; no outlined visual system is added. Standalone icon-size Button variants own a full circular radius. Responsive icon-only toolbar controls use equal width/height and the same circle; joined split buttons preserve their connected shape. Default and icon buttons are 32px, compact and mini buttons are 28px, and the 36px field size is reserved for an action that must align with an input or select. Size changes never introduce a different radius. Primary and Destructive own their paired foreground tokens; Secondary and Ghost stay on the neutral text hierarchy so the four variants read as one control family. diff --git a/docs/sdlc/changes/2026-09-28-codex-layout-reset/intent.md b/docs/sdlc/changes/2026-09-28-codex-layout-reset/intent.md new file mode 100644 index 00000000..50a28dd7 --- /dev/null +++ b/docs/sdlc/changes/2026-09-28-codex-layout-reset/intent.md @@ -0,0 +1,45 @@ +--- +id: 2026-09-28-codex-layout-reset +schema: 5 +stage: intent +status: accepted +owner: codex +created: 2026-09-28 +source: user +risk: medium +approved_by: chenli +approved_at: 2026-09-28 +approval_source: "User request in this chat: 参考Codex 重置 layout UX 框架, with reference screenshot." +--- + +# Intent: Codex Layout Reset + +## Intent + +Reorganize the desktop shell around the supplied Codex reference: persistent narrow app navigation, task sidebar, quiet workspace and bottom composer. Preserve task data, drafts, navigation features, provider behavior and existing pane/dock state. Bounded local UI work is authorized; no PR, native process replacement or release requested. + +Follow-up direction (same requester, 2026-09-28): “参考这个需要一点不太张扬,但又很可靠的感觉,让 UI 框架按照我们之前就 OK 的。” The supplied T3 Code image guides restrained visual hierarchy; retain the previously agreed layout and behavior. + +Further direction (same requester, 2026-09-28): “采用类似的浮动面板”. Apply restrained floating treatment to the workspace composer and right tool panel, retaining the established layout. + +Corner refinement (same requester, 2026-09-28): “面板添加圆角”. Round the main workspace to match the already rounded tool panel, keeping the composer radius. + +Shadow refinement (same requester, 2026-09-28): “统一面板的阴影”. Use one existing raised shadow for the main workspace, right tool panel and composer. + +Top chrome refinement (same requester, 2026-09-28): “参考这个优化顶部表现”, with a compact Codex top-strip reference. Improve header density, title hierarchy and tool tab context while preserving native controls, drag routing and existing actions. + +Sidebar chrome follow-up (same requester, 2026-09-28): “也移动上去,另外不要codetwo展示了”, with the sidebar brand/search/toggle crop. Move search and toggle to the window strip and remove visible CodeTwo branding there and in the rail. + +Shadow weight follow-up (same requester, 2026-09-28): “现在阴影过重了,轻一点”. Reduce the shared raised shadow while keeping all floating workspace surfaces consistent. + +Icon shape follow-up (same requester, 2026-09-28): “单Icon 按钮改成全圆”. Make standalone icon-only buttons circular throughout the shared controls and workspace, including responsive toolbar icons. + +Visual simplification (same requester, 2026-09-28): “页面有点喧嚣,不够干净”. Reduce redundant new-task chrome and flatten plugin action surfaces, preserving named actions and descriptions on demand. + +Project-entry refinement (same requester, 2026-09-28): “add a Project 的UX 很扎眼”. Make the empty-sidebar project entry blend into the task list. + +Dock titlebar refinement (same requester, 2026-09-28): “右边面板的 tab和关闭按钮 提升到标题栏”, with a Codex reference. Move right-panel controls into the shared top strip, freeing panel content height. + +Spacing refinement (same requester, 2026-09-28): “有一些间距问题需要修复”. Correct measured inconsistencies in top controls, inter-panel gutters and composer context spacing. + +File-panel refinement (same requester, 2026-09-28): “这个地方显得很乱七八糟,而且上下的间距都不一样。有重复的那个导航栏。” The supplied crop identifies the empty file-tab row and mismatched search/action spacing beneath the Files surface tab. diff --git a/docs/sdlc/changes/2026-09-28-codex-layout-reset/plan.md b/docs/sdlc/changes/2026-09-28-codex-layout-reset/plan.md new file mode 100644 index 00000000..0ee5c553 --- /dev/null +++ b/docs/sdlc/changes/2026-09-28-codex-layout-reset/plan.md @@ -0,0 +1,48 @@ +--- +id: 2026-09-28-codex-layout-reset +schema: 5 +stage: plan +status: accepted +owner: codex +created: 2026-09-28 +based_on: spec.md +scope: apps/desktop/src/files/FileDockContent.tsx, apps/desktop/src/files/FilePanel.tsx, apps/desktop/tests/fileDockContentRendered.test.tsx, apps/desktop/src/design/ui-lab/UiLab.tsx, apps/desktop/src/plugins/PluginUiSlot.tsx, apps/desktop/tests/pluginManagerRendered.test.tsx, apps/desktop/src/components/ui/button.tsx, apps/desktop/src/design/tokens.css, docs/design/system.md, apps/desktop/src/dock/Dock.tsx, apps/desktop/src/session/SessionHeaderActions.tsx, apps/desktop/src/i18n/strings.ts, apps/desktop/tests/dockPluginGateRendered.test.tsx, apps/desktop/tests/sessionHeaderActionsRendered.test.tsx, apps/desktop/tests/composerGeometryContract.test.ts, apps/desktop/src/App.tsx, apps/desktop/src/sidebar/SessionRail.tsx, apps/desktop/src/styles.css, apps/desktop/src/session/Composer.tsx, apps/desktop/tests/sessionRailRendered.test.tsx, apps/desktop/tests/windowChromeContract.test.ts, apps/desktop/tests/checkoutPickerRendered.test.tsx, Design.md, docs/sdlc/changes/2026-09-28-codex-layout-reset/ +--- + +# Plan: Codex Layout Reset + +## Plan + +Codex owns the shell composition, affected tests and design contract. Move existing feature/utility controls to a persistent icon strip, compact the task header/search, add window drag strip, and bottom-align compact composition without remounting it. Preserve task organization, pane and provider logic. + +Check existing rendered rail interactions and shell/composer/dock contracts, desktop lint/types and renderer build. Inspect the actual renderer in light/dark/narrow states and exercise navigation, sidebar and composer draft retention. Run documentation and SDLC worktree checks. Core, provider execution, packaging and remote CI are outside this renderer-only change. + +Temporary resources: task-owned renderer server on 127.0.0.1:1428 and `.codex/run/codex-layout-reset/` for compact visual evidence. Stop the server before handoff, remove disposable build outputs; retain screenshots for this review until the next continuation. Installed workspace dependencies are shared reusable tooling. + +Rollback: revert only this change's scoped files; no persisted schema or state migration. + +Follow-up: reuse NavigationRow for flat panel choices, remove the old responsive card anatomy, use existing Ghost button states and keep one constant composer elevation. No new visual tokens, routing, dependencies or backend changes. Reuse valid previous behavior evidence and recheck the affected renderer suites and actual light/dark/narrow UI. + +Floating-panel follow-up: reuse the existing raised Card for the dock, keeping its gutter inside the current width allocation. Apply existing raised tokens to the composer and expanded control bar. Do not add drag-window state, new tokens or another surface system. Update the explicit design exceptions and rerun affected contracts plus rendered checks. + +Corner refinement: use existing rounded-card and overflow clipping on the main workspace, with the same 8px inset as the tool panel. Preserve the editor tree and existing 24px composer corners. This cosmetic adjustment needs existing checks and rendered inspection, not new behavior tests. + +Shadow refinement: apply the existing shadow-raised utility to the main workspace; the dock and composer already use that token. Keep radius, gutters, backgrounds and all interaction state unchanged. Update the existing chrome contract and verify actual computed shadows and light/dark rendering. + +Top chrome refinement: add scoped geometry tokens, reuse activeTitle in the drag strip and preserve pane titles for split layouts. Compact the workspace headers only, raise the toolbar label breakpoint and retain selected dock-tab text. Existing handlers and tab ownership remain authoritative; no window-manager or native changes. Verify chrome/action/dock contracts, build/style checks and actual rendering. + +Sidebar chrome follow-up: reuse IconAction in the window strip with existing search and toggle handlers. Remove obsolete rail props/header/styles and per-page duplicate expansion entries. Reserve 168px minimum action-group width for native controls; align to inline sidebar width when expanded. Verify rail/chrome tests, live search, collapse/expand, overlay and feature-page navigation; no native code changes. + +Shadow weight follow-up: reduce the existing raised elevation token in root/light/dark definitions. Reuse the established shared surface contract instead of creating panel-specific overrides; menus/popovers also inherit the lighter token. No geometry or interaction changes. Verify style/build checks and live shadow equality in both themes; reuse prior behavior evidence. + +Icon shape follow-up: change the shared icon-size Button variants to rounded-full, remove the Quick Chat radius override and migrate dock close buttons to icon size. Responsive toolbar icons use square bounds and full rounding; connected split-button segments are excluded. Inspect rendered rest/focus/selected states and existing control/rail/dock contracts; no behavior changes or new test suite. + +Visual simplification: merge composer-above and transcript-before action presentation at the existing PluginUiSlot boundary. Remove nested cards, duplicate Run controls and always-visible descriptions; reuse Ghost buttons and Tooltip. Omit only the new-task pane title already represented by the strip and greeting. Shorten empty-sidebar copy, retaining Add project. Keep routing, slots and persisted state unchanged. + +Project-entry refinement: replace the empty-state block with one shared Ghost row, using the existing plus icon and onAddProject handler. Remove unused empty-copy translations; reuse the existing click test and verify actual rendering. + +Dock titlebar refinement: reuse a shell-owned DOM destination and React portal for the existing Dock tab controls, preserving their Tabs context and content tree. Dock remains the width and tab-presentation owner; portaled chrome uses its applied width and resize state. Share one header for home/selected modes; no duplicate top-bar state or new tab system. Verify portal tab/close behavior and shell/dock contracts plus live rendering. + +Spacing refinement: remove the doubled workspace/dock margin only while open, remove the extra page inset inside CheckoutBar, and reuse mini-control height for window-strip tabs. Reduce stacked plugin/composer spacing through the existing owner classes; no new layout state or spacing tokens. Recheck affected chrome/composer/dock/checkout contracts, style/build checks and actual normal/narrow bounds. + +File-panel refinement: condition the existing document-tab row on actual open documents or an active editor, retaining the tree-return control when needed. Remove the empty folder-only row and decorative divider. Use shared compact Input and normal icon Button sizes rather than a conflicting height override; apply one 8px toolbar inset. Add a focused rendered check for empty/populated document navigation and reuse dock checks. diff --git a/docs/sdlc/changes/2026-09-28-codex-layout-reset/spec.md b/docs/sdlc/changes/2026-09-28-codex-layout-reset/spec.md new file mode 100644 index 00000000..044fdf09 --- /dev/null +++ b/docs/sdlc/changes/2026-09-28-codex-layout-reset/spec.md @@ -0,0 +1,50 @@ +--- +id: 2026-09-28-codex-layout-reset +schema: 5 +stage: spec +status: accepted +owner: codex +created: 2026-09-28 +based_on: intent.md +--- + +# Spec: Codex Layout Reset + +## Design + +Keep App as the single owner of page, pane and sidebar state. SessionRail composes a persistent icon navigation beside the existing resizable task list; collapsing the list leaves global navigation accessible. A separate window drag strip reserves native controls. Existing handlers remain authoritative; no second routing system. + +The previous quiet-shell change adjusted styling but left global destinations mixed with task rows. A local CSS patch cannot separate their visibility/scroll ownership. Replace that composition boundary only; a full App rewrite would unnecessarily migrate session and draft state. Reuse shared controls and semantic tokens without new dependencies. Keep Composer at the same React tree position across document/compact modes. Empty-state heading takes spare space; the input stays at the bottom, with scrolling at short heights. Include navigation width in dock/overlay calculations. + +## Acceptance criteria + +- [x] AC-1: Persistent icon navigation reaches existing destinations when the task sidebar is collapsed, with accessible labels and selected states; task/new/search/quick-chat controls remain usable. +- [x] AC-2: New-task greeting is centered in available space above the bottom composer; draft survives compact/document toggles and sidebar toggles. +- [x] AC-3: Rendered light, dark and narrow layouts keep controls within the window; sidebar overlay and dock sizing reserve navigation space. +- [x] AC-4: Affected tests, desktop lint/types/renderer build, documentation and worktree scope checks pass; verification distinguishes browser rendering from native/runtime acceptance. + +- [x] AC-5: The existing shell remains intact while the dock picker uses a centered flat list, toolbar actions use shared quiet states, and composer focus does not inflate its shadow. Names, selection, focus and disabled states remain clear; descriptions remain available from each panel entry. Verify light/dark and narrow rendering. + +- [x] AC-6: Composer and right tool panel use restrained constant raised surfaces. The right panel has an 8px gutter, remains resizable, and reserves no space when closed. Preserve draft and mode behavior; verify light/dark and narrow bounds. + +- [x] AC-7: The main workspace and right panel share visible 16px corners with content clipped inside; the workspace has an 8px inset and retains flat elevation. Confirm actual rendering at normal and narrow widths. + +- [x] AC-8: Main workspace, right tool panel and composer use the same theme-managed raised shadow, constant across interaction states. This supersedes AC-7's flat workspace elevation. Verify computed shadows and actual light/dark rendering. + +- [x] AC-9: Workspace window strip is 32px with the existing focused title; panel headers are 40px. Toolbar labels collapse below 48rem while actions retain names. Selected dock tabs retain text in narrow panels. Verify actual normal/narrow light/dark rendering and existing action routes; native control placement and dedicated page geometry remain unchanged. + +- [x] AC-10: Search and sidebar toggle have one persistent home in the window strip, including collapsed/overlay states. Remove the rail header and duplicate pane expand controls; feature pages show no brand fallback. Search and collapse/expand remain usable at normal/narrow widths and controls clear native window space. + +- [x] AC-11: Shared raised elevation is lighter: 0 2px 8px at 4% opacity in light mode and 12% in dark mode. Main workspace, dock and composer remain equal; confirm actual light/dark rendering. Shared raised menus/popovers inherit the same restrained treatment. + +- [x] AC-12: Shared icon-size buttons are circular. Workspace icon-only controls and inactive icon-only dock tabs remain square in bounds with full rounding, including responsive headers; text buttons and joined split controls retain their shape. Verify actual light/dark rendering and computed geometry. + +- [x] AC-13: Empty new-task panes omit the duplicate title and orphan breadcrumb divider. Plugin actions above the composer/transcript use one flat row with named buttons and hover/focus descriptions; existing dispatch and busy states remain. Empty-sidebar copy is concise. Verify actual light/dark normal/narrow rendering and affected tests. + +- [x] AC-14: Empty sidebar shows one muted, transparent Add project row with a plus icon aligned to task actions. Remove redundant empty-state copy while retaining the existing click handler and keyboard focus. Inspect actual light/dark rendering. This supersedes AC-13 empty-sidebar copy. + +- [x] AC-15: Dock tabs and close control occupy the global 32px titlebar aligned with the panel width; no duplicate inner panel header. Existing tab selection, content ownership, resizing and close/reopen behavior remain. Closed docks expose no header controls. Verify actual light/dark normal/narrow layouts and keyboard tab navigation. Supersedes AC-9 dock header placement. + +- [x] AC-16: Titlebar tabs and close control use 28px height centered in the 32px strip. Adjacent workspace/dock panels have one 8px gap; closed layout retains its outer inset. Checkout context uses 8px side inset within the composer measure; plugin-to-checkout spacing is 8px. Verify normal/narrow rendering and preserved keyboard/close behavior. Supersedes earlier summed panel gutters and 32px inactive tab geometry. + +- [x] AC-17: Files browser with no open documents shows only its search/action toolbar, without the redundant folder-only navigation row or stacked dividers. Search and action controls share 32px height and 8px toolbar padding. Open documents retain their file tabs and return-to-tree action; closing the last document removes the empty tab row. Verify rendered empty/populated tab states and light/dark normal/narrow layouts. diff --git a/docs/sdlc/changes/2026-09-28-codex-layout-reset/verification.md b/docs/sdlc/changes/2026-09-28-codex-layout-reset/verification.md new file mode 100644 index 00000000..9d302f7d --- /dev/null +++ b/docs/sdlc/changes/2026-09-28-codex-layout-reset/verification.md @@ -0,0 +1,74 @@ +--- +id: 2026-09-28-codex-layout-reset +schema: 5 +stage: verification +status: passed +owner: codex +created: 2026-09-28 +based_on: plan.md +revision: "6ed3561f plus this scoped worktree diff" +verification_mode: owner +verified_by: codex +verified_at: 2026-09-28 +release_target: none +cleanup_status: complete +--- + +# Verification: Codex Layout Reset + +## Verification + +- AC-1: PASS — `bun test tests/sessionRailRendered.test.tsx` checks accessible global navigation outside the inert collapsed sidebar, selected state, click routing, search, quick chat, task grouping and resize. Actual renderer navigation to Scheduled tasks and back preserved the collapsed sidebar after removing the six legacy forced-expansion paths. +- AC-2: PASS — Actual browser rendering at 1280×720 places the compact composer at bottom 720. Entered a Chinese draft, expanded/collapsed the document, collapsed the sidebar, visited Scheduled tasks and returned; the draft remained. Repeated after moving CheckoutBar before the editor in DOM order. Cleared the disposable draft after testing. `composerDrafts`, `composerGeometryContract` and `checkoutPickerRendered` tests passed. +- AC-3: PASS — Browser-rendered light/dark states, 800×500 narrow state, sidebar overlay and the open right panel were inspected. DOM measurements at 800×500: navigation width 48, composer bottom 500; open sidebar bounds x=48, y=46, width=288, bottom=500. Overlay background is opaque. Dock reserve includes navigation width. Local screenshots: `.codex/run/codex-layout-reset/light.png`, `narrow-dark.png`, `narrow-light.png`, `narrow-overlay.png` in that same directory. +- AC-4: PASS — Initial layout evidence: the eight affected suites passed 77 tests / 577 assertions; `bun run build:renderer` passed lint, TypeScript and production Vite build; `bun run lint:styles`, `bun script/verify/docs.ts`, `bun script/verify/sdlc.ts --worktree`, and `git diff --check` passed. Final comment-only cleanup also passed targeted `bunx oxfmt --check`. The follow-up reran all suites affected by its visual changes (see AC-5), reused unchanged behavior evidence, and repeated documentation/scope checks. + +Affected suite command (from `apps/desktop`): +`bun test tests/sessionRailRendered.test.tsx tests/windowChromeContract.test.ts tests/shellChromeContract.test.ts tests/composerGeometryContract.test.ts tests/composerDrafts.test.ts tests/checkoutPickerRendered.test.tsx tests/dockArchitecture.test.ts tests/dockPluginGateRendered.test.tsx`. + +- AC-5: PASS — Follow-up rendered at 1280×720 and 800×500 in light/dark. Right-panel entries are 32px transparent rows, vertically centered; keyboard Tab exposes the execution-trajectory description and a visible focus ring. At 800×500 with dock open, the last row ends at y=438, the input card ends at y=488, and the document has no horizontal overflow. Input focus retains the semantic surface elevation (computed box-shadow `none`), without a raised glow. `bun test tests/dockPluginGateRendered.test.tsx tests/sessionHeaderActionsRendered.test.tsx tests/composerGeometryContract.test.ts tests/windowChromeContract.test.ts` passed 36 tests / 252 assertions after the refinement; `bun run build:renderer` and `bun run lint:styles` passed. Local follow-up screenshots: `.codex/run/codex-layout-reset/quiet-light.png`, `quiet-dark.png`, `quiet-narrow-dark.png`, `quiet-narrow-light.png`. + +- AC-6: PASS — Composer and document-mode controls use constant raised tokens; right tool panel uses the existing raised Card with an 8px gutter inside the reserved width. Actual light/dark rendering at 1280×720 and 800×500 inspected, including opening execution trajectory. At 800×500 the panel surface bounds are x=508, y=54, width=284, bottom=492; composer bottom=488, last picker row bottom=438, no horizontal overflow. Keyboard resizing changed allocated width from 324 to 314; closing produced width=0. A Chinese draft survived document expansion/collapse and panel closure, then was cleared. Affected dock/plugin, geometry, window chrome and architecture suites passed 30 tests / 217 assertions; renderer build (lint, types, Vite) and style lint passed. Documentation and worktree scope checks passed. Screenshots: `.codex/run/codex-layout-reset/floating-light.png`, `floating-dark.png`, `floating-narrow-light.png`, `floating-narrow-dark.png`. This supersedes AC-5's earlier flat composer elevation while retaining its focus behavior. + +- AC-7: PASS — The workspace uses the existing 16px card radius, an 8px inset and clipped content over the sidebar-toned shell. The right tool panel retains matching 16px corners; composer retains 24px. Live computed bounds confirm 16px radius and overflow hidden on both panels. Light 1280×720 and light/dark 800×500 were inspected; no horizontal overflow. The short-window content scroll remains usable: a 28px scroll exposes the full composer with bottom=468 within the workspace bottom=492. Existing chrome, composer and dock suites passed 28 tests / 193 assertions after updating the old literal class contract; renderer build, documentation and scope checks passed. Screenshots: `.codex/run/codex-layout-reset/rounded-light.png`, `rounded-narrow-light.png`, `rounded-narrow-dark.png`. + +- AC-8: PASS — The main workspace now shares shadow-raised with the dock and composer. Actual rendering at 1280×720 confirmed all three computed box-shadow values are identical in each theme: light 0 4px 12px at 8% opacity; dark 0 4px 14px at 22%. No geometry or interaction changes; reuse AC-7 narrow-window evidence. The same three suites passed 28 tests / 193 assertions; renderer build, docs, scope and diff checks passed. Screenshots: `.codex/run/codex-layout-reset/unified-shadow-light.png` and `unified-shadow-dark.png`. + +- AC-9: PASS — Browser measurements confirmed a 32px window strip and 40px rail/session/dock headers. At 1280×720 with dock open, named actions render as icons and the task title remains readable. At 800×500, no horizontal page overflow; the selected PR tab is within its 284px header. Keyboard End then Enter reached and selected the final tab and changed its content, with its label visible. The Open menu retains editor, file-manager and task-move entries; navigation to Automations replaces the strip title with CodeTwo and returning restores the task title. Light/dark normal and narrow rendering inspected. The existing chrome/action/dock suites passed 30 tests / 214 assertions; style lint passed. Screenshots: `.codex/run/codex-layout-reset/top-light.png`, `top-dark.png`, `top-narrow-light.png`, `top-narrow-dark.png`. Native window control coordinates and drag handlers are unchanged; actual native hit testing was not run. + +Final top-chrome checks: renderer build (lint, types, Vite), style lint, docs, worktree scope and diff checks passed. + +- AC-10: PASS — Search and sidebar toggle now reuse the existing handlers in the window strip. Removed the rail brand/header, unused rail props/styles and duplicate pane expansion controls. Browser rendering at 1280×720 light and 800×500 dark verified search opens the command palette, top toggle collapses/expands the sidebar, and narrow overlay opens/closes through the still-accessible top toggle. The overlay backdrop is scoped to the content shell. Collapsed control bounds are x=100–128 and x=132–160, y=2–30, leaving native control clearance. Automations keeps both controls with empty strip text; returning restores the task title. No page horizontal overflow. Rail/chrome suites passed 46 tests / 391 assertions; renderer build, style lint, docs, scope and diff checks passed. Screenshots: `.codex/run/codex-layout-reset/top-controls-light.png`, `top-controls-overlay.png`, `top-controls-narrow-dark.png`. Native hit testing remains unverified; native window code is unchanged. + +- AC-11: PASS — Updated the existing raised elevation token in root/light/dark definitions, reducing offset from 4px to 2px, blur to 8px, light opacity from 8% to 4%, and dark opacity from 22% to 12%. Actual browser rendering at 1280×720 in light/dark confirmed workspace, dock and composer computed box-shadows are equal and use the new values. Shared raised popovers inherit the token; no interaction or geometry changes, so prior behavior and narrow-window evidence reused. Renderer build, style lint, docs, scope and diff checks passed. Screenshots: `.codex/run/codex-layout-reset/lighter-shadow-light.png` and `lighter-shadow-dark.png`. + +- AC-12: PASS — Shared icon, icon-xs, icon-sm and icon-lg Button sizes now use rounded-full. Removed the Quick Chat override; dock close controls use a square 32px icon control. Responsive header icons are 32×32 with 50% rounding while connected split-button segments retain their shape. Live light 1280×720 measurements confirmed 28×28 window-strip/composer icons and 32×32 navigation, header and dock icons with full rounding; focus and selected states were inspected. Dark 800×500 showed no horizontal overflow. Initial inspection caught utility-layer precedence retaining 12px dock-tab corners; moved the responsive override to the existing unlayered header styling level and rechecked all six inactive tabs at 32×32 with 50% radius. Existing design-system, rail, chrome, dock and header-action suites passed 62 tests / 501 assertions after updating obsolete shape assertions; renderer build, style lint, docs, scope and diff checks passed. Screenshots: `.codex/run/codex-layout-reset/circle-light.png` and `circle-narrow-dark.png`. + +- AC-13: PASS — Removed the duplicate new-task pane title and orphan divider, shortened empty-project copy and unified composer/transcript plugin actions into flat named Ghost buttons. Descriptions appear on keyboard focus in the actual renderer; action dispatch remains covered. Affected plugin-manager, window-chrome and rail suites passed 65 tests / 521 assertions. Renderer build (lint, types, Vite) and style lint passed. Light/dark 1280×720 and dark 800×500 rendering inspected: plugin action row is 32px high, duplicate pane title count is zero, no horizontal overflow. Screenshots: `.codex/run/codex-layout-reset/clean-light.png` and `clean-narrow-dark.png`. Existing disabled/busy logic and all contribution slots remain unchanged; no browser fallback fixture was changed. + +- AC-14: PASS — Empty-sidebar project entry is now a transparent Ghost row with a plus icon, muted normal-weight text and the same alignment as task actions. Removed redundant empty copy and unused translations. Existing click test passed with the other rail checks: 31 tests / 279 assertions. Renderer build (lint, types, Vite), docs, scope and diff checks passed. Actual light/dark 1280×720 inspection confirmed 32px row height, transparent background, weight 400 and visible keyboard focus. Screenshots: `.codex/run/codex-layout-reset/project-row-light.png` and `project-row-dark-focus.png`. The existing project-add handler remains unchanged; no native folder dialog or project data was modified. + +- AC-15: PASS — Dock owns one header portaled to the shell window-strip host; tabs retain their existing React context and content stays in the panel. Removed duplicate inner headers, added the close control's explicit accessible name and updated the UI Lab caller. Rendered portal/selection/close/reopen test plus dock and chrome suites passed 25 tests / 187 assertions. Final renderer build (lint, types, Vite), docs, scope and diff checks passed. Actual light 1280×720 and dark 800×500 inspected: header y=0–32; header and panel share width/left edge; no inner dock titlebar or horizontal overflow. Keyboard End/Enter selected PR, resizing changed width 314→304 with matching header width, close removed header controls, and reopening restored the picker. At 800×500 the header is x=500–800 and close control x=764–792/y=2–30. The UI Lab PR scenario also rendered correctly with its host above the preview. Screenshots: `.codex/run/codex-layout-reset/dock-titlebar-light.png` and `dock-titlebar-narrow-dark.png`. Native window dragging/control hit testing remains outside browser verification. + +- AC-16: PASS — Corrected measured spacing at the existing layout owners: dock tabs now match 28px close controls at y=2–30, panel margins no longer sum to 16px, CheckoutBar no longer repeats a 24px page inset, and plugin/composer spacing no longer sums to 16px. Actual light 1280×720 measured panel gap, checkout inset and plugin-to-checkout gap all at 8px. Dark 800×500 retained the 8px panel gap, six inactive circular tabs at 28×28, and no horizontal overflow. Keyboard End/Enter selected PR; closing restored the 8px outer inset and removed dock chrome. Affected chrome/composer/checkout/dock suites passed 32 tests / 228 assertions after updating obsolete geometry expectations; renderer build (lint, types, Vite), style lint, docs, scope and diff checks passed. Screenshots: `.codex/run/codex-layout-reset/spacing-light.png` and `spacing-narrow-dark.png`. No interaction state or persistence changes. + +- AC-17: PASS — Empty file browsers no longer render the document-tab row containing only the browse icon. Actual open documents retain their tabs and tree-return control; the rendered component test covers selecting a file, returning to browsing and removing the last document from props. Removed the file toolbar divider and replaced the conflicting input-height class with the shared compact Input size. Light 1280×720 and dark 800×500 rendering measured all four search/action controls at 32px with 8px above/below inside a 48px toolbar, zero empty document-tab rows and no horizontal overflow. New file opened an inline draft and Escape dismissed it without creating a file. File-content and dock suites passed 12 tests / 89 assertions; renderer build (lint, types, Vite), docs, scope and diff checks passed. Screenshots: `.codex/run/codex-layout-reset/files-toolbar-light.png`, `files-toolbar-detail.png` and `files-toolbar-narrow-dark.png`. Populated document navigation is component-rendered evidence; native file persistence and editing were not exercised. + +Verdict: verified. +Residual risk: browser renderer uses the existing bridge fallback, including demo plugin contributions; no Core, live provider execution, native macOS traffic-light interaction, Windows runtime, remote CI or release acceptance was exercised. Vite reported its existing large-chunk advisory. Saved document-mode preferences remain unchanged; compact mode is the new-install default. No dependencies added. + +## Cleanup + +Removed: File-toolbar refinement removed generated `apps/desktop/dist` and `/tmp/codetwo-files-toolbar-{build,tests}.log`. Spacing refinement removed generated `apps/desktop/dist` and `/tmp/codetwo-spacing-{build,tests}.log`. Dock titlebar refinement removed generated `apps/desktop/dist` and `/tmp/codetwo-dock-top-{build,tests}.log`. Project-entry refinement removed generated `apps/desktop/dist` and `/tmp/codetwo-project-row-{build,tests}.log`. Visual simplification removed generated `apps/desktop/dist` and `/tmp/codetwo-clean-{build,tests}.log`. Icon-shape follow-up removed generated `apps/desktop/dist` and `/tmp/codetwo-circle-{build,tests}.log`; replaced the intermediate narrow screenshot after fixing the tab override. Lighter-shadow follow-up removed generated `apps/desktop/dist` and `/tmp/codetwo-light-shadow-build.log`. Sidebar chrome follow-up removed generated `apps/desktop/dist` and `/tmp/codetwo-nav-{build,tests}.log`. Top chrome refinement removed generated `apps/desktop/dist` and `/tmp/codetwo-top-{build,tests}.log`. Shadow refinement removed generated `apps/desktop/dist` and `/tmp/codetwo-shadows-{build,tests}.log`. Corner refinement removed 48 MB generated `apps/desktop/dist` and `/tmp/codetwo-corners-{build,tests}.log`. Floating-panel follow-up removed 48 MB generated `apps/desktop/dist`, `/tmp/codetwo-floating-tests.log` and `/tmp/codetwo-floating-build.log`. Follow-up 48 MB generated `apps/desktop/dist` and `/tmp/codetwo-quiet-tests.log` plus `/tmp/codetwo-quiet-build.log`. Initial run removed 48 MB generated `apps/desktop/dist`, two transient/mis-scaled screenshot attempts, and `/tmp/codetwo-layout-tests.log` plus `/tmp/codetwo-layout-build.log`. +Retained: 1.2 MB of thirty-nine local review screenshots (including the already delivered initial reference) under `.codex/run/codex-layout-reset/`; installed `apps/desktop/node_modules` is reusable workspace dependency tooling. +Retention owner: codex for review screenshots; workspace owner for dependency tooling. +Cleanup trigger: inspect/remove screenshots at the next continuation after review; dependency tooling follows the worktree lifecycle. +Processes: File-toolbar Vite PID 73491 stopped; its tab closed, empty new-file draft cancelled and media/viewport emulation reset. Spacing Vite PID 33437 stopped; its tab closed and media/viewport emulation reset. Dock titlebar Vite PID 24328 stopped; both its workspace and UI Lab tabs closed and media/viewport emulation reset. Project-entry Vite PID 21284 stopped; its tab closed and media emulation reset. Visual simplification Vite PID 80677 stopped; its tab closed and media/viewport emulation reset. Icon-shape Vite PID 37504 stopped; both task tabs closed and media/viewport emulation reset. Lighter-shadow Vite PID 31123 stopped; its tab closed and media emulation reset. Sidebar chrome follow-up Vite PID 29032 stopped; its tab closed and media/viewport emulation reset. Top chrome refinement Vite PID 83313 stopped, its tab closed and media/viewport emulation reset. Shadow refinement Vite PID 70634 stopped and port released; its tab closed and media emulation reset. Corner refinement Vite PID 66691 stopped; both task tabs closed and emulation reset. Floating-panel Vite PID 16615 stopped, port 1428 released; its tab closed, draft cleared and emulation reset. Earlier follow-up Vite PID 40192 and initial Vite PID 84925 stopped with SIGTERM; port 1428 released. All three task browser tabs closed, test draft cleared, viewport and media emulation reset. No Core launched or user process stopped. +Evidence: `ls -lh .codex/run/codex-layout-reset`, `du -sh apps/desktop/dist` before removal; `du -sh .codex/run/codex-layout-reset` after cleanup; `ps -p 84925 -o pid=,command=` and `lsof -nP -iTCP:1428 -sTCP:LISTEN` both returned no process after the initial shutdown; repeated `ps -p 40192 -o pid=,command=` and the port check after follow-up shutdown. Exact generated output path was resolved and checked before removal. Floating follow-up repeated PID/command, port, output-path and size checks. + +## Review and release + +Approval: local implementation authorized by the request recorded in Intent; PR creation and push authorized by the user’s “pr” request in this chat. Merge approval was not requested. +Rollback: See plan.md; revert the scoped UI changes without migrating saved state. +Release: PR delivery requested; creation pending. No merge, publication or release performed. +Feedback: none pending for this bounded renderer change. From 743dcd295c0e456a58390d07d88f20f41ad65f53 Mon Sep 17 00:00:00 2001 From: idevlab Date: Mon, 28 Sep 2026 15:20:05 +0800 Subject: [PATCH 2/3] record merged layout verification --- .../changes/2026-09-28-codex-layout-reset/verification.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/sdlc/changes/2026-09-28-codex-layout-reset/verification.md b/docs/sdlc/changes/2026-09-28-codex-layout-reset/verification.md index 9d302f7d..90571998 100644 --- a/docs/sdlc/changes/2026-09-28-codex-layout-reset/verification.md +++ b/docs/sdlc/changes/2026-09-28-codex-layout-reset/verification.md @@ -6,7 +6,7 @@ status: passed owner: codex created: 2026-09-28 based_on: plan.md -revision: "6ed3561f plus this scoped worktree diff" +revision: "codex/layout-ux-reset merged with origin/main c49b7db4" verification_mode: owner verified_by: codex verified_at: 2026-09-28 @@ -54,16 +54,19 @@ Final top-chrome checks: renderer build (lint, types, Vite), style lint, docs, w - AC-17: PASS — Empty file browsers no longer render the document-tab row containing only the browse icon. Actual open documents retain their tabs and tree-return control; the rendered component test covers selecting a file, returning to browsing and removing the last document from props. Removed the file toolbar divider and replaced the conflicting input-height class with the shared compact Input size. Light 1280×720 and dark 800×500 rendering measured all four search/action controls at 32px with 8px above/below inside a 48px toolbar, zero empty document-tab rows and no horizontal overflow. New file opened an inline draft and Escape dismissed it without creating a file. File-content and dock suites passed 12 tests / 89 assertions; renderer build (lint, types, Vite), docs, scope and diff checks passed. Screenshots: `.codex/run/codex-layout-reset/files-toolbar-light.png`, `files-toolbar-detail.png` and `files-toolbar-narrow-dark.png`. Populated document navigation is component-rendered evidence; native file persistence and editing were not exercised. +- PR handoff: PASS — Merged current `origin/main` (`c49b7db4`) into the feature branch without textual conflicts. After the merge, twelve affected desktop suites passed 110 tests / 802 assertions; `bun run build:renderer` passed lint, types and Vite build; documentation, worktree scope, diff and ready-PR Gate checks passed. A fresh 1280×720 light browser render of the Files panel confirmed the 32px title strip, 8px panel gap, no empty document-tab row, 32px file search/actions and no horizontal overflow. Screenshot: `.codex/run/codex-layout-reset/pr-main-merged.png`. These checks cover the merged local branch; remote CI is pending. + Verdict: verified. Residual risk: browser renderer uses the existing bridge fallback, including demo plugin contributions; no Core, live provider execution, native macOS traffic-light interaction, Windows runtime, remote CI or release acceptance was exercised. Vite reported its existing large-chunk advisory. Saved document-mode preferences remain unchanged; compact mode is the new-install default. No dependencies added. ## Cleanup Removed: File-toolbar refinement removed generated `apps/desktop/dist` and `/tmp/codetwo-files-toolbar-{build,tests}.log`. Spacing refinement removed generated `apps/desktop/dist` and `/tmp/codetwo-spacing-{build,tests}.log`. Dock titlebar refinement removed generated `apps/desktop/dist` and `/tmp/codetwo-dock-top-{build,tests}.log`. Project-entry refinement removed generated `apps/desktop/dist` and `/tmp/codetwo-project-row-{build,tests}.log`. Visual simplification removed generated `apps/desktop/dist` and `/tmp/codetwo-clean-{build,tests}.log`. Icon-shape follow-up removed generated `apps/desktop/dist` and `/tmp/codetwo-circle-{build,tests}.log`; replaced the intermediate narrow screenshot after fixing the tab override. Lighter-shadow follow-up removed generated `apps/desktop/dist` and `/tmp/codetwo-light-shadow-build.log`. Sidebar chrome follow-up removed generated `apps/desktop/dist` and `/tmp/codetwo-nav-{build,tests}.log`. Top chrome refinement removed generated `apps/desktop/dist` and `/tmp/codetwo-top-{build,tests}.log`. Shadow refinement removed generated `apps/desktop/dist` and `/tmp/codetwo-shadows-{build,tests}.log`. Corner refinement removed 48 MB generated `apps/desktop/dist` and `/tmp/codetwo-corners-{build,tests}.log`. Floating-panel follow-up removed 48 MB generated `apps/desktop/dist`, `/tmp/codetwo-floating-tests.log` and `/tmp/codetwo-floating-build.log`. Follow-up 48 MB generated `apps/desktop/dist` and `/tmp/codetwo-quiet-tests.log` plus `/tmp/codetwo-quiet-build.log`. Initial run removed 48 MB generated `apps/desktop/dist`, two transient/mis-scaled screenshot attempts, and `/tmp/codetwo-layout-tests.log` plus `/tmp/codetwo-layout-build.log`. -Retained: 1.2 MB of thirty-nine local review screenshots (including the already delivered initial reference) under `.codex/run/codex-layout-reset/`; installed `apps/desktop/node_modules` is reusable workspace dependency tooling. +Retained: 1.3 MB of forty local review screenshots (including the already delivered initial reference and post-merge render) under `.codex/run/codex-layout-reset/`; installed `apps/desktop/node_modules` is reusable workspace dependency tooling. Retention owner: codex for review screenshots; workspace owner for dependency tooling. Cleanup trigger: inspect/remove screenshots at the next continuation after review; dependency tooling follows the worktree lifecycle. Processes: File-toolbar Vite PID 73491 stopped; its tab closed, empty new-file draft cancelled and media/viewport emulation reset. Spacing Vite PID 33437 stopped; its tab closed and media/viewport emulation reset. Dock titlebar Vite PID 24328 stopped; both its workspace and UI Lab tabs closed and media/viewport emulation reset. Project-entry Vite PID 21284 stopped; its tab closed and media emulation reset. Visual simplification Vite PID 80677 stopped; its tab closed and media/viewport emulation reset. Icon-shape Vite PID 37504 stopped; both task tabs closed and media/viewport emulation reset. Lighter-shadow Vite PID 31123 stopped; its tab closed and media emulation reset. Sidebar chrome follow-up Vite PID 29032 stopped; its tab closed and media/viewport emulation reset. Top chrome refinement Vite PID 83313 stopped, its tab closed and media/viewport emulation reset. Shadow refinement Vite PID 70634 stopped and port released; its tab closed and media emulation reset. Corner refinement Vite PID 66691 stopped; both task tabs closed and emulation reset. Floating-panel Vite PID 16615 stopped, port 1428 released; its tab closed, draft cleared and emulation reset. Earlier follow-up Vite PID 40192 and initial Vite PID 84925 stopped with SIGTERM; port 1428 released. All three task browser tabs closed, test draft cleared, viewport and media emulation reset. No Core launched or user process stopped. +Post-merge cleanup: Removed the generated 48 MB `apps/desktop/dist`; stopped this turn's Vite PID 9752 and closed its preview tab. No Core or user process was stopped. Evidence: `ls -lh .codex/run/codex-layout-reset`, `du -sh apps/desktop/dist` before removal; `du -sh .codex/run/codex-layout-reset` after cleanup; `ps -p 84925 -o pid=,command=` and `lsof -nP -iTCP:1428 -sTCP:LISTEN` both returned no process after the initial shutdown; repeated `ps -p 40192 -o pid=,command=` and the port check after follow-up shutdown. Exact generated output path was resolved and checked before removal. Floating follow-up repeated PID/command, port, output-path and size checks. ## Review and release From ed5997ae281ac6a06a0d7c0a4a2f2a9e050e0e27 Mon Sep 17 00:00:00 2001 From: idevlab Date: Mon, 28 Sep 2026 15:21:08 +0800 Subject: [PATCH 3/3] link layout review pull request --- docs/sdlc/changes/2026-09-28-codex-layout-reset/verification.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sdlc/changes/2026-09-28-codex-layout-reset/verification.md b/docs/sdlc/changes/2026-09-28-codex-layout-reset/verification.md index 90571998..36b806aa 100644 --- a/docs/sdlc/changes/2026-09-28-codex-layout-reset/verification.md +++ b/docs/sdlc/changes/2026-09-28-codex-layout-reset/verification.md @@ -73,5 +73,5 @@ Evidence: `ls -lh .codex/run/codex-layout-reset`, `du -sh apps/desktop/dist` bef Approval: local implementation authorized by the request recorded in Intent; PR creation and push authorized by the user’s “pr” request in this chat. Merge approval was not requested. Rollback: See plan.md; revert the scoped UI changes without migrating saved state. -Release: PR delivery requested; creation pending. No merge, publication or release performed. +Release: Review PR https://github.com/IchenDEV/codeTwo/pull/243 created and pushed. No merge, publication or release performed. Feedback: none pending for this bounded renderer change.