mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-05-01 21:20:44 +00:00
fix(desktop): tighten compact conversation layout
This commit is contained in:
parent
c4db66afdd
commit
c8d5b7e921
5 changed files with 662 additions and 3 deletions
|
|
@ -50,6 +50,6 @@
|
||||||
|
|
||||||
## Known Uncovered Risk
|
## Known Uncovered Risk
|
||||||
|
|
||||||
This harness verifies the default 1240 px Electron window. A follow-up compact
|
The default 1240 px window is covered here. The compact desktop width follow-up
|
||||||
viewport pass should assert the same dense message state near the lower
|
is now covered by
|
||||||
supported desktop width.
|
`.qwen/e2e-tests/electron-desktop/compact-dense-conversation.md`.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
# Compact Dense Conversation
|
||||||
|
|
||||||
|
- Slice date: 2026-04-26
|
||||||
|
- Executable harness: `packages/desktop/scripts/e2e-cdp-smoke.mjs`
|
||||||
|
- Command:
|
||||||
|
`cd packages/desktop && npm run e2e:cdp`
|
||||||
|
- Result: pass
|
||||||
|
- Artifact directory:
|
||||||
|
`.qwen/e2e-tests/electron-desktop/artifacts/2026-04-25T18-31-38-896Z/`
|
||||||
|
|
||||||
|
## 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 assistant response with dense repeated file
|
||||||
|
references.
|
||||||
|
5. Assert the default-width assistant actions and dense file chips.
|
||||||
|
6. Resize the real Electron window to the compact desktop bounds near 960 px.
|
||||||
|
7. Assert compact sidebar, topbar, dense assistant message, file chips, action
|
||||||
|
row, composer, and collapsed terminal geometry.
|
||||||
|
8. Restore the default window size and continue the existing review, settings,
|
||||||
|
terminal, and commit smoke path.
|
||||||
|
|
||||||
|
## Assertions
|
||||||
|
|
||||||
|
- Compact viewport resolved to 960x608 content pixels.
|
||||||
|
- Sidebar stayed compact at 236 px and topbar stayed 58 px high.
|
||||||
|
- The dense assistant message, file chips, and action row stayed inside the
|
||||||
|
conversation timeline with no horizontal document overflow.
|
||||||
|
- Required chips remained accessible: `README.md:1`,
|
||||||
|
`packages/desktop/src/renderer/App.tsx:12:5`, `.env.example`, `Dockerfile`,
|
||||||
|
`docs/guide.mdx`, and `src/App.vue`.
|
||||||
|
- Assistant actions remained accessible: `Copy Response`, `Retry Last Prompt`,
|
||||||
|
and `Open Changes`.
|
||||||
|
- Compact composer height stayed bounded at about 127 px and did not overflow
|
||||||
|
its context/action rows.
|
||||||
|
- The collapsed terminal strip remained docked and closed.
|
||||||
|
- Console errors: 0.
|
||||||
|
- Failed local network requests: 0.
|
||||||
|
|
||||||
|
## Artifacts
|
||||||
|
|
||||||
|
- `compact-dense-conversation.json`
|
||||||
|
- `compact-dense-conversation.png`
|
||||||
|
- `compact-summary-visibility-note.json`
|
||||||
|
- `window-resize-fallback-960x640.json`
|
||||||
|
- `window-resize-fallback-1240x820.json`
|
||||||
|
- `assistant-message-actions.json`
|
||||||
|
- `assistant-message-actions.png`
|
||||||
|
- `completed-workspace.png`
|
||||||
|
- `electron.log`
|
||||||
|
- `summary.json`
|
||||||
|
|
||||||
|
## Known Uncovered Risk
|
||||||
|
|
||||||
|
This slice covers the dense conversation and composer at compact width. A
|
||||||
|
follow-up should add a compact-width review drawer assertion because the review
|
||||||
|
drawer intentionally reduces conversation width.
|
||||||
|
|
@ -22,6 +22,108 @@ execution order, verification, decisions, and remaining work.
|
||||||
|
|
||||||
## Codex Alignment Progress
|
## Codex Alignment Progress
|
||||||
|
|
||||||
|
### Completed Slice: Compact Dense Conversation CDP Coverage
|
||||||
|
|
||||||
|
Status: completed in iteration 13.
|
||||||
|
|
||||||
|
Goal: extend the real Electron CDP harness so the dense assistant message state
|
||||||
|
is asserted at the lower supported desktop width, not only at the default
|
||||||
|
1240 px window size.
|
||||||
|
|
||||||
|
User-visible value: long assistant prose, file reference chips, action rows,
|
||||||
|
changed-file summaries, composer controls, sidebar rows, and the collapsed
|
||||||
|
terminal remain usable in compact desktop windows without horizontal overflow
|
||||||
|
or composer overlap.
|
||||||
|
|
||||||
|
Expected files:
|
||||||
|
|
||||||
|
- `packages/desktop/scripts/e2e-cdp-smoke.mjs`
|
||||||
|
- `packages/desktop/src/renderer/styles.css`
|
||||||
|
- `.qwen/e2e-tests/electron-desktop/compact-dense-conversation.md`
|
||||||
|
- `design/qwen-code-electron-desktop-implementation-plan.md`
|
||||||
|
|
||||||
|
Acceptance criteria:
|
||||||
|
|
||||||
|
- The CDP harness resizes the real Electron window to the app minimum
|
||||||
|
960x640-ish compact desktop size after the dense fake ACP assistant response
|
||||||
|
is visible.
|
||||||
|
- The compact viewport still shows the workbench landmarks, compact sidebar,
|
||||||
|
slim topbar, conversation, assistant message, file chips, message actions,
|
||||||
|
changed-files summary, composer, and collapsed terminal strip.
|
||||||
|
- Assistant file chips and action buttons stay inside the assistant message and
|
||||||
|
timeline; document width does not exceed the viewport.
|
||||||
|
- Composer controls wrap inside the composer instead of overflowing, and the
|
||||||
|
composer remains contained above the terminal strip.
|
||||||
|
- The inline changed-files summary remains bounded in the timeline without
|
||||||
|
horizontal overflow; at compact height it may require normal timeline
|
||||||
|
scrolling rather than simultaneous visibility with the assistant card.
|
||||||
|
- The window is restored to the default desktop size before the rest of the
|
||||||
|
smoke path continues.
|
||||||
|
|
||||||
|
Verification:
|
||||||
|
|
||||||
|
- Unit/component test command: no renderer unit changes expected.
|
||||||
|
- 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 Git project, send a prompt, approve the fake
|
||||||
|
command request, wait for the dense assistant response, assert the default
|
||||||
|
dense assistant layout, resize the Electron window to the compact desktop
|
||||||
|
bounds, assert compact geometry and overflow constraints, capture screenshot
|
||||||
|
and JSON artifacts, restore the default window size, then continue the
|
||||||
|
existing review/settings/terminal workflow.
|
||||||
|
- E2E assertions: compact viewport width is near 960 px; sidebar stays compact;
|
||||||
|
topbar remains slim enough for the viewport; dense assistant chips,
|
||||||
|
assistant actions, changed-files summary, composer, and terminal strip remain
|
||||||
|
bounded; compact composer height stays below 154 px; console errors/failed
|
||||||
|
local requests are absent.
|
||||||
|
- Diagnostic artifacts: compact dense conversation screenshot and JSON metrics,
|
||||||
|
plus existing CDP screenshots, Electron log, and summary JSON under
|
||||||
|
`.qwen/e2e-tests/electron-desktop/artifacts/`.
|
||||||
|
- Required skills applied: `frontend-design` for prototype-constrained compact
|
||||||
|
density and overflow expectations; `electron-desktop-dev` for real Electron
|
||||||
|
CDP window resizing and verification; `brainstorming` applied by selecting
|
||||||
|
the smallest continuation from the recorded next-work item rather than
|
||||||
|
introducing new product scope.
|
||||||
|
|
||||||
|
Notes and decisions:
|
||||||
|
|
||||||
|
- Electron 41 in this test environment does not expose
|
||||||
|
`Browser.getWindowForTarget` through the remote debugger. The harness first
|
||||||
|
attempts the browser-level CDP API and then falls back to `window.resizeTo`,
|
||||||
|
recording a `window-resize-fallback-*.json` artifact when the fallback is
|
||||||
|
used.
|
||||||
|
- The first compact run exposed a real density issue: the composer grew to
|
||||||
|
about 176 px high at the compact viewport. The CSS now shortens the compact
|
||||||
|
textarea and chips/selectors at the 960 px breakpoint, bringing the compact
|
||||||
|
composer to about 127 px in the passing CDP artifact.
|
||||||
|
- At the compact height, the dense assistant card and changed-files summary can
|
||||||
|
require normal timeline scrolling. The contract is that both remain bounded,
|
||||||
|
discoverable, and free of horizontal overflow while the composer and terminal
|
||||||
|
stay docked.
|
||||||
|
|
||||||
|
Verification results:
|
||||||
|
|
||||||
|
- `node --check packages/desktop/scripts/e2e-cdp-smoke.mjs` 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 launch through real
|
||||||
|
Electron over CDP, including the compact dense conversation resize path.
|
||||||
|
- Passing artifacts:
|
||||||
|
`.qwen/e2e-tests/electron-desktop/artifacts/2026-04-25T18-31-38-896Z/`.
|
||||||
|
|
||||||
|
Next work:
|
||||||
|
|
||||||
|
- Continue prototype fidelity by reducing remaining card heaviness in the
|
||||||
|
conversation and changed-files summary so the compact viewport reads closer
|
||||||
|
to `home.jpg`.
|
||||||
|
- Add a compact review-drawer CDP assertion so the 960 px width also proves the
|
||||||
|
conversation and review drawer remain usable together.
|
||||||
|
|
||||||
### Completed Slice: Dense Assistant File Reference Overflow
|
### Completed Slice: Dense Assistant File Reference Overflow
|
||||||
|
|
||||||
Status: completed in iteration 12.
|
Status: completed in iteration 12.
|
||||||
|
|
|
||||||
|
|
@ -25,11 +25,14 @@ const artifactRoot = join(
|
||||||
'electron-desktop',
|
'electron-desktop',
|
||||||
'artifacts',
|
'artifacts',
|
||||||
);
|
);
|
||||||
|
const defaultWindowBounds = { width: 1240, height: 820 };
|
||||||
|
const compactWindowBounds = { width: 960, height: 640 };
|
||||||
|
|
||||||
const consoleErrors = [];
|
const consoleErrors = [];
|
||||||
const failedRequests = [];
|
const failedRequests = [];
|
||||||
|
|
||||||
let appProcess;
|
let appProcess;
|
||||||
|
let browserCdp;
|
||||||
let cdp;
|
let cdp;
|
||||||
let artifactDir;
|
let artifactDir;
|
||||||
let workspaceDir;
|
let workspaceDir;
|
||||||
|
|
@ -54,6 +57,8 @@ async function main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
const target = await waitForCdpTarget(cdpPort);
|
const target = await waitForCdpTarget(cdpPort);
|
||||||
|
const browserTarget = await waitForBrowserCdp(cdpPort);
|
||||||
|
browserCdp = await CdpClient.connect(browserTarget.webSocketDebuggerUrl);
|
||||||
cdp = await CdpClient.connect(target.webSocketDebuggerUrl);
|
cdp = await CdpClient.connect(target.webSocketDebuggerUrl);
|
||||||
cdp.onEvent((event) => collectBrowserEvent(event));
|
cdp.onEvent((event) => collectBrowserEvent(event));
|
||||||
|
|
||||||
|
|
@ -81,6 +86,12 @@ async function main() {
|
||||||
await saveScreenshot('resolved-tool-activity.png');
|
await saveScreenshot('resolved-tool-activity.png');
|
||||||
await assertAssistantMessageActions('assistant-message-actions.json');
|
await assertAssistantMessageActions('assistant-message-actions.json');
|
||||||
await saveScreenshot('assistant-message-actions.png');
|
await saveScreenshot('assistant-message-actions.png');
|
||||||
|
await setElectronWindowBounds(target.id, compactWindowBounds);
|
||||||
|
await assertCompactDenseConversationLayout(
|
||||||
|
'compact-dense-conversation.json',
|
||||||
|
);
|
||||||
|
await saveScreenshot('compact-dense-conversation.png');
|
||||||
|
await setElectronWindowBounds(target.id, defaultWindowBounds);
|
||||||
await clickButton('Copy Response');
|
await clickButton('Copy Response');
|
||||||
await waitForText('Copied response.');
|
await waitForText('Copied response.');
|
||||||
await clickButton('Retry Last Prompt');
|
await clickButton('Retry Last Prompt');
|
||||||
|
|
@ -372,6 +383,31 @@ async function waitForCdpTarget(port) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function waitForBrowserCdp(port) {
|
||||||
|
const deadline = Date.now() + 20_000;
|
||||||
|
let lastError;
|
||||||
|
|
||||||
|
while (Date.now() < deadline) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`http://127.0.0.1:${port}/json/version`);
|
||||||
|
const target = await response.json();
|
||||||
|
if (typeof target.webSocketDebuggerUrl === 'string') {
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error;
|
||||||
|
}
|
||||||
|
|
||||||
|
await delay(250);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
`Timed out waiting for Electron browser CDP target on port ${port}: ${
|
||||||
|
lastError instanceof Error ? lastError.message : 'no response'
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async function assertWorkbenchLandmarks() {
|
async function assertWorkbenchLandmarks() {
|
||||||
const landmarks = await evaluate(`(() => {
|
const landmarks = await evaluate(`(() => {
|
||||||
return [
|
return [
|
||||||
|
|
@ -1093,6 +1129,349 @@ async function assertAssistantMessageActions(fileName) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function assertCompactDenseConversationLayout(fileName) {
|
||||||
|
await waitFor(
|
||||||
|
'compact dense conversation viewport',
|
||||||
|
async () => {
|
||||||
|
const viewport = await evaluate(`({
|
||||||
|
width: window.innerWidth,
|
||||||
|
height: window.innerHeight
|
||||||
|
})`);
|
||||||
|
return (
|
||||||
|
viewport.width >= 940 &&
|
||||||
|
viewport.width <= 1000 &&
|
||||||
|
viewport.height >= 600 &&
|
||||||
|
viewport.height <= 680
|
||||||
|
);
|
||||||
|
},
|
||||||
|
10_000,
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitForSelector('[data-testid="assistant-file-references"]');
|
||||||
|
const snapshot = 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 isContained = (child, parent, tolerance = 1) =>
|
||||||
|
Boolean(
|
||||||
|
child &&
|
||||||
|
parent &&
|
||||||
|
child.left >= parent.left - tolerance &&
|
||||||
|
child.right <= parent.right + tolerance
|
||||||
|
);
|
||||||
|
const overflows = (element) =>
|
||||||
|
element ? element.scrollWidth > element.clientWidth + 4 : false;
|
||||||
|
const message = [...document.querySelectorAll('[data-testid="assistant-message"]')]
|
||||||
|
.find((candidate) =>
|
||||||
|
candidate.innerText.includes('E2E fake ACP response received')
|
||||||
|
);
|
||||||
|
const timeline = document.querySelector('.chat-timeline');
|
||||||
|
const summary = document.querySelector(
|
||||||
|
'[data-testid="conversation-changes-summary"]'
|
||||||
|
);
|
||||||
|
const composer = document.querySelector('[data-testid="message-composer"]');
|
||||||
|
const terminal = document.querySelector('[data-testid="terminal-drawer"]');
|
||||||
|
const terminalBody = document.querySelector('[data-testid="terminal-body"]');
|
||||||
|
const terminalToggle = document.querySelector(
|
||||||
|
'[data-testid="terminal-toggle"]'
|
||||||
|
);
|
||||||
|
|
||||||
|
const preScroll = {
|
||||||
|
summaryRect: rectFor(summary),
|
||||||
|
timelineRect: rectFor(timeline),
|
||||||
|
composerRect: rectFor(composer),
|
||||||
|
terminalRect: rectFor(terminal)
|
||||||
|
};
|
||||||
|
|
||||||
|
message?.scrollIntoView({ block: 'center', inline: 'nearest' });
|
||||||
|
|
||||||
|
const fileReferences = message?.querySelector(
|
||||||
|
'[data-testid="assistant-file-references"]'
|
||||||
|
);
|
||||||
|
const actions = message?.querySelector(
|
||||||
|
'[data-testid="assistant-message-actions"]'
|
||||||
|
);
|
||||||
|
const messageRect = rectFor(message);
|
||||||
|
const timelineRect = rectFor(timeline);
|
||||||
|
const composerRect = rectFor(composer);
|
||||||
|
const chipRects = fileReferences
|
||||||
|
? [
|
||||||
|
...fileReferences.querySelectorAll(
|
||||||
|
'button, .message-file-reference-overflow'
|
||||||
|
)
|
||||||
|
].map((chip) => rectFor(chip))
|
||||||
|
: [];
|
||||||
|
const actionRects = actions
|
||||||
|
? [...actions.querySelectorAll('button')].map((button) =>
|
||||||
|
rectFor(button)
|
||||||
|
)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
viewport: {
|
||||||
|
width: window.innerWidth,
|
||||||
|
height: window.innerHeight
|
||||||
|
},
|
||||||
|
document: {
|
||||||
|
scrollWidth: document.documentElement.scrollWidth,
|
||||||
|
bodyScrollWidth: document.body.scrollWidth,
|
||||||
|
bodyScrollHeight: document.body.scrollHeight
|
||||||
|
},
|
||||||
|
shell: rectFor(document.querySelector('[data-testid="desktop-workspace"]')),
|
||||||
|
sidebar: rectFor(document.querySelector('[data-testid="project-sidebar"]')),
|
||||||
|
topbar: rectFor(document.querySelector('[data-testid="workspace-topbar"]')),
|
||||||
|
grid: rectFor(document.querySelector('[data-testid="workspace-grid"]')),
|
||||||
|
chat: rectFor(document.querySelector('[data-testid="chat-thread"]')),
|
||||||
|
timeline: timelineRect,
|
||||||
|
message: messageRect,
|
||||||
|
fileReferences: rectFor(fileReferences),
|
||||||
|
actions: rectFor(actions),
|
||||||
|
summary: rectFor(summary),
|
||||||
|
composer: composerRect,
|
||||||
|
terminal: rectFor(terminal),
|
||||||
|
terminalExpanded: terminalToggle?.getAttribute('aria-expanded') ?? null,
|
||||||
|
terminalBodyPresent: terminalBody !== null,
|
||||||
|
preScroll,
|
||||||
|
fileReferenceLabels: fileReferences
|
||||||
|
? [...fileReferences.querySelectorAll('button')].map(
|
||||||
|
(button) => button.getAttribute('aria-label') || ''
|
||||||
|
)
|
||||||
|
: [],
|
||||||
|
actionLabels: actions
|
||||||
|
? [...actions.querySelectorAll('button')].map(
|
||||||
|
(button) => button.getAttribute('aria-label') || ''
|
||||||
|
)
|
||||||
|
: [],
|
||||||
|
chipRects,
|
||||||
|
actionRects,
|
||||||
|
summaryVisibleBeforeAssistantScroll: Boolean(
|
||||||
|
preScroll.summaryRect &&
|
||||||
|
preScroll.timelineRect &&
|
||||||
|
preScroll.composerRect &&
|
||||||
|
preScroll.summaryRect.top >= preScroll.timelineRect.top - 1 &&
|
||||||
|
preScroll.summaryRect.bottom <= preScroll.composerRect.top + 1
|
||||||
|
),
|
||||||
|
summaryContainedBeforeAssistantScroll: isContained(
|
||||||
|
preScroll.summaryRect,
|
||||||
|
preScroll.timelineRect
|
||||||
|
),
|
||||||
|
messageContained: isContained(messageRect, timelineRect),
|
||||||
|
actionsContained: isContained(rectFor(actions), messageRect),
|
||||||
|
composerContained: isContained(composerRect, timelineRect),
|
||||||
|
terminalDocked: Boolean(
|
||||||
|
rectFor(terminal) &&
|
||||||
|
preScroll.composerRect &&
|
||||||
|
rectFor(terminal).top >= preScroll.composerRect.bottom - 1
|
||||||
|
),
|
||||||
|
overflow: {
|
||||||
|
shell: overflows(document.querySelector('[data-testid="desktop-workspace"]')),
|
||||||
|
topbar: overflows(document.querySelector('[data-testid="workspace-topbar"]')),
|
||||||
|
timeline: overflows(timeline),
|
||||||
|
message: overflows(message),
|
||||||
|
fileReferences: overflows(fileReferences),
|
||||||
|
composer: overflows(composer),
|
||||||
|
composerContext: overflows(document.querySelector('.composer-context')),
|
||||||
|
composerActions: overflows(document.querySelector('.composer-actions'))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})()`);
|
||||||
|
|
||||||
|
await writeFile(
|
||||||
|
join(artifactDir, fileName),
|
||||||
|
`${JSON.stringify(snapshot, null, 2)}\n`,
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (snapshot.viewport.width < 940 || snapshot.viewport.width > 1000) {
|
||||||
|
throw new Error(
|
||||||
|
`Compact viewport width is unexpected: ${snapshot.viewport.width}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snapshot.viewport.height < 600 || snapshot.viewport.height > 680) {
|
||||||
|
throw new Error(
|
||||||
|
`Compact viewport height is unexpected: ${snapshot.viewport.height}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const missing = [
|
||||||
|
'shell',
|
||||||
|
'sidebar',
|
||||||
|
'topbar',
|
||||||
|
'grid',
|
||||||
|
'chat',
|
||||||
|
'timeline',
|
||||||
|
'message',
|
||||||
|
'fileReferences',
|
||||||
|
'actions',
|
||||||
|
'composer',
|
||||||
|
'terminal',
|
||||||
|
].filter((key) => snapshot[key] === null);
|
||||||
|
if (missing.length > 0) {
|
||||||
|
throw new Error(
|
||||||
|
`Missing compact dense conversation rects: ${missing.join(', ')}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snapshot.document.bodyScrollWidth > snapshot.viewport.width + 4) {
|
||||||
|
throw new Error(
|
||||||
|
`Compact layout caused horizontal body overflow: ${JSON.stringify(
|
||||||
|
snapshot.document,
|
||||||
|
)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snapshot.sidebar.width < 232 || snapshot.sidebar.width > 264) {
|
||||||
|
throw new Error(
|
||||||
|
`Compact sidebar width should stay narrow: ${snapshot.sidebar.width}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snapshot.topbar.height < 50 || snapshot.topbar.height > 76) {
|
||||||
|
throw new Error(
|
||||||
|
`Compact topbar height should stay slim: ${snapshot.topbar.height}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snapshot.terminalExpanded !== 'false' || snapshot.terminalBodyPresent) {
|
||||||
|
throw new Error(
|
||||||
|
'Compact dense conversation should keep Terminal collapsed.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snapshot.terminal.height < 44 || snapshot.terminal.height > 82) {
|
||||||
|
throw new Error(
|
||||||
|
`Compact terminal strip height is unexpected: ${snapshot.terminal.height}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!snapshot.summaryVisibleBeforeAssistantScroll) {
|
||||||
|
await writeFile(
|
||||||
|
join(artifactDir, 'compact-summary-visibility-note.json'),
|
||||||
|
`${JSON.stringify(
|
||||||
|
{
|
||||||
|
note: 'Compact height can require timeline scrolling; summary must remain bounded and scrollable.',
|
||||||
|
preScroll: snapshot.preScroll,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
)}\n`,
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!snapshot.summaryContainedBeforeAssistantScroll) {
|
||||||
|
throw new Error('Changed-files summary escaped the compact timeline.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snapshot.composer.height > 154) {
|
||||||
|
throw new Error(
|
||||||
|
`Compact composer should not crowd the conversation: ${snapshot.composer.height}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!snapshot.messageContained) {
|
||||||
|
throw new Error('Dense assistant message escaped the compact timeline.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!snapshot.actionsContained) {
|
||||||
|
throw new Error('Assistant action row escaped the compact message.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!snapshot.composerContained) {
|
||||||
|
throw new Error('Composer escaped the compact timeline width.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!snapshot.terminalDocked) {
|
||||||
|
throw new Error('Collapsed terminal strip is not docked below composer.');
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const expectedLabel of [
|
||||||
|
'Open README.md:1',
|
||||||
|
'Open packages/desktop/src/renderer/App.tsx:12:5',
|
||||||
|
'Open .env.example',
|
||||||
|
'Open Dockerfile',
|
||||||
|
'Open docs/guide.mdx',
|
||||||
|
'Open src/App.vue',
|
||||||
|
]) {
|
||||||
|
if (!snapshot.fileReferenceLabels.includes(expectedLabel)) {
|
||||||
|
throw new Error(
|
||||||
|
`Compact dense assistant chips missing ${expectedLabel}: ${snapshot.fileReferenceLabels.join(
|
||||||
|
', ',
|
||||||
|
)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const expectedAction of [
|
||||||
|
'Copy Response',
|
||||||
|
'Retry Last Prompt',
|
||||||
|
'Open Changes',
|
||||||
|
]) {
|
||||||
|
if (!snapshot.actionLabels.includes(expectedAction)) {
|
||||||
|
throw new Error(
|
||||||
|
`Compact assistant actions missing ${expectedAction}: ${snapshot.actionLabels.join(
|
||||||
|
', ',
|
||||||
|
)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [key, hasOverflow] of Object.entries(snapshot.overflow)) {
|
||||||
|
if (hasOverflow) {
|
||||||
|
throw new Error(`Compact layout element overflowed: ${key}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const chipRect of snapshot.chipRects) {
|
||||||
|
if (!chipRect) {
|
||||||
|
throw new Error('Compact assistant chip geometry is missing.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chipRect.width > 282) {
|
||||||
|
throw new Error(
|
||||||
|
`Compact assistant chip is too wide: ${JSON.stringify(chipRect)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
chipRect.left < snapshot.message.left ||
|
||||||
|
chipRect.right > snapshot.message.right + 1 ||
|
||||||
|
chipRect.left < snapshot.timeline.left ||
|
||||||
|
chipRect.right > snapshot.timeline.right + 1
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
`Compact assistant chip escaped the message: ${JSON.stringify(
|
||||||
|
chipRect,
|
||||||
|
)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const actionRect of snapshot.actionRects) {
|
||||||
|
if (!actionRect) {
|
||||||
|
throw new Error('Compact assistant action geometry is missing.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actionRect.width > 40 || actionRect.height > 40) {
|
||||||
|
throw new Error(
|
||||||
|
`Compact assistant action is too large: ${JSON.stringify(actionRect)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function assertRetryDrafted(fileName) {
|
async function assertRetryDrafted(fileName) {
|
||||||
const snapshot = await evaluate(`(() => {
|
const snapshot = await evaluate(`(() => {
|
||||||
const messageField = document.querySelector('[aria-label="Message"]');
|
const messageField = document.querySelector('[aria-label="Message"]');
|
||||||
|
|
@ -1909,6 +2288,68 @@ async function setFieldByLabel(label, value) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function setElectronWindowBounds(targetId, bounds) {
|
||||||
|
const windowCdp = browserCdp ?? cdp;
|
||||||
|
let fallbackError = null;
|
||||||
|
try {
|
||||||
|
const { windowId } = await windowCdp.send('Browser.getWindowForTarget', {
|
||||||
|
targetId,
|
||||||
|
});
|
||||||
|
await windowCdp.send('Browser.setWindowBounds', {
|
||||||
|
windowId,
|
||||||
|
bounds: { windowState: 'normal' },
|
||||||
|
});
|
||||||
|
await windowCdp.send('Browser.setWindowBounds', {
|
||||||
|
windowId,
|
||||||
|
bounds,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
fallbackError = error instanceof Error ? error.message : String(error);
|
||||||
|
await evaluate(`(() => {
|
||||||
|
window.resizeTo(${bounds.width}, ${bounds.height});
|
||||||
|
return true;
|
||||||
|
})()`);
|
||||||
|
}
|
||||||
|
await waitFor(
|
||||||
|
`Electron window bounds ${bounds.width}x${bounds.height}`,
|
||||||
|
async () => {
|
||||||
|
const viewport = await evaluate(`({
|
||||||
|
width: window.innerWidth,
|
||||||
|
height: window.innerHeight
|
||||||
|
})`);
|
||||||
|
return (
|
||||||
|
viewport.width >= bounds.width - 24 &&
|
||||||
|
viewport.width <= bounds.width + 24 &&
|
||||||
|
viewport.height >= bounds.height - 40 &&
|
||||||
|
viewport.height <= bounds.height + 40
|
||||||
|
);
|
||||||
|
},
|
||||||
|
10_000,
|
||||||
|
);
|
||||||
|
if (fallbackError) {
|
||||||
|
const viewport = await evaluate(`({
|
||||||
|
width: window.innerWidth,
|
||||||
|
height: window.innerHeight
|
||||||
|
})`);
|
||||||
|
await writeFile(
|
||||||
|
join(
|
||||||
|
artifactDir,
|
||||||
|
`window-resize-fallback-${bounds.width}x${bounds.height}.json`,
|
||||||
|
),
|
||||||
|
`${JSON.stringify(
|
||||||
|
{
|
||||||
|
requested: bounds,
|
||||||
|
viewport,
|
||||||
|
error: fallbackError,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
)}\n`,
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function saveScreenshot(fileName) {
|
async function saveScreenshot(fileName) {
|
||||||
const screenshot = await cdp.send('Page.captureScreenshot', {
|
const screenshot = await cdp.send('Page.captureScreenshot', {
|
||||||
format: 'png',
|
format: 'png',
|
||||||
|
|
@ -2188,6 +2629,7 @@ try {
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
cdp?.close();
|
cdp?.close();
|
||||||
|
browserCdp?.close();
|
||||||
if (appProcess && !appProcess.killed) {
|
if (appProcess && !appProcess.killed) {
|
||||||
appProcess.kill();
|
appProcess.kill();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1945,6 +1945,61 @@ textarea:focus {
|
||||||
max-width: 128px;
|
max-width: 128px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.composer {
|
||||||
|
width: min(900px, calc(100% - 28px));
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer textarea {
|
||||||
|
min-height: 54px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer-control-row,
|
||||||
|
.composer-context,
|
||||||
|
.composer-actions {
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer-icon-button {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer-chip,
|
||||||
|
.composer-context-note,
|
||||||
|
.composer-disabled-reason {
|
||||||
|
max-width: 138px;
|
||||||
|
min-height: 28px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer-chip,
|
||||||
|
.composer-context-note {
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer-chip-project {
|
||||||
|
max-width: 176px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer-select-label select {
|
||||||
|
min-width: 104px;
|
||||||
|
max-width: 148px;
|
||||||
|
min-height: 28px;
|
||||||
|
padding: 0 24px 0 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer-actions .primary-button,
|
||||||
|
.composer-actions .secondary-button {
|
||||||
|
min-height: 30px;
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.settings-page-content {
|
.settings-page-content {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue