diff --git a/apps/web/components/task/chat/messages/thinking-message.test.tsx b/apps/web/components/task/chat/messages/thinking-message.test.tsx
new file mode 100644
index 0000000000..8f6332534d
--- /dev/null
+++ b/apps/web/components/task/chat/messages/thinking-message.test.tsx
@@ -0,0 +1,113 @@
+import { afterEach, describe, expect, it } from "vitest";
+import { cleanup, fireEvent, render, screen } from "@testing-library/react";
+import { ThinkingMessage } from "./thinking-message";
+import { sessionId as toSessionId, taskId as toTaskId, type Message } from "@/lib/types/http";
+
+afterEach(cleanup);
+
+const PREVIEW_TEST_ID = "thinking-message-preview";
+
+function thinkingMessage(content: string): Message {
+ return {
+ id: "thinking-1",
+ session_id: toSessionId("session-1"),
+ task_id: toTaskId("task-1"),
+ author_type: "agent",
+ content: "",
+ type: "thinking",
+ created_at: "2026-08-30T00:00:00Z",
+ metadata: { thinking: content },
+ };
+}
+
+describe("ThinkingMessage", () => {
+ // @covers AC-UI-THINKING-MESSAGE-PREVIEW-001.1, AC-UI-THINKING-MESSAGE-PREVIEW-001.2, AC-UI-THINKING-MESSAGE-PREVIEW-001.7
+ it("shows the first meaningful markdown-stripped line in an expandable header", () => {
+ render(
+ ,
+ );
+
+ const preview = screen.queryByText("Choose the implementation path with context", {
+ exact: true,
+ });
+
+ expect(preview).not.toBeNull();
+ expect(preview?.className).toContain("min-w-0");
+ expect(preview?.className).toContain("truncate");
+ expect(screen.queryByText("Later details remain in full.", { exact: true })).toBeNull();
+ });
+
+ // @covers AC-UI-THINKING-MESSAGE-PREVIEW-001.2
+ it("keeps the label-only fallback when decoration produces no visible text", () => {
+ render();
+
+ expect(screen.getByText("Thinking", { exact: true })).toBeTruthy();
+ expect(screen.queryByTestId(PREVIEW_TEST_ID)).toBeNull();
+ });
+
+ // @covers AC-UI-THINKING-MESSAGE-PREVIEW-001.2
+ it("skips fenced and thematic Markdown-only lines", () => {
+ render(
+ ,
+ );
+
+ expect(screen.getByTestId(PREVIEW_TEST_ID).textContent).toBe("First visible reasoning");
+ });
+
+ // @covers AC-UI-THINKING-MESSAGE-PREVIEW-001.1
+ it("preserves identifier punctuation in the preview", () => {
+ render();
+
+ expect(screen.getByTestId(PREVIEW_TEST_ID).textContent).toBe("foo_bar_baz");
+ });
+
+ // @covers AC-UI-THINKING-MESSAGE-PREVIEW-001.3
+ it("keeps the original preview when later reasoning is appended", () => {
+ const { rerender } = render(
+ ,
+ );
+
+ rerender(
+ ,
+ );
+
+ expect(screen.getByTestId(PREVIEW_TEST_ID).textContent).toBe("Original subject");
+ expect(screen.queryByText("A later subject", { exact: true })).toBeNull();
+ });
+
+ // @covers AC-UI-THINKING-MESSAGE-PREVIEW-001.5
+ it("reveals the complete markdown content when expanded", () => {
+ render(
+ ,
+ );
+
+ fireEvent.click(screen.getByText("Thinking", { exact: true }));
+
+ expect(screen.getByText("The later details remain in full.", { exact: true })).toBeTruthy();
+ });
+
+ // @covers AC-UI-THINKING-MESSAGE-PREVIEW-001.6
+ it("keeps compact single-line thinking inline and non-expandable", () => {
+ const { container } = render(
+ ,
+ );
+
+ const compactText = screen.getByText("Compact thought", { exact: true });
+ expect(compactText).toBeTruthy();
+ expect(compactText.className).toContain("min-w-0");
+ expect(compactText.className).toContain("break-words");
+ expect(compactText.className).not.toContain("truncate");
+ expect(screen.queryByTestId(PREVIEW_TEST_ID)).toBeNull();
+ expect(container.querySelector(".markdown-body")).toBeNull();
+ });
+});
diff --git a/apps/web/components/task/chat/messages/thinking-message.tsx b/apps/web/components/task/chat/messages/thinking-message.tsx
index 00495d1127..83a6cea1c2 100644
--- a/apps/web/components/task/chat/messages/thinking-message.tsx
+++ b/apps/web/components/task/chat/messages/thinking-message.tsx
@@ -13,8 +13,8 @@ function stripMarkdown(text: string): string {
return (
text
// Bold/italic: **text** or __text__ or *text* or _text_
- .replace(/(\*\*|__)(.*?)\1/g, "$2")
- .replace(/(\*|_)(.*?)\1/g, "$2")
+ .replace(/(^|[^\w])(\*\*|__)(?=\S)(.*?\S)\2(?=$|[^\w])/g, "$1$3")
+ .replace(/(^|[^\w])(\*|_)(?=\S)(.*?\S)\2(?=$|[^\w])/g, "$1$3")
// Code: `code` or ```code```
.replace(/`{1,3}([^`]+)`{1,3}/g, "$1")
// Links: [text](url) -> text
@@ -23,11 +23,26 @@ function stripMarkdown(text: string): string {
.replace(/^#{1,6}\s+/gm, "")
// Strikethrough: ~~text~~
.replace(/~~(.*?)~~/g, "$1")
+ // Markdown-only block markers do not provide preview text
+ .replace(/^[ \t]*#{1,6}[ \t]*$/gm, "")
+ .replace(/^[ \t]*(?:\*{1,3}|_{1,3}|~{2,}|`{1,3})[ \t]*$/gm, "")
+ .replace(/^[ \t]*(?:`{3,}|~{3,})[ \t]*[\w-]*[ \t]*$/gm, "")
+ .replace(/^[ \t]*(?:(?:\*|_|-)[ \t]*){3,}$/gm, "")
+ .replace(/^[ \t]*(?:[-+*]|\d+[.)]|>)[ \t]*$/gm, "")
// Remove any remaining special chars at start/end
.trim()
);
}
+function firstMeaningfulLine(text: string): string {
+ for (const line of text.split(/\r?\n/)) {
+ const plainText = stripMarkdown(line);
+ if (plainText) return plainText;
+ }
+
+ return "";
+}
+
export const ThinkingMessage = memo(function ThinkingMessage({
comment,
worktreePath,
@@ -48,16 +63,31 @@ export const ThinkingMessage = memo(function ThinkingMessage({
// Short = no newlines and less than 100 characters
const isShort = !text.includes("\n") && text.length <= 100;
const displayText = isShort ? stripMarkdown(text) : text;
+ const preview = isShort ? "" : firstMeaningfulLine(text);
return (
}
header={
-
-
- {t("task:thinking")}
- {isShort && {displayText}}
+
+
+
+ {t("task:thinking")}
+
+ {isShort && (
+
+ {displayText}
+
+ )}
+ {!isShort && preview && (
+
+ {preview}
+
+ )}
}
hasExpandableContent={!isShort && !!text}
diff --git a/apps/web/e2e/tests/chat/mobile-markdown-wrap.spec.ts b/apps/web/e2e/tests/chat/mobile-markdown-wrap.spec.ts
index e0e0d7f0cf..43c069c991 100644
--- a/apps/web/e2e/tests/chat/mobile-markdown-wrap.spec.ts
+++ b/apps/web/e2e/tests/chat/mobile-markdown-wrap.spec.ts
@@ -123,4 +123,143 @@ test.describe("mobile: Markdown table wrapping", () => {
),
).toBe(true);
});
+
+ test("thinking preview stays visible and contained before expansion", async ({
+ testPage,
+ apiClient,
+ seedData,
+ }) => {
+ test.setTimeout(90_000);
+
+ const firstLine =
+ "First meaningful reasoning summary stays visible while this long subject is truncated to the available mobile row width";
+ const laterLine = "Later reasoning detail remains available after expansion";
+ const task = await apiClient.createTaskWithAgent(
+ seedData.workspaceId,
+ "Mobile Thinking Message Preview",
+ seedData.agentProfileId,
+ {
+ description: `e2e:thinking("${["", "## ", `**${firstLine}**`, "", laterLine].join("\\n")}")`,
+ workflow_id: seedData.workflowId,
+ workflow_step_id: seedData.startStepId,
+ repository_ids: [seedData.repositoryId],
+ },
+ );
+ if (!task.session_id) throw new Error("preview task did not create a session");
+
+ await expect
+ .poll(
+ async () => {
+ const { messages } = await apiClient.listSessionMessages(task.session_id!);
+ return messages.some(
+ (message) =>
+ message.type === "thinking" &&
+ String(message.metadata?.thinking ?? message.content ?? "").includes(firstLine),
+ );
+ },
+ { timeout: 30_000, message: "Waiting for thinking preview content to persist" },
+ )
+ .toBe(true);
+
+ await testPage.goto(`/t/${task.id}`);
+ const session = new SessionPage(testPage);
+ await session.waitForLoad();
+
+ const chat = session.activeChat();
+ const preview = chat.getByTestId("thinking-message-preview");
+ await expect(preview).toBeVisible();
+ await expect(preview).toHaveText(firstLine, { exact: true });
+ await expect(chat.getByText(laterLine, { exact: true })).toHaveCount(0);
+
+ const previewMetrics = await preview.evaluate((element) => ({
+ clientWidth: element.clientWidth,
+ scrollWidth: element.scrollWidth,
+ clientHeight: element.clientHeight,
+ lineHeight: Number.parseFloat(getComputedStyle(element).lineHeight),
+ }));
+ expect(previewMetrics.clientWidth).toBeGreaterThan(0);
+ expect(previewMetrics.scrollWidth).toBeGreaterThan(previewMetrics.clientWidth);
+ expect(previewMetrics.clientHeight).toBeLessThanOrEqual(previewMetrics.lineHeight + 1);
+ expect(await chat.evaluate((element) => element.scrollWidth <= element.clientWidth + 1)).toBe(
+ true,
+ );
+ expect(
+ await testPage.evaluate(
+ () => document.documentElement.scrollWidth <= document.documentElement.clientWidth + 1,
+ ),
+ ).toBe(true);
+
+ await chat.getByText("Thinking", { exact: true }).tap();
+ await expect(chat.getByText(laterLine, { exact: true })).toBeVisible();
+ });
+
+ test("compact thinking preview stays contained on mobile", async ({
+ testPage,
+ apiClient,
+ seedData,
+ }) => {
+ test.setTimeout(90_000);
+
+ const compactLine =
+ "Compact thinking line remains within the available mobile row width for safe rendering";
+ const task = await apiClient.createTaskWithAgent(
+ seedData.workspaceId,
+ "Mobile Compact Thinking Preview",
+ seedData.agentProfileId,
+ {
+ description: `e2e:thinking("${compactLine}")`,
+ workflow_id: seedData.workflowId,
+ workflow_step_id: seedData.startStepId,
+ repository_ids: [seedData.repositoryId],
+ },
+ );
+ if (!task.session_id) throw new Error("compact preview task did not create a session");
+
+ await expect
+ .poll(
+ async () => {
+ const { messages } = await apiClient.listSessionMessages(task.session_id!);
+ return messages.some(
+ (message) =>
+ message.type === "thinking" &&
+ String(message.metadata?.thinking ?? message.content ?? "").includes(compactLine),
+ );
+ },
+ { timeout: 30_000, message: "Waiting for compact thinking content to persist" },
+ )
+ .toBe(true);
+
+ await testPage.goto(`/t/${task.id}`);
+ const session = new SessionPage(testPage);
+ await session.waitForLoad();
+
+ const chat = session.activeChat();
+ const compact = chat.getByText(compactLine, { exact: true });
+ await expect(compact).toBeVisible();
+ await expect(chat.getByTestId("thinking-message-preview")).toHaveCount(0);
+
+ const compactMetrics = await compact.evaluate((element) => ({
+ clientWidth: element.clientWidth,
+ scrollWidth: element.scrollWidth,
+ clientHeight: element.clientHeight,
+ scrollHeight: element.scrollHeight,
+ lineHeight: Number.parseFloat(getComputedStyle(element).lineHeight),
+ text: element.textContent,
+ whiteSpace: getComputedStyle(element).whiteSpace,
+ }));
+ expect(compactMetrics.clientWidth).toBeGreaterThan(0);
+ expect(compactMetrics.scrollWidth).toBeLessThanOrEqual(compactMetrics.clientWidth + 1);
+ expect(compactMetrics.clientHeight).toBeGreaterThan(compactMetrics.lineHeight);
+ expect(compactMetrics.scrollHeight).toBe(compactMetrics.clientHeight);
+ expect(compactMetrics.text).toBe(compactLine);
+ expect(compactMetrics.whiteSpace).toBe("normal");
+ expect(await chat.evaluate((element) => element.scrollWidth <= element.clientWidth + 1)).toBe(
+ true,
+ );
+ expect(
+ await testPage.evaluate(
+ () => document.documentElement.scrollWidth <= document.documentElement.clientWidth + 1,
+ ),
+ ).toBe(true);
+ });
});
diff --git a/docs/plans/thinking-message-preview/plan.md b/docs/plans/thinking-message-preview/plan.md
new file mode 100644
index 0000000000..3037c4ebbd
--- /dev/null
+++ b/docs/plans/thinking-message-preview/plan.md
@@ -0,0 +1,138 @@
+---
+created: 2026-08-30
+status: implemented
+requirements:
+ - REQ-UI-THINKING-MESSAGE-PREVIEW-001
+system_design:
+ - ../../specs/ui/system-design/thinking-message-preview.md
+legacy_specs: []
+---
+
+# Implementation Plan: Preview Collapsed Thinking Messages
+
+## Overview
+
+Derive a model-agnostic preview from the first meaningful reasoning line and
+show it in expandable thinking-message headers. Implement the behavior as one
+frontend TDD slice because the derivation, row layout, component evidence, and
+mobile overflow proof share one component contract.
+
+## Scope
+
+### In scope
+
+- Skip leading blank or decoration-only reasoning lines.
+- Strip inline Markdown from the selected preview line.
+- Keep the localized Thinking label and add a one-line truncated preview for
+ expandable messages.
+- Preserve compact single-line and expanded Markdown behavior; compact text
+ remains complete and may wrap on narrow rows.
+- Prove streaming stability and desktop/mobile containment.
+
+### Out of scope
+
+- Backend, ACP, persistence, WebSocket, and message-schema changes.
+- Provider-specific preview parsing or generated summaries.
+- New settings, copy, localization keys, or public documentation.
+- Expandable-row interaction and accessibility redesign.
+
+## Technical approach
+
+### Preview helper and component
+
+- Add a pure first-meaningful-line helper near `stripMarkdown` in
+ `apps/web/components/task/chat/messages/thinking-message.tsx`.
+- Derive the preview from `metadata.thinking` or the current content fallback
+ on every render.
+- Keep the current compact single-line predicate and expandable-content
+ condition.
+- Render the preview for expandable content instead of limiting header text to
+ compact messages.
+- Give the header and preview flex regions `min-w-0`, keep the localized label
+ non-shrinking, truncate only the expandable preview to one visual line, and
+ keep compact text in a shrinkable wrapping region.
+
+### Component regression tests
+
+- Add
+ `apps/web/components/task/chat/messages/thinking-message.test.tsx`.
+- First write a failing test for a Codex-shaped source containing leading blank
+ lines, a bold first summary, and later details.
+- Cover the empty-preview fallback, conservative inline Markdown stripping,
+ compact single-line preservation and wrapping classes, rerendered appended
+ content, expansion, and truncation classes.
+
+### Mobile browser evidence
+
+- Extend `apps/web/e2e/tests/chat/mobile-markdown-wrap.spec.ts` with a focused
+ preview scenario.
+- Seed leading blank lines, a long bold summary, and a later detail line through
+ the existing mock-agent thinking command.
+- Require the first summary in the collapsed row, reveal the later detail only
+ after expansion, and assert that the preview truncates without chat or
+ document horizontal overflow.
+
+## Tests
+
+| Acceptance criterion | Test evidence |
+| -------------------------------------- | ------------------------------------------------------------------------------------------------------- |
+| `AC-UI-THINKING-MESSAGE-PREVIEW-001.1` | Component and mobile browser tests require the first meaningful line in the collapsed header. |
+| `AC-UI-THINKING-MESSAGE-PREVIEW-001.2` | Component tests cover leading blank, decoration-only, and empty content. |
+| `AC-UI-THINKING-MESSAGE-PREVIEW-001.3` | A component rerender test appends a later summary and keeps the original preview. |
+| `AC-UI-THINKING-MESSAGE-PREVIEW-001.4` | Header class assertions and the mobile browser geometry scenario prove expandable one-line containment. |
+| `AC-UI-THINKING-MESSAGE-PREVIEW-001.5` | Component and mobile browser tests expand the row and find the complete later content. |
+| `AC-UI-THINKING-MESSAGE-PREVIEW-001.6` | A component test preserves compact inline and non-expandable rendering. |
+| `AC-UI-THINKING-MESSAGE-PREVIEW-001.7` | The pure helper uses only the provider-neutral thinking string. |
+
+## E2E tests
+
+- Mobile: run the new thinking-preview scenario in
+ `apps/web/e2e/tests/chat/mobile-markdown-wrap.spec.ts` with the
+ `mobile-chrome` project. This proves the shared component's visible preview,
+ expansion path, expandable-preview truncation, compact full-text wrapping,
+ and containment on the narrowest supported chat surface.
+- Desktop behavior is covered by the shared component test because the change
+ does not branch by viewport or pointer mode.
+
+## Mobile design contract
+
+- Desktop outcome: an expandable thinking row shows its subject before the
+ complete Markdown block is opened.
+- Mobile entry point: the same thinking row in the existing task Chat tab.
+- Nearest exemplar: the thinking-message scenario in
+ `apps/web/e2e/tests/chat/mobile-markdown-wrap.spec.ts`.
+- Hierarchy and surface: the localized label remains first; the preview is
+ secondary inline content; the existing transcript remains the scroll owner.
+- Presentation rationale: this shallow scan aid belongs in the existing row;
+ a drawer, new route, or separate surface would add interaction without new
+ content depth.
+- Shared logic: desktop and mobile use the same preview derivation, expansion
+ state, and complete Markdown content.
+- Mobile proof: the new `mobile-chrome` scenario verifies preview visibility,
+ expansion, expandable-preview truncation, compact full-text wrapping, and
+ the absence of document overflow.
+
+## Work orders
+
+- [x] [Task 01: Render thinking-message previews](task-01-render-thinking-message-previews.md)
+
+## Verification results
+
+Implementation is complete. The exact task verification commands passed:
+
+- Thinking-message component tests: 7 passed.
+- Web typecheck, targeted ESLint, and Prettier checks passed.
+- Specification tests and all specification files passed lint.
+- Mobile Pixel 5 thinking-preview E2E: 2 passed with expandable-preview
+ truncation, compact full-text wrapping, and no chat or document overflow.
+- Fresh desktop and 393px mobile PR screenshots captured, inspected, and compressed.
+
+## Risks
+
+- A source can begin with Markdown structure rather than a natural-language
+ heading. The contract intentionally selects the first visible line instead
+ of adding provider-specific or semantic ranking.
+- Streaming can initially contain only blank lines. The label-only fallback
+ must transition cleanly when the first meaningful content arrives.
+- A long preview can widen nested flex content unless every ancestor in the
+ header path permits shrinking.
diff --git a/docs/plans/thinking-message-preview/task-01-render-thinking-message-previews.md b/docs/plans/thinking-message-preview/task-01-render-thinking-message-previews.md
new file mode 100644
index 0000000000..ed57956301
--- /dev/null
+++ b/docs/plans/thinking-message-preview/task-01-render-thinking-message-previews.md
@@ -0,0 +1,129 @@
+---
+id: "01-render-thinking-message-previews"
+title: "Render thinking-message previews"
+status: done
+wave: 1
+depends_on: []
+plan: "plan.md"
+requirements:
+ - REQ-UI-THINKING-MESSAGE-PREVIEW-001
+acceptance_criteria:
+ - AC-UI-THINKING-MESSAGE-PREVIEW-001.1
+ - AC-UI-THINKING-MESSAGE-PREVIEW-001.2
+ - AC-UI-THINKING-MESSAGE-PREVIEW-001.3
+ - AC-UI-THINKING-MESSAGE-PREVIEW-001.4
+ - AC-UI-THINKING-MESSAGE-PREVIEW-001.5
+ - AC-UI-THINKING-MESSAGE-PREVIEW-001.6
+ - AC-UI-THINKING-MESSAGE-PREVIEW-001.7
+system_design:
+ - ../../specs/ui/system-design/thinking-message-preview.md
+---
+
+# Task 01: Render Thinking-Message Previews
+
+## Summary
+
+Show the first meaningful reasoning line in expandable thinking-message
+headers. Preserve complete Markdown content, compact single-line behavior, and
+provider-neutral message contracts while containing the preview on mobile.
+
+## In scope
+
+- Write the focused component regression first.
+- Derive a plain-text preview from the first meaningful source line.
+- Render and truncate the preview in expandable thinking-message headers.
+- Preserve label-only, compact-inline, streaming, and expanded-content paths.
+- Add focused mobile browser evidence for visibility and containment.
+
+## Out of scope
+
+- Agent adapter, lifecycle, task-message service, persistence, and WebSocket
+ changes.
+- Generated summaries, provider branches, new translations, or settings.
+- Changes to `ExpandableRow`, the Markdown renderer, or shared transcript
+ navigation.
+
+## Acceptance
+
+- A Codex-shaped multiline thought shows its first non-empty Markdown-stripped
+ line beside Thinking before expansion, while later appended lines do not
+ replace it.
+- Empty or decoration-only thoughts retain the label-only fallback, and compact
+ single-line thoughts retain their current full inline behavior while wrapping
+ when the row is narrow.
+- The expandable preview stays on one truncated line without widening the
+ mobile chat or document, and expansion reveals the complete source.
+
+## Verification
+
+Bootstrap workspace dependencies once when `apps/node_modules` is absent:
+
+```bash
+cd apps && pnpm install --frozen-lockfile
+```
+
+Run the focused TDD and validation commands:
+
+```bash
+cd apps && pnpm --filter @kandev/web test -- components/task/chat/messages/thinking-message.test.tsx
+cd apps/web && pnpm run typecheck
+cd apps/web && pnpm exec eslint components/task/chat/messages/thinking-message.tsx components/task/chat/messages/thinking-message.test.tsx e2e/tests/chat/mobile-markdown-wrap.spec.ts
+cd apps/web && pnpm exec prettier --check components/task/chat/messages/thinking-message.tsx components/task/chat/messages/thinking-message.test.tsx e2e/tests/chat/mobile-markdown-wrap.spec.ts
+python3 scripts/lint-spec-files.test.py
+python3 scripts/lint-spec-files.py --all
+git diff --check -- docs/specs docs/plans apps/web/components/task/chat/messages apps/web/e2e/tests/chat/mobile-markdown-wrap.spec.ts
+cd apps/web && pnpm e2e:run --host --project mobile-chrome tests/chat/mobile-markdown-wrap.spec.ts -- --grep "thinking preview" --retries=0
+```
+
+## Files likely touched
+
+- `apps/web/components/task/chat/messages/thinking-message.tsx`
+- `apps/web/components/task/chat/messages/thinking-message.test.tsx`
+- `apps/web/e2e/tests/chat/mobile-markdown-wrap.spec.ts`
+- `docs/specs/ui/requirements/thinking-message-preview.md`
+- `docs/specs/ui/system-design/thinking-message-preview.md`
+- `docs/specs/ui/README.md`
+- `docs/plans/thinking-message-preview/plan.md`
+- `docs/plans/thinking-message-preview/task-01-render-thinking-message-previews.md`
+
+## Dependencies
+
+None.
+
+## Risks
+
+- A Markdown structural line can be a technically valid but less descriptive
+ preview. Do not add semantic ranking beyond the specified first visible line.
+- Flex truncation fails if a header ancestor cannot shrink. Cover the final DOM
+ geometry in the mobile browser scenario.
+
+## Parallelism
+
+`sequential`
+
+## Inputs
+
+- `REQ-UI-THINKING-MESSAGE-PREVIEW-001` and all acceptance criteria.
+- Preview derivation, rendering, streaming, and responsive sections in the
+ thinking-message-preview system design.
+- Existing `ThinkingMessage`, `ExpandableRow`, message-renderer tests, and
+ mobile Markdown containment scenario.
+
+## Results
+
+Implemented a model-agnostic first-meaningful-line helper and rendered its
+plain-text result beside the localized Thinking label for expandable rows.
+Compact single-line messages remain complete, non-expandable, and readable when
+their inline text wraps; expanded Markdown content keeps its existing behavior.
+The header uses shrink-safe flex regions and truncates only the expandable
+preview; structural Markdown-only lines are skipped and identifier punctuation
+is preserved in previews.
+
+Verification:
+
+- `cd apps && pnpm --filter @kandev/web test -- components/task/chat/messages/thinking-message.test.tsx` — 7 passed.
+- `cd apps/web && pnpm run typecheck` — passed.
+- Targeted ESLint and Prettier checks — passed.
+- `python3 scripts/lint-spec-files.test.py` and `python3 scripts/lint-spec-files.py --all` — passed.
+- `cd apps/web && pnpm e2e:run --host --project mobile-chrome tests/chat/mobile-markdown-wrap.spec.ts -- --grep "thinking preview" --retries=0` — 2 passed, including complete compact text wrapping.
+- Disposable PR capture E2E — 1 passed; desktop and 393px mobile screenshots were inspected and compressed.
diff --git a/docs/specs/ui/README.md b/docs/specs/ui/README.md
index 0c887a2edc..7a76305603 100644
--- a/docs/specs/ui/README.md
+++ b/docs/specs/ui/README.md
@@ -40,7 +40,7 @@ with their owning systems.
- [Agent-message inline comments](requirements/agent-message-comments.md)
- [Agent Todo List Panel](requirements/agent-todo-list-panel.md)
- [App Status Bar](requirements/app-status-bar.md)
-- [Per-workflow column visibility on the kanban board](requirements/board-step-visibility-filter.md)
+- [Workflow column visibility](requirements/board-step-visibility-filter.md)
- [Browser inspect annotation submission](requirements/browser-inspect-annotations-save.md)
- [Backend-owned cancel-turn progress](requirements/cancel-turn-progress.md)
- [Changes File Row Containment](requirements/changes-file-row-containment.md)
@@ -64,7 +64,7 @@ with their owning systems.
- [Executor settings card spacing](requirements/executor-settings-card-spacing.md)
- [External VCS File Links](requirements/external-vcs-file-links.md)
- [File Tree Chat Context](requirements/file-tree-chat-context.md)
-- [Reload Kandev when a tab is restored from a frozen browser snapshot](requirements/fix-duplicated-tab-stale-data.md)
+- [Reload Kandev after frozen-tab restore](requirements/fix-duplicated-tab-stale-data.md)
- [GitHub PR Review Actions](requirements/github-pr-review-actions.md)
- [GitHub Saved-Query Default Views](requirements/github-saved-query-defaults.md)
- [Kandev MCP Tool Results](requirements/kandev-mcp-tool-results.md)
@@ -105,7 +105,7 @@ with their owning systems.
- [Review File Status Cues](requirements/review-file-status.md)
- [Review Markdown Preview](requirements/review-markdown-preview.md)
- [Search/filter dropdown scroll reset](requirements/search-filter-scroll-reset.md)
-- [Selected option prominence in single-choice pickers](requirements/selected-option-picker-prominence.md)
+- [Selected option prominence](requirements/selected-option-picker-prominence.md)
- [Session tab delete feedback](requirements/session-tab-delete-feedback.md)
- [Settings Discovery](requirements/settings-discovery.md)
- [Settings Manual Save](requirements/settings-manual-save.md)
@@ -130,10 +130,11 @@ with their owning systems.
- [Task Listing Display Preferences](requirements/task-listing-display-preferences.md)
- [Task transcript history visibility](requirements/task-prompt-transcript-visibility.md)
- [Task Review Shortcut Switcher](requirements/task-review-shortcut.md)
-- [Task Surface Foreground Refresh and Mobile Create Action](requirements/task-surface-refresh.md)
+- [Task surface refresh and mobile create](requirements/task-surface-refresh.md)
- [Task Workspace Content Search](requirements/task-workspace-content-search.md)
- [Terminal close feedback](requirements/terminal-close-feedback.md)
- [Terminal Rendering](requirements/terminal-rendering.md)
+- [Thinking Message Preview](requirements/thinking-message-preview.md)
- [Transcript Auto-scroll Stability](requirements/transcript-auto-scroll.md)
- [Transcript Navigation Settings](requirements/transcript-navigation-settings.md)
- [Voice Mode In Task Behavior](requirements/voice-mode-task-behavior.md)
@@ -177,6 +178,7 @@ with their owning systems.
- [Task Agent Tab Reconciliation](system-design/task-agent-tab-reconciliation.md)
- [Command-panel Sidebar Task Reveal](system-design/command-panel-sidebar-task-reveal.md)
- [Terminal Rendering](system-design/terminal-rendering.md)
+- [Thinking Message Preview](system-design/thinking-message-preview.md)
- [Task Transcript History Visibility](system-design/task-prompt-transcript-visibility.md)
- [Transcript Auto-scroll Stability](system-design/transcript-auto-scroll.md)
diff --git a/docs/specs/ui/requirements/thinking-message-preview.md b/docs/specs/ui/requirements/thinking-message-preview.md
new file mode 100644
index 0000000000..9852d400d1
--- /dev/null
+++ b/docs/specs/ui/requirements/thinking-message-preview.md
@@ -0,0 +1,75 @@
+---
+status: draft
+system: ui
+created: 2026-08-30
+owners:
+ - kandev
+---
+
+# Thinking Message Preview Requirements
+
+## Overview
+
+Thinking messages can contain several concise reasoning headings. Collapsed
+rows currently hide those headings behind the generic Thinking label whenever
+the source contains a newline. A compact preview helps users scan the agent's
+progress without expanding every reasoning block.
+
+The UI system owns this presentation contract. Agent runtimes and the task
+system continue to own reasoning production, delivery, and persistence.
+
+## Terminology
+
+- **Thinking message:** An agent transcript message with the `thinking` type.
+- **Meaningful line:** The first source line that contains visible text after
+ surrounding whitespace and inline Markdown decoration are removed.
+- **Preview:** The plain-text meaningful line shown beside the localized
+ Thinking label in the collapsed row.
+
+## Requirements
+
+### REQ-UI-THINKING-MESSAGE-PREVIEW-001: Collapsed thinking message preview
+
+**Intent:** Let users identify the subject of an expandable thinking message
+without opening the complete reasoning block.
+
+**User story:** As a user reading an agent transcript, I want collapsed
+thinking rows to describe their subject, so that I can scan the agent's
+progress quickly.
+
+#### Acceptance criteria
+
+- **AC-UI-THINKING-MESSAGE-PREVIEW-001.1:** When an expandable thinking
+ message contains a meaningful line, Kandev shall show that line as plain
+ text beside the localized Thinking label while the message is collapsed.
+- **AC-UI-THINKING-MESSAGE-PREVIEW-001.2:** When a thinking message starts
+ with blank lines or Markdown decoration that produces no visible text,
+ Kandev shall skip those lines. If no meaningful line exists, the row shall
+ show only the localized Thinking label.
+- **AC-UI-THINKING-MESSAGE-PREVIEW-001.3:** When later reasoning content is
+ appended to a thinking message after its first meaningful line, Kandev shall
+ keep the original preview instead of replacing it with a later line.
+- **AC-UI-THINKING-MESSAGE-PREVIEW-001.4:** On desktop and mobile, an
+ expandable thinking preview shall occupy one visual line, truncate within
+ the available row width, and shall not widen the transcript or document.
+- **AC-UI-THINKING-MESSAGE-PREVIEW-001.5:** Expanding a thinking row shall
+ continue to show the complete original reasoning content with its existing
+ Markdown rendering. The preview shall not modify persisted or expanded
+ content.
+- **AC-UI-THINKING-MESSAGE-PREVIEW-001.6:** Compact single-line thinking
+ messages that already render their complete text inline shall keep their
+ current inline and non-expandable behavior.
+- **AC-UI-THINKING-MESSAGE-PREVIEW-001.7:** Preview derivation shall apply to
+ thinking messages from every agent provider without provider-specific
+ labels or parsing rules.
+
+## Out of scope
+
+- Generating a new summary or choosing a later line because it appears more
+ descriptive.
+- Changing ACP reasoning events, task-message persistence, WebSocket payloads,
+ or streaming coalescing.
+- Changing the expanded Markdown renderer or the compact single-line
+ threshold.
+- Redesigning the shared expandable-row interaction or its accessibility
+ contract.
diff --git a/docs/specs/ui/system-design/thinking-message-preview.md b/docs/specs/ui/system-design/thinking-message-preview.md
new file mode 100644
index 0000000000..00a6df9366
--- /dev/null
+++ b/docs/specs/ui/system-design/thinking-message-preview.md
@@ -0,0 +1,133 @@
+---
+status: draft
+system: ui
+requirements:
+ - REQ-UI-THINKING-MESSAGE-PREVIEW-001
+---
+
+# Thinking Message Preview System Design
+
+## Purpose and boundaries
+
+This design derives a stable collapsed preview from the thinking content that
+the web client already receives. It changes only the task-chat presentation.
+The ACP adapter, lifecycle coalescer, task-message persistence, boot payload,
+and WebSocket message contract remain unchanged.
+
+An ACP probe against Codex 1.6.0 emitted four thought chunks under one stable
+message ID: blank lines, a bold summary, blank lines, and a second bold
+summary. Kandev persisted their ordered concatenation. The current
+`ThinkingMessage` classifies any text containing a newline as expandable but
+only renders inline text for non-expandable messages. The useful first summary
+therefore disappears from the collapsed row.
+
+## Requirement mapping
+
+| Requirement | Design section |
+| ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `REQ-UI-THINKING-MESSAGE-PREVIEW-001` | [Preview derivation](#preview-derivation), [Rendering contract](#rendering-contract), [Streaming behavior](#streaming-behavior), [Responsive behavior](#responsive-behavior), [Verification](#verification) |
+
+## Components and responsibilities
+
+- `ThinkingMessage` reads the existing `metadata.thinking` string, with
+ `comment.content` as its current fallback, and owns preview derivation and
+ header layout.
+- `ExpandableRow` continues to own collapse and expansion without a new prop or
+ interaction mode.
+- `MemoizedMarkdown` continues to render the complete source only in the
+ expanded content region.
+- The task-message store and WebSocket handlers continue to replace each live
+ message with the latest complete public state.
+
+## Preview derivation
+
+`ThinkingMessage` uses a pure, model-agnostic helper:
+
+1. Split the source on Unix or Windows line endings.
+2. Process lines in source order with conservative preview-specific inline
+ Markdown normalization.
+3. Trim the plain-text result and select the first non-empty value.
+4. Return an empty preview when no line produces visible text.
+
+The helper does not parse provider metadata, translate agent content, generate
+new copy, or persist the preview. Markdown links contribute their visible
+label, balanced emphasis and code delimiters are removed, structural-only
+Markdown lines are skipped, and identifier punctuation is preserved. React
+renders the result as escaped text rather than Markdown or HTML.
+
+Compact single-line detection remains unchanged. Its complete stripped text
+continues to render inline, and the row remains non-expandable. The compact
+text stays complete and may wrap at narrow widths, but it is not truncated or
+replaced by the expandable preview. Expandable messages use the derived
+preview in the same header position.
+
+## Rendering contract
+
+The header keeps the localized Thinking label as a non-shrinking prefix. The
+preview occupies the remaining width in a `min-w-0` flex region and uses
+single-line truncation. The complete source remains in the current expanded
+Markdown region, so truncation never changes or discards transcript content.
+
+The compact inline text uses a separate shrinkable, wrapping flex item. Its
+complete text remains readable on narrow screens without widening the
+transcript.
+
+When preview derivation returns an empty string, the header renders only the
+Thinking label. No placeholder, tooltip, or new user-facing copy is added.
+
+## Streaming behavior
+
+Reasoning updates append ordered content to one thinking message. The client
+derives the preview from the current complete string on each render. Leading
+blank chunks initially produce no preview. The preview appears when the first
+meaningful line arrives. Later appended lines cannot precede that line, so the
+derived preview remains stable without local state, memoized protocol IDs, or
+an additional streaming lifecycle.
+
+Animation-frame WebSocket replacement can skip intermediate renders, but each
+delivered message contains the complete current thinking string. Preview
+derivation therefore reaches the same result after replacement or reload.
+
+## Responsive behavior
+
+Desktop and mobile use the same `ThinkingMessage` component and derivation.
+This is an inline content change inside the existing task-chat row. It adds no
+navigation, overlay, scroll owner, control, or touch target.
+
+The nearest mobile exemplar is the current thinking row in
+`apps/web/e2e/tests/chat/mobile-markdown-wrap.spec.ts`. The localized label
+keeps its current priority. An expandable preview truncates before it can
+widen the chat or document, while compact text remains complete and can wrap
+within the row. Expanding the row continues to use the existing full-width
+Markdown region and its local overflow handling.
+
+## Failure and security behavior
+
+- Empty, whitespace-only, or decoration-only content produces no preview and
+ keeps the existing label-only fallback.
+- A malformed or unavailable thinking value follows the component's existing
+ empty-message handling; this change does not broaden accepted message data.
+- Preview text is rendered as a React text node. Markdown cannot create links,
+ raw HTML, executable content, or additional interactive elements in the
+ header.
+- Expanded content continues through the existing Markdown safety boundary.
+
+## Verification
+
+Focused component tests cover leading blank lines, conservative Markdown
+stripping, structural-only lines, identifier punctuation, the label-only
+fallback, compact single-line preservation and wrapping classes, later
+appended content, expansion, and the truncating header classes.
+
+A `mobile-chrome` Playwright scenario seeds Codex-shaped multiline and compact
+thinking messages, waits for their persisted content, verifies the meaningful
+preview and expansion behavior, checks complete compact text with allowed
+wrapping, and checks that the preview, chat, and document remain horizontally
+contained.
+
+No production telemetry is added. This is a deterministic presentation rule
+with component and browser evidence.
+
+## Related decisions
+
+- [Isolate Replaceable Session Stream Traffic](../../../decisions/2026-08-02-isolate-replaceable-session-stream-traffic.md)