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.tsx b/apps/web/src/components/chat/V2LifecycleRow.tsx index 40fffaf608412..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, @@ -193,17 +196,6 @@ const SETTLED_SUBAGENT_STATUSES = new Set([ "interrupted", ]); -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; } @@ -442,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 + ); +}