mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-28 11:41:04 +00:00
feat(desktop): align sidebar app rail
This commit is contained in:
parent
3bf70ebb57
commit
18d5552cc3
6 changed files with 450 additions and 102 deletions
|
|
@ -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.
|
||||||
|
|
@ -22,6 +22,103 @@ execution order, verification, decisions, and remaining work.
|
||||||
|
|
||||||
## Codex Alignment Progress
|
## 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
|
### Completed Slice: Inline Tool Activity Prototype Fidelity
|
||||||
|
|
||||||
Status: completed in iteration 17.
|
Status: completed in iteration 17.
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,7 @@ async function main() {
|
||||||
await setFieldByAriaLabel('Message', '');
|
await setFieldByAriaLabel('Message', '');
|
||||||
await assertConversationChangesSummary('conversation-changes-summary.json');
|
await assertConversationChangesSummary('conversation-changes-summary.json');
|
||||||
await waitForSelector('[data-testid="thread-list"]');
|
await waitForSelector('[data-testid="thread-list"]');
|
||||||
|
await assertSidebarAppRail('sidebar-app-rail.json');
|
||||||
|
|
||||||
await clickButton('Review Changes');
|
await clickButton('Review Changes');
|
||||||
await waitForText('README.md');
|
await waitForText('README.md');
|
||||||
|
|
@ -419,6 +420,8 @@ async function assertWorkbenchLandmarks() {
|
||||||
return [
|
return [
|
||||||
'desktop-workspace',
|
'desktop-workspace',
|
||||||
'project-sidebar',
|
'project-sidebar',
|
||||||
|
'sidebar-app-actions',
|
||||||
|
'sidebar-footer-settings',
|
||||||
'workspace-topbar',
|
'workspace-topbar',
|
||||||
'workspace-grid',
|
'workspace-grid',
|
||||||
'chat-thread',
|
'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) {
|
async function assertConversationChangesSummary(fileName) {
|
||||||
await waitForSelector('[data-testid="conversation-changes-summary"]');
|
await waitForSelector('[data-testid="conversation-changes-summary"]');
|
||||||
const snapshot = await evaluate(`(() => {
|
const snapshot = await evaluate(`(() => {
|
||||||
|
|
|
||||||
|
|
@ -50,44 +50,49 @@ export function ProjectSidebar({
|
||||||
aria-label="Projects and threads"
|
aria-label="Projects and threads"
|
||||||
data-testid="project-sidebar"
|
data-testid="project-sidebar"
|
||||||
>
|
>
|
||||||
<div className="sidebar-toolbar">
|
<nav
|
||||||
<h1>Projects</h1>
|
className="sidebar-app-actions"
|
||||||
<div className="sidebar-toolbar-actions">
|
aria-label="Workspace actions"
|
||||||
<button
|
data-testid="sidebar-app-actions"
|
||||||
aria-label="Settings"
|
>
|
||||||
className="sidebar-icon-button"
|
<button
|
||||||
title="Settings"
|
aria-label="New Thread"
|
||||||
type="button"
|
className="sidebar-action-row"
|
||||||
onClick={onOpenSettings}
|
disabled={loadState.state !== 'ready' || !activeProject}
|
||||||
>
|
title="New Thread"
|
||||||
<SlidersIcon />
|
type="button"
|
||||||
<span className="sr-only">Settings</span>
|
onClick={onCreateSession}
|
||||||
</button>
|
>
|
||||||
<button
|
<NewThreadIcon />
|
||||||
aria-label="New Thread"
|
<span>New Thread</span>
|
||||||
className="sidebar-icon-button"
|
</button>
|
||||||
disabled={loadState.state !== 'ready' || !activeProject}
|
<button
|
||||||
title="New Thread"
|
aria-label="Open Project"
|
||||||
type="button"
|
className="sidebar-action-row"
|
||||||
onClick={onCreateSession}
|
title="Open Project"
|
||||||
>
|
type="button"
|
||||||
<NewThreadIcon />
|
onClick={onChooseWorkspace}
|
||||||
<span className="sr-only">New Thread</span>
|
>
|
||||||
</button>
|
<FolderPlusIcon />
|
||||||
<button
|
<span>Open Project</span>
|
||||||
aria-label="Open Project"
|
</button>
|
||||||
className="sidebar-icon-button"
|
<button
|
||||||
title="Open Project"
|
aria-label="Models"
|
||||||
type="button"
|
className="sidebar-action-row"
|
||||||
onClick={onChooseWorkspace}
|
title="Models"
|
||||||
>
|
type="button"
|
||||||
<FolderPlusIcon />
|
onClick={onOpenSettings}
|
||||||
<span className="sr-only">Open Project</span>
|
>
|
||||||
</button>
|
<SlidersIcon />
|
||||||
</div>
|
<span>Models</span>
|
||||||
</div>
|
</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
<section className="sidebar-section project-navigator">
|
<section className="sidebar-section project-navigator">
|
||||||
|
<div className="sidebar-section-heading">
|
||||||
|
<h2>Projects</h2>
|
||||||
|
<span>{projects.length}</span>
|
||||||
|
</div>
|
||||||
<ProjectList
|
<ProjectList
|
||||||
activeProjectId={activeProjectId}
|
activeProjectId={activeProjectId}
|
||||||
projects={projects}
|
projects={projects}
|
||||||
|
|
@ -107,6 +112,19 @@ export function ProjectSidebar({
|
||||||
onSelect={onSelectSession}
|
onSelect={onSelectSession}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
<div className="sidebar-footer">
|
||||||
|
<button
|
||||||
|
aria-label="Settings"
|
||||||
|
className="sidebar-action-row sidebar-footer-action"
|
||||||
|
data-testid="sidebar-footer-settings"
|
||||||
|
title="Settings"
|
||||||
|
type="button"
|
||||||
|
onClick={onOpenSettings}
|
||||||
|
>
|
||||||
|
<SlidersIcon />
|
||||||
|
<span>Settings</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ describe('WorkspacePage', () => {
|
||||||
for (const testId of [
|
for (const testId of [
|
||||||
'desktop-workspace',
|
'desktop-workspace',
|
||||||
'project-sidebar',
|
'project-sidebar',
|
||||||
|
'sidebar-app-actions',
|
||||||
'workspace-topbar',
|
'workspace-topbar',
|
||||||
'workspace-grid',
|
'workspace-grid',
|
||||||
'chat-thread',
|
'chat-thread',
|
||||||
|
|
@ -64,9 +65,18 @@ describe('WorkspacePage', () => {
|
||||||
expect(
|
expect(
|
||||||
renderedContainer.querySelector('[data-testid="settings-page"]'),
|
renderedContainer.querySelector('[data-testid="settings-page"]'),
|
||||||
).toBeNull();
|
).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('example-workspace');
|
||||||
expect(renderedContainer.textContent).toContain('main');
|
expect(renderedContainer.textContent).toContain('main');
|
||||||
|
expect(renderedContainer.textContent).toContain('New Thread');
|
||||||
|
expect(renderedContainer.textContent).toContain('Open Project');
|
||||||
|
expect(renderedContainer.textContent).toContain('Models');
|
||||||
expect(
|
expect(
|
||||||
renderedContainer.querySelector('[data-testid="project-sidebar"]')
|
renderedContainer.querySelector('[data-testid="project-sidebar"]')
|
||||||
?.textContent,
|
?.textContent,
|
||||||
|
|
|
||||||
|
|
@ -96,44 +96,21 @@ summary:focus-visible {
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 9px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 16px 12px 14px;
|
padding: 12px 10px;
|
||||||
border-right: 1px solid var(--line);
|
border-right: 1px solid var(--line);
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(37, 58, 61, 0.95), rgba(22, 28, 29, 0.98)),
|
linear-gradient(180deg, rgba(20, 26, 51, 0.98), rgba(13, 17, 32, 0.98)),
|
||||||
radial-gradient(
|
#101322;
|
||||||
circle at 32px 0,
|
|
||||||
rgba(242, 199, 112, 0.08),
|
|
||||||
transparent 34%
|
|
||||||
),
|
|
||||||
#151b1d;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.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 h2,
|
||||||
.topbar p,
|
.topbar p,
|
||||||
.panel h3 {
|
.panel h3 {
|
||||||
margin: 0;
|
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,
|
.eyebrow,
|
||||||
.message-role {
|
.message-role {
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
|
|
@ -143,35 +120,48 @@ summary:focus-visible {
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-toolbar-actions {
|
.sidebar-app-actions {
|
||||||
display: flex;
|
display: grid;
|
||||||
align-items: center;
|
gap: 2px;
|
||||||
gap: 6px;
|
padding-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-icon-button {
|
.sidebar-action-row {
|
||||||
position: relative;
|
|
||||||
display: grid;
|
display: grid;
|
||||||
width: 24px;
|
grid-template-columns: 22px minmax(0, 1fr);
|
||||||
height: 24px;
|
align-items: center;
|
||||||
place-items: center;
|
min-height: 32px;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 0 8px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
background: transparent;
|
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:
|
transition:
|
||||||
background 140ms ease,
|
background 140ms ease,
|
||||||
color 140ms ease,
|
color 140ms ease;
|
||||||
transform 140ms ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-icon-button:not(:disabled):hover {
|
.sidebar-action-row:not(:disabled):hover {
|
||||||
background: rgba(238, 244, 239, 0.08);
|
background: rgba(213, 224, 255, 0.07);
|
||||||
color: rgba(245, 248, 244, 0.9);
|
color: rgba(247, 249, 255, 0.96);
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-icon-button svg {
|
.sidebar-action-row svg {
|
||||||
display: block;
|
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 {
|
.sr-only {
|
||||||
|
|
@ -188,7 +178,7 @@ summary:focus-visible {
|
||||||
display: flex;
|
display: flex;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 4px;
|
gap: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-section-fill {
|
.sidebar-section-fill {
|
||||||
|
|
@ -213,8 +203,8 @@ summary:focus-visible {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
min-height: 24px;
|
min-height: 22px;
|
||||||
padding: 4px 8px 0 36px;
|
padding: 4px 8px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-section-heading span {
|
.sidebar-section-heading span {
|
||||||
|
|
@ -223,10 +213,10 @@ summary:focus-visible {
|
||||||
|
|
||||||
.empty-row,
|
.empty-row,
|
||||||
.workspace-path {
|
.workspace-path {
|
||||||
min-height: 40px;
|
min-height: 34px;
|
||||||
padding: 10px 2px;
|
padding: 8px 2px;
|
||||||
color: rgba(225, 232, 228, 0.52);
|
color: rgba(225, 232, 228, 0.52);
|
||||||
font-size: 15px;
|
font-size: 13px;
|
||||||
font-weight: 680;
|
font-weight: 680;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -286,7 +276,7 @@ summary:focus-visible {
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-list {
|
.project-list {
|
||||||
max-height: 132px;
|
max-height: 120px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.session-list {
|
.session-list {
|
||||||
|
|
@ -299,7 +289,7 @@ summary:focus-visible {
|
||||||
display: grid;
|
display: grid;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 42px;
|
min-height: 36px;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
|
@ -311,44 +301,44 @@ summary:focus-visible {
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-row {
|
.project-row {
|
||||||
grid-template-columns: 28px minmax(0, 1fr);
|
grid-template-columns: 24px minmax(0, 1fr);
|
||||||
padding: 6px 8px 6px 6px;
|
padding: 5px 8px 5px 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.session-row {
|
.session-row {
|
||||||
grid-template-columns: minmax(0, 1fr) auto;
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
column-gap: 8px;
|
column-gap: 7px;
|
||||||
padding: 6px 8px 6px 36px;
|
padding: 5px 8px 5px 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-row-active,
|
.project-row-active,
|
||||||
.session-row-active {
|
.session-row-active {
|
||||||
background: rgba(238, 244, 239, 0.052);
|
background: rgba(213, 224, 255, 0.062);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-row-active::before,
|
.project-row-active::before,
|
||||||
.session-row-active::before {
|
.session-row-active::before {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 9px;
|
top: 7px;
|
||||||
bottom: 9px;
|
bottom: 7px;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 2px;
|
width: 2px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: rgba(242, 199, 112, 0.74);
|
background: rgba(117, 131, 255, 0.78);
|
||||||
content: '';
|
content: '';
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-row:not(.project-row-active):hover,
|
.project-row:not(.project-row-active):hover,
|
||||||
.session-row:not(.session-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);
|
color: rgba(248, 250, 247, 0.95);
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-row-icon {
|
.project-row-icon {
|
||||||
justify-self: start;
|
justify-self: start;
|
||||||
width: 18px;
|
width: 16px;
|
||||||
height: 18px;
|
height: 16px;
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
color: rgba(225, 232, 228, 0.82);
|
color: rgba(225, 232, 228, 0.82);
|
||||||
}
|
}
|
||||||
|
|
@ -376,8 +366,8 @@ summary:focus-visible {
|
||||||
|
|
||||||
.project-row-copy span,
|
.project-row-copy span,
|
||||||
.session-row-title {
|
.session-row-title {
|
||||||
font-size: 14px;
|
font-size: 13px;
|
||||||
font-weight: 680;
|
font-weight: 660;
|
||||||
line-height: 1.25;
|
line-height: 1.25;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -419,11 +409,22 @@ summary:focus-visible {
|
||||||
.session-row-meta {
|
.session-row-meta {
|
||||||
max-width: 48px;
|
max-width: 48px;
|
||||||
color: rgba(225, 232, 228, 0.54);
|
color: rgba(225, 232, 228, 0.54);
|
||||||
font-size: 11px;
|
font-size: 10px;
|
||||||
font-weight: 720;
|
font-weight: 720;
|
||||||
line-height: 1;
|
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 {
|
.session-row-draft {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue