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 (