import { ApprovalRequestId, isToolLifecycleItemType, ProviderApprovalOption, ProviderRequestKind,} from "@t3tools/contracts";import type { OrchestrationLatestTurn, OrchestrationThread, OrchestrationThreadActivity, ToolLifecycleItemType, TurnId, UserInputQuestion,} from "@t3tools/contracts";import { formatDuration } from "@t3tools/shared/orchestrationTiming";import { commandDetailRepeatsCommand, extractCommandOutputText, isWorktreeSetupActivity, liveActivityToolStatus, normalizeCompactToolLabel, omitSupersededLifecycleMarkers, resolveWorkEntryToolPresentation, summarizeToolGroup, toolGroupAction, toolGroupSummaryKind, type ToolGroupSummaryKind,} from "@t3tools/client-runtime/work-log/presentation";import { extractToolActivityPresentation } from "@t3tools/client-runtime/work-log/tool-presentation";import { commandProgramName } from "@t3tools/client-runtime/work-log/command-label"; import * as Arr from "effect/Array";import * as Order from "effect/Order";import * as Schema from "effect/Schema"; export interface PendingApproval { readonly requestId: ApprovalRequestId; readonly requestKind: ProviderRequestKind; readonly createdAt: string; readonly detail?: string; readonly appName?: string; readonly options?: ReadonlyArray<ProviderApprovalOption>;} const isProviderRequestKind = Schema.is(ProviderRequestKind);const isProviderApprovalOption = Schema.is(ProviderApprovalOption); export interface PendingUserInput { readonly requestId: ApprovalRequestId; readonly createdAt: string; readonly questions: ReadonlyArray<UserInputQuestion>;} export interface PendingUserInputDraftAnswer { readonly selectedOptionValues?: ReadonlyArray<string>; readonly customAnswer?: string;} export interface ThreadFeedActivity { readonly id: string; readonly createdAt: string; readonly turnId: TurnId | null; readonly summary: string; readonly detail: string | null; readonly canExpand: boolean; readonly getFullDetail: () => string | null; readonly getCopyText: () => string; readonly icon: | "agent" | "alert" | "browser" | "check" | "command" | "computer" | "edit" | "eye" | "globe" | "hammer" | "message" | "warning" | "wrench" | "zap"; readonly toolLike: boolean; readonly status: "success" | "failure" | "neutral" | null; readonly lifecycleStatus?: WorkLogToolLifecycleStatus; readonly workEntry: WorkLogEntry; readonly groupedToolDetail?: boolean; readonly live?: boolean;} type WorkLogToolLifecycleStatus = "inProgress" | "completed" | "failed" | "declined" | "stopped"; export interface WorkLogEntry { id: string; createdAt: string; turnId: TurnId | null; label: string; detail?: string; viewedImagePath?: string; command?: string; rawCommand?: string; changedFiles?: ReadonlyArray<string>; tone: "thinking" | "tool" | "info" | "error"; toolTitle?: string; toolSurface?: import("@t3tools/contracts").ToolActivitySurface; toolIcon?: import("@t3tools/contracts").ToolActivityIcon; toolSource?: import("@t3tools/contracts").ToolActivitySource; itemType?: ToolLifecycleItemType; requestKind?: PendingApproval["requestKind"]; toolLifecycleStatus?: WorkLogToolLifecycleStatus; sourceActivityKind?: OrchestrationThreadActivity["kind"]; toolCallId?: string; agentSpawn?: boolean; toolData?: unknown;} interface DerivedWorkLogEntry extends WorkLogEntry { sourceActivityKind: OrchestrationThreadActivity["kind"]; collapseKey?: string; /** Grouping key for subagent lifecycle rows (one row per agent). */ taskId?: string;} type RawThreadFeedEntry = | { readonly type: "message"; readonly id: string; readonly createdAt: string; readonly message: OrchestrationThread["messages"][number]; } | { readonly type: "activity"; readonly id: string; readonly createdAt: string; readonly turnId: TurnId | null; readonly activity: ThreadFeedActivity; }; export type ThreadFeedEntry = | Extract<RawThreadFeedEntry, { type: "message" }> | { readonly type: "activity-group"; readonly id: string; readonly createdAt: string; readonly turnId: TurnId | null; readonly activities: ReadonlyArray<ThreadFeedActivity>; } | { readonly type: "work-toggle"; readonly id: string; readonly createdAt: string; readonly turnId: TurnId | null; readonly groupId: string; readonly hiddenCount: number; readonly expanded: boolean; readonly summary: string; readonly summaryKind: ToolGroupSummaryKind; readonly toolSurface?: WorkLogEntry["toolSurface"]; readonly toolIcon?: WorkLogEntry["toolIcon"]; readonly summaryToolIcon?: "browser" | "t3-code"; readonly hasFailure: boolean; readonly live: boolean; readonly shimmer: boolean; } | { readonly type: "turn-fold"; readonly id: string; readonly createdAt: string; readonly turnId: TurnId; readonly label: string; readonly expanded: boolean; }; export type ThreadFeedLatestTurn = Pick< OrchestrationLatestTurn, "turnId" | "state" | "startedAt" | "completedAt">; type ThreadFeedActivityGroup = Extract<ThreadFeedEntry, { readonly type: "activity-group" }>; // These keys are immutable inputs. Weak caches release old histories with their source data.const activityEntriesCache = new WeakMap< ReadonlyArray<OrchestrationThreadActivity>, ReadonlyArray<Extract<RawThreadFeedEntry, { readonly type: "activity" }>>>();const messageEntriesCache = new WeakMap< OrchestrationThread["messages"][number], Extract<RawThreadFeedEntry, { readonly type: "message" }>>();const activityGroupsCache = new WeakMap<ThreadFeedActivity, ThreadFeedActivityGroup>();const presentedActivityGroupsCache = new WeakMap< ThreadFeedActivityGroup, { readonly unsettledTurnId: TurnId | null; readonly isWorking: boolean; readonly activeTail: boolean; readonly rows: ReadonlyArray<ThreadFeedEntry>; }>();const turnFoldRowsCache = new WeakMap< ThreadFeedEntry, Extract<ThreadFeedEntry, { readonly type: "turn-fold" }>>(); export function isContextCompactionActivityGroup( entry: Extract<ThreadFeedEntry, { readonly type: "activity-group" }>,): boolean { return ( entry.activities.length === 1 && entry.activities[0]?.workEntry.sourceActivityKind === "context-compaction" );} function requestKindFromRequestType(requestType: unknown): PendingApproval["requestKind"] | null { switch (requestType) { case "command_execution_approval": case "exec_command_approval": return "command"; case "file_read_approval": return "file-read"; case "file_change_approval": case "apply_patch_approval": return "file-change"; case "mcp_elicitation_approval": return "mcp-elicitation"; default: return null; }} function isStalePendingRequestFailureDetail(detail: string | undefined): boolean { const normalized = detail?.toLowerCase(); if (!normalized) { return false; } return ( normalized.includes("stale pending approval request") || normalized.includes("stale pending user-input request") || normalized.includes("unknown pending approval request") || normalized.includes("unknown pending permission request") || normalized.includes("unknown pending user-input request") );} function parseApprovalRequestId(value: unknown): ApprovalRequestId | null { return typeof value === "string" && value.length > 0 ? ApprovalRequestId.make(value) : null;} function parseUserInputQuestions( payload: Record<string, unknown> | null,): ReadonlyArray<UserInputQuestion> | null { const questions = payload?.questions; if (!Array.isArray(questions)) { return null; } const parsed = questions .map<UserInputQuestion | null>((entry) => { if (!entry || typeof entry !== "object") return null; const question = entry as Record<string, unknown>; if ( typeof question.id !== "string" || typeof question.header !== "string" || typeof question.question !== "string" || !Array.isArray(question.options) ) { return null; } const options = question.options .map<UserInputQuestion["options"][number] | null>((option) => { if (!option || typeof option !== "object") return null; const record = option as Record<string, unknown>; if (typeof record.label !== "string" || typeof record.description !== "string") { return null; } return { label: record.label, description: record.description, ...(typeof record.value === "string" ? { value: record.value } : {}), }; }) .filter((option): option is UserInputQuestion["options"][number] => option !== null); if (options.length === 0 && question.allowCustomAnswer === false) { return null; } return { id: question.id, header: question.header, question: question.question, options, multiSelect: question.multiSelect === true, ...(typeof question.allowCustomAnswer === "boolean" ? { allowCustomAnswer: question.allowCustomAnswer } : {}), }; }) .filter((question): question is UserInputQuestion => question !== null); return parsed.length > 0 ? parsed : null;} function normalizeDraftAnswer(value: string | undefined): string | null { if (typeof value !== "string") { return null; } const trimmed = value.trim(); return trimmed.length > 0 ? trimmed : null;} function resolvePendingUserInputOptionValue( question: UserInputQuestion, value: string,): string | null { if (question.options.some((option) => option.value === value)) { return value; } const label = value.trim(); return label.length > 0 && question.options.some((option) => option.value === undefined && option.label.trim() === label) ? label : null;} function normalizeSelectedOptionValues( question: UserInputQuestion, value: ReadonlyArray<string> | undefined,): ReadonlyArray<string> { if (!Array.isArray(value)) { return []; } return Array.from( new Set( value .map((entry) => resolvePendingUserInputOptionValue(question, entry)) .filter((entry): entry is string => entry !== null), ), );} function resolvePendingUserInputAnswer( question: UserInputQuestion, draft: PendingUserInputDraftAnswer | undefined,): string | ReadonlyArray<string> | null { const customAnswer = question.allowCustomAnswer === false ? null : normalizeDraftAnswer(draft?.customAnswer); if (customAnswer) { return customAnswer; } const selectedOptionValues = normalizeSelectedOptionValues(question, draft?.selectedOptionValues); if (question.multiSelect) { return selectedOptionValues.length > 0 ? selectedOptionValues : null; } return selectedOptionValues[0] ?? null;} /** Some providers settle agents through task.updated instead of task.completed. */const MOBILE_TERMINAL_UPDATE_STATUSES: ReadonlySet<string> = new Set([ "completed", "failed", "cancelled", "interrupted",]); function isTerminalTaskUpdate(activity: OrchestrationThreadActivity): boolean { if (activity.kind !== "task.updated") { return false; } const payload = activity.payload && typeof activity.payload === "object" ? (activity.payload as Record<string, unknown>) : null; return ( typeof payload?.status === "string" && (MOBILE_TERMINAL_UPDATE_STATUSES.has(payload.status) || (payload.timelineBypass === true && payload.status === "idle")) );} /** * Quiet-timeline guarantee (mirrors web's session-logic): agent-internal * activity lives in the Agents sheet, not the work log. Terminal rows are * kept — with no Agents surface on mobile they are the terminal signal * (a surface that hides rows must keep its own terminal signal). That means * task.completed and terminal task.updated, including Antigravity cancellation. */function isAgentInternalActivity(activity: OrchestrationThreadActivity): boolean { const payload = activity.payload && typeof activity.payload === "object" ? (activity.payload as Record<string, unknown>) : null; if (!payload) { return false; } const isTerminalTaskRow = activity.kind === "task.completed" || isTerminalTaskUpdate(activity); if (payload.timelineBypass === true && !isTerminalTaskRow) { return true; } // agentId marks ownership, not "hide me": a NESTED AGENT's terminal row is // the only signal mobile gets (no Agents sheet), so it stays. Only an // agent's own background work (stamped "background") is internal — same // rule as web (review finding: hiding on agentId alone dropped nested // completions with no replacement UI). const ownedByAgent = typeof payload.agentId === "string" && payload.agentId.trim().length > 0; if (!ownedByAgent) { return false; } return !(isTerminalTaskRow && payload.agentKind === "agent");} function deriveWorkLogEntries( activities: ReadonlyArray<OrchestrationThreadActivity>,): DerivedWorkLogEntry[] { const ordered = Arr.sort(activities, activityOrder); const entries: DerivedWorkLogEntry[] = []; for (const activity of ordered) { if (activity.tone !== "error" && isWorktreeSetupActivity(activity.kind)) continue; if (activity.kind === "tool.started") continue; if (activity.kind === "task.started") continue; if (activity.kind === "task.updated" && !isTerminalTaskUpdate(activity)) continue; if (activity.kind === "tool.progress") continue; if (activity.kind === "context-window.updated") continue; if (activity.summary === "Checkpoint captured") continue; if (isNoContentRuntimeWarning(activity)) continue; if (isPlanBoundaryToolActivity(activity)) continue; if (isAgentInternalActivity(activity)) continue; entries.push(toDerivedWorkLogEntry(activity)); } return collapseDerivedWorkLogEntries(entries);} /** Adapters forward unknown wire-only SDK messages (background_tasks_changed, * commands_changed, ...) as runtime warnings. The suffix comes from * describeUnknownSdkMessage in the Claude adapter; a row with no displayable * text carries nothing a user can act on, so it does not render. */function isNoContentRuntimeWarning(activity: OrchestrationThreadActivity): boolean { return ( activity.kind === "runtime.warning" && activity.summary.endsWith("(no displayable text content)") );} function isPlanBoundaryToolActivity(activity: OrchestrationThreadActivity): boolean { if (activity.kind !== "tool.updated" && activity.kind !== "tool.completed") { return false; } const payload = activity.payload && typeof activity.payload === "object" ? (activity.payload as Record<string, unknown>) : null; return typeof payload?.detail === "string" && payload.detail.startsWith("ExitPlanMode:");} function toDerivedWorkLogEntry(activity: OrchestrationThreadActivity): DerivedWorkLogEntry { const payload = activity.payload && typeof activity.payload === "object" ? (activity.payload as Record<string, unknown>) : null; const commandPreview = extractToolCommand(payload); const changedFiles = extractChangedFiles(payload); const title = extractToolTitle(payload); const toolPresentation = extractToolActivityPresentation(payload); // Terminal task updates carry identity so they replace each child's progress row. const isTaskActivity = activity.kind === "task.progress" || activity.kind === "task.completed" || activity.kind === "task.updated"; const taskSummary = isTaskActivity && typeof payload?.summary === "string" && payload.summary.length > 0 ? payload.summary : null; const taskDetailAsLabel = isTaskActivity && !taskSummary && !title && typeof payload?.detail === "string" && payload.detail.length > 0 ? payload.detail : null; const taskLabel = taskSummary || taskDetailAsLabel; const taskId = isTaskActivity && typeof payload?.taskId === "string" && payload.taskId.length > 0 ? payload.taskId : undefined; const entry: DerivedWorkLogEntry = { id: activity.id, createdAt: activity.createdAt, turnId: activity.turnId, ...(taskId ? { taskId } : {}), label: taskLabel || activity.summary, tone: activity.kind === "task.progress" ? "thinking" : activity.tone === "approval" ? "info" : activity.tone, sourceActivityKind: activity.kind,Lines 1–500 of 2085. Load more
import { ApprovalRequestId, isToolLifecycleItemType, ProviderApprovalOption, ProviderRequestKind, } from "@t3tools/contracts"; import type { OrchestrationLatestTurn, OrchestrationThread, OrchestrationThreadActivity, ToolLifecycleItemType, TurnId, UserInputQuestion, } from "@t3tools/contracts"; import { formatDuration } from "@t3tools/shared/orchestrationTiming"; import { commandDetailRepeatsCommand, extractCommandOutputText, isWorktreeSetupActivity, liveActivityToolStatus, normalizeCompactToolLabel, omitSupersededLifecycleMarkers, resolveWorkEntryToolPresentation, summarizeToolGroup, toolGroupAction, toolGroupSummaryKind, type ToolGroupSummaryKind, } from "@t3tools/client-runtime/work-log/presentation"; import { extractToolActivityPresentation } from "@t3tools/client-runtime/work-log/tool-presentation"; import { commandProgramName } from "@t3tools/client-runtime/work-log/command-label"; import * as Arr from "effect/Array"; import * as Order from "effect/Order"; import * as Schema from "effect/Schema"; export interface PendingApproval { readonly requestId: ApprovalRequestId; readonly requestKind: ProviderRequestKind; readonly createdAt: string; readonly detail?: string; readonly appName?: string; readonly options?: ReadonlyArray<ProviderApprovalOption>; } const isProviderRequestKind = Schema.is(ProviderRequestKind); const isProviderApprovalOption = Schema.is(ProviderApprovalOption); export interface PendingUserInput { readonly requestId: ApprovalRequestId; readonly createdAt: string; readonly questions: ReadonlyArray<UserInputQuestion>; } export interface PendingUserInputDraftAnswer { readonly selectedOptionValues?: ReadonlyArray<string>; readonly customAnswer?: string; } export interface ThreadFeedActivity { readonly id: string; readonly createdAt: string; readonly turnId: TurnId | null; readonly summary: string; readonly detail: string | null; readonly canExpand: boolean; readonly getFullDetail: () => string | null; readonly getCopyText: () => string; readonly icon: | "agent" | "alert" | "browser" | "check" | "command" | "computer" | "edit" | "eye" | "globe" | "hammer" | "message" | "warning" | "wrench" | "zap"; readonly toolLike: boolean; readonly status: "success" | "failure" | "neutral" | null; readonly lifecycleStatus?: WorkLogToolLifecycleStatus; readonly workEntry: WorkLogEntry; readonly groupedToolDetail?: boolean; readonly live?: boolean; } type WorkLogToolLifecycleStatus = "inProgress" | "completed" | "failed" | "declined" | "stopped"; export interface WorkLogEntry { id: string; createdAt: string; turnId: TurnId | null; label: string; detail?: string; viewedImagePath?: string; command?: string; rawCommand?: string; changedFiles?: ReadonlyArray<string>; tone: "thinking" | "tool" | "info" | "error"; toolTitle?: string; toolSurface?: import("@t3tools/contracts").ToolActivitySurface; toolIcon?: import("@t3tools/contracts").ToolActivityIcon; toolSource?: import("@t3tools/contracts").ToolActivitySource; itemType?: ToolLifecycleItemType; requestKind?: PendingApproval["requestKind"]; toolLifecycleStatus?: WorkLogToolLifecycleStatus; sourceActivityKind?: OrchestrationThreadActivity["kind"]; toolCallId?: string; agentSpawn?: boolean; toolData?: unknown; } interface DerivedWorkLogEntry extends WorkLogEntry { sourceActivityKind: OrchestrationThreadActivity["kind"]; collapseKey?: string; /** Grouping key for subagent lifecycle rows (one row per agent). */ taskId?: string; } type RawThreadFeedEntry = | { readonly type: "message"; readonly id: string; readonly createdAt: string; readonly message: OrchestrationThread["messages"][number]; } | { readonly type: "activity"; readonly id: string; readonly createdAt: string; readonly turnId: TurnId | null; readonly activity: ThreadFeedActivity; }; export type ThreadFeedEntry = | Extract<RawThreadFeedEntry, { type: "message" }> | { readonly type: "activity-group"; readonly id: string; readonly createdAt: string; readonly turnId: TurnId | null; readonly activities: ReadonlyArray<ThreadFeedActivity>; } | { readonly type: "work-toggle"; readonly id: string; readonly createdAt: string; readonly turnId: TurnId | null; readonly groupId: string; readonly hiddenCount: number; readonly expanded: boolean; readonly summary: string; readonly summaryKind: ToolGroupSummaryKind; readonly toolSurface?: WorkLogEntry["toolSurface"]; readonly toolIcon?: WorkLogEntry["toolIcon"]; readonly summaryToolIcon?: "browser" | "t3-code"; readonly hasFailure: boolean; readonly live: boolean; readonly shimmer: boolean; } | { readonly type: "turn-fold"; readonly id: string; readonly createdAt: string; readonly turnId: TurnId; readonly label: string; readonly expanded: boolean; }; export type ThreadFeedLatestTurn = Pick< OrchestrationLatestTurn, "turnId" | "state" | "startedAt" | "completedAt" >; type ThreadFeedActivityGroup = Extract<ThreadFeedEntry, { readonly type: "activity-group" }>; // These keys are immutable inputs. Weak caches release old histories with their source data. const activityEntriesCache = new WeakMap< ReadonlyArray<OrchestrationThreadActivity>, ReadonlyArray<Extract<RawThreadFeedEntry, { readonly type: "activity" }>> >(); const messageEntriesCache = new WeakMap< OrchestrationThread["messages"][number], Extract<RawThreadFeedEntry, { readonly type: "message" }> >(); const activityGroupsCache = new WeakMap<ThreadFeedActivity, ThreadFeedActivityGroup>(); const presentedActivityGroupsCache = new WeakMap< ThreadFeedActivityGroup, { readonly unsettledTurnId: TurnId | null; readonly isWorking: boolean; readonly activeTail: boolean; readonly rows: ReadonlyArray<ThreadFeedEntry>; } >(); const turnFoldRowsCache = new WeakMap< ThreadFeedEntry, Extract<ThreadFeedEntry, { readonly type: "turn-fold" }> >(); export function isContextCompactionActivityGroup( entry: Extract<ThreadFeedEntry, { readonly type: "activity-group" }>, ): boolean { return ( entry.activities.length === 1 && entry.activities[0]?.workEntry.sourceActivityKind === "context-compaction" ); } function requestKindFromRequestType(requestType: unknown): PendingApproval["requestKind"] | null { switch (requestType) { case "command_execution_approval": case "exec_command_approval": return "command"; case "file_read_approval": return "file-read"; case "file_change_approval": case "apply_patch_approval": return "file-change"; case "mcp_elicitation_approval": return "mcp-elicitation"; default: return null; } } function isStalePendingRequestFailureDetail(detail: string | undefined): boolean { const normalized = detail?.toLowerCase(); if (!normalized) { return false; } return ( normalized.includes("stale pending approval request") || normalized.includes("stale pending user-input request") || normalized.includes("unknown pending approval request") || normalized.includes("unknown pending permission request") || normalized.includes("unknown pending user-input request") ); } function parseApprovalRequestId(value: unknown): ApprovalRequestId | null { return typeof value === "string" && value.length > 0 ? ApprovalRequestId.make(value) : null; } function parseUserInputQuestions( payload: Record<string, unknown> | null, ): ReadonlyArray<UserInputQuestion> | null { const questions = payload?.questions; if (!Array.isArray(questions)) { return null; } const parsed = questions .map<UserInputQuestion | null>((entry) => { if (!entry || typeof entry !== "object") return null; const question = entry as Record<string, unknown>; if ( typeof question.id !== "string" || typeof question.header !== "string" || typeof question.question !== "string" || !Array.isArray(question.options) ) { return null; } const options = question.options .map<UserInputQuestion["options"][number] | null>((option) => { if (!option || typeof option !== "object") return null; const record = option as Record<string, unknown>; if (typeof record.label !== "string" || typeof record.description !== "string") { return null; } return { label: record.label, description: record.description, ...(typeof record.value === "string" ? { value: record.value } : {}), }; }) .filter((option): option is UserInputQuestion["options"][number] => option !== null); if (options.length === 0 && question.allowCustomAnswer === false) { return null; } return { id: question.id, header: question.header, question: question.question, options, multiSelect: question.multiSelect === true, ...(typeof question.allowCustomAnswer === "boolean" ? { allowCustomAnswer: question.allowCustomAnswer } : {}), }; }) .filter((question): question is UserInputQuestion => question !== null); return parsed.length > 0 ? parsed : null; } function normalizeDraftAnswer(value: string | undefined): string | null { if (typeof value !== "string") { return null; } const trimmed = value.trim(); return trimmed.length > 0 ? trimmed : null; } function resolvePendingUserInputOptionValue( question: UserInputQuestion, value: string, ): string | null { if (question.options.some((option) => option.value === value)) { return value; } const label = value.trim(); return label.length > 0 && question.options.some((option) => option.value === undefined && option.label.trim() === label) ? label : null; } function normalizeSelectedOptionValues( question: UserInputQuestion, value: ReadonlyArray<string> | undefined, ): ReadonlyArray<string> { if (!Array.isArray(value)) { return []; } return Array.from( new Set( value .map((entry) => resolvePendingUserInputOptionValue(question, entry)) .filter((entry): entry is string => entry !== null), ), ); } function resolvePendingUserInputAnswer( question: UserInputQuestion, draft: PendingUserInputDraftAnswer | undefined, ): string | ReadonlyArray<string> | null { const customAnswer = question.allowCustomAnswer === false ? null : normalizeDraftAnswer(draft?.customAnswer); if (customAnswer) { return customAnswer; } const selectedOptionValues = normalizeSelectedOptionValues(question, draft?.selectedOptionValues); if (question.multiSelect) { return selectedOptionValues.length > 0 ? selectedOptionValues : null; } return selectedOptionValues[0] ?? null; } /** Some providers settle agents through task.updated instead of task.completed. */ const MOBILE_TERMINAL_UPDATE_STATUSES: ReadonlySet<string> = new Set([ "completed", "failed", "cancelled", "interrupted", ]); function isTerminalTaskUpdate(activity: OrchestrationThreadActivity): boolean { if (activity.kind !== "task.updated") { return false; } const payload = activity.payload && typeof activity.payload === "object" ? (activity.payload as Record<string, unknown>) : null; return ( typeof payload?.status === "string" && (MOBILE_TERMINAL_UPDATE_STATUSES.has(payload.status) || (payload.timelineBypass === true && payload.status === "idle")) ); } /** * Quiet-timeline guarantee (mirrors web's session-logic): agent-internal * activity lives in the Agents sheet, not the work log. Terminal rows are * kept — with no Agents surface on mobile they are the terminal signal * (a surface that hides rows must keep its own terminal signal). That means * task.completed and terminal task.updated, including Antigravity cancellation. */ function isAgentInternalActivity(activity: OrchestrationThreadActivity): boolean { const payload = activity.payload && typeof activity.payload === "object" ? (activity.payload as Record<string, unknown>) : null; if (!payload) { return false; } const isTerminalTaskRow = activity.kind === "task.completed" || isTerminalTaskUpdate(activity); if (payload.timelineBypass === true && !isTerminalTaskRow) { return true; } // agentId marks ownership, not "hide me": a NESTED AGENT's terminal row is // the only signal mobile gets (no Agents sheet), so it stays. Only an // agent's own background work (stamped "background") is internal — same // rule as web (review finding: hiding on agentId alone dropped nested // completions with no replacement UI). const ownedByAgent = typeof payload.agentId === "string" && payload.agentId.trim().length > 0; if (!ownedByAgent) { return false; } return !(isTerminalTaskRow && payload.agentKind === "agent"); } function deriveWorkLogEntries( activities: ReadonlyArray<OrchestrationThreadActivity>, ): DerivedWorkLogEntry[] { const ordered = Arr.sort(activities, activityOrder); const entries: DerivedWorkLogEntry[] = []; for (const activity of ordered) { if (activity.tone !== "error" && isWorktreeSetupActivity(activity.kind)) continue; if (activity.kind === "tool.started") continue; if (activity.kind === "task.started") continue; if (activity.kind === "task.updated" && !isTerminalTaskUpdate(activity)) continue; if (activity.kind === "tool.progress") continue; if (activity.kind === "context-window.updated") continue; if (activity.summary === "Checkpoint captured") continue; if (isNoContentRuntimeWarning(activity)) continue; if (isPlanBoundaryToolActivity(activity)) continue; if (isAgentInternalActivity(activity)) continue; entries.push(toDerivedWorkLogEntry(activity)); } return collapseDerivedWorkLogEntries(entries); } /** Adapters forward unknown wire-only SDK messages (background_tasks_changed, * commands_changed, ...) as runtime warnings. The suffix comes from * describeUnknownSdkMessage in the Claude adapter; a row with no displayable * text carries nothing a user can act on, so it does not render. */ function isNoContentRuntimeWarning(activity: OrchestrationThreadActivity): boolean { return ( activity.kind === "runtime.warning" && activity.summary.endsWith("(no displayable text content)") ); } function isPlanBoundaryToolActivity(activity: OrchestrationThreadActivity): boolean { if (activity.kind !== "tool.updated" && activity.kind !== "tool.completed") { return false; } const payload = activity.payload && typeof activity.payload === "object" ? (activity.payload as Record<string, unknown>) : null; return typeof payload?.detail === "string" && payload.detail.startsWith("ExitPlanMode:"); } function toDerivedWorkLogEntry(activity: OrchestrationThreadActivity): DerivedWorkLogEntry { const payload = activity.payload && typeof activity.payload === "object" ? (activity.payload as Record<string, unknown>) : null; const commandPreview = extractToolCommand(payload); const changedFiles = extractChangedFiles(payload); const title = extractToolTitle(payload); const toolPresentation = extractToolActivityPresentation(payload); // Terminal task updates carry identity so they replace each child's progress row. const isTaskActivity = activity.kind === "task.progress" || activity.kind === "task.completed" || activity.kind === "task.updated"; const taskSummary = isTaskActivity && typeof payload?.summary === "string" && payload.summary.length > 0 ? payload.summary : null; const taskDetailAsLabel = isTaskActivity && !taskSummary && !title && typeof payload?.detail === "string" && payload.detail.length > 0 ? payload.detail : null; const taskLabel = taskSummary || taskDetailAsLabel; const taskId = isTaskActivity && typeof payload?.taskId === "string" && payload.taskId.length > 0 ? payload.taskId : undefined; const entry: DerivedWorkLogEntry = { id: activity.id, createdAt: activity.createdAt, turnId: activity.turnId, ...(taskId ? { taskId } : {}), label: taskLabel || activity.summary, tone: activity.kind === "task.progress" ? "thinking" : activity.tone === "approval" ? "info" : activity.tone, sourceActivityKind: activity.kind,