Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions apps/web/src/components/chat/MessagesTimeline.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<MessagesTimeline
{...buildProps()}
timelineEntries={[
{
id: "entry-1",
kind: "work",
createdAt: "2026-03-17T19:12:28.000Z",
entry: workEntry,
},
]}
/>,
);
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(
Expand Down
54 changes: 1 addition & 53 deletions apps/web/src/components/chat/MessagesTimeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ import {
formatInlineTerminalContextLabel,
textContainsInlineTerminalContextLabels,
} from "./userMessageTerminalContexts";
import { buildToolCallExpandedBody, workEntryPreview } from "./workEntryDetails";
import { SkillInlineText } from "./SkillInlineText";
import { formatWorkspaceRelativePath } from "../../filePathDisplay";
import {
Expand Down Expand Up @@ -1796,59 +1797,6 @@ function workToneIcon(tone: TimelineWorkEntry["tone"]): {
};
}

function workEntryPreview(
workEntry: Pick<TimelineWorkEntry, "detail" | "command" | "changedFiles">,
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<TimelineWorkEntry, "command" | "rawCommand">,
): 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" ||
Expand Down
138 changes: 138 additions & 0 deletions apps/web/src/components/chat/workEntryDetails.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import type { WorkLogEntry } from "../../session-logic";
import { formatWorkspaceRelativePath } from "../../filePathDisplay";

function asPlainRecord(value: unknown): Record<string, unknown> | null {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: 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<string, unknown> | 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<WorkLogEntry, "detail" | "command" | "changedFiles" | "itemType" | "toolData">,
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<WorkLogEntry, "command" | "rawCommand">,
): 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;
}
11 changes: 11 additions & 0 deletions apps/web/src/session-logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -1179,6 +1188,7 @@ describe("deriveWorkLogEntries", () => {
data: {
toolCallId: "tool-grep-1",
kind: "search",
item: searchItem,
rawInput: {},
},
},
Expand Down Expand Up @@ -1211,6 +1221,7 @@ describe("deriveWorkLogEntries", () => {
toolTitle: "grep",
detail: "19 files",
itemType: "web_search",
toolData: searchItem,
});
});

Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/session-logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading