diff --git a/apps/web/src/components/chat/MessagesTimeline.test.tsx b/apps/web/src/components/chat/MessagesTimeline.test.tsx
index 0957e025311..28899e6f2f4 100644
--- a/apps/web/src/components/chat/MessagesTimeline.test.tsx
+++ b/apps/web/src/components/chat/MessagesTimeline.test.tsx
@@ -459,6 +459,57 @@ describe("MessagesTimeline", () => {
expect(markup).not.toContain("C:/Users/mike/dev-stuff/t3code/apps/web/src/session-logic.ts");
});
+ it("renders web search queries compactly and includes details in the expanded body", async () => {
+ const [{ MessagesTimeline }, { buildToolCallExpandedBody }] = await Promise.all([
+ import("./MessagesTimeline"),
+ import("./workEntryDetails"),
+ ]);
+ const workEntry = {
+ id: "web-search-work-1",
+ createdAt: "2026-03-17T19:12:28.000Z",
+ label: "Web search",
+ tone: "tool" as const,
+ toolTitle: "Web search",
+ itemType: "web_search" as const,
+ toolLifecycleStatus: "completed" as const,
+ toolData: {
+ type: "webSearch",
+ id: "web-search-1",
+ query: "OpenAI Codex web search payload preview",
+ action: {
+ type: "search",
+ queries: ["OpenAI Codex web search payload", "OpenAI Codex web search result payload"],
+ },
+ results: [{ title: "Codex docs", url: "https://example.com/codex" }],
+ },
+ };
+ const markup = renderToStaticMarkup(
+ ,
+ );
+ const expandedBody = buildToolCallExpandedBody(workEntry, undefined);
+
+ expect(markup).toContain("Web search");
+ expect(markup).toContain("OpenAI Codex web search payload +1 more");
+ expect(markup).toContain("lucide-globe");
+ expect(expandedBody).toContain('"query": "OpenAI Codex web search payload preview"');
+ expect(expandedBody).toContain('"queries": [');
+ expect(expandedBody).toContain('"OpenAI Codex web search result payload"');
+ expect(expandedBody).not.toContain("Web search\n");
+ expect(expandedBody).not.toContain("Action: search");
+ expect(expandedBody).not.toContain("Raw item");
+ expect(expandedBody).toContain("https://example.com/codex");
+ });
+
it("renders review comment contexts as structured cards instead of raw tags", async () => {
const { MessagesTimeline } = await import("./MessagesTimeline");
const markup = renderToStaticMarkup(
diff --git a/apps/web/src/components/chat/MessagesTimeline.tsx b/apps/web/src/components/chat/MessagesTimeline.tsx
index 61d7855844d..8ea3255b8df 100644
--- a/apps/web/src/components/chat/MessagesTimeline.tsx
+++ b/apps/web/src/components/chat/MessagesTimeline.tsx
@@ -104,6 +104,7 @@ import {
formatInlineTerminalContextLabel,
textContainsInlineTerminalContextLabels,
} from "./userMessageTerminalContexts";
+import { buildToolCallExpandedBody, workEntryPreview } from "./workEntryDetails";
import { SkillInlineText } from "./SkillInlineText";
import { formatWorkspaceRelativePath } from "../../filePathDisplay";
import {
@@ -1796,59 +1797,6 @@ function workToneIcon(tone: TimelineWorkEntry["tone"]): {
};
}
-function workEntryPreview(
- workEntry: Pick,
- workspaceRoot: string | undefined,
-) {
- if (workEntry.command) return workEntry.command;
- if (workEntry.detail) return workEntry.detail;
- if ((workEntry.changedFiles?.length ?? 0) === 0) return null;
- const [firstPath] = workEntry.changedFiles ?? [];
- if (!firstPath) return null;
- const displayPath = formatWorkspaceRelativePath(firstPath, workspaceRoot);
- return workEntry.changedFiles!.length === 1
- ? displayPath
- : `${displayPath} +${workEntry.changedFiles!.length - 1} more`;
-}
-
-function workEntryRawCommand(
- workEntry: Pick,
-): string | null {
- const rawCommand = workEntry.rawCommand?.trim();
- if (!rawCommand || !workEntry.command) {
- return null;
- }
- return rawCommand === workEntry.command.trim() ? null : rawCommand;
-}
-
-function buildToolCallExpandedBody(
- workEntry: TimelineWorkEntry,
- workspaceRoot: string | undefined,
-): string | null {
- const blocks: string[] = [];
- if (workEntry.itemType === "mcp_tool_call" && workEntry.toolData !== undefined) {
- blocks.push(`MCP call\n${JSON.stringify(workEntry.toolData, null, 2)}`);
- }
- const raw = workEntryRawCommand(workEntry);
- if (raw?.trim()) {
- blocks.push(raw.trim());
- } else if (workEntry.command?.trim()) {
- blocks.push(workEntry.command.trim());
- }
- if (workEntry.detail?.trim()) {
- blocks.push(workEntry.detail.trim());
- }
- const changedFiles = workEntry.changedFiles ?? [];
- if (changedFiles.length > 0) {
- blocks.push(
- changedFiles
- .map((filePath) => formatWorkspaceRelativePath(filePath, workspaceRoot))
- .join("\n"),
- );
- }
- return blocks.length > 0 ? blocks.join("\n\n") : null;
-}
-
function workEntryIconName(workEntry: TimelineWorkEntry): WorkEntryIconName {
if (
workEntry.sourceActivityKind === "user-input.requested" ||
diff --git a/apps/web/src/components/chat/workEntryDetails.ts b/apps/web/src/components/chat/workEntryDetails.ts
new file mode 100644
index 00000000000..3767f5bde9c
--- /dev/null
+++ b/apps/web/src/components/chat/workEntryDetails.ts
@@ -0,0 +1,138 @@
+import type { WorkLogEntry } from "../../session-logic";
+import { formatWorkspaceRelativePath } from "../../filePathDisplay";
+
+function asPlainRecord(value: unknown): Record | null {
+ return value && typeof value === "object" && !Array.isArray(value)
+ ? (value as Record)
+ : null;
+}
+
+function asTrimmedPlainString(value: unknown): string | null {
+ if (typeof value !== "string") {
+ return null;
+ }
+ const trimmed = value.trim();
+ return trimmed.length > 0 ? trimmed : null;
+}
+
+function webSearchActionRecord(toolData: unknown): Record | null {
+ return asPlainRecord(asPlainRecord(toolData)?.action);
+}
+
+function webSearchQueries(toolData: unknown): string[] {
+ const item = asPlainRecord(toolData);
+ const action = webSearchActionRecord(toolData);
+ if (Array.isArray(action?.queries)) {
+ const queries: string[] = [];
+ for (const query of action.queries) {
+ const trimmed = asTrimmedPlainString(query);
+ if (trimmed) {
+ queries.push(trimmed);
+ }
+ }
+ if (queries.length > 0) {
+ return queries;
+ }
+ }
+
+ const actionQuery = asTrimmedPlainString(action?.query);
+ if (actionQuery) {
+ return [actionQuery];
+ }
+
+ const itemQuery = asTrimmedPlainString(item?.query);
+ if (itemQuery) {
+ return [itemQuery];
+ }
+ return [];
+}
+
+function webSearchPreview(toolData: unknown): string | null {
+ const action = webSearchActionRecord(toolData);
+ const actionType = asTrimmedPlainString(action?.type);
+ const queries = webSearchQueries(toolData);
+ if (queries.length > 0) {
+ const [firstQuery] = queries;
+ if (!firstQuery) {
+ return null;
+ }
+ return queries.length === 1 ? firstQuery : `${firstQuery} +${queries.length - 1} more`;
+ }
+ const url = asTrimmedPlainString(action?.url);
+ const pattern = asTrimmedPlainString(action?.pattern);
+ if ((actionType === "findInPage" || actionType === "find_in_page") && pattern && url) {
+ return `${pattern} in ${url}`;
+ }
+ return pattern ?? url;
+}
+
+function formatWebSearchExpandedBody(toolData: unknown): string | null {
+ try {
+ return JSON.stringify(toolData, null, 2);
+ } catch {
+ return null;
+ }
+}
+
+export function workEntryPreview(
+ workEntry: Pick,
+ workspaceRoot: string | undefined,
+) {
+ if (workEntry.command) return workEntry.command;
+ if (workEntry.itemType === "web_search") {
+ const preview = webSearchPreview(workEntry.toolData);
+ if (preview) return preview;
+ }
+ if (workEntry.detail) return workEntry.detail;
+ if ((workEntry.changedFiles?.length ?? 0) === 0) return null;
+ const [firstPath] = workEntry.changedFiles ?? [];
+ if (!firstPath) return null;
+ const displayPath = formatWorkspaceRelativePath(firstPath, workspaceRoot);
+ return workEntry.changedFiles!.length === 1
+ ? displayPath
+ : `${displayPath} +${workEntry.changedFiles!.length - 1} more`;
+}
+
+function workEntryRawCommand(
+ workEntry: Pick,
+): string | null {
+ const rawCommand = workEntry.rawCommand?.trim();
+ if (!rawCommand || !workEntry.command) {
+ return null;
+ }
+ return rawCommand === workEntry.command.trim() ? null : rawCommand;
+}
+
+export function buildToolCallExpandedBody(
+ workEntry: WorkLogEntry,
+ workspaceRoot: string | undefined,
+): string | null {
+ const blocks: string[] = [];
+ if (workEntry.itemType === "mcp_tool_call" && workEntry.toolData !== undefined) {
+ blocks.push(`MCP call\n${JSON.stringify(workEntry.toolData, null, 2)}`);
+ }
+ if (workEntry.itemType === "web_search" && workEntry.toolData !== undefined) {
+ const body = formatWebSearchExpandedBody(workEntry.toolData);
+ if (body) {
+ blocks.push(body);
+ }
+ }
+ const raw = workEntryRawCommand(workEntry);
+ if (raw?.trim()) {
+ blocks.push(raw.trim());
+ } else if (workEntry.command?.trim()) {
+ blocks.push(workEntry.command.trim());
+ }
+ if (workEntry.detail?.trim()) {
+ blocks.push(workEntry.detail.trim());
+ }
+ const changedFiles = workEntry.changedFiles ?? [];
+ if (changedFiles.length > 0) {
+ blocks.push(
+ changedFiles
+ .map((filePath) => formatWorkspaceRelativePath(filePath, workspaceRoot))
+ .join("\n"),
+ );
+ }
+ return blocks.length > 0 ? blocks.join("\n\n") : null;
+}
diff --git a/apps/web/src/session-logic.test.ts b/apps/web/src/session-logic.test.ts
index 0f12e672f66..d0ed9817c66 100644
--- a/apps/web/src/session-logic.test.ts
+++ b/apps/web/src/session-logic.test.ts
@@ -1166,6 +1166,15 @@ describe("deriveWorkLogEntries", () => {
});
it("uses grep raw output summaries instead of repeating the generic tool label", () => {
+ const searchItem = {
+ type: "webSearch",
+ id: "web-search-1",
+ query: "React 19 hydration warnings",
+ action: {
+ type: "search",
+ queries: ["React 19 hydration warnings"],
+ },
+ };
const activities: OrchestrationThreadActivity[] = [
makeActivity({
id: "grep-update",
@@ -1179,6 +1188,7 @@ describe("deriveWorkLogEntries", () => {
data: {
toolCallId: "tool-grep-1",
kind: "search",
+ item: searchItem,
rawInput: {},
},
},
@@ -1211,6 +1221,7 @@ describe("deriveWorkLogEntries", () => {
toolTitle: "grep",
detail: "19 files",
itemType: "web_search",
+ toolData: searchItem,
});
});
diff --git a/apps/web/src/session-logic.ts b/apps/web/src/session-logic.ts
index 5d5051f748e..1cff0de41ca 100644
--- a/apps/web/src/session-logic.ts
+++ b/apps/web/src/session-logic.ts
@@ -734,7 +734,7 @@ function toDerivedWorkLogEntry(activity: OrchestrationThreadActivity): DerivedWo
if (title) {
entry.toolTitle = title;
}
- if (itemType === "mcp_tool_call") {
+ if (itemType === "mcp_tool_call" || itemType === "web_search") {
const data = asRecord(payload?.data);
if (data?.item !== undefined) {
entry.toolData = data.item;