Files
tutti/packages/agent/gui/shared/workspaceAgentTimelineProjectionHelpers.ts
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

365 lines
12 KiB
TypeScript

import type { WorkspaceAgentActivityTimelineItem } from "./workspaceAgentTimelineTypes";
import { resolveWorkspaceAgentNoticeCommandSemantics } from "./workspaceAgentSystemNoticeSemantics";
import {
buildWorkspaceAgentToolCallDisplay,
resolveWorkspaceAgentToolName,
type ToolCallStatusKind
} from "./workspaceAgentToolCallDisplay";
import type {
BuildWorkspaceAgentSessionDetailInput,
WorkspaceAgentSessionDetailMessage,
WorkspaceAgentSessionDetailToolCall,
WorkspaceAgentSessionDetailTurn
} from "./workspaceAgentSessionDetailViewModel";
export function delegatedToolStepFromCall(
call: WorkspaceAgentSessionDetailToolCall
): Record<string, unknown> {
const payload = normalizedPayload(call.payload ?? undefined);
return {
id: call.id,
toolUseId: call.id.replace(/^call:/, ""),
name: call.name,
toolName: call.toolName,
callType: call.callType,
status: call.status,
toolInput: normalizedPayload(
payload?.input as WorkspaceAgentActivityTimelineItem["payload"]
),
toolResult: normalizedPayload(
payload?.output as WorkspaceAgentActivityTimelineItem["payload"]
),
toolError: normalizedPayload(
payload?.error as WorkspaceAgentActivityTimelineItem["payload"]
),
payload,
metadata: normalizedPayload(
payload?.metadata as WorkspaceAgentActivityTimelineItem["payload"]
),
locations: Array.isArray(payload?.locations) ? payload.locations : null,
occurredAtUnixMs: call.occurredAtUnixMs ?? null
};
}
export function compareToolCallsAscending(
left: WorkspaceAgentSessionDetailToolCall,
right: WorkspaceAgentSessionDetailToolCall
): number {
return (
(left.occurredAtUnixMs ?? 0) - (right.occurredAtUnixMs ?? 0) ||
left.id.localeCompare(right.id)
);
}
export function parentToolUseIdFromCall(
call: WorkspaceAgentSessionDetailToolCall
): string | null {
const metadata = normalizedPayload(
call.payload?.metadata as WorkspaceAgentActivityTimelineItem["payload"]
);
return firstPresentString(
stringRecordValue(metadata, "parentToolUseId"),
stringRecordValue(call.payload, "parentToolUseId")
);
}
export function isTaskLikeToolCall(
call: WorkspaceAgentSessionDetailToolCall
): boolean {
return [
"task",
"subagent",
"delegatetask",
"delegateagent",
"agent"
].includes(normalizeToolName(call.toolName));
}
export function toolCallView(
item: WorkspaceAgentActivityTimelineItem
): WorkspaceAgentSessionDetailToolCall {
const display = buildWorkspaceAgentToolCallDisplay(item);
const preserveTitle =
item.itemType.trim().toLowerCase().startsWith("approval.") ||
item.itemType.trim().toLowerCase().startsWith("interactive.") ||
firstPresentString(
item.callType,
stringRecordValue(item.payload, "callType")
) === "approval";
const fallbackName = preserveTitle
? firstPresentString(item.name, display.name)
: display.name;
return withSourceTimelineItems(
{
id: display.id,
name: fallbackName || display.name,
toolName: resolveWorkspaceAgentToolName(item),
callType: firstPresentString(
item.callType,
stringRecordValue(item.payload, "callType")
),
status: display.status,
statusKind: display.statusKind,
summary: display.detail ?? "",
payload: normalizedPayload(item.payload),
turnId: item.turnId?.trim() || undefined,
compactSummary: display.detail ?? "",
occurredAtUnixMs: item.occurredAtUnixMs ?? item.createdAtUnixMs ?? null
},
[item]
);
}
export function withSourceTimelineItems<T extends object>(
value: T,
sourceTimelineItems: readonly WorkspaceAgentActivityTimelineItem[] | undefined
): T & { sourceTimelineItems?: WorkspaceAgentActivityTimelineItem[] } {
if (!sourceTimelineItems || sourceTimelineItems.length === 0) return value;
Object.defineProperty(value, "sourceTimelineItems", {
configurable: true,
enumerable: false,
value: [...sourceTimelineItems],
writable: true
});
return value as T & {
sourceTimelineItems?: WorkspaceAgentActivityTimelineItem[];
};
}
export function mergeSourceTimelineItems(
previous: readonly WorkspaceAgentActivityTimelineItem[] | undefined,
next: readonly WorkspaceAgentActivityTimelineItem[] | undefined
): WorkspaceAgentActivityTimelineItem[] | undefined {
const merged = [...(previous ?? []), ...(next ?? [])];
if (merged.length === 0) return undefined;
const byKey = new Map<string, WorkspaceAgentActivityTimelineItem>();
for (const item of merged) byKey.set(sourceTimelineItemKey(item), item);
return [...byKey.values()].sort(compareTimelineItemsAscending);
}
export function normalizedPayload(
payload: WorkspaceAgentActivityTimelineItem["payload"]
): Record<string, unknown> | null {
return payload && typeof payload === "object" ? payload : null;
}
export function visibleErrorFromPayload(
payload: Record<string, unknown> | null
): WorkspaceAgentSessionDetailMessage["visibleError"] {
if (stringRecordValue(payload, "kind") !== "agent_visible_error") return null;
const origin = stringRecordValue(payload, "origin");
return {
code: stringRecordValue(payload, "code"),
phase: stringRecordValue(payload, "phase"),
provider: stringRecordValue(payload, "provider"),
...(origin ? { origin } : {}),
detail: stringRecordValue(payload, "detail"),
retryable: booleanRecordValue(payload, "retryable")
};
}
export function systemNoticeFromPayload(
payload: Record<string, unknown> | null,
context: Pick<
WorkspaceAgentActivityTimelineItem,
"eventId" | "messageSemantics" | "status"
>
): WorkspaceAgentSessionDetailMessage["systemNotice"] {
const commandSemantics = resolveWorkspaceAgentNoticeCommandSemantics({
eventId: context.eventId,
messageSemantics: context.messageSemantics,
payload,
status: context.status
});
if (
stringRecordValue(payload, "kind") !== "agent_system_notice" &&
!commandSemantics
) {
return null;
}
const source = stringRecordValue(payload, "source");
const noticeKind = stringRecordValue(payload, "noticeKind");
const semanticKind =
noticeKind === "context_handoff_required" &&
commandSemantics?.command === "compact" &&
commandSemantics.commandStatus === "failed"
? "context-handoff-required"
: null;
return {
noticeKind,
...(semanticKind ? { semanticKind } : {}),
severity: stringRecordValue(payload, "severity"),
...(source ? { source } : {}),
...(commandSemantics
? {
command: commandSemantics.command,
commandStatus: commandSemantics.commandStatus
}
: {}),
title: stringRecordValue(payload, "title"),
detail: stringRecordValue(payload, "detail"),
retryable: booleanRecordValue(payload, "retryable")
};
}
export function stringRecordValue(record: unknown, key: string): string | null {
if (!record || typeof record !== "object" || Array.isArray(record))
return null;
const value = (record as Record<string, unknown>)[key];
return typeof value === "string" && value.trim() ? value.trim() : null;
}
export function firstPresentString(
...values: Array<string | null | undefined>
): string | null {
for (const value of values) {
if (typeof value === "string" && value.trim()) return value.trim();
}
return null;
}
export function summarizeToolCallGroup(
calls: readonly WorkspaceAgentSessionDetailToolCall[]
): string | null {
if (calls.length < 2) return null;
const targets = [
...new Set(
calls
.filter((call) =>
["edit", "multiedit", "write"].includes(
normalizeToolName(call.toolName)
)
)
.map((call) => summarizeCallTarget(call.summary))
.filter((value): value is string => value !== null)
)
];
if (targets.length === 0) return null;
return targets.length === 1
? `Changed ${targets[0]}`
: `Changed ${targets[0]} and ${targets.length - 1} more files`;
}
export function shouldShowProcessingIndicator(
session: BuildWorkspaceAgentSessionDetailInput["session"],
turns: readonly WorkspaceAgentSessionDetailTurn[]
): boolean {
const activeTurn = session.activeTurn;
const settledTurn =
activeTurn == null
? null
: (turns.find((turn) => turn.id === activeTurn.turnId) ??
turns.at(-1) ??
null);
if (sessionHasRunnableIndicatorState(session)) {
if (!settledTurn) return true;
const lastAgentItem = settledTurn.agentItems.at(-1);
if (
lastAgentItem?.kind === "message" &&
isTerminalAgentMessageStatus(lastAgentItem.message.statusKind) &&
!hasActiveRunningTurn(session)
) {
return false;
}
return !settledTurn.toolCalls.some(
(call) => call.statusKind === "working" || call.statusKind === "waiting"
);
}
// Settle-to-final observation gap: the engine can mark the active turn
// settled before authoritative terminal transcript rows are observable.
// Keep the processing placeholder until that turn has terminal content.
if (!activeTurn || activeTurn.phase !== "settled") {
return false;
}
if (!settledTurn) {
return true;
}
if (settledTurn.agentItems.length === 0) {
return true;
}
const lastAgentItem = settledTurn.agentItems.at(-1);
if (
lastAgentItem?.kind === "message" &&
isTerminalAgentMessageStatus(lastAgentItem.message.statusKind)
) {
return false;
}
if (
settledTurn.toolCalls.some(
(call) => call.statusKind === "working" || call.statusKind === "waiting"
)
) {
return false;
}
return !settledTurn.agentItems.some(
(item) =>
item.kind === "message" &&
isTerminalAgentMessageStatus(item.message.statusKind)
);
}
function sourceTimelineItemKey(
item: WorkspaceAgentActivityTimelineItem
): string {
const eventId = item.eventId?.trim();
if (eventId) return `event:${eventId}`;
if (Number.isFinite(item.id) && item.id > 0) return `id:${item.id}`;
const seq = item.seq ?? 0;
return seq > 0
? `seq:${seq}`
: `local:${item.itemType}:${item.occurredAtUnixMs ?? 0}`;
}
function compareTimelineItemsAscending(
left: WorkspaceAgentActivityTimelineItem,
right: WorkspaceAgentActivityTimelineItem
): number {
return (
(left.seq ?? 0) - (right.seq ?? 0) ||
(left.occurredAtUnixMs ?? left.createdAtUnixMs ?? 0) -
(right.occurredAtUnixMs ?? right.createdAtUnixMs ?? 0) ||
left.id - right.id
);
}
function booleanRecordValue(record: unknown, key: string): boolean | null {
if (!record || typeof record !== "object" || Array.isArray(record))
return null;
const value = (record as Record<string, unknown>)[key];
return typeof value === "boolean" ? value : null;
}
function normalizeToolName(name: string | null): string {
return (name ?? "")
.trim()
.replace(/[_\s-]+/g, "")
.toLowerCase();
}
function summarizeCallTarget(summary: string): string | null {
const firstLine = summary.trim().split("\n")[0]?.trim() ?? "";
if (!firstLine) return null;
return firstLine.split(/[\\/]/).filter(Boolean).at(-1) ?? firstLine;
}
function hasActiveRunningTurn(
session: BuildWorkspaceAgentSessionDetailInput["session"]
): boolean {
const activeTurn = session.activeTurn;
return Boolean(activeTurn && activeTurn.phase !== "settled");
}
function sessionHasRunnableIndicatorState(
session: BuildWorkspaceAgentSessionDetailInput["session"]
): boolean {
return session.activeTurn ? session.activeTurn.phase !== "settled" : false;
}
function isTerminalAgentMessageStatus(
status: ToolCallStatusKind | null | undefined
): boolean {
return status === "completed" || status === "failed" || status === "canceled";
}