From 18d5552cc36c0f73f14dcafeac454360443dbb1b Mon Sep 17 00:00:00 2001 From: DragonnZhang <731557579@qq.com> Date: Sun, 26 Apr 2026 09:48:33 +0800 Subject: [PATCH] feat(desktop): align sidebar app rail --- .../sidebar-app-rail-fidelity.md | 53 ++++++ ...de-electron-desktop-implementation-plan.md | 97 ++++++++++ packages/desktop/scripts/e2e-cdp-smoke.mjs | 169 ++++++++++++++++++ .../components/layout/ProjectSidebar.tsx | 90 ++++++---- .../components/layout/WorkspacePage.test.tsx | 10 ++ packages/desktop/src/renderer/styles.css | 133 +++++++------- 6 files changed, 450 insertions(+), 102 deletions(-) create mode 100644 .qwen/e2e-tests/electron-desktop/sidebar-app-rail-fidelity.md diff --git a/.qwen/e2e-tests/electron-desktop/sidebar-app-rail-fidelity.md b/.qwen/e2e-tests/electron-desktop/sidebar-app-rail-fidelity.md new file mode 100644 index 000000000..4a679fe8b --- /dev/null +++ b/.qwen/e2e-tests/electron-desktop/sidebar-app-rail-fidelity.md @@ -0,0 +1,53 @@ +# Sidebar App Rail Prototype Fidelity + +- Slice date: 2026-04-26 +- Executable harness: `packages/desktop/scripts/e2e-cdp-smoke.mjs` +- Command: + `cd packages/desktop && npm run e2e:cdp` +- Result: pass +- Passing artifact directory: + `.qwen/e2e-tests/electron-desktop/artifacts/2026-04-26T01-46-17-523Z/` + +## Scenario + +1. Launch the real Electron app with isolated HOME, runtime, user-data, and a + fake dirty Git workspace. +2. Open the fake project through the desktop directory picker path. +3. Send the first composer prompt and approve the fake command request. +4. Wait for the fake ACP response and changed-files summary. +5. Assert the left sidebar uses compact top app actions, project/thread browser + sections, and a persistent bottom Settings row. +6. Continue the existing assistant actions, changed-files summary, review + drawer, compact layout, settings, terminal, discard safety, and commit smoke + path. + +## Assertions + +- `data-testid="sidebar-app-actions"` and + `data-testid="sidebar-footer-settings"` are present in the real Electron DOM. +- Top app action labels are `New Thread`, `Open Project`, and `Models`. +- The old `.sidebar-toolbar` is absent. +- Bottom Settings is below the project/thread browser and contained in the + sidebar. +- Sidebar width stays compact at `272` px in the default viewport. +- App action rows are `32` px high, the project row is `39.75` px high, and the + thread row is `36` px high. +- Sidebar rows and regions have no horizontal overflow. +- Sidebar text does not expose fake ACP session IDs, `Connected to ...` + protocol text, or temp full paths. +- Console errors: 0. +- Failed local network requests: 0. + +## Artifacts + +- `sidebar-app-rail.json` +- `initial-workspace.png` +- `completed-workspace.png` +- `electron.log` +- `summary.json` + +## Known Uncovered Risk + +The harness covers one project, one thread, and bounded labels. A dedicated +long-label CDP path is still needed for very long project names, branch names, +model names, and review-open compact widths. diff --git a/design/qwen-code-electron-desktop-implementation-plan.md b/design/qwen-code-electron-desktop-implementation-plan.md index c30070bb9..c53edb280 100644 --- a/design/qwen-code-electron-desktop-implementation-plan.md +++ b/design/qwen-code-electron-desktop-implementation-plan.md @@ -22,6 +22,103 @@ execution order, verification, decisions, and remaining work. ## Codex Alignment Progress +### Completed Slice: Sidebar App Rail Prototype Fidelity + +Status: completed in iteration 18. + +Goal: make the left sidebar read more like the `home.jpg` prototype by moving +primary app actions into compact top rows, pinning Settings to the bottom, and +tightening project/thread row density without exposing raw paths or prompt +noise. + +User-visible value: users get a clearer desktop-native navigation rail: start +a thread, open a project, reach model/settings, scan projects, scan threads, +and find Settings at the expected persistent bottom position. + +Expected files: + +- `packages/desktop/src/renderer/components/layout/ProjectSidebar.tsx` +- `packages/desktop/src/renderer/styles.css` +- `packages/desktop/scripts/e2e-cdp-smoke.mjs` +- `packages/desktop/src/renderer/components/layout/WorkspacePage.test.tsx` +- `.qwen/e2e-tests/electron-desktop/sidebar-app-rail-fidelity.md` +- `design/qwen-code-electron-desktop-implementation-plan.md` + +Acceptance criteria: + +- Sidebar primary actions render as compact icon+label rows at the top. +- Settings is available as a persistent bottom row and no longer competes in + the top project toolbar. +- Project and thread rows stay compact, active rows keep a subtle left accent, + and long project/thread/model labels remain truncated. +- Thread rows do not show raw full paths or protocol/session IDs. +- Real Electron CDP coverage records sidebar geometry and fails if the + navigation loses the top action group or bottom Settings placement. + +Verification: + +- Unit/component test command: + `cd packages/desktop && SHELL=/bin/bash npx vitest run src/renderer/components/layout/WorkspacePage.test.tsx` +- Syntax command: `node --check packages/desktop/scripts/e2e-cdp-smoke.mjs` +- Build/typecheck/lint commands: + `cd packages/desktop && npm run typecheck && npm run lint && npm run build` +- Real Electron harness: + `cd packages/desktop && npm run e2e:cdp` +- Harness path: `packages/desktop/scripts/e2e-cdp-smoke.mjs` +- E2E scenario steps: launch real Electron with isolated HOME/runtime/user-data + and fake ACP, open the fake project, send/approve the prompt, assert the + populated sidebar app rail layout, continue the existing review/settings/ + terminal/commit smoke, and capture first-viewport screenshots and JSON + metrics. +- E2E assertions: top app action rows include New Thread/Open Project/Models, + bottom Settings is visually below the project/thread lists, rows stay under + the compact height limit, sidebar width remains compact at desktop and + compact widths, and no sidebar row overflows horizontally. +- Diagnostic artifacts: `sidebar-app-rail.json`, `initial-workspace.png`, + `completed-workspace.png`, Electron log, and summary JSON under + `.qwen/e2e-tests/electron-desktop/artifacts/`. +- Required skills applied: `frontend-design` for prototype-constrained + information hierarchy and density; `electron-desktop-dev` for real Electron + CDP verification; `brainstorming` applied by selecting the smallest recorded + fidelity gap, using the prototype over a new visual direction. + +Notes and decisions: + +- The slice keeps the existing local server, preload, IPC, ACP, review, and + settings behavior unchanged; this is a renderer layout and style fidelity + pass. +- `frontend-design` is applied with the Ralph constraint that `home.jpg` wins: + the sidebar should become quieter and more navigational, not more decorative. +- `electron-desktop-dev` applies because sidebar layout and navigation order + must be verified in the real Electron shell with actual viewport geometry. + +Verification results: + +- `node --check packages/desktop/scripts/e2e-cdp-smoke.mjs` passed. +- `cd packages/desktop && SHELL=/bin/bash npx vitest run src/renderer/components/layout/WorkspacePage.test.tsx` + passed. +- `cd packages/desktop && npm run typecheck` passed. +- `cd packages/desktop && npm run lint` passed. +- `cd packages/desktop && npm run build` passed. +- `cd packages/desktop && npm run e2e:cdp` passed after launching real + Electron over CDP, opening the fake project, sending/approving the fake ACP + prompt, checking the new sidebar app rail metrics, and completing the + existing review, settings, terminal, discard safety, and commit workflows. +- Passing artifacts: + `.qwen/e2e-tests/electron-desktop/artifacts/2026-04-26T01-46-17-523Z/`. +- Key recorded metrics: sidebar width `272`, top app action rows + `32` px high, project row `39.75` px high, thread row `36` px high, bottom + Settings row `32` px high, no legacy sidebar toolbar, no sidebar overflows, + and no console errors or failed local requests. + +Next work: + +- Continue prototype fidelity by reducing the remaining topbar/status pill + weight and making the title/action cluster closer to the slim `home.jpg` + header. +- Add focused long branch/model/project-name CDP coverage with review open, + since compact review and composer chips rely on truncation to avoid overflow. + ### Completed Slice: Inline Tool Activity Prototype Fidelity Status: completed in iteration 17. diff --git a/packages/desktop/scripts/e2e-cdp-smoke.mjs b/packages/desktop/scripts/e2e-cdp-smoke.mjs index 968bb5b9a..717d911b6 100644 --- a/packages/desktop/scripts/e2e-cdp-smoke.mjs +++ b/packages/desktop/scripts/e2e-cdp-smoke.mjs @@ -101,6 +101,7 @@ async function main() { await setFieldByAriaLabel('Message', ''); await assertConversationChangesSummary('conversation-changes-summary.json'); await waitForSelector('[data-testid="thread-list"]'); + await assertSidebarAppRail('sidebar-app-rail.json'); await clickButton('Review Changes'); await waitForText('README.md'); @@ -419,6 +420,8 @@ async function assertWorkbenchLandmarks() { return [ 'desktop-workspace', 'project-sidebar', + 'sidebar-app-actions', + 'sidebar-footer-settings', 'workspace-topbar', 'workspace-grid', 'chat-thread', @@ -621,6 +624,172 @@ async function assertRalphWorkspaceLayout(fileName) { } } +async function assertSidebarAppRail(fileName) { + const metrics = await evaluate(`(() => { + const rectFor = (element) => { + if (!element) { + return null; + } + const rect = element.getBoundingClientRect(); + return { + top: rect.top, + right: rect.right, + bottom: rect.bottom, + left: rect.left, + width: rect.width, + height: rect.height + }; + }; + const overflows = (element) => + Boolean(element && element.scrollWidth > element.clientWidth + 4); + const sidebar = document.querySelector('[data-testid="project-sidebar"]'); + const appActions = document.querySelector('[data-testid="sidebar-app-actions"]'); + const footerSettings = document.querySelector( + '[data-testid="sidebar-footer-settings"]' + ); + const projectList = document.querySelector('[data-testid="project-list"]'); + const threadList = document.querySelector('[data-testid="thread-list"]'); + const rowSelector = + '.sidebar-action-row, .project-row, .session-row'; + const rows = [...document.querySelectorAll(rowSelector)].map((row) => { + const label = + row.getAttribute('aria-label') || + row.getAttribute('title') || + row.textContent.trim(); + return { + label, + text: row.textContent.trim(), + rect: rectFor(row), + scrollWidth: row.scrollWidth, + clientWidth: row.clientWidth, + overflows: overflows(row) + }; + }); + + return { + viewport: { + width: window.innerWidth, + height: window.innerHeight + }, + sidebar: rectFor(sidebar), + appActions: rectFor(appActions), + footerSettings: rectFor(footerSettings), + projectList: rectFor(projectList), + threadList: rectFor(threadList), + hasLegacyToolbar: document.querySelector('.sidebar-toolbar') !== null, + appActionLabels: appActions + ? [...appActions.querySelectorAll('button')].map( + (button) => button.getAttribute('aria-label') || '' + ) + : [], + footerLabel: + footerSettings?.getAttribute('aria-label') || + footerSettings?.textContent.trim() || + '', + rows, + sidebarText: sidebar?.innerText ?? '', + overflows: { + sidebar: overflows(sidebar), + appActions: overflows(appActions), + projectList: overflows(projectList), + threadList: overflows(threadList), + footerSettings: overflows(footerSettings) + } + }; + })()`); + + await writeFile( + join(artifactDir, fileName), + `${JSON.stringify(metrics, null, 2)}\n`, + 'utf8', + ); + + const missing = [ + 'sidebar', + 'appActions', + 'footerSettings', + 'projectList', + 'threadList', + ].filter((key) => metrics[key] === null); + if (missing.length > 0) { + throw new Error(`Missing sidebar app rail rects: ${missing.join(', ')}`); + } + + if (metrics.hasLegacyToolbar) { + throw new Error('Sidebar should not render the old project toolbar.'); + } + + for (const expectedLabel of ['New Thread', 'Open Project', 'Models']) { + if (!metrics.appActionLabels.includes(expectedLabel)) { + throw new Error( + `Sidebar app actions missing ${expectedLabel}: ${metrics.appActionLabels.join( + ', ', + )}`, + ); + } + } + + if (metrics.footerLabel !== 'Settings') { + throw new Error( + `Sidebar footer label should be Settings: ${metrics.footerLabel}`, + ); + } + + if (metrics.sidebar.width < 236 || metrics.sidebar.width > 320) { + throw new Error( + `Sidebar width is no longer compact: ${metrics.sidebar.width}`, + ); + } + + if (metrics.appActions.top > metrics.sidebar.top + 24) { + throw new Error('Sidebar app actions are not pinned near the top.'); + } + + if (metrics.footerSettings.bottom > metrics.sidebar.bottom + 1) { + throw new Error('Sidebar Settings footer overflows the sidebar.'); + } + + if (metrics.footerSettings.top < metrics.threadList.bottom - 1) { + throw new Error( + 'Sidebar Settings should stay below the project/thread browser.', + ); + } + + const tallRows = metrics.rows.filter((row) => row.rect.height > 44); + if (tallRows.length > 0) { + throw new Error( + `Sidebar rows are too tall for the compact rail: ${JSON.stringify( + tallRows, + )}`, + ); + } + + const overflowingRows = metrics.rows.filter((row) => row.overflows); + if (overflowingRows.length > 0) { + throw new Error( + `Sidebar rows overflow horizontally: ${JSON.stringify(overflowingRows)}`, + ); + } + + if (Object.values(metrics.overflows).some(Boolean)) { + throw new Error( + `Sidebar rail regions overflow horizontally: ${JSON.stringify( + metrics.overflows, + )}`, + ); + } + + if ( + metrics.sidebarText.includes('session-e2e') || + metrics.sidebarText.includes('/tmp/') || + metrics.sidebarText.includes('Connected to') + ) { + throw new Error( + `Sidebar leaked protocol or path noise: ${metrics.sidebarText}`, + ); + } +} + async function assertConversationChangesSummary(fileName) { await waitForSelector('[data-testid="conversation-changes-summary"]'); const snapshot = await evaluate(`(() => { diff --git a/packages/desktop/src/renderer/components/layout/ProjectSidebar.tsx b/packages/desktop/src/renderer/components/layout/ProjectSidebar.tsx index 0fb5a79f2..8fb8e88cd 100644 --- a/packages/desktop/src/renderer/components/layout/ProjectSidebar.tsx +++ b/packages/desktop/src/renderer/components/layout/ProjectSidebar.tsx @@ -50,44 +50,49 @@ export function ProjectSidebar({ aria-label="Projects and threads" data-testid="project-sidebar" > -
-

Projects

-
- - - -
-
+
+
+

Projects

+ {projects.length} +
+
+ +
); } diff --git a/packages/desktop/src/renderer/components/layout/WorkspacePage.test.tsx b/packages/desktop/src/renderer/components/layout/WorkspacePage.test.tsx index 0c838d002..2644b12ba 100644 --- a/packages/desktop/src/renderer/components/layout/WorkspacePage.test.tsx +++ b/packages/desktop/src/renderer/components/layout/WorkspacePage.test.tsx @@ -47,6 +47,7 @@ describe('WorkspacePage', () => { for (const testId of [ 'desktop-workspace', 'project-sidebar', + 'sidebar-app-actions', 'workspace-topbar', 'workspace-grid', 'chat-thread', @@ -64,9 +65,18 @@ describe('WorkspacePage', () => { expect( renderedContainer.querySelector('[data-testid="settings-page"]'), ).toBeNull(); + expect(renderedContainer.querySelector('.sidebar-toolbar')).toBeNull(); + expect( + renderedContainer.querySelector( + '[data-testid="sidebar-footer-settings"]', + ), + ).toBeTruthy(); expect(renderedContainer.textContent).toContain('example-workspace'); expect(renderedContainer.textContent).toContain('main'); + expect(renderedContainer.textContent).toContain('New Thread'); + expect(renderedContainer.textContent).toContain('Open Project'); + expect(renderedContainer.textContent).toContain('Models'); expect( renderedContainer.querySelector('[data-testid="project-sidebar"]') ?.textContent, diff --git a/packages/desktop/src/renderer/styles.css b/packages/desktop/src/renderer/styles.css index 657875f81..cfb274ae6 100644 --- a/packages/desktop/src/renderer/styles.css +++ b/packages/desktop/src/renderer/styles.css @@ -96,44 +96,21 @@ summary:focus-visible { height: 100vh; min-height: 0; flex-direction: column; - gap: 10px; + gap: 9px; overflow: hidden; - padding: 16px 12px 14px; + padding: 12px 10px; border-right: 1px solid var(--line); background: - linear-gradient(180deg, rgba(37, 58, 61, 0.95), rgba(22, 28, 29, 0.98)), - radial-gradient( - circle at 32px 0, - rgba(242, 199, 112, 0.08), - transparent 34% - ), - #151b1d; + linear-gradient(180deg, rgba(20, 26, 51, 0.98), rgba(13, 17, 32, 0.98)), + #101322; } -.sidebar-toolbar { - display: flex; - align-items: center; - justify-content: space-between; - min-height: 30px; - gap: 12px; - padding: 0 4px 4px; -} - -.sidebar-toolbar h1, .topbar h2, .topbar p, .panel h3 { margin: 0; } -.sidebar-toolbar h1 { - color: rgba(225, 232, 228, 0.62); - font-size: 16px; - font-weight: 720; - letter-spacing: 0; - line-height: 1; -} - .eyebrow, .message-role { color: var(--muted); @@ -143,35 +120,48 @@ summary:focus-visible { text-transform: uppercase; } -.sidebar-toolbar-actions { - display: flex; - align-items: center; - gap: 6px; +.sidebar-app-actions { + display: grid; + gap: 2px; + padding-bottom: 4px; } -.sidebar-icon-button { - position: relative; +.sidebar-action-row { display: grid; - width: 24px; - height: 24px; - place-items: center; + grid-template-columns: 22px minmax(0, 1fr); + align-items: center; + min-height: 32px; + gap: 8px; + padding: 0 8px; border-radius: 6px; background: transparent; - color: rgba(225, 232, 228, 0.58); + color: rgba(225, 232, 242, 0.72); + font-size: 13px; + font-weight: 680; + letter-spacing: 0; + text-align: left; transition: background 140ms ease, - color 140ms ease, - transform 140ms ease; + color 140ms ease; } -.sidebar-icon-button:not(:disabled):hover { - background: rgba(238, 244, 239, 0.08); - color: rgba(245, 248, 244, 0.9); - transform: translateY(-1px); +.sidebar-action-row:not(:disabled):hover { + background: rgba(213, 224, 255, 0.07); + color: rgba(247, 249, 255, 0.96); } -.sidebar-icon-button svg { +.sidebar-action-row svg { display: block; + width: 16px; + height: 16px; + justify-self: center; +} + +.sidebar-action-row span { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .sr-only { @@ -188,7 +178,7 @@ summary:focus-visible { display: flex; min-height: 0; flex-direction: column; - gap: 4px; + gap: 3px; } .sidebar-section-fill { @@ -213,8 +203,8 @@ summary:focus-visible { display: flex; align-items: center; justify-content: space-between; - min-height: 24px; - padding: 4px 8px 0 36px; + min-height: 22px; + padding: 4px 8px 0; } .sidebar-section-heading span { @@ -223,10 +213,10 @@ summary:focus-visible { .empty-row, .workspace-path { - min-height: 40px; - padding: 10px 2px; + min-height: 34px; + padding: 8px 2px; color: rgba(225, 232, 228, 0.52); - font-size: 15px; + font-size: 13px; font-weight: 680; } @@ -286,7 +276,7 @@ summary:focus-visible { } .project-list { - max-height: 132px; + max-height: 120px; } .session-list { @@ -299,7 +289,7 @@ summary:focus-visible { display: grid; align-items: center; width: 100%; - min-height: 42px; + min-height: 36px; border: 0; border-radius: 6px; background: transparent; @@ -311,44 +301,44 @@ summary:focus-visible { } .project-row { - grid-template-columns: 28px minmax(0, 1fr); - padding: 6px 8px 6px 6px; + grid-template-columns: 24px minmax(0, 1fr); + padding: 5px 8px 5px 6px; } .session-row { grid-template-columns: minmax(0, 1fr) auto; - column-gap: 8px; - padding: 6px 8px 6px 36px; + column-gap: 7px; + padding: 5px 8px 5px 28px; } .project-row-active, .session-row-active { - background: rgba(238, 244, 239, 0.052); + background: rgba(213, 224, 255, 0.062); color: var(--text); } .project-row-active::before, .session-row-active::before { position: absolute; - top: 9px; - bottom: 9px; + top: 7px; + bottom: 7px; left: 0; width: 2px; border-radius: 999px; - background: rgba(242, 199, 112, 0.74); + background: rgba(117, 131, 255, 0.78); content: ''; } .project-row:not(.project-row-active):hover, .session-row:not(.session-row-active):hover { - background: rgba(238, 244, 239, 0.035); + background: rgba(213, 224, 255, 0.045); color: rgba(248, 250, 247, 0.95); } .project-row-icon { justify-self: start; - width: 18px; - height: 18px; + width: 16px; + height: 16px; margin-left: 0; color: rgba(225, 232, 228, 0.82); } @@ -376,8 +366,8 @@ summary:focus-visible { .project-row-copy span, .session-row-title { - font-size: 14px; - font-weight: 680; + font-size: 13px; + font-weight: 660; line-height: 1.25; } @@ -419,11 +409,22 @@ summary:focus-visible { .session-row-meta { max-width: 48px; color: rgba(225, 232, 228, 0.54); - font-size: 11px; + font-size: 10px; font-weight: 720; line-height: 1; } +.sidebar-footer { + flex: 0 0 auto; + margin-top: auto; + padding-top: 6px; + border-top: 1px solid rgba(213, 224, 255, 0.08); +} + +.sidebar-footer-action { + width: 100%; +} + .session-row-draft { cursor: default; }