2272 lines · 76.2 KB · TypeScript
RawDownload
import * as Option from "effect/Option";import { foldUserInputActivities } from "@t3tools/client-runtime/work-log/user-input";import * as Schema from "effect/Schema";import {  requestKindFromRequestType,  type PendingApproval,} from "@t3tools/client-runtime/pending-requests";import { UserInputAttachmentAnswerPayload, isToolLifecycleItemType } from "@t3tools/contracts";import type {  OrchestrationLatestTurn,  OrchestrationThread,  OrchestrationThreadActivity,  ToolLifecycleItemType,  TurnId,  UserInputQuestion,} from "@t3tools/contracts";import { formatDuration } from "@t3tools/shared/orchestrationTiming";import {  commandDetailRepeatsCommand,  extractCommandOutputText,  extractWorkLogToolLifecycleStatus,  isWorktreeSetupActivity,  liveActivityToolStatus,  normalizeCompactToolLabel,  omitSupersededLifecycleMarkers,  resolveWorkEntryToolPresentation,  summarizeToolGroup,  toolGroupAction,  toolGroupSummaryKind,  workEntryIndicatesToolFailure,  workEntryIndicatesToolSuccess,  workLogEntryIsToolLike,  type ToolGroupSummaryKind,  type WorkLogToolLifecycleStatus,} 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"; export type { PendingApproval, PendingUserInput } from "@t3tools/client-runtime/pending-requests"; export interface PendingUserInputDraftAnswer {  readonly selectedOptionValues?: ReadonlyArray<string>;  readonly customAnswer?: string;  readonly attachmentCount?: number;  readonly attachmentsBlocked?: boolean;} 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;} export interface WorkLogEntry {  readonly questionAnswer?: UserInputAttachmentAnswerPayload;  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;  /**   * One row per workflow run or per-turn batch of direct spawns, like web's   * "Kicked off N subagents" CTA. Mobile has no Agents sheet, so the row   * also carries each agent's terminal state to derive its status label.   */  agentSpawn?: {    readonly workflowId: string | null;    readonly agentTaskIds: ReadonlyArray<string>;    readonly agents: ReadonlyArray<{      readonly title: string;      readonly status: WorkLogToolLifecycleStatus | undefined;      readonly detail: string | undefined;      /** When this member last reported, so the card can show the newest activity. */      readonly updatedAt: string;    }>;  };  toolData?: unknown;} interface DerivedWorkLogEntry extends WorkLogEntry {  sourceActivityKind: OrchestrationThreadActivity["kind"];  collapseKey?: string;  /** Grouping key for subagent lifecycle rows (one row per agent). */  taskId?: string;  /** The tool call that launched this agent, when the provider reports one. */  agentSpawnToolCallId?: string;  isWorkflowCoordinator?: boolean;  /** Shell/monitor/plan tasks: ordinary work-log rows, never spawn batches. */  isBackgroundTask?: boolean;} 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" | "device" | "t3-code" | "pull-request";      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;    }  | {      /**       * The turn's single live slot. Web keys its live tool row and its       * "Thinking" row identically so the slot updates in place; here the       * slot holds "Thinking" whenever no tool row is shimmering, so a tool       * failing does not insert a row under the group it lives in.       */      readonly type: "thinking";      readonly id: string;      readonly createdAt: string;      readonly turnId: TurnId | null;    }  | {      /**       * One batch of spawned subagents. Rendered as its own card because a       * single-line tool row has no room for what the agents are doing now,       * which on a phone is the one thing worth showing.       */      readonly type: "agent-spawn";      readonly id: string;      readonly createdAt: string;      readonly turnId: TurnId | null;      readonly activity: ThreadFeedActivity;      readonly expanded: boolean;      readonly summary: AgentSpawnSummary;    }; export interface AgentSpawnSummary {  /** "Locate UNO hand rendering code" for one agent, "3 subagents" for a batch. */  readonly title: string;  /** Latest member activity while working, else the batch outcome. */  readonly status: string;  readonly tone: "working" | "completed" | "failed" | "stopped";  readonly members: ReadonlyArray<{    readonly title: string;    readonly status: string;    readonly tone: "working" | "completed" | "failed" | "stopped";    readonly detail: string | undefined;    readonly updatedAt: string;  }>;} 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" }>>();let cachedThinkingRow: Extract<ThreadFeedEntry, { readonly type: "thinking" }> | null = null; export function isContextCompactionActivityGroup(  entry: Extract<ThreadFeedEntry, { readonly type: "activity-group" }>,): boolean {  return (    entry.activities.length === 1 &&    entry.activities[0]?.workEntry.sourceActivityKind === "context-compaction"  );} 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 {  if (draft?.attachmentsBlocked) return 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      : question.allowCustomAnswer !== false && (draft?.attachmentCount ?? 0) > 0        ? ""        : null;  }  return (    selectedOptionValues[0] ??    (question.allowCustomAnswer !== false && (draft?.attachmentCount ?? 0) > 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. Agent lifecycle rows * pass even when bypassed or owned by another agent, because they fold into * their spawn batch rather than rendering on their own; that is how Codex * children (all bypassed) and Claude workflow members reach the batch row. * Terminal rows are kept regardless — with no Agents surface on mobile they * are the terminal signal. */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 isTaskRow =    activity.kind === "task.started" ||    activity.kind === "task.progress" ||    activity.kind === "task.updated" ||    activity.kind === "task.completed";  const ownedByAgent = typeof payload.agentId === "string" && payload.agentId.trim().length > 0;  if (isTaskRow) {    if (!ownedByAgent && payload.timelineBypass !== true) {      return false;    }    // An agent's own shells stay internal; the agents themselves fold into    // their batch. A bypassed batch marker keeps its terminal row.    if (typeof payload.taskId === "string" && payload.agentKind === "agent") {      return false;    }    if (ownedByAgent) {      return true;    }    return !(activity.kind === "task.completed" || isTerminalTaskUpdate(activity));  }  return payload.timelineBypass === true || ownedByAgent;} /** Agent (non-background) task.started rows seed spawn batches. */function isAgentTaskStartedActivity(activity: OrchestrationThreadActivity): boolean {  const payload =    activity.payload && typeof activity.payload === "object"      ? (activity.payload as Record<string, unknown>)      : null;  return typeof payload?.taskId === "string" && payload.agentKind === "agent";} function deriveWorkLogEntries(  activities: ReadonlyArray<OrchestrationThreadActivity>,): DerivedWorkLogEntry[] {  const ordered = Arr.sort(activities, activityOrder);  const entries: DerivedWorkLogEntry[] = [];  for (const activity of foldUserInputActivities(ordered)) {    if (activity.tone !== "error" && isWorktreeSetupActivity(activity.kind)) continue;    if (activity.kind === "tool.started") continue;    // Like web: an agent's task.started row anchors its batch. It has a fixed    // id and timestamp, unlike progress ticks, whose stable per-task id is    // rewritten with a new createdAt on every update (and would otherwise    // make the batch row a "fresh" row again on each tick).    if (activity.kind === "task.started" && !isAgentTaskStartedActivity(activity)) 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:");} const decodeQuestionAttachmentAnswer = Schema.decodeUnknownOption(UserInputAttachmentAnswerPayload); 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.started" ||    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,    ...(() => {      if (activity.kind !== "user-input.answer-submitted") return {};

Lines 1–500 of 2272.