diff --git a/.qwen/e2e-tests/electron-desktop/rich-tool-call-activity-cards.md b/.qwen/e2e-tests/electron-desktop/rich-tool-call-activity-cards.md
new file mode 100644
index 000000000..44f034a1b
--- /dev/null
+++ b/.qwen/e2e-tests/electron-desktop/rich-tool-call-activity-cards.md
@@ -0,0 +1,47 @@
+# Rich Tool-Call Activity Cards
+
+- 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-25T17-57-31-788Z/`
+
+## 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 resolved tool update.
+5. Assert the conversation renders a compact tool activity card with command
+ title, status, command input, output summary, and file chip.
+6. Continue the existing changed-files, review, settings, terminal, and final
+ layout smoke path.
+
+## Assertions
+
+- The resolved activity card is inside the chat timeline and stays above the
+ composer without overlap.
+- The card includes `Run desktop E2E command`, `completed`,
+ `printf desktop-e2e`, `desktop-e2e command completed`, and `README.md:1`.
+- The card does not show the fake tool call ID or session ID.
+- Legacy `.chat-tool` rows are absent.
+- Console errors: 0.
+- Failed local network requests: 0.
+
+## Artifacts
+
+- `resolved-tool-activity.json`
+- `resolved-tool-activity.png`
+- `conversation-changes-summary.json`
+- `completed-workspace.png`
+- `electron.log`
+- `summary.json`
+
+## Known Uncovered Risk
+
+The harness covers deterministic fake ACP tool updates with one file reference
+and bounded string output. Live ACP tools with richer structured outputs, many
+file references, and long command output still need broader coverage.
diff --git a/design/qwen-code-electron-desktop-implementation-plan.md b/design/qwen-code-electron-desktop-implementation-plan.md
index ec013200f..3b7b113df 100644
--- a/design/qwen-code-electron-desktop-implementation-plan.md
+++ b/design/qwen-code-electron-desktop-implementation-plan.md
@@ -22,6 +22,99 @@ execution order, verification, decisions, and remaining work.
## Codex Alignment Progress
+### Completed Slice: Rich Tool-Call Activity Cards
+
+Status: completed in iteration 10.
+
+Goal: make completed and in-progress tool calls read as useful task activity
+inside the conversation instead of a sparse tool row.
+
+User-visible value: users can see what the agent did, what command/input was
+used, which files were referenced, and whether the tool completed or failed
+without reading ACP IDs or opening diagnostics.
+
+Expected files:
+
+- `packages/desktop/src/main/acp/createE2eAcpClient.ts`
+- `packages/desktop/src/renderer/components/layout/ChatThread.tsx`
+- `packages/desktop/src/renderer/components/layout/WorkspacePage.test.tsx`
+- `packages/desktop/src/renderer/stores/chatStore.test.ts`
+- `packages/desktop/src/renderer/styles.css`
+- `packages/desktop/scripts/e2e-cdp-smoke.mjs`
+- `.qwen/e2e-tests/electron-desktop/rich-tool-call-activity-cards.md`
+- `design/qwen-code-electron-desktop-implementation-plan.md`
+
+Acceptance criteria:
+
+- Tool calls render as compact inline conversation activity cards with kind,
+ title, status, and stable `data-testid` hooks.
+- Tool cards show a bounded command/input preview when safe user-facing input
+ is present.
+- Completed or failed tool cards show a bounded output/result summary without
+ exposing request/session IDs.
+- File locations render as compact chips with path and optional line number.
+- The previous generic `.chat-tool` row no longer appears for tool activity.
+- Cards stay within the timeline and do not overlap the composer in real
+ Electron.
+
+Verification:
+
+- Unit/component test command:
+ `cd packages/desktop && SHELL=/bin/bash npx vitest run src/renderer/stores/chatStore.test.ts 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, send from the composer, approve the
+ fake command request, then assert the resolved tool activity card includes
+ command title, status, command preview, output summary, and file chips before
+ continuing the existing review/settings/terminal smoke path.
+- E2E assertions: activity card is present after approval, uses compact
+ geometry inside the chat timeline, contains `README.md:1`, does not render
+ the raw tool call ID or session ID, no legacy `.chat-tool` node remains, and
+ console errors/failed local requests are absent.
+- Diagnostic artifacts: CDP screenshots, rich tool-call JSON, conversation
+ summary JSON, Electron log, summary JSON under
+ `.qwen/e2e-tests/electron-desktop/artifacts/`.
+- Required skills applied: `frontend-design` for prototype-constrained compact
+ activity-card hierarchy and file chip density; `electron-desktop-dev` for
+ renderer changes and real Electron CDP verification; `brainstorming` applied
+ by deriving the slice from the repo plan and immutable prototype instead of
+ asking ordinary product questions during the autonomous loop.
+
+Notes and decisions:
+
+- The prototype keeps agent activity in the reading flow, so this slice
+ replaces the generic tool row with an inline card rather than adding another
+ panel.
+- The card intentionally surfaces title/kind/status, bounded input/output, and
+ file locations only. ACP request IDs, session IDs, and transport details stay
+ out of the main conversation.
+- The fake ACP path will emit deterministic location/output data so the CDP
+ harness can assert a real user-visible resolved tool card.
+
+Verification results:
+
+- `cd packages/desktop && SHELL=/bin/bash npx vitest run src/renderer/stores/chatStore.test.ts src/renderer/components/layout/WorkspacePage.test.tsx`
+ passed with 13 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-57-31-788Z/`.
+
+Next work:
+
+- Continue rich conversation primitives by adding assistant message action rows
+ for copy/retry/open changed files and by turning file references in assistant
+ prose into compact open/reveal chips.
+- Tighten tool-card density at compact viewport widths after adding a second
+ fake ACP scenario with multiple file references and longer command output.
+
### Completed Slice: Inline Command Approval Cards
Status: completed in iteration 9.
diff --git a/packages/desktop/scripts/e2e-cdp-smoke.mjs b/packages/desktop/scripts/e2e-cdp-smoke.mjs
index 002e1995e..d27ac7578 100644
--- a/packages/desktop/scripts/e2e-cdp-smoke.mjs
+++ b/packages/desktop/scripts/e2e-cdp-smoke.mjs
@@ -77,6 +77,8 @@ async function main() {
await saveScreenshot('inline-command-approval.png');
await clickButton('Approve Once');
await waitForText('E2E fake ACP response received');
+ await assertResolvedToolActivity('resolved-tool-activity.json');
+ await saveScreenshot('resolved-tool-activity.png');
await assertConversationChangesSummary('conversation-changes-summary.json');
await waitForSelector('[data-testid="thread-list"]');
@@ -775,6 +777,106 @@ async function assertInlineCommandApproval(fileName) {
}
}
+async function assertResolvedToolActivity(fileName) {
+ await waitForSelector('[data-testid="conversation-tool-card"]');
+ const snapshot = await evaluate(`(() => {
+ const card = document.querySelector(
+ '[data-testid="conversation-tool-card"]'
+ );
+ const timeline = document.querySelector('.chat-timeline');
+ const composer = document.querySelector('[data-testid="message-composer"]');
+ 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
+ };
+ };
+ return {
+ bodyText: document.body.innerText,
+ cardText: card?.innerText ?? '',
+ cardRect: rectFor(card),
+ timelineRect: rectFor(timeline),
+ composerRect: rectFor(composer),
+ legacyToolRows: document.querySelectorAll('.chat-tool').length,
+ fileChipText:
+ document.querySelector('.conversation-tool-files')?.innerText ?? ''
+ };
+ })()`);
+
+ await writeFile(
+ join(artifactDir, fileName),
+ `${JSON.stringify(snapshot, null, 2)}\n`,
+ 'utf8',
+ );
+
+ const cardText = snapshot.cardText.toLowerCase();
+ for (const expectedText of [
+ 'run desktop e2e command',
+ 'completed',
+ 'printf desktop-e2e',
+ 'desktop-e2e command completed',
+ ]) {
+ if (!cardText.includes(expectedText)) {
+ throw new Error(
+ `Resolved tool activity is missing ${expectedText}: ${snapshot.cardText}`,
+ );
+ }
+ }
+
+ if (!snapshot.fileChipText.includes('README.md:1')) {
+ throw new Error(
+ `Resolved tool activity is missing the file chip: ${snapshot.fileChipText}`,
+ );
+ }
+
+ for (const internalText of ['e2e-terminal-check', 'session-e2e']) {
+ if (snapshot.cardText.includes(internalText)) {
+ throw new Error(
+ `Resolved tool activity leaked internal text ${internalText}: ${snapshot.cardText}`,
+ );
+ }
+ }
+
+ if (snapshot.legacyToolRows !== 0) {
+ throw new Error(
+ `Resolved tool activity should not render legacy rows: ${snapshot.legacyToolRows}`,
+ );
+ }
+
+ if (!snapshot.cardRect || !snapshot.timelineRect || !snapshot.composerRect) {
+ throw new Error(
+ `Resolved tool activity geometry is missing: ${JSON.stringify(snapshot)}`,
+ );
+ }
+
+ if (snapshot.cardRect.width < 360 || snapshot.cardRect.height > 240) {
+ throw new Error(
+ `Resolved tool activity geometry is unexpected: ${JSON.stringify(
+ snapshot.cardRect,
+ )}`,
+ );
+ }
+
+ if (
+ snapshot.cardRect.left < snapshot.timelineRect.left ||
+ snapshot.cardRect.right > snapshot.timelineRect.right + 1
+ ) {
+ throw new Error('Resolved tool activity should stay inside the timeline.');
+ }
+
+ if (snapshot.cardRect.bottom > snapshot.composerRect.top) {
+ throw new Error('Resolved tool activity overlaps the composer.');
+ }
+}
+
async function assertReviewDrawerLayout(fileName) {
const metrics = await evaluate(`(() => {
const rectFor = (selector) => {
diff --git a/packages/desktop/src/main/acp/createE2eAcpClient.ts b/packages/desktop/src/main/acp/createE2eAcpClient.ts
index 956e0b46e..76f331044 100644
--- a/packages/desktop/src/main/acp/createE2eAcpClient.ts
+++ b/packages/desktop/src/main/acp/createE2eAcpClient.ts
@@ -168,7 +168,12 @@ export class E2eAcpClient implements AcpSessionClient {
permission.outcome.optionId !== 'deny'
? 'completed'
: 'failed',
- rawOutput: permission.outcome.outcome,
+ rawInput: 'printf desktop-e2e',
+ rawOutput:
+ permission.outcome.outcome === 'selected'
+ ? 'desktop-e2e command completed'
+ : permission.outcome.outcome,
+ locations: [{ path: 'README.md', line: 1 }],
});
this.emit(sessionId, {
sessionUpdate: 'agent_message_chunk',
diff --git a/packages/desktop/src/renderer/components/layout/ChatThread.tsx b/packages/desktop/src/renderer/components/layout/ChatThread.tsx
index dab040b85..cccd881d0 100644
--- a/packages/desktop/src/renderer/components/layout/ChatThread.tsx
+++ b/packages/desktop/src/renderer/components/layout/ChatThread.tsx
@@ -294,13 +294,7 @@ function TimelineItem({ item }: { item: ChatTimelineItem }) {
}
if (item.type === 'tool') {
- return (
-
{inputPreview}
+ {outputPreview}
+