From 292b9bbfd451d15f715a087f63bb930dfde941e7 Mon Sep 17 00:00:00 2001 From: Alex Southwell <4596216+saphid@users.noreply.github.com> Date: Tue, 3 Nov 2026 08:54:32 +1100 Subject: [PATCH 1/2] fix(web): subagent previews drop image marks and full link URLs Co-Authored-By: Claude Opus 5.5 --- .../components/chat/V2LifecycleRow.logic.test.ts | 13 ++++++++++++- .../web/src/components/chat/V2LifecycleRow.logic.ts | 9 +++++++++ apps/web/src/components/chat/V2LifecycleRow.tsx | 11 +---------- 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/apps/web/src/components/chat/V2LifecycleRow.logic.test.ts b/apps/web/src/components/chat/V2LifecycleRow.logic.test.ts index df7905e4e7860..5d6935431a37f 100644 --- a/apps/web/src/components/chat/V2LifecycleRow.logic.test.ts +++ b/apps/web/src/components/chat/V2LifecycleRow.logic.test.ts @@ -1,7 +1,7 @@ import * as DateTime from "effect/DateTime"; import { describe, expect, it } from "vite-plus/test"; -import { subagentResumedSince } from "./V2LifecycleRow.logic"; +import { plainDetail, subagentResumedSince } from "./V2LifecycleRow.logic"; const failedAt = "2026-10-09T12:21:00.000Z"; const beforeFailure = DateTime.makeUnsafe("2026-10-09T12:05:00.000Z"); @@ -30,3 +30,14 @@ describe("subagentResumedSince", () => { ); }); }); + +describe("plainDetail", () => { + it.each([ + ["![screenshot](screenshots/shot.png) Verified", "screenshot Verified"], + ["[docs](https://en.wikipedia.org/wiki/T3_(protocol))", "docs"], + ["[report](outputs/report%20(final).md)", "report"], + ["[report](outputs/report\\(final.md)", "report"], + ])("strips image and link markup from %s", (detail, expected) => { + expect(plainDetail(detail)).toBe(expected); + }); +}); diff --git a/apps/web/src/components/chat/V2LifecycleRow.logic.ts b/apps/web/src/components/chat/V2LifecycleRow.logic.ts index bfc0954617e43..800d0734e4a24 100644 --- a/apps/web/src/components/chat/V2LifecycleRow.logic.ts +++ b/apps/web/src/components/chat/V2LifecycleRow.logic.ts @@ -19,3 +19,12 @@ export function subagentResumedSince( !Number.isNaN(notifiedAtMillis) && DateTime.toEpochMillis(agent.startedAt) > notifiedAtMillis ); } + +export function plainDetail(text: string): string { + return text + .replace(/!?\[([^\]]+)\]\((?:\\.|[^\\()]|\((?:\\.|[^\\()])*\))*\)/g, "$1") + .replace(/`/g, "") + .replace(/^[ \t]*[-*][ \t]+/gm, "") + .replace(/\s+/g, " ") + .trim(); +} diff --git a/apps/web/src/components/chat/V2LifecycleRow.tsx b/apps/web/src/components/chat/V2LifecycleRow.tsx index c0dd0e0900d37..b6558bfcd3227 100644 --- a/apps/web/src/components/chat/V2LifecycleRow.tsx +++ b/apps/web/src/components/chat/V2LifecycleRow.tsx @@ -60,7 +60,7 @@ import { getTriggerDisplayModelName } from "./providerIconUtils"; import { ProviderInstanceIcon, providerTextColor } from "./ProviderInstanceIcon"; import { cn } from "~/lib/utils"; import { TimelineSystemDivider } from "./TimelineSystemDivider"; -import { subagentResumedSince } from "./V2LifecycleRow.logic"; +import { plainDetail, subagentResumedSince } from "./V2LifecycleRow.logic"; import { Button, InlineButton } from "../ui/button"; import { T3Wordmark } from "../T3Wordmark"; @@ -194,15 +194,6 @@ const SETTLED_SUBAGENT_STATUSES = new Set([ const GENERIC_CHILD_END = /^Child task ended with status\b/i; -function plainDetail(text: string): string { - return text - .replace(/\[([^\]]+)\]\([^)]*\)/g, "$1") - .replace(/`/g, "") - .replace(/^[ \t]*[-*][ \t]+/gm, "") - .replace(/\s+/g, " ") - .trim(); -} - function isoOrNull(value: DateTime.Utc | null | undefined): string | null { return value ? DateTime.formatIso(value) : null; } From 89f727dfc8c0bdafc2d61e567e681869a5b37e47 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 14 Nov 2026 14:22:49 +0000 Subject: [PATCH 2/2] refactor: share subagent detail formatter between web and mobile Move mobile's subagentCardDetail into client-runtime and use it from the web lifecycle row, so both clients strip image marks and links with nested parentheses the same way, tested once. Co-Authored-By: Claude Opus 5.5 --- apps/mobile/src/features/threads/SubagentRow.tsx | 6 ++++-- .../threads/subagent-card-presentation.test.ts | 11 +---------- .../features/threads/subagent-card-presentation.ts | 12 ------------ .../features/threads/threadAgentsPresentation.ts | 3 +-- .../components/chat/V2LifecycleRow.logic.test.ts | 13 +------------ .../web/src/components/chat/V2LifecycleRow.logic.ts | 9 --------- apps/web/src/components/chat/V2LifecycleRow.tsx | 12 ++++++------ .../src/state/subagentDisplay.test.ts | 13 ++++++++++++- .../client-runtime/src/state/subagentDisplay.ts | 12 ++++++++++++ 9 files changed, 37 insertions(+), 54 deletions(-) diff --git a/apps/mobile/src/features/threads/SubagentRow.tsx b/apps/mobile/src/features/threads/SubagentRow.tsx index 7c71ac2680de1..96f6f659aa5a1 100644 --- a/apps/mobile/src/features/threads/SubagentRow.tsx +++ b/apps/mobile/src/features/threads/SubagentRow.tsx @@ -4,7 +4,10 @@ import { resolveProviderInstanceDisplayName, shouldShowInstanceBadge, } from "@t3tools/client-runtime/state/provider-instance-display"; -import { resolveSubagentMetadata } from "@t3tools/client-runtime/state/subagent-display"; +import { + resolveSubagentMetadata, + subagentCardDetail, +} from "@t3tools/client-runtime/state/subagent-display"; import { formatSubagentEffort } from "@t3tools/client-runtime/state/thread-execution"; import type { EnvironmentId, OrchestrationV2Subagent } from "@t3tools/contracts"; import type { ReactNode } from "react"; @@ -15,7 +18,6 @@ import { SymbolView } from "../../components/ui/AppSymbol"; import { ProviderIcon, ProviderInstanceIcon } from "../../components/ProviderIcon"; import { cn } from "../../lib/cn"; import { useEnvironmentServerConfig, useProject, useThreadShell } from "../../state/entities"; -import { subagentCardDetail } from "./subagent-card-presentation"; import { SUBAGENT_TONE_TEXT_CLASS, SubagentStatusDot } from "./SubagentStatusDot"; import { resolveSubagentRowPresentation } from "./threadAgentsPresentation"; diff --git a/apps/mobile/src/features/threads/subagent-card-presentation.test.ts b/apps/mobile/src/features/threads/subagent-card-presentation.test.ts index 2b32f84fcb9cc..14a8e6ce99dfe 100644 --- a/apps/mobile/src/features/threads/subagent-card-presentation.test.ts +++ b/apps/mobile/src/features/threads/subagent-card-presentation.test.ts @@ -1,17 +1,8 @@ import * as DateTime from "effect/DateTime"; import { describe, expect, it } from "vite-plus/test"; -import { subagentCardDetail, subagentCardElapsed } from "./subagent-card-presentation"; +import { subagentCardElapsed } from "./subagent-card-presentation"; describe("subagent card", () => { - it.each([ - ["![screenshot](screenshots/shot.png) Verified", "screenshot Verified"], - ["[docs](https://en.wikipedia.org/wiki/T3_(protocol))", "docs"], - ["[report](outputs/report%20(final).md)", "report"], - ["[report](outputs/report\\(final.md)", "report"], - ])("strips image and link markup from %s", (detail, expected) => { - expect(subagentCardDetail(detail)).toBe(expected); - }); - const start = DateTime.makeUnsafe("2026-09-21T12:00:00Z"); const end = DateTime.makeUnsafe("2026-09-21T12:01:00Z"); const later = DateTime.makeUnsafe("2026-09-21T12:02:00Z"); diff --git a/apps/mobile/src/features/threads/subagent-card-presentation.ts b/apps/mobile/src/features/threads/subagent-card-presentation.ts index 42e75c5cf214e..239f48cf441cc 100644 --- a/apps/mobile/src/features/threads/subagent-card-presentation.ts +++ b/apps/mobile/src/features/threads/subagent-card-presentation.ts @@ -3,18 +3,6 @@ import type { OrchestrationV2Subagent } from "@t3tools/contracts"; import { formatDuration } from "@t3tools/shared/orchestrationTiming"; import * as DateTime from "effect/DateTime"; -export function subagentCardDetail(detail: string | null): string | null { - if (!detail || /^Child task ended with status\b/i.test(detail)) return null; - return ( - detail - .replace(/!?\[([^\]]+)\]\((?:\\.|[^\\()]|\((?:\\.|[^\\()])*\))*\)/g, "$1") - .replace(/`/g, "") - .replace(/^[ \t]*[-*][ \t]+/gm, "") - .replace(/\s+/g, " ") - .trim() || null - ); -} - export function subagentCardElapsed( agents: ReadonlyArray>, nowMs: number, diff --git a/apps/mobile/src/features/threads/threadAgentsPresentation.ts b/apps/mobile/src/features/threads/threadAgentsPresentation.ts index 7f560497da254..64c9b232ed424 100644 --- a/apps/mobile/src/features/threads/threadAgentsPresentation.ts +++ b/apps/mobile/src/features/threads/threadAgentsPresentation.ts @@ -1,6 +1,7 @@ import { formatSubagentDisplayTitle, resolveSubagentMetadata, + subagentCardDetail, subagentDetailPreview, } from "@t3tools/client-runtime/state/subagent-display"; import { @@ -17,8 +18,6 @@ import type { } from "@t3tools/contracts"; import { resolveSelectableModel } from "@t3tools/shared/model"; -import { subagentCardDetail } from "./subagent-card-presentation"; - const PROMPT_TITLE_LIMIT = 80; export type SubagentRowTone = "working" | "completed" | "failed" | "stopped"; diff --git a/apps/web/src/components/chat/V2LifecycleRow.logic.test.ts b/apps/web/src/components/chat/V2LifecycleRow.logic.test.ts index 5d6935431a37f..df7905e4e7860 100644 --- a/apps/web/src/components/chat/V2LifecycleRow.logic.test.ts +++ b/apps/web/src/components/chat/V2LifecycleRow.logic.test.ts @@ -1,7 +1,7 @@ import * as DateTime from "effect/DateTime"; import { describe, expect, it } from "vite-plus/test"; -import { plainDetail, subagentResumedSince } from "./V2LifecycleRow.logic"; +import { subagentResumedSince } from "./V2LifecycleRow.logic"; const failedAt = "2026-10-09T12:21:00.000Z"; const beforeFailure = DateTime.makeUnsafe("2026-10-09T12:05:00.000Z"); @@ -30,14 +30,3 @@ describe("subagentResumedSince", () => { ); }); }); - -describe("plainDetail", () => { - it.each([ - ["![screenshot](screenshots/shot.png) Verified", "screenshot Verified"], - ["[docs](https://en.wikipedia.org/wiki/T3_(protocol))", "docs"], - ["[report](outputs/report%20(final).md)", "report"], - ["[report](outputs/report\\(final.md)", "report"], - ])("strips image and link markup from %s", (detail, expected) => { - expect(plainDetail(detail)).toBe(expected); - }); -}); diff --git a/apps/web/src/components/chat/V2LifecycleRow.logic.ts b/apps/web/src/components/chat/V2LifecycleRow.logic.ts index 800d0734e4a24..bfc0954617e43 100644 --- a/apps/web/src/components/chat/V2LifecycleRow.logic.ts +++ b/apps/web/src/components/chat/V2LifecycleRow.logic.ts @@ -19,12 +19,3 @@ export function subagentResumedSince( !Number.isNaN(notifiedAtMillis) && DateTime.toEpochMillis(agent.startedAt) > notifiedAtMillis ); } - -export function plainDetail(text: string): string { - return text - .replace(/!?\[([^\]]+)\]\((?:\\.|[^\\()]|\((?:\\.|[^\\()])*\))*\)/g, "$1") - .replace(/`/g, "") - .replace(/^[ \t]*[-*][ \t]+/gm, "") - .replace(/\s+/g, " ") - .trim(); -} diff --git a/apps/web/src/components/chat/V2LifecycleRow.tsx b/apps/web/src/components/chat/V2LifecycleRow.tsx index 08241da730d3e..1854bc74181d0 100644 --- a/apps/web/src/components/chat/V2LifecycleRow.tsx +++ b/apps/web/src/components/chat/V2LifecycleRow.tsx @@ -20,7 +20,10 @@ import { type HandoffTimelineRun, resolveModelSwitchChange, } from "@t3tools/client-runtime/handoff"; -import { formatSubagentDisplayTitle } from "@t3tools/client-runtime/state/subagent-display"; +import { + formatSubagentDisplayTitle, + subagentCardDetail, +} from "@t3tools/client-runtime/state/subagent-display"; import { ProviderDriverKind, type ModelSelection, @@ -61,7 +64,7 @@ import { getTriggerDisplayModelName } from "./providerIconUtils"; import { ProviderInstanceIcon, providerTextColor } from "./ProviderInstanceIcon"; import { cn } from "~/lib/utils"; import { TimelineSystemDivider } from "./TimelineSystemDivider"; -import { plainDetail, subagentResumedSince } from "./V2LifecycleRow.logic"; +import { subagentResumedSince } from "./V2LifecycleRow.logic"; import { Button, InlineButton } from "../ui/button"; import { T3Wordmark } from "../T3Wordmark"; @@ -193,8 +196,6 @@ const SETTLED_SUBAGENT_STATUSES = new Set([ "interrupted", ]); -const GENERIC_CHILD_END = /^Child task ended with status\b/i; - function isoOrNull(value: DateTime.Utc | null | undefined): string | null { return value ? DateTime.formatIso(value) : null; } @@ -433,8 +434,7 @@ function SubagentTimelineLink(props: { const progress = (agent?.progress ?? props.progress)?.trim(); const settled = SETTLED_SUBAGENT_STATUSES.has(status ?? liveStatus); const rawDetail = settled ? result || progress : progress || result; - const detail = - rawDetail && !GENERIC_CHILD_END.test(rawDetail) ? plainDetail(rawDetail) || null : null; + const detail = subagentCardDetail(rawDetail); const failed = status === "failed"; const timing = { status: liveStatus, diff --git a/packages/client-runtime/src/state/subagentDisplay.test.ts b/packages/client-runtime/src/state/subagentDisplay.test.ts index 9b1536a7aacb1..631723736dbe5 100644 --- a/packages/client-runtime/src/state/subagentDisplay.test.ts +++ b/packages/client-runtime/src/state/subagentDisplay.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vite-plus/test"; -import { subagentDetailPreview } from "./subagentDisplay.js"; +import { subagentCardDetail, subagentDetailPreview } from "./subagentDisplay.js"; describe("subagentDetailPreview", () => { it("prefers progress for live work and results for settled work", () => { @@ -39,3 +39,14 @@ it("normalizes Unicode whitespace and ignores a large hidden suffix", () => { "x".repeat(279) + "…", ); }); + +describe("subagentCardDetail", () => { + it.each([ + ["![screenshot](screenshots/shot.png) Verified", "screenshot Verified"], + ["[docs](https://en.wikipedia.org/wiki/T3_(protocol))", "docs"], + ["[report](outputs/report%20(final).md)", "report"], + ["[report](outputs/report\\(final.md)", "report"], + ])("strips image and link markup from %s", (detail, expected) => { + expect(subagentCardDetail(detail)).toBe(expected); + }); +}); diff --git a/packages/client-runtime/src/state/subagentDisplay.ts b/packages/client-runtime/src/state/subagentDisplay.ts index 1968f48b90a1d..22d0892f2f5bc 100644 --- a/packages/client-runtime/src/state/subagentDisplay.ts +++ b/packages/client-runtime/src/state/subagentDisplay.ts @@ -146,3 +146,15 @@ export function subagentDetailPreview(input: { } return compact || null; } + +export function subagentCardDetail(detail: string | null | undefined): string | null { + if (!detail || /^Child task ended with status\b/i.test(detail)) return null; + return ( + detail + .replace(/!?\[([^\]]+)\]\((?:\\.|[^\\()]|\((?:\\.|[^\\()])*\))*\)/g, "$1") + .replace(/`/g, "") + .replace(/^[ \t]*[-*][ \t]+/gm, "") + .replace(/\s+/g, " ") + .trim() || null + ); +}