From 4b16fa4dfe3f1c1c0b86f1a47f15c09ff53c2e69 Mon Sep 17 00:00:00 2001 From: Coy Geek <65363919+coygeek@users.noreply.github.com> Date: Tue, 10 Nov 2026 14:24:01 -0700 Subject: [PATCH] feat(web): a Messages only level shows just what you and the agent say Hide all in Settings > Chat > Tool calls in chat removes commands, edits, searches, and MCP tools, but thinking rows, task lists, and subagent cards stay, and they still fill the chat on a long turn. Zen removes those too, along with the agent's messages between steps, so a running turn cannot be followed. Add a fifth level, Messages only, between Hide all and Zen. It applies Zen's rule for everything that is not a message and keeps every message. Finished turns still fold under "Worked for", and Find and citations open the fold that holds their message. Approvals, questions, errors, compaction dividers, plans, and app output stay, as in Zen. Mobile gets the same option and the ported feed rule. Existing levels, the default, and stored values are unchanged. Co-Authored-By: Claude Fable 5.2 --- .../sections/ToolCallsAppearanceSection.tsx | 6 +- .../src/features/settings/settings-search.ts | 2 + apps/mobile/src/lib/threadActivity.test.ts | 21 ++++- apps/mobile/src/lib/threadActivity.ts | 6 +- .../chat/MessagesTimeline.logic.test.ts | 87 ++++++++++++++++++- .../components/chat/MessagesTimeline.logic.ts | 9 +- .../components/settings/SettingsPanels.tsx | 3 +- .../src/components/settings/settingsSearch.ts | 2 +- docs/user/activity-log.md | 11 +-- packages/contracts/src/settings.ts | 8 +- 10 files changed, 138 insertions(+), 17 deletions(-) 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 3c336b0292389..720f8948588fb 100644 --- a/apps/mobile/src/lib/threadActivity.ts +++ b/apps/mobile/src/lib/threadActivity.ts @@ -1359,7 +1359,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 || @@ -1378,8 +1378,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 ba0f508ebeecd..ae360ab855f32 100644 --- a/apps/web/src/components/chat/MessagesTimeline.logic.test.ts +++ b/apps/web/src/components/chat/MessagesTimeline.logic.test.ts @@ -2813,14 +2813,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, @@ -2883,6 +2885,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 4536e9a250386..908c42202b254 100644 --- a/apps/web/src/components/chat/MessagesTimeline.logic.ts +++ b/apps/web/src/components/chat/MessagesTimeline.logic.ts @@ -1364,7 +1364,7 @@ function settleSupersededReasoning(entries: ReadonlyArray) { }); } -function workShownAsFinalAnswerOnly( +function shownWithoutWorkLog( entry: TimelineEntry, pendingApprovalRequestIds: ReadonlySet | undefined, ): boolean { @@ -1446,7 +1446,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 ( @@ -1458,6 +1458,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 a37bf1581990e..98a5b2d110559 100644 --- a/apps/web/src/components/settings/SettingsPanels.tsx +++ b/apps/web/src/components/settings/SettingsPanels.tsx @@ -1217,6 +1217,7 @@ const TOOL_CALL_VISIBILITY_LABELS: Record = { showAll: "Show all", hideSuccessful: "Hide successful", hideAll: "Hide all", + messagesOnly: "Messages only", zen: "Zen", }; @@ -1228,7 +1229,7 @@ function ToolCallVisibilityRow() { return (