diff --git a/.qwen/e2e-tests/electron-desktop/terminal-drawer.md b/.qwen/e2e-tests/electron-desktop/terminal-drawer.md index df5f6cc36..f91b896f2 100644 --- a/.qwen/e2e-tests/electron-desktop/terminal-drawer.md +++ b/.qwen/e2e-tests/electron-desktop/terminal-drawer.md @@ -15,8 +15,9 @@ Slice 13 basic scoped terminal. 5. Copy the terminal transcript and verify the UI reports copy success. 6. Start a command that waits for stdin, send input through the drawer, and verify the command output includes that stdin. -7. Send the terminal output to the active AI thread and approve the fake ACP - command request. +7. Attach the terminal output to the composer, verify no AI turn starts until + the composer Send action is clicked, then approve the fake ACP command + request. 8. Start a long-running command and click Kill. 9. Click Clear and verify the drawer output resets. @@ -32,8 +33,9 @@ Slice 13 basic scoped terminal. are visible in the bottom drawer and do not use Node integration. - Copy output uses the preload-whitelisted Electron clipboard IPC, not renderer Node integration or an unbounded IPC channel. -- Send to AI uses the existing authenticated WebSocket user-message path with - a bounded terminal transcript. +- Attach Output appends a bounded terminal transcript to the composer draft, + does not touch the session WebSocket immediately, and requires the normal + composer Send action before a new agent turn starts. ## Diagnostics on Failure @@ -97,6 +99,34 @@ Additional artifacts collected: - `completed-layout.json` - `completed-workspace.png` +## Automated Coverage Added In Codex Alignment Iteration 5 + +Iteration 5 changes terminal follow-up from direct AI send to explicit +attach-to-composer and updates the real Electron CDP smoke to cover the safer +workflow: + +1. Launch real Electron with isolated HOME/runtime/user-data and fake ACP. +2. Open the fake Git project, create a thread from the project composer, and + complete the existing approval/review/commit path. +3. Expand Terminal, run stdout and stdin commands, then click `Attach Output`. +4. Assert the composer contains the bounded terminal transcript, the terminal + action is labeled `Attach Output`, the legacy `Send to AI` action is absent, + and no new `Approve Once` request appears before composer Send. +5. Click composer `Send`, approve the fake ACP request, and verify the fake ACP + response includes the terminal-output prompt. + +Executable harness: + +- `packages/desktop/scripts/e2e-cdp-smoke.mjs` + +Additional artifacts collected: + +- `terminal-attachment.json` +- `terminal-expanded-layout.json` +- `terminal-expanded.png` +- `completed-layout.json` +- `completed-workspace.png` + ## Execution Results Codex alignment iteration 4: @@ -110,6 +140,17 @@ Codex alignment iteration 4: - Success artifacts: `.qwen/e2e-tests/electron-desktop/artifacts/2026-04-25T17-00-08-461Z/`. +Codex alignment iteration 5: + +- `cd packages/desktop && SHELL=/bin/bash npx vitest run src/renderer/components/layout/WorkspacePage.test.tsx` + passed: 5 tests. +- `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. +- Success artifacts: + `.qwen/e2e-tests/electron-desktop/artifacts/2026-04-25T17-08-17-022Z/`. + Slice 16: - `npm run test --workspace=packages/desktop` passed: 9 files, 55 tests. diff --git a/design/qwen-code-electron-desktop-implementation-plan.md b/design/qwen-code-electron-desktop-implementation-plan.md index 502efbb72..d0f13729b 100644 --- a/design/qwen-code-electron-desktop-implementation-plan.md +++ b/design/qwen-code-electron-desktop-implementation-plan.md @@ -22,7 +22,101 @@ execution order, verification, decisions, and remaining work. ## Codex Alignment Progress -### Active Slice: Collapsed Terminal Status Strip Alignment +### Completed Slice: Terminal Attach-to-Composer Workflow + +Status: completed in iteration 5. + +Goal: change terminal output follow-up from an immediate `Send to AI` action +into an explicit attach-to-composer flow, so users can review and edit command +output before deciding whether to send it to the agent. + +User-visible value: terminal output becomes contextual material in the task +composer rather than a hidden second send path that can unexpectedly trigger a +new agent turn. This keeps the conversation-first workbench aligned with +`home.jpg` while preserving the terminal as a supporting tool. + +Expected files: + +- `packages/desktop/src/renderer/App.tsx` +- `packages/desktop/src/renderer/api/websocket.ts` +- `packages/desktop/src/renderer/components/layout/WorkspacePage.tsx` +- `packages/desktop/src/renderer/components/layout/TerminalDrawer.tsx` +- `packages/desktop/src/renderer/components/layout/SidebarIcons.tsx` +- `packages/desktop/src/renderer/components/layout/WorkspacePage.test.tsx` +- `packages/desktop/scripts/e2e-cdp-smoke.mjs` +- `.qwen/e2e-tests/electron-desktop/terminal-drawer.md` +- `design/qwen-code-electron-desktop-implementation-plan.md` + +Acceptance criteria: + +- The expanded Terminal action is labeled as attaching output to the composer, + not sending directly to AI. +- Attaching terminal output appends a bounded terminal transcript to the + existing composer text and shows a clear success notice. +- The attach action works whenever terminal output exists, including before a + thread is selected, and does not require or write to the session WebSocket. +- The user must still click Send from the composer before a new agent turn is + created. +- Copy, clear, kill, run command, stdin, expand, and collapse behavior is + unchanged. + +Verification: + +- Unit/component test command: + `cd packages/desktop && SHELL=/bin/bash npx vitest run src/renderer/components/layout/WorkspacePage.test.tsx` +- 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, create a composer-first thread, + approve the fake command, review and commit changes, expand Terminal, run + stdout and stdin commands, attach the resulting output to the composer, + assert no fake ACP follow-up happens until Send is clicked, then send the + composer text and approve the fake command request. +- E2E assertions: attach button is present and `Send to AI` is absent; composer + contains the terminal transcript after attach; terminal notice confirms the + attachment; the output stays editable in the composer; console errors and + failed local requests are absent. +- Diagnostic artifacts: CDP screenshots, terminal layout JSON, composer attach + JSON, Electron log, summary JSON under + `.qwen/e2e-tests/electron-desktop/artifacts/`. +- Required skills applied: `frontend-design` for prototype-constrained terminal + action wording and compact composer-centric hierarchy; `electron-desktop-dev` + for renderer changes and real Electron CDP verification. + +Notes and decisions: + +- The prototype keeps the composer as the task control center, so terminal + output should land there for user review rather than bypassing it. +- This slice intentionally preserves the transcript formatting and bounding + logic from the existing send path, but changes the destination from WebSocket + send to composer draft text. +- The WebSocket helper no longer needs a separate terminal-output send method + because the final send is the same explicit user-message path as any other + composer submit. + +Verification results: + +- `cd packages/desktop && SHELL=/bin/bash npx vitest run src/renderer/components/layout/WorkspacePage.test.tsx` + passed with 5 tests. +- `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. +- Passing artifacts: + `.qwen/e2e-tests/electron-desktop/artifacts/2026-04-25T17-08-17-022Z/`. + +Next work: + +- Improve review safety by replacing `Accept`/`Revert` terminology with + Stage/Unstage/Discard and confirming destructive discard paths. +- Continue prototype fidelity work in the conversation timeline by hiding + protocol/session noise and adding inline changed-file summaries. + +### Completed Slice: Collapsed Terminal Status Strip Alignment Status: completed in iteration 4. diff --git a/packages/desktop/scripts/e2e-cdp-smoke.mjs b/packages/desktop/scripts/e2e-cdp-smoke.mjs index c2ffab966..96cac9ed7 100644 --- a/packages/desktop/scripts/e2e-cdp-smoke.mjs +++ b/packages/desktop/scripts/e2e-cdp-smoke.mjs @@ -136,8 +136,10 @@ async function main() { await clickButton('Send Input'); await waitForText('Input sent.'); await waitForText('stdin:desktop-e2e-stdin'); - await clickButton('Send to AI'); - await waitForText('Sent terminal output to AI.'); + await clickButton('Attach Output'); + await waitForText('Attached terminal output to composer.'); + await assertTerminalOutputAttached('terminal-attachment.json'); + await clickButton('Send'); await waitForText('Approve Once'); await clickButton('Approve Once'); await waitForText( @@ -747,6 +749,59 @@ async function assertTerminalExpandedLayout(fileName) { } } +async function assertTerminalOutputAttached(fileName) { + const snapshot = await evaluate(`(() => { + const textarea = document.querySelector('textarea[aria-label="Message"]'); + const terminalActions = document.querySelector('.terminal-actions'); + const text = textarea?.value ?? ''; + return { + composerValue: text, + hasTerminalPrompt: text.includes('Review this terminal output'), + hasCommand: text.includes('$ node -e'), + hasStdinOutput: text.includes('stdin:desktop-e2e-stdin'), + hasAttachAction: Boolean( + document.querySelector('button[aria-label="Attach Output"]') + ), + hasLegacySendAction: Boolean( + document.querySelector('button[aria-label="Send to AI"]') + ) || (terminalActions?.textContent ?? '').includes('Send to AI'), + hasPendingApproval: document.body.innerText.includes('Approve Once') + }; + })()`); + + await writeFile( + join(artifactDir, fileName), + `${JSON.stringify(snapshot, null, 2)}\n`, + 'utf8', + ); + + if (!snapshot.hasAttachAction) { + throw new Error('Terminal attach action was not rendered.'); + } + + if (snapshot.hasLegacySendAction) { + throw new Error('Terminal should attach output, not show Send to AI.'); + } + + if ( + !snapshot.hasTerminalPrompt || + !snapshot.hasCommand || + !snapshot.hasStdinOutput + ) { + throw new Error( + `Terminal output was not attached to composer: ${JSON.stringify( + snapshot, + )}`, + ); + } + + if (snapshot.hasPendingApproval) { + throw new Error( + 'Attaching terminal output should not create an agent approval request.', + ); + } +} + async function assertSettingsPageLayout(fileName) { const metrics = await evaluate(`(() => { const rectFor = (selector) => { diff --git a/packages/desktop/src/renderer/App.tsx b/packages/desktop/src/renderer/App.tsx index 8fc154101..c8526efd8 100644 --- a/packages/desktop/src/renderer/App.tsx +++ b/packages/desktop/src/renderer/App.tsx @@ -713,14 +713,9 @@ export function App() { } }, [terminal]); - const sendTerminalOutputToAi = useCallback(() => { - if ( - !activeSessionId || - !socketRef.current || - chatState.connection !== 'connected' || - !terminal - ) { - setTerminalError('Open a thread before sending terminal output to AI.'); + const attachTerminalOutputToComposer = useCallback(() => { + if (!terminal) { + setTerminalError('Run a terminal command before attaching output.'); return; } @@ -730,12 +725,15 @@ export function App() { return; } - const content = buildTerminalOutputPrompt(terminal); - dispatchChat({ type: 'append_user_message', content }); - socketRef.current.sendTerminalOutput(content); + const content = buildTerminalAttachmentDraft(terminal); + setMessageText((current) => + current.trim().length > 0 + ? `${current.trimEnd()}\n\n${content}` + : content, + ); setTerminalError(null); - setTerminalNotice('Sent terminal output to AI.'); - }, [activeSessionId, chatState.connection, terminal]); + setTerminalNotice('Attached terminal output to composer.'); + }, [terminal]); useEffect(() => { if (loadState.state !== 'ready' || terminal?.status !== 'running') { @@ -972,7 +970,7 @@ export function App() { onRevertReviewTarget={revertReviewTarget} onRunTerminalCommand={runTerminalCommand} onSaveSettings={saveSettings} - onSendTerminalOutputToAi={sendTerminalOutputToAi} + onAttachTerminalOutput={attachTerminalOutputToComposer} onSelectProject={selectProject} onSelectSession={selectSession} onSendMessage={sendMessage} @@ -1041,7 +1039,7 @@ function formatTerminalTranscript(terminal: DesktopTerminal): string { return `$ ${terminal.command}\n[${terminal.status}]${exitText}\n${terminal.output}`; } -function buildTerminalOutputPrompt(terminal: DesktopTerminal): string { +function buildTerminalAttachmentDraft(terminal: DesktopTerminal): string { const transcript = formatTerminalTranscript(terminal); const boundedTranscript = transcript.length > 12_000 diff --git a/packages/desktop/src/renderer/api/websocket.ts b/packages/desktop/src/renderer/api/websocket.ts index d1a3b89a6..71ef30b7e 100644 --- a/packages/desktop/src/renderer/api/websocket.ts +++ b/packages/desktop/src/renderer/api/websocket.ts @@ -20,7 +20,6 @@ export interface SessionSocketHandlers { export interface SessionSocketClient { sendUserMessage(content: string): void; - sendTerminalOutput(content: string): void; respondToPermission(requestId: string, optionId: string): void; respondToAskUserQuestion( requestId: string, @@ -55,9 +54,6 @@ export function connectSessionSocket( sendUserMessage(content: string): void { sendClientMessage(socket, { type: 'user_message', content }); }, - sendTerminalOutput(content: string): void { - sendClientMessage(socket, { type: 'user_message', content }); - }, respondToPermission(requestId: string, optionId: string): void { sendClientMessage(socket, { type: 'permission_response', diff --git a/packages/desktop/src/renderer/components/layout/SidebarIcons.tsx b/packages/desktop/src/renderer/components/layout/SidebarIcons.tsx index b82bc4ac6..b5273cd4d 100644 --- a/packages/desktop/src/renderer/components/layout/SidebarIcons.tsx +++ b/packages/desktop/src/renderer/components/layout/SidebarIcons.tsx @@ -312,6 +312,33 @@ export function CopyIcon(props: SidebarIconProps) { ); } +export function PaperclipIcon(props: SidebarIconProps) { + return ( + + ); +} + export function SendIcon(props: SidebarIconProps) { return ( void; onClear: () => void; onCommandChange: (command: string) => void; onCopyOutput: () => void; onInputChange: (input: string) => void; onKill: () => void; onRun: () => void; - onSendOutputToAi: () => void; onToggleExpanded: () => void; onWriteInput: () => void; project: DesktopProject | null; @@ -110,15 +110,15 @@ export function TerminalDrawer({ Copy Output