源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
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
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
This commit is contained in:
+763
@@ -0,0 +1,763 @@
|
||||
import type {
|
||||
WorkspaceAgentSessionDetailTurn,
|
||||
WorkspaceAgentSessionDetailViewModel
|
||||
} from "../../workspaceAgentSessionDetailViewModel";
|
||||
import { extractImageGenerationPreview } from "../../imageGenerationTool";
|
||||
import type { AgentConversationVM } from "../contracts/agentConversationVM";
|
||||
import type { AgentGeneratedImageRowVM } from "../contracts/agentGeneratedImageRowVM";
|
||||
import type { AgentGoalControlRowVM } from "../contracts/agentGoalControlRowVM";
|
||||
import type {
|
||||
AgentMessageContentVM,
|
||||
AgentMessageRowVM
|
||||
} from "../contracts/agentMessageRowVM";
|
||||
import type { AgentToolCallVM } from "../contracts/agentToolCallVM";
|
||||
import type { AgentTranscriptRowVM } from "../contracts/agentTranscriptRowVM";
|
||||
import { projectAgentMessageFinalText } from "./agentMessageFinalTextProjection";
|
||||
import { computeAgentToolGroups } from "./agentToolGroupingProjection";
|
||||
import { buildAgentTurnSequenceItems } from "./agentTurnSequenceProjection";
|
||||
import { projectTurnRows } from "./agentTurnRowProjection";
|
||||
import { projectAgentProcessingRow } from "./agentProcessingProjection";
|
||||
import { projectAgentTurnSummaryRows } from "./agentTurnSummaryProjection";
|
||||
|
||||
export interface AgentConversationProjectionOptions {
|
||||
avoidGroupingEdits?: boolean;
|
||||
}
|
||||
|
||||
const RENDER_IRRELEVANT_TRANSCRIPT_ROW_FIELDS = new Set(["occurredAtUnixMs"]);
|
||||
// Codex has emitted both the older "2%" wording and the current wording
|
||||
// without a percentage; the percentage is diagnostic context, not identity.
|
||||
const CODEX_SKILLS_CONTEXT_BUDGET_NOTICE_PATTERN =
|
||||
/skill descriptions were shortened to fit the(?:\s+\d+(?:\.\d+)?%)?\s+skills context budget/i;
|
||||
const CODEX_MODEL_METADATA_FALLBACK_NOTICE_FRAGMENT =
|
||||
"defaulting to fallback metadata";
|
||||
const MARKDOWN_IMAGE_PATTERN =
|
||||
/!\[[^\]]*]\(\s*(?:<([^>]+)>|([^)\s]+))(?:\s+["'][^"']*["'])?\s*\)/g;
|
||||
|
||||
export function projectAgentConversationVM(
|
||||
detail: WorkspaceAgentSessionDetailViewModel,
|
||||
options: AgentConversationProjectionOptions = {}
|
||||
): AgentConversationVM {
|
||||
const rows: AgentTranscriptRowVM[] = [];
|
||||
const turns = detail.turns;
|
||||
const summariesByTurnId = new Map(
|
||||
projectAgentTurnSummaryRows(detail).map((summary) => [
|
||||
summary.turnId,
|
||||
summary
|
||||
])
|
||||
);
|
||||
|
||||
turns.forEach((turn) => {
|
||||
rows.push(
|
||||
...projectTurnConversationRows(turn, detail.session.workspaceId, {
|
||||
agentSessionId: detail.session.agentSessionId,
|
||||
avoidGroupingEdits: options.avoidGroupingEdits
|
||||
})
|
||||
);
|
||||
const summary = summariesByTurnId.get(turn.id);
|
||||
if (summary) rows.push(summary);
|
||||
});
|
||||
|
||||
const normalizedRows = dropCodexDiagnosticNotices(
|
||||
dropRedundantErrorWarningNotices(
|
||||
mergeAdjacentAssistantMessageRows(
|
||||
dropRedundantCompactFailureEchoRows(
|
||||
mergeAdjacentTransportRetryNoticeRows(rows)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
const timelineRows = insertGoalControlRows(normalizedRows, detail);
|
||||
const processing = projectAgentProcessingRow(detail, timelineRows);
|
||||
const projectedRows = projectAgentMessageFinalText(
|
||||
processing ? [...timelineRows, processing] : timelineRows,
|
||||
detail
|
||||
);
|
||||
|
||||
return {
|
||||
activity: detail.activity,
|
||||
workspaceRoot: detail.workspaceRoot,
|
||||
sourceDetail: detail,
|
||||
rows: projectedRows
|
||||
};
|
||||
}
|
||||
|
||||
function insertGoalControlRows(
|
||||
rows: readonly AgentTranscriptRowVM[],
|
||||
detail: WorkspaceAgentSessionDetailViewModel
|
||||
): AgentTranscriptRowVM[] {
|
||||
const controls = detail.goalControls ?? [];
|
||||
if (controls.length === 0) {
|
||||
return [...rows];
|
||||
}
|
||||
const merged = [...rows];
|
||||
for (const control of controls) {
|
||||
const row: AgentGoalControlRowVM = {
|
||||
kind: "goal-control",
|
||||
id: `goal-control:${control.id}`,
|
||||
turnId: null,
|
||||
action: control.action,
|
||||
body: control.body,
|
||||
occurredAtUnixMs: control.occurredAtUnixMs ?? null,
|
||||
sourceTimelineItems: control.sourceTimelineItems
|
||||
};
|
||||
const insertionIndex = merged.findIndex(
|
||||
(candidate) =>
|
||||
row.occurredAtUnixMs !== null &&
|
||||
candidate.occurredAtUnixMs !== null &&
|
||||
candidate.occurredAtUnixMs > row.occurredAtUnixMs
|
||||
);
|
||||
if (insertionIndex < 0) {
|
||||
merged.push(row);
|
||||
} else {
|
||||
merged.splice(insertionIndex, 0, row);
|
||||
}
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
function dropRedundantCompactFailureEchoRows(
|
||||
rows: readonly AgentTranscriptRowVM[]
|
||||
): AgentTranscriptRowVM[] {
|
||||
const filtered: AgentTranscriptRowVM[] = [];
|
||||
for (const row of rows) {
|
||||
const previous = filtered.at(-1);
|
||||
if (
|
||||
isSingleCompactFailureNoticeRow(previous) &&
|
||||
isSinglePlainAssistantMessageRow(row) &&
|
||||
previous.turnId === row.turnId &&
|
||||
previous.messages[0]?.systemNotice?.detail?.trim() ===
|
||||
row.messages[0]?.body.trim()
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
filtered.push(row);
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
|
||||
function isSingleCompactFailureNoticeRow(
|
||||
row: AgentTranscriptRowVM | undefined
|
||||
): row is AgentMessageRowVM {
|
||||
if (
|
||||
!row ||
|
||||
row.kind !== "message" ||
|
||||
row.speaker !== "assistant" ||
|
||||
row.thinking.length > 0 ||
|
||||
row.messages.length !== 1
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const notice = row.messages[0]?.systemNotice;
|
||||
return (
|
||||
notice?.command === "compact" &&
|
||||
notice.commandStatus === "failed" &&
|
||||
Boolean(notice.detail?.trim())
|
||||
);
|
||||
}
|
||||
|
||||
function isSinglePlainAssistantMessageRow(
|
||||
row: AgentTranscriptRowVM
|
||||
): row is AgentMessageRowVM {
|
||||
return (
|
||||
row.kind === "message" &&
|
||||
row.speaker === "assistant" &&
|
||||
row.thinking.length === 0 &&
|
||||
row.messages.length === 1 &&
|
||||
!row.messages[0]?.visibleError &&
|
||||
!row.messages[0]?.systemNotice
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Latest plain assistant reply in the projected transcript (newest first).
|
||||
* Used as the optional consult context; error cards, system notices, plan and
|
||||
* collaboration cards are skipped so only ordinary reply text qualifies.
|
||||
*/
|
||||
export function latestAssistantMessageText(
|
||||
conversation: Pick<AgentConversationVM, "rows"> | null | undefined
|
||||
): string | null {
|
||||
if (!conversation) {
|
||||
return null;
|
||||
}
|
||||
for (let rowIndex = conversation.rows.length - 1; rowIndex >= 0; rowIndex--) {
|
||||
const row = conversation.rows[rowIndex];
|
||||
if (row?.kind !== "message" || row.speaker !== "assistant") {
|
||||
continue;
|
||||
}
|
||||
for (
|
||||
let messageIndex = row.messages.length - 1;
|
||||
messageIndex >= 0;
|
||||
messageIndex--
|
||||
) {
|
||||
const message = row.messages[messageIndex];
|
||||
if (!message || message.visibleError || message.systemNotice) {
|
||||
continue;
|
||||
}
|
||||
if (message.contentKind && message.contentKind !== "text") {
|
||||
continue;
|
||||
}
|
||||
const body = message.body.trim();
|
||||
if (body) {
|
||||
return body;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function reconcileProjectedAgentConversationVM(
|
||||
previous: AgentConversationVM | null | undefined,
|
||||
next: AgentConversationVM
|
||||
): AgentConversationVM {
|
||||
if (!previous) {
|
||||
return next;
|
||||
}
|
||||
|
||||
const previousRowsById = new Map(previous.rows.map((row) => [row.id, row]));
|
||||
let reusedRowCount = 0;
|
||||
const rows = next.rows.map((row) => {
|
||||
const previousRow = previousRowsById.get(row.id);
|
||||
if (previousRow && equivalentTranscriptRowForRender(previousRow, row)) {
|
||||
reusedRowCount += 1;
|
||||
return previousRow;
|
||||
}
|
||||
return row;
|
||||
});
|
||||
const rowsArrayReused =
|
||||
reusedRowCount === next.rows.length &&
|
||||
next.rows.length === previous.rows.length &&
|
||||
next.rows.every((_row, index) => rows[index] === previous.rows[index]);
|
||||
|
||||
return {
|
||||
...next,
|
||||
rows: rowsArrayReused ? previous.rows : rows
|
||||
};
|
||||
}
|
||||
|
||||
function equivalentTranscriptRowForRender(
|
||||
previous: AgentTranscriptRowVM,
|
||||
next: AgentTranscriptRowVM
|
||||
): boolean {
|
||||
return equivalentValueIgnoringKeys(
|
||||
previous,
|
||||
next,
|
||||
RENDER_IRRELEVANT_TRANSCRIPT_ROW_FIELDS
|
||||
);
|
||||
}
|
||||
|
||||
function equivalentValueIgnoringKeys(
|
||||
left: unknown,
|
||||
right: unknown,
|
||||
ignoredKeys: ReadonlySet<string>
|
||||
): boolean {
|
||||
if (Object.is(left, right)) {
|
||||
return true;
|
||||
}
|
||||
if (typeof left !== typeof right || left === null || right === null) {
|
||||
return false;
|
||||
}
|
||||
if (Array.isArray(left) || Array.isArray(right)) {
|
||||
return (
|
||||
Array.isArray(left) &&
|
||||
Array.isArray(right) &&
|
||||
left.length === right.length &&
|
||||
left.every((value, index) =>
|
||||
equivalentValueIgnoringKeys(value, right[index], ignoredKeys)
|
||||
)
|
||||
);
|
||||
}
|
||||
if (typeof left !== "object" || typeof right !== "object") {
|
||||
return false;
|
||||
}
|
||||
const leftRecord = left as Record<string, unknown>;
|
||||
const rightRecord = right as Record<string, unknown>;
|
||||
const leftKeys = Object.keys(leftRecord).filter(
|
||||
(key) => !ignoredKeys.has(key)
|
||||
);
|
||||
const rightKeys = Object.keys(rightRecord).filter(
|
||||
(key) => !ignoredKeys.has(key)
|
||||
);
|
||||
return (
|
||||
leftKeys.length === rightKeys.length &&
|
||||
leftKeys.every(
|
||||
(key) =>
|
||||
Object.prototype.hasOwnProperty.call(rightRecord, key) &&
|
||||
equivalentValueIgnoringKeys(
|
||||
leftRecord[key],
|
||||
rightRecord[key],
|
||||
ignoredKeys
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function mergeAdjacentAssistantMessageRows(
|
||||
rows: readonly AgentTranscriptRowVM[]
|
||||
): AgentTranscriptRowVM[] {
|
||||
const merged: AgentTranscriptRowVM[] = [];
|
||||
for (const row of rows) {
|
||||
const previous = merged.at(-1);
|
||||
if (
|
||||
isMergeableAssistantMessageRow(previous) &&
|
||||
canMergeAdjacentAssistantMessageRows(previous, row)
|
||||
) {
|
||||
const lastMessage = previous.messages.at(-1);
|
||||
const nextMessage = row.messages[0];
|
||||
if (lastMessage && nextMessage) {
|
||||
lastMessage.body = joinAdjacentAssistantMessageBodies(
|
||||
lastMessage.body,
|
||||
nextMessage.body,
|
||||
nextMessage.statusKind
|
||||
);
|
||||
lastMessage.statusKind =
|
||||
nextMessage.statusKind ??
|
||||
(lastMessage.statusKind === "working" ||
|
||||
lastMessage.statusKind === "waiting"
|
||||
? null
|
||||
: lastMessage.statusKind) ??
|
||||
null;
|
||||
lastMessage.occurredAtUnixMs =
|
||||
nextMessage.occurredAtUnixMs ?? lastMessage.occurredAtUnixMs;
|
||||
}
|
||||
if (row.messages.length > 1) {
|
||||
previous.messages.push(...row.messages.slice(1));
|
||||
}
|
||||
previous.occurredAtUnixMs =
|
||||
row.occurredAtUnixMs ?? previous.occurredAtUnixMs;
|
||||
continue;
|
||||
}
|
||||
merged.push(row);
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
/**
|
||||
* Join adjacent assistant bodies without fusing independent Markdown blocks.
|
||||
* Active streaming fragments remain byte-for-byte continuations, while a
|
||||
* settled fragment gains a line boundary when neither side provides one.
|
||||
*/
|
||||
function joinAdjacentAssistantMessageBodies(
|
||||
previousBody: string,
|
||||
nextBody: string,
|
||||
nextStatusKind: AgentMessageContentVM["statusKind"]
|
||||
): string {
|
||||
if (nextStatusKind === "working" || nextStatusKind === "waiting") {
|
||||
return previousBody + nextBody;
|
||||
}
|
||||
if (
|
||||
previousBody.endsWith("\n") ||
|
||||
nextBody.startsWith("\n") ||
|
||||
nextBody.startsWith("\r")
|
||||
) {
|
||||
return previousBody + nextBody;
|
||||
}
|
||||
return `${previousBody}\n${nextBody}`;
|
||||
}
|
||||
|
||||
function dropCodexDiagnosticNotices(
|
||||
rows: readonly AgentTranscriptRowVM[]
|
||||
): AgentTranscriptRowVM[] {
|
||||
const filteredRows: AgentTranscriptRowVM[] = [];
|
||||
for (const row of rows) {
|
||||
if (row.kind !== "message" || row.speaker !== "assistant") {
|
||||
filteredRows.push(row);
|
||||
continue;
|
||||
}
|
||||
const messages = row.messages.filter(
|
||||
(message) => !isCodexDiagnosticNotice(message)
|
||||
);
|
||||
if (messages.length === 0 && row.thinking.length === 0) {
|
||||
continue;
|
||||
}
|
||||
if (messages.length === row.messages.length) {
|
||||
filteredRows.push(row);
|
||||
continue;
|
||||
}
|
||||
filteredRows.push({ ...row, messages });
|
||||
}
|
||||
return filteredRows;
|
||||
}
|
||||
|
||||
function isCodexDiagnosticNotice(message: AgentMessageContentVM): boolean {
|
||||
const notice = message.systemNotice;
|
||||
if (!notice || notice.noticeKind !== "warning") {
|
||||
return false;
|
||||
}
|
||||
const text = [notice.title, notice.detail, message.body]
|
||||
.filter(Boolean)
|
||||
.join("\n")
|
||||
.toLowerCase();
|
||||
if (CODEX_SKILLS_CONTEXT_BUDGET_NOTICE_PATTERN.test(text)) {
|
||||
// Persisted skill notices may omit source, but an explicitly non-runtime
|
||||
// notice must remain visible even when its text matches this diagnostic.
|
||||
return !notice.source || notice.source === "runtime";
|
||||
}
|
||||
return (
|
||||
notice.source === "runtime" &&
|
||||
text.includes(CODEX_MODEL_METADATA_FALLBACK_NOTICE_FRAGMENT)
|
||||
);
|
||||
}
|
||||
|
||||
function dropRedundantErrorWarningNotices(
|
||||
rows: readonly AgentTranscriptRowVM[]
|
||||
): AgentTranscriptRowVM[] {
|
||||
const turnsWithVisibleError = new Set<string>();
|
||||
for (const row of rows) {
|
||||
if (row.kind !== "message" || row.speaker !== "assistant") {
|
||||
continue;
|
||||
}
|
||||
for (const message of row.messages) {
|
||||
if (message.visibleError) {
|
||||
turnsWithVisibleError.add(row.turnId);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (turnsWithVisibleError.size === 0) {
|
||||
return [...rows];
|
||||
}
|
||||
|
||||
const filteredRows: AgentTranscriptRowVM[] = [];
|
||||
for (const row of rows) {
|
||||
if (row.kind !== "message" || row.speaker !== "assistant") {
|
||||
filteredRows.push(row);
|
||||
continue;
|
||||
}
|
||||
if (!turnsWithVisibleError.has(row.turnId)) {
|
||||
filteredRows.push(row);
|
||||
continue;
|
||||
}
|
||||
const messages = row.messages.filter(
|
||||
(message) => !isRedundantErrorWarningNotice(message)
|
||||
);
|
||||
if (messages.length === 0) {
|
||||
continue;
|
||||
}
|
||||
if (messages.length === row.messages.length) {
|
||||
filteredRows.push(row);
|
||||
continue;
|
||||
}
|
||||
filteredRows.push({ ...row, messages });
|
||||
}
|
||||
return filteredRows;
|
||||
}
|
||||
|
||||
function isRedundantErrorWarningNotice(
|
||||
message: AgentMessageContentVM
|
||||
): boolean {
|
||||
const notice = message.systemNotice;
|
||||
if (!notice || notice.noticeKind !== "warning") {
|
||||
return false;
|
||||
}
|
||||
const title = notice.title?.trim() ?? "";
|
||||
return title === "Codex reported an error.";
|
||||
}
|
||||
|
||||
function mergeAdjacentTransportRetryNoticeRows(
|
||||
rows: readonly AgentTranscriptRowVM[]
|
||||
): AgentTranscriptRowVM[] {
|
||||
const merged: AgentTranscriptRowVM[] = [];
|
||||
for (const row of rows) {
|
||||
const previous = merged.at(-1);
|
||||
if (
|
||||
isSingleTransportRetryNoticeRow(previous) &&
|
||||
isSingleTransportRetryNoticeRow(row) &&
|
||||
previous.turnId === row.turnId
|
||||
) {
|
||||
const previousMessage = previous.messages[0];
|
||||
const nextMessage = row.messages[0];
|
||||
if (!previousMessage || !nextMessage) {
|
||||
merged.push(row);
|
||||
continue;
|
||||
}
|
||||
const sourceTimelineItems = mergeSourceTimelineItems(
|
||||
previousMessage.sourceTimelineItems,
|
||||
nextMessage.sourceTimelineItems
|
||||
);
|
||||
previous.messages[0] = {
|
||||
...previousMessage,
|
||||
body: nextMessage.body || previousMessage.body,
|
||||
systemNotice: nextMessage.systemNotice ?? previousMessage.systemNotice,
|
||||
statusKind:
|
||||
nextMessage.statusKind ?? previousMessage.statusKind ?? null,
|
||||
occurredAtUnixMs:
|
||||
nextMessage.occurredAtUnixMs ?? previousMessage.occurredAtUnixMs,
|
||||
...(sourceTimelineItems ? { sourceTimelineItems } : {})
|
||||
};
|
||||
previous.occurredAtUnixMs =
|
||||
row.occurredAtUnixMs ?? previous.occurredAtUnixMs;
|
||||
continue;
|
||||
}
|
||||
merged.push(row);
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
function isSingleTransportRetryNoticeRow(
|
||||
row: AgentTranscriptRowVM | undefined
|
||||
): row is AgentMessageRowVM {
|
||||
if (
|
||||
!row ||
|
||||
row.kind !== "message" ||
|
||||
row.speaker !== "assistant" ||
|
||||
row.thinking.length > 0 ||
|
||||
row.messages.length !== 1
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return row.messages[0]?.systemNotice?.noticeKind === "transport_retry";
|
||||
}
|
||||
|
||||
function mergeSourceTimelineItems(
|
||||
previous: AgentMessageContentVM["sourceTimelineItems"],
|
||||
next: AgentMessageContentVM["sourceTimelineItems"]
|
||||
): AgentMessageContentVM["sourceTimelineItems"] {
|
||||
if (!previous || previous.length === 0) {
|
||||
return next;
|
||||
}
|
||||
if (!next || next.length === 0) {
|
||||
return previous;
|
||||
}
|
||||
return [...previous, ...next];
|
||||
}
|
||||
|
||||
function isMergeableAssistantMessageRow(
|
||||
row: AgentTranscriptRowVM | undefined
|
||||
): row is AgentMessageRowVM {
|
||||
return Boolean(
|
||||
row &&
|
||||
row.kind === "message" &&
|
||||
row.speaker === "assistant" &&
|
||||
row.thinking.length === 0 &&
|
||||
!row.messages.some(isSpecialAssistantMessage)
|
||||
);
|
||||
}
|
||||
|
||||
function canMergeAdjacentAssistantMessageRows(
|
||||
previous: AgentMessageRowVM,
|
||||
next: AgentTranscriptRowVM
|
||||
): next is AgentMessageRowVM {
|
||||
return (
|
||||
next.kind === "message" &&
|
||||
next.speaker === "assistant" &&
|
||||
next.thinking.length === 0 &&
|
||||
previous.turnId === next.turnId &&
|
||||
!next.messages.some(isSpecialAssistantMessage)
|
||||
);
|
||||
}
|
||||
|
||||
function isSpecialAssistantMessage(message: {
|
||||
visibleError?: unknown;
|
||||
systemNotice?: unknown;
|
||||
contentKind?: string;
|
||||
}): boolean {
|
||||
return Boolean(
|
||||
message.visibleError ||
|
||||
message.systemNotice ||
|
||||
message.contentKind === "plan" ||
|
||||
message.contentKind === "collaboration" ||
|
||||
message.contentKind === "tutti-plan-issue-link"
|
||||
);
|
||||
}
|
||||
|
||||
function projectTurnConversationRows(
|
||||
turn: WorkspaceAgentSessionDetailTurn,
|
||||
workspaceId: string | null | undefined,
|
||||
options: {
|
||||
agentSessionId: string;
|
||||
avoidGroupingEdits?: boolean;
|
||||
}
|
||||
): AgentTranscriptRowVM[] {
|
||||
const sequence = buildAgentTurnSequenceItems(turn, workspaceId);
|
||||
const { groups, groupedIndices, suppressedIndices } = computeAgentToolGroups(
|
||||
sequence,
|
||||
options
|
||||
);
|
||||
const skippedIndices = new Set([...groupedIndices, ...suppressedIndices]);
|
||||
return promoteGeneratedImageRows(
|
||||
projectTurnRows(
|
||||
sequence,
|
||||
groups,
|
||||
skippedIndices,
|
||||
turn.id,
|
||||
options.agentSessionId
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function promoteGeneratedImageRows(
|
||||
rows: readonly AgentTranscriptRowVM[]
|
||||
): AgentTranscriptRowVM[] {
|
||||
const promoted: AgentTranscriptRowVM[] = [];
|
||||
for (const row of rows) {
|
||||
promoted.push(row);
|
||||
if (row.kind !== "tool-group") {
|
||||
continue;
|
||||
}
|
||||
for (const call of row.calls) {
|
||||
const artifact = projectGeneratedImageRow(call, row.turnId);
|
||||
if (artifact) {
|
||||
promoted.push(artifact);
|
||||
}
|
||||
}
|
||||
}
|
||||
return dropDuplicateGeneratedImageMarkdown(promoted);
|
||||
}
|
||||
|
||||
function dropDuplicateGeneratedImageMarkdown(
|
||||
rows: readonly AgentTranscriptRowVM[]
|
||||
): AgentTranscriptRowVM[] {
|
||||
const generatedImagesByTurn = new Map<string, Set<string>>();
|
||||
for (const row of rows) {
|
||||
if (row.kind !== "generated-image") {
|
||||
continue;
|
||||
}
|
||||
const reference = normalizeGeneratedImageReference(row.uri);
|
||||
if (!reference) {
|
||||
continue;
|
||||
}
|
||||
const references = generatedImagesByTurn.get(row.turnId) ?? new Set();
|
||||
references.add(reference);
|
||||
generatedImagesByTurn.set(row.turnId, references);
|
||||
}
|
||||
if (generatedImagesByTurn.size === 0) {
|
||||
return [...rows];
|
||||
}
|
||||
|
||||
const filtered: AgentTranscriptRowVM[] = [];
|
||||
for (const row of rows) {
|
||||
if (row.kind !== "message" || row.speaker !== "assistant") {
|
||||
filtered.push(row);
|
||||
continue;
|
||||
}
|
||||
const generatedImages = generatedImagesByTurn.get(row.turnId);
|
||||
if (!generatedImages) {
|
||||
filtered.push(row);
|
||||
continue;
|
||||
}
|
||||
let changed = false;
|
||||
const messages = row.messages.flatMap((message) => {
|
||||
const body = removeGeneratedImageMarkdown(message.body, generatedImages);
|
||||
if (body === message.body) {
|
||||
return [message];
|
||||
}
|
||||
changed = true;
|
||||
return body ? [{ ...message, body }] : [];
|
||||
});
|
||||
if (messages.length === 0 && row.thinking.length === 0) {
|
||||
continue;
|
||||
}
|
||||
filtered.push(changed ? { ...row, messages } : row);
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
|
||||
function removeGeneratedImageMarkdown(
|
||||
body: string,
|
||||
generatedImages: ReadonlySet<string>
|
||||
): string {
|
||||
let removed = false;
|
||||
const next = body.replace(
|
||||
MARKDOWN_IMAGE_PATTERN,
|
||||
(
|
||||
match,
|
||||
angleReference: string | undefined,
|
||||
plainReference: string | undefined
|
||||
) => {
|
||||
const reference = normalizeGeneratedImageReference(
|
||||
angleReference ?? plainReference ?? ""
|
||||
);
|
||||
if (!reference || !generatedImages.has(reference)) {
|
||||
return match;
|
||||
}
|
||||
removed = true;
|
||||
return "";
|
||||
}
|
||||
);
|
||||
return removed
|
||||
? next
|
||||
.replace(/[ \t]+\n/g, "\n")
|
||||
.replace(/\n{3,}/g, "\n\n")
|
||||
.trim()
|
||||
: body;
|
||||
}
|
||||
|
||||
function normalizeGeneratedImageReference(reference: string): string | null {
|
||||
let normalized = reference.trim();
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
if (/^file:\/\//i.test(normalized)) {
|
||||
if (!URL.canParse(normalized)) {
|
||||
return null;
|
||||
}
|
||||
normalized = new URL(normalized).pathname;
|
||||
} else if (/^[a-z][a-z\d+.-]*:\/\//i.test(normalized)) {
|
||||
return URL.canParse(normalized) ? new URL(normalized).href : normalized;
|
||||
}
|
||||
normalized = decodePercentEncodedReference(normalized);
|
||||
normalized = normalized.replaceAll("\\", "/");
|
||||
const prefix = normalized.startsWith("/") ? "/" : "";
|
||||
const parts: string[] = [];
|
||||
for (const part of normalized.split("/")) {
|
||||
if (!part || part === ".") {
|
||||
continue;
|
||||
}
|
||||
if (part === ".." && parts.length > 0) {
|
||||
parts.pop();
|
||||
continue;
|
||||
}
|
||||
parts.push(part);
|
||||
}
|
||||
const collapsed = `${prefix}${parts.join("/")}`;
|
||||
return /^[a-z]:\//i.test(collapsed) ? collapsed.toLowerCase() : collapsed;
|
||||
}
|
||||
|
||||
function decodePercentEncodedReference(reference: string): string {
|
||||
return reference.replace(/(?:%[a-f\d]{2})+/gi, (encodedRun) => {
|
||||
const bytes = encodedRun
|
||||
.slice(1)
|
||||
.split("%")
|
||||
.map((hex) => Number.parseInt(hex, 16));
|
||||
return new TextDecoder().decode(Uint8Array.from(bytes));
|
||||
});
|
||||
}
|
||||
|
||||
function projectGeneratedImageRow(
|
||||
call: AgentToolCallVM,
|
||||
turnId: string
|
||||
): AgentGeneratedImageRowVM | null {
|
||||
if (
|
||||
call.rendererKind !== "image-generation" ||
|
||||
call.statusKind !== "completed"
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const image = extractImageGenerationPreview({
|
||||
toolName: call.toolName,
|
||||
displayName: call.name,
|
||||
outputSavedPath: call.output?.savedPath,
|
||||
outputSavedPaths: call.output?.savedPaths,
|
||||
outputMimeType: call.output?.imageMimeType,
|
||||
outputText: call.output?.text,
|
||||
inputPrompt: call.input?.prompt,
|
||||
payloadInputPrompt:
|
||||
call.payload?.input &&
|
||||
typeof call.payload.input === "object" &&
|
||||
!Array.isArray(call.payload.input)
|
||||
? (call.payload.input as Record<string, unknown>).prompt
|
||||
: null
|
||||
});
|
||||
if (!image.imageUri) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
kind: "generated-image",
|
||||
id: `generated-image:${call.id}`,
|
||||
turnId,
|
||||
sourceCallId: call.id,
|
||||
uri: image.imageUri,
|
||||
mimeType: image.mimeType,
|
||||
prompt: image.prompt,
|
||||
occurredAtUnixMs: call.occurredAtUnixMs
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user