# Files changed · PR \#10210 · pingdotgg/t3code

[View on GitCafe](https://git.cafe/pingdotgg/t3code/pull/10210/diffs)

Repository: [pingdotgg/t3code](https://git.cafe/pingdotgg/t3code)

Visibility: public

[fix\(mobile\): only make work rows expandable when the body adds something](https://git.cafe/pingdotgg/t3code/pull/10210.md)

Version: 2

Head commit: e4d0f1d58a4424c17f0ffb5335da27cd66fa21ae

Base commit: ce0945840c7d5e429db1b863c145637a7d3f0ff8

Comparison base: ce0945840c7d5e429db1b863c145637a7d3f0ff8

Showing up to 20 entries from this revision. If the pull request changes, reopen the first page before continuing.

## apps/mobile/src/features/threads/thread-work-log.tsx

Status: modified; +2; −18

```diff
@@ -34,7 +34,7 @@
 import { T3Wordmark } from "../../components/T3Wordmark";
 import { cn } from "../../lib/cn";
 import { THREAD_WORK_ROW_MIN_HEIGHT, type deriveThreadWorkLogSizing } from "../../lib/layout";
-import type { ThreadFeedActivity } from "../../lib/threadActivity";
+import { type ThreadFeedActivity, workEntryRowLabel } from "../../lib/threadActivity";
 import {
   resolveThreadWorkGroupInitialScroll,
   shouldFollowThreadWorkGroupAppend,
@@ -307,21 +307,6 @@
   );
 }
 
-function stripShellWrapper(value: string): string {
-  const trimmed = value.trim();
-  const match = trimmed.match(/^\/bin\/zsh -lc ['"]?([\s\S]*?)['"]?$/);
-  return (match?.[1] ?? trimmed).trim();
-}
-
-function compactActivityDetail(detail: string | null): string | null {
-  if (!detail) {
-    return null;
-  }
-
-  const cleaned = stripShellWrapper(detail).replace(/\s+/g, " ").trim();
-  return cleaned.length > 0 ? cleaned : null;
-}
-
 function workRowSymbolName(icon: ThreadFeedActivity["icon"]): AppSymbolName {
   switch (icon) {
     case "agent":
@@ -697,8 +682,7 @@
   const fullDetail = expanded ? row.getFullDetail() : null;
   const viewedImagePath = workEntryViewedImagePath(row.workEntry);
   const toolPresentation = resolveWorkEntryToolPresentation(row.workEntry);
+  const previewText = workEntryRowLabel(row.workEntry);
-  const previewText =
-    toolPresentation?.displayName ?? compactActivityDetail(row.detail) ?? row.summary;
   const displayText =
     !toolPresentation && expanded && row.workEntry.command?.trim() ? "Command" : previewText;
   const iconIsDestructive = row.icon === "alert" || row.icon === "warning";
```

## apps/mobile/src/lib/threadActivity.test.ts

Status: modified; +90; −0

```diff
@@ -21,6 +21,7 @@
   isPendingUserInputOptionSelected,
   setPendingUserInputCustomAnswer,
   togglePendingUserInputOptionSelection,
+  workEntryRowLabel,
   type ThreadFeedActivity,
   type ThreadFeedEntry,
 } from "./threadActivity";
@@ -702,8 +703,97 @@
     const [row] = group.activities;
     expect(row?.workEntry.detail).toBe(command);
     expect(row?.getFullDetail()).toBe(`${command}\n\n${command}`);
+    // Opening it would only repeat the command the row already shows.
+    expect(row?.canExpand).toBe(false);
   });
 
+  it.each([
+    {
+      name: "a task summary that is its own detail",
+      activity: {
+        kind: "task.completed" as const,
+        tone: "info" as const,
+        summary: "Task completed",
+        payload: {
+          taskId: "bh2p996o4",
+          status: "completed",
+          title: "Check CI on the new head",
+          summary: "Check CI on the new head",
+          detail: "Check CI on the new head",
+          agentKind: "background",
+          taskType: "local_bash",
+        },
+      },
+      label: "Check CI on the new head",
+      canExpand: false,
+    },
+    {
+      name: "a runtime warning with only its message",
+      activity: {
+        kind: "runtime.warning" as const,
+        tone: "info" as const,
+        summary: "Bash is unusable in this environment",
+        payload: { detail: "Bash is unusable in this environment" },
+      },
+      label: "Bash is unusable in this environment",
+      canExpand: false,
+    },
+    {
+      name: "a multi-line task report",
+      activity: {
+        kind: "task.completed" as const,
+        tone: "info" as const,
+        summary: "Task completed",
+        payload: {
+          taskId: "ae3f85a",
+          status: "completed",
+          title: "Audit the PR",
+          detail: "**Tooling note:** Bash is unusable.\n\n# Audit\n\nNo blockers.",
+          agentKind: "agent",
+          taskType: "local_agent",
+        },
+      },
+      label: "**Tooling note:** Bash is unusable. # Audit No blockers.",
+      canExpand: true,
+    },
+    {
+      name: "a command whose output differs from the command",
+      activity: {
+        kind: "tool.completed" as const,
+        tone: "tool" as const,
+        summary: "Command run",
+        payload: {
+          itemType: "command_execution",
+          title: "Command run",
+          detail: "Bash: printf hello",
+          data: { toolName: "Bash", command: "printf hello", rawOutput: { content: "hello" } },
+        },
+      },
+      label: "printf hello",
+      canExpand: true,
+    },
+  ])("only lets $name expand when the body adds something: $canExpand", (input) => {
+    const thread = makeThread({
+      id: ThreadId.make("thread-expand-rule"),
+      projectId: ProjectId.make("project-1"),
+      title: "Expand rule",
+      activities: [
+        makeActivity({
+          id: EventId.make("expand-rule"),
+          createdAt: "2026-09-01T00:00:00.000Z",
+          ...input.activity,
+        }),
+      ],
+    });
+
+    const [group] = buildThreadFeed(thread);
+    expect(group?.type).toBe("activity-group");
+    if (group?.type !== "activity-group") return;
+    const [row] = group.activities;
+    expect(workEntryRowLabel(row!.workEntry)).toBe(input.label);
+    expect(row?.canExpand).toBe(input.canExpand);
+  });
+
   it("drops a truncated Claude echo of a long command", () => {
     const command = `git add -A && git commit -m "${"x".repeat(200)}"`;
     const thread = makeThread({
```

## apps/mobile/src/lib/threadActivity.ts

Status: modified; +38; −8

```diff
@@ -878,15 +878,45 @@
   return blocks.length > 0 ? blocks.join("\n\n") : null;
 }
 
+/**
+ * A row only opens when its body says more than its collapsed line. A row
+ * whose only detail is the single-line text it already shows (a runtime
+ * warning, a task summary, a short command) has nothing to reveal.
+ * Multi-line text still expands: the collapsed row truncates it to one line.
+ * Cheap field checks come first so large tool payloads are not serialized
+ * for every row (see the deferred-expansion test).
+ */
+function workEntryHasExpandedBody(entry: WorkLogEntry, collapsedText: string): boolean {
+  if (entry.itemType === "mcp_tool_call" && entry.toolData !== undefined) return true;
+  if (entry.changedFiles?.some((path) => path.trim().length > 0)) return true;
+  const parts = [entry.rawCommand ?? entry.command, entry.detail]
-function workEntryHasExpandedBody(entry: WorkLogEntry): boolean {
+    .map((value) => value?.trim())
-  return (
+    .filter((value): value is string => Boolean(value));
+  if (parts.length === 0) return false;
+  if (parts.length > 1 && new Set(parts).size > 1) return true;
-    (entry.itemType === "mcp_tool_call" && entry.toolData !== undefined) ||
-    Boolean((entry.rawCommand ?? entry.command)?.trim()) ||
-    Boolean(entry.detail?.trim()) ||
-    (entry.changedFiles?.some((path) => path.trim().length > 0) ?? false)
-  );
+  const only = parts[0]!;
+  return only.includes("\n") || collapseWhitespace(only) !== collapseWhitespace(collapsedText);
+}
+
+function collapseWhitespace(value: string): string {
+  return value.replace(/\s+/g, " ").trim();
 }
 
+function stripShellWrapper(value: string): string {
+  const trimmed = value.trim();
+  const match = trimmed.match(/^\/bin\/zsh -lc ['"]?([\s\S]*?)['"]?$/);
+  return (match?.[1] ?? trimmed).trim();
+}
+
+/** The one-line text a collapsed work row shows. */
+export function workEntryRowLabel(entry: WorkLogEntry): string {
+  const presentation = resolveWorkEntryToolPresentation(entry);
+  if (presentation) return presentation.displayName;
+  const preview = workEntryPreview(entry);
+  const compactPreview = preview === null ? null : collapseWhitespace(stripShellWrapper(preview));
+  return compactPreview || workEntryHeading(entry);
+}
+
 function memoizeValue<T>(build: () => T): () => T {
   let value: T;
   let initialized = false;
@@ -2098,7 +2128,7 @@
       turnId: entry.turnId,
       summary,
       detail,
-      canExpand: workEntryHasExpandedBody(entry),
+      canExpand: workEntryHasExpandedBody(entry, workEntryRowLabel(entry)),
       getFullDetail,
       getCopyText,
       icon: workEntryIcon(entry),
```
