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 (