diff --git a/apps/mobile/src/features/settings/appearance/sections/ToolCallsAppearanceSection.tsx b/apps/mobile/src/features/settings/appearance/sections/ToolCallsAppearanceSection.tsx index aa1c46595d697..c18f58d523879 100644 --- a/apps/mobile/src/features/settings/appearance/sections/ToolCallsAppearanceSection.tsx +++ b/apps/mobile/src/features/settings/appearance/sections/ToolCallsAppearanceSection.tsx @@ -15,6 +15,7 @@ const TOOL_CALL_VISIBILITY_OPTIONS: ReadonlyArray<{ { value: "showAll", label: "Show all" }, { value: "hideSuccessful", label: "Hide successful" }, { value: "hideAll", label: "Hide all" }, + { value: "messagesOnly", label: "Messages only" }, { value: "zen", label: "Zen" }, ]; @@ -40,8 +41,9 @@ export function ToolCallsAppearanceSection() { ))} - Hide the commands, edits, and searches agents run. Hide successful keeps failures. Zen shows - only final answers. Approvals, questions, and errors always show. + Hide the commands, edits, and searches agents run. Hide successful keeps failures. Messages + only also hides thinking and subagents. Zen shows only final answers. Approvals, questions, + and errors always show. ); diff --git a/apps/mobile/src/features/settings/settings-search.ts b/apps/mobile/src/features/settings/settings-search.ts index 4d02bd9789b1d..85a8cd95a6e97 100644 --- a/apps/mobile/src/features/settings/settings-search.ts +++ b/apps/mobile/src/features/settings/settings-search.ts @@ -32,6 +32,8 @@ const SETTINGS_SEARCH_TERMS = { "work log", "zen", "final answers only", + "messages only", + "hide thinking", ], SettingsNavigation: ["tabs", "tab order", "reorder", "hide tabs"], SettingsKeyboard: ["return key", "send message", "new line", "hardware keyboard"], diff --git a/apps/mobile/src/lib/threadActivity.test.ts b/apps/mobile/src/lib/threadActivity.test.ts index f5b4d0232a902..ea45eb33be76d 100644 --- a/apps/mobile/src/lib/threadActivity.test.ts +++ b/apps/mobile/src/lib/threadActivity.test.ts @@ -1281,7 +1281,7 @@ describe("buildThreadFeed", () => { const running = { runId, status: "running" as const, startedAt: at(1), completedAt: null }; const rows = ( items: ReadonlyArray, - toolCallVisibility: "hideSuccessful" | "hideAll" | "zen", + toolCallVisibility: "hideSuccessful" | "hideAll" | "messagesOnly" | "zen", latestRun: ThreadFeedLatestRun | null = null, ) => deriveThreadFeedPresentation( @@ -1340,6 +1340,25 @@ describe("buildThreadFeed", () => { "wake", "thinking:", ]); + + const thought = { + ...base("thought", at(3), 9), + type: "reasoning" as const, + text: "Checking the captures", + streaming: false, + }; + expect(rows([user, step, thought, passed, failed, final], "messagesOnly")).toEqual([ + "user", + "fold", + "step", + "final", + ]); + expect(rows([user, thought, passed, final], "messagesOnly")).toEqual(["user", "final"]); + expect(rows([user, step, thought, passed], "messagesOnly", running)).toEqual([ + "user", + "step", + "thinking:", + ]); }); }); diff --git a/apps/mobile/src/lib/threadActivity.ts b/apps/mobile/src/lib/threadActivity.ts index cd2313b6dccb1..f8004c4dd3999 100644 --- a/apps/mobile/src/lib/threadActivity.ts +++ b/apps/mobile/src/lib/threadActivity.ts @@ -1372,7 +1372,7 @@ const visibleActivityGroups = new WeakMap< { readonly visibility: ToolCallVisibility; readonly group: ThreadFeedActivityGroup | null } >(); -function zenKeepsActivity(activity: ThreadFeedActivity): boolean { +function shownWithoutWorkLog(activity: ThreadFeedActivity): boolean { const item = activity.projectedItem.item; return ( activity.prominent || @@ -1391,8 +1391,8 @@ function activityGroupForVisibility( const cached = visibleActivityGroups.get(entry); if (cached?.visibility === visibility) return cached.group; const activities = entry.activities.filter((activity) => - visibility === "zen" - ? zenKeepsActivity(activity) + visibility === "zen" || visibility === "messagesOnly" + ? shownWithoutWorkLog(activity) : !workEntryHiddenByToolCallVisibility(activity.workEntry, visibility), ); const group = diff --git a/apps/web/src/components/chat/MessagesTimeline.logic.test.ts b/apps/web/src/components/chat/MessagesTimeline.logic.test.ts index 5d242f1efc20e..d933d3de9cf66 100644 --- a/apps/web/src/components/chat/MessagesTimeline.logic.test.ts +++ b/apps/web/src/components/chat/MessagesTimeline.logic.test.ts @@ -2980,14 +2980,16 @@ describe("v2 run and attempt history", () => { const ended = { ...running, turnEnded: true }; const rows = ( timelineEntries: ReadonlyArray, - toolCallVisibility: "hideSuccessful" | "hideAll" | "zen", + toolCallVisibility: "hideSuccessful" | "hideAll" | "messagesOnly" | "zen", latestRun?: typeof ended | typeof running, + pendingApprovalRequestIds?: ReadonlySet, ) => deriveMessagesTimelineRows({ timelineEntries, expandedRunIds: new Set([runId]), isWorking: latestRun !== undefined, ...(latestRun ? { latestRun } : {}), + ...(pendingApprovalRequestIds ? { pendingApprovalRequestIds } : {}), turnDiffSummaries: [], supportsConversationRollback: false, toolCallVisibility, @@ -3050,6 +3052,89 @@ describe("v2 run and attempt history", () => { "working-indicator-row", "live-activity-row", ]); + + const work = ( + id: string, + itemType: "reasoning" | "todo_list" | "error" | "approval_request" | "user_input_request", + second: number, + extra: Partial["entry"]> = {}, + ): Extract => ({ + id, + kind: "work", + createdAt: at(second), + entry: { + id, + createdAt: at(second), + runId, + label: id, + tone: "info", + itemType, + toolLifecycleStatus: "completed", + ...extra, + }, + }); + const thought = work("thought", "reasoning", 6, { detail: "Checking the captures" }); + const tasks = work("tasks", "todo_list", 6); + const child: TimelineEntry = { + id: "child", + kind: "event", + createdAt: at(7), + projectedItem: { + item: { id: "child", type: "subagent", runId }, + } as OrchestrationV2ProjectedTurnItem, + }; + const steps = [thought, tasks, child, passed, failed]; + expect(rows([user, step, ...steps, final], "messagesOnly")).toEqual([ + "user", + "turn-fold:turn-1", + "step", + "final", + ]); + expect(rows([user, ...steps, final], "messagesOnly")).toEqual(["user", "final"]); + expect( + rows( + [ + user, + step, + { ...thought, entry: { ...thought.entry, toolLifecycleStatus: "inProgress" } }, + ], + "messagesOnly", + running, + ), + ).toEqual(["user", "working-indicator-row", "step", "live-activity-row"]); + const requestId = RuntimeRequestId.make("approval-request"); + const approval = work("approval", "approval_request", 10, { + toolLifecycleStatus: "inProgress", + projectedItem: { + item: { id: "approval", type: "approval_request", requestId, runId }, + } as OrchestrationV2ProjectedTurnItem, + }); + const answered = work("answered", "user_input_request", 11, { + questionAnswer: { + requestId: RuntimeRequestId.make("input-request"), + questionTextById: { scope: "Which scope?" }, + answers: { scope: "Web" }, + attachmentsByQuestionId: {}, + }, + }); + const errored = work("errored", "error", 12, { tone: "error", toolLifecycleStatus: "failed" }); + expect( + rows( + [user, step, thought, approval, answered, errored], + "messagesOnly", + running, + new Set([requestId]), + ), + ).toEqual(["user", "working-indicator-row", "step", "approval", "answered", "errored"]); + // Find and citations open the fold that holds an in-between message. + const foldRunIds = (toolCallVisibility: "messagesOnly" | "zen") => + timelineTurnFoldRunIdsByEntryId({ + timelineEntries: [user, step, ...steps, final], + isWorking: false, + toolCallVisibility, + }); + expect(foldRunIds("messagesOnly").get("step")).toBe(runId); + expect(foldRunIds("zen").get("step")).toBeUndefined(); }); it("collapses only output from a superseded V2 attempt within the active logical run", () => { const runId = "run-steered" as never; diff --git a/apps/web/src/components/chat/MessagesTimeline.logic.ts b/apps/web/src/components/chat/MessagesTimeline.logic.ts index 9e9d82a11fcbd..fd7eb67b1dede 100644 --- a/apps/web/src/components/chat/MessagesTimeline.logic.ts +++ b/apps/web/src/components/chat/MessagesTimeline.logic.ts @@ -1378,7 +1378,7 @@ function settleSupersededReasoning(entries: ReadonlyArray) { }); } -function workShownAsFinalAnswerOnly( +function shownWithoutWorkLog( entry: TimelineEntry, pendingApprovalRequestIds: ReadonlySet | undefined, ): boolean { @@ -1460,7 +1460,7 @@ function timelineRowEntries( const steerAnswerIds = deriveInFlightSteerAnswerIds(entries); shown = entries.filter((entry, index) => { if (entry.kind !== "message") { - return workShownAsFinalAnswerOnly(entry, input.pendingApprovalRequestIds); + return shownWithoutWorkLog(entry, input.pendingApprovalRequestIds); } const { message } = entry; return ( @@ -1472,6 +1472,11 @@ function timelineRowEntries( (answeringRunId === null || message.runId !== answeringRunId)) ); }); + } else if (visibility === "messagesOnly") { + shown = entries.filter( + (entry) => + entry.kind === "message" || shownWithoutWorkLog(entry, input.pendingApprovalRequestIds), + ); } else if (visibility !== "showAll") { shown = entries.filter( (entry) => diff --git a/apps/web/src/components/settings/SettingsPanels.tsx b/apps/web/src/components/settings/SettingsPanels.tsx index 3dc9566e8981d..510b038d90b9b 100644 --- a/apps/web/src/components/settings/SettingsPanels.tsx +++ b/apps/web/src/components/settings/SettingsPanels.tsx @@ -1223,6 +1223,7 @@ const TOOL_CALL_VISIBILITY_LABELS: Record = { showAll: "Show all", hideSuccessful: "Hide successful", hideAll: "Hide all", + messagesOnly: "Messages only", zen: "Zen", }; @@ -1234,7 +1235,7 @@ function ToolCallVisibilityRow() { return (