源码快照 @ 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:
@@ -0,0 +1,540 @@
|
||||
import type {
|
||||
AgentActivityMessage,
|
||||
AgentActivityNeedsAttentionItem
|
||||
} from "@tutti-os/agent-activity-core";
|
||||
import type { AgentConversationPromptVM } from "../shared/agentConversation/contracts/agentConversationVM";
|
||||
import { extractAgentMcpToolTarget } from "../shared/agentMcpToolTarget";
|
||||
import type { WorkspaceAgentActivityStatus } from "../shared/workspaceAgentActivityListViewModel";
|
||||
|
||||
export type WorkspaceAgentMessageCenterDigestPrimaryKind =
|
||||
| "input-required"
|
||||
| "error"
|
||||
| "outcome"
|
||||
| "progress"
|
||||
| "summary";
|
||||
|
||||
export interface WorkspaceAgentMessageCenterDigestPrimary {
|
||||
kind: WorkspaceAgentMessageCenterDigestPrimaryKind;
|
||||
summary: string;
|
||||
occurredAtUnixMs: number | null;
|
||||
}
|
||||
|
||||
export interface WorkspaceAgentMessageCenterDigest {
|
||||
primary: WorkspaceAgentMessageCenterDigestPrimary;
|
||||
}
|
||||
|
||||
export interface WorkspaceAgentMessageCenterDigestAgentSummary {
|
||||
summary: string;
|
||||
occurredAtUnixMs: number;
|
||||
}
|
||||
|
||||
export interface BuildWorkspaceAgentMessageCenterDigestInput {
|
||||
fallbackTitle: string;
|
||||
latestAgentMessage: WorkspaceAgentMessageCenterDigestAgentSummary | null;
|
||||
needsAttention: AgentActivityNeedsAttentionItem | null;
|
||||
pendingPrompt: AgentConversationPromptVM | null;
|
||||
status: WorkspaceAgentActivityStatus;
|
||||
}
|
||||
|
||||
interface DigestAgentMessageSummaryCacheEntry {
|
||||
kind: string;
|
||||
payload: AgentActivityMessage["payload"];
|
||||
role: string;
|
||||
status: string | null | undefined;
|
||||
summary: string;
|
||||
version: number;
|
||||
}
|
||||
|
||||
const digestAgentMessageSummaryByMessage = new WeakMap<
|
||||
AgentActivityMessage,
|
||||
DigestAgentMessageSummaryCacheEntry
|
||||
>();
|
||||
|
||||
export function buildWorkspaceAgentMessageCenterDigest(
|
||||
input: BuildWorkspaceAgentMessageCenterDigestInput
|
||||
): WorkspaceAgentMessageCenterDigest {
|
||||
const {
|
||||
fallbackTitle,
|
||||
latestAgentMessage,
|
||||
needsAttention,
|
||||
pendingPrompt,
|
||||
status
|
||||
} = input;
|
||||
const fallbackSummary = firstNonEmptyString(
|
||||
latestAgentMessage?.summary ?? null,
|
||||
needsAttention?.summary ?? null,
|
||||
pendingPromptSummary(pendingPrompt),
|
||||
fallbackTitle
|
||||
);
|
||||
if (pendingPrompt || needsAttention) {
|
||||
return {
|
||||
primary: {
|
||||
kind: "input-required",
|
||||
summary: firstNonEmptyString(
|
||||
pendingPromptSummary(pendingPrompt),
|
||||
needsAttention?.summary ?? null,
|
||||
fallbackSummary
|
||||
),
|
||||
occurredAtUnixMs:
|
||||
needsAttention?.occurredAtUnixMs ??
|
||||
promptTimeUnixMs(pendingPrompt) ??
|
||||
latestAgentMessage?.occurredAtUnixMs ??
|
||||
null
|
||||
}
|
||||
};
|
||||
}
|
||||
if (status === "failed") {
|
||||
return {
|
||||
primary: {
|
||||
kind: "error",
|
||||
summary: fallbackSummary,
|
||||
occurredAtUnixMs: latestAgentMessage?.occurredAtUnixMs ?? null
|
||||
}
|
||||
};
|
||||
}
|
||||
if (status === "completed" || status === "canceled" || status === "idle") {
|
||||
return {
|
||||
primary: {
|
||||
kind: "outcome",
|
||||
summary: fallbackSummary,
|
||||
occurredAtUnixMs: latestAgentMessage?.occurredAtUnixMs ?? null
|
||||
}
|
||||
};
|
||||
}
|
||||
if (status === "working") {
|
||||
return {
|
||||
primary: {
|
||||
kind: "progress",
|
||||
summary: fallbackSummary,
|
||||
occurredAtUnixMs: latestAgentMessage?.occurredAtUnixMs ?? null
|
||||
}
|
||||
};
|
||||
}
|
||||
return {
|
||||
primary: {
|
||||
kind: "summary",
|
||||
summary: fallbackSummary,
|
||||
occurredAtUnixMs: latestAgentMessage?.occurredAtUnixMs ?? null
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveWorkspaceAgentMessageCenterDigestAgentMessageSummary(
|
||||
message: AgentActivityMessage
|
||||
): string {
|
||||
const cached = digestAgentMessageSummaryByMessage.get(message);
|
||||
if (
|
||||
cached &&
|
||||
cached.role === message.role &&
|
||||
cached.kind === message.kind &&
|
||||
cached.status === message.status &&
|
||||
cached.payload === message.payload &&
|
||||
cached.version === message.version
|
||||
) {
|
||||
return cached.summary;
|
||||
}
|
||||
if (
|
||||
!isAgentMessageRole(message.role) ||
|
||||
isReasoningMessageKind(message.kind)
|
||||
) {
|
||||
digestAgentMessageSummaryByMessage.set(message, {
|
||||
kind: message.kind,
|
||||
payload: message.payload,
|
||||
role: message.role,
|
||||
status: message.status,
|
||||
summary: "",
|
||||
version: message.version
|
||||
});
|
||||
return "";
|
||||
}
|
||||
const summary = meaningfulMessageSummary(message);
|
||||
digestAgentMessageSummaryByMessage.set(message, {
|
||||
kind: message.kind,
|
||||
payload: message.payload,
|
||||
role: message.role,
|
||||
status: message.status,
|
||||
summary,
|
||||
version: message.version
|
||||
});
|
||||
return summary;
|
||||
}
|
||||
|
||||
function pendingPromptSummary(
|
||||
prompt: AgentConversationPromptVM | null
|
||||
): string | null {
|
||||
if (!prompt) {
|
||||
return null;
|
||||
}
|
||||
switch (prompt.kind) {
|
||||
case "ask-user":
|
||||
return firstNonEmptyString(
|
||||
prompt.title,
|
||||
prompt.questions[0]?.question ?? null
|
||||
);
|
||||
case "approval":
|
||||
case "exit-plan":
|
||||
case "plan-implementation":
|
||||
return prompt.title;
|
||||
}
|
||||
}
|
||||
|
||||
function promptTimeUnixMs(
|
||||
prompt: AgentConversationPromptVM | null
|
||||
): number | null {
|
||||
if (!prompt || !("occurredAtUnixMs" in prompt)) {
|
||||
return null;
|
||||
}
|
||||
return positiveNumber(prompt.occurredAtUnixMs);
|
||||
}
|
||||
|
||||
function isAgentMessageRole(role: string): boolean {
|
||||
const normalized = role.trim().toLowerCase();
|
||||
return normalized === "assistant" || normalized === "agent";
|
||||
}
|
||||
|
||||
/**
|
||||
* Mirrors the same-named guard in workspaceAgentMessageCenterModel.ts: a
|
||||
* reasoning/thinking message carries an assistant-like `role` but
|
||||
* `kind: "reasoning"` and must never be summarized as a normal agent reply.
|
||||
*/
|
||||
function isReasoningMessageKind(kind: string): boolean {
|
||||
return kind.trim().toLowerCase() === "reasoning";
|
||||
}
|
||||
|
||||
type MessageSummarySource =
|
||||
| "payload.summary"
|
||||
| "payload.displayPrompt"
|
||||
| "payload.text"
|
||||
| "payload.content"
|
||||
| "payload.message"
|
||||
| "payload.body"
|
||||
| "tool.error"
|
||||
| "tool.output"
|
||||
| "tool.input"
|
||||
| "payload.title";
|
||||
|
||||
interface MessageSummaryCandidate {
|
||||
source: MessageSummarySource;
|
||||
summary: string;
|
||||
}
|
||||
|
||||
function meaningfulMessageSummary(message: AgentActivityMessage): string {
|
||||
const payload = recordValue(message.payload);
|
||||
const isToolMessage = isToolLikeMessage(message, payload);
|
||||
const candidates = messageSummaryCandidates(payload, isToolMessage);
|
||||
const structuralLabelTokens = isToolMessage
|
||||
? structuralToolLabelTokens(message, payload)
|
||||
: null;
|
||||
const hasToolDigestSignal =
|
||||
isToolMessage && candidates.some(isToolDigestSignalCandidate);
|
||||
|
||||
for (const candidate of candidates) {
|
||||
if (
|
||||
!isGenericToolLabelSummary(candidate, {
|
||||
hasToolDigestSignal,
|
||||
isToolMessage,
|
||||
structuralLabelTokens
|
||||
})
|
||||
) {
|
||||
return decorateMessageSummary(message, candidate, isToolMessage);
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function messageSummaryCandidates(
|
||||
payload: Record<string, unknown>,
|
||||
isToolMessage: boolean
|
||||
): MessageSummaryCandidate[] {
|
||||
const explicitCandidates = [
|
||||
summaryCandidate("payload.summary", payload.summary),
|
||||
summaryCandidate("payload.displayPrompt", payload.displayPrompt),
|
||||
summaryCandidate("payload.text", payload.text),
|
||||
summaryCandidate("payload.message", payload.message),
|
||||
summaryCandidate("payload.body", payload.body)
|
||||
];
|
||||
if (!isToolMessage) {
|
||||
return [
|
||||
...explicitCandidates,
|
||||
summaryCandidate(
|
||||
"payload.content",
|
||||
textFromContentValue(payload.content)
|
||||
),
|
||||
summaryCandidate("payload.title", payload.title)
|
||||
].filter((candidate): candidate is MessageSummaryCandidate =>
|
||||
Boolean(candidate)
|
||||
);
|
||||
}
|
||||
return [
|
||||
...explicitCandidates,
|
||||
toolErrorSummaryCandidate(payload),
|
||||
toolOutputSummaryCandidate(payload),
|
||||
toolInputSummaryCandidate(payload),
|
||||
summaryCandidate("payload.title", payload.title)
|
||||
].filter((candidate): candidate is MessageSummaryCandidate =>
|
||||
Boolean(candidate)
|
||||
);
|
||||
}
|
||||
|
||||
function summaryCandidate(
|
||||
source: MessageSummarySource,
|
||||
value: unknown
|
||||
): MessageSummaryCandidate | null {
|
||||
const summary = normalizeSummaryText(stringValue(value));
|
||||
return summary ? { source, summary } : null;
|
||||
}
|
||||
|
||||
function toolErrorSummaryCandidate(
|
||||
payload: Record<string, unknown>
|
||||
): MessageSummaryCandidate | null {
|
||||
const error = recordValue(payload.error);
|
||||
return summaryCandidate(
|
||||
"tool.error",
|
||||
firstNonEmptyString(
|
||||
stringValue(payload.error),
|
||||
stringValue(error.summary),
|
||||
stringValue(error.message),
|
||||
stringValue(error.text),
|
||||
stringValue(error.stdout),
|
||||
stringValue(error.stderr),
|
||||
stringValue(error.status)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function toolOutputSummaryCandidate(
|
||||
payload: Record<string, unknown>
|
||||
): MessageSummaryCandidate | null {
|
||||
const output = recordValue(payload.output);
|
||||
return summaryCandidate(
|
||||
"tool.output",
|
||||
firstNonEmptyString(
|
||||
stringValue(output.summary),
|
||||
stringValue(output.message),
|
||||
stringValue(output.text),
|
||||
stringValue(output.stdout),
|
||||
stringValue(output.stderr),
|
||||
stringValue(output.status)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function toolInputSummaryCandidate(
|
||||
payload: Record<string, unknown>
|
||||
): MessageSummaryCandidate | null {
|
||||
const rootInput = recordValue(payload.input);
|
||||
const nestedInput = recordValue(recordValue(rootInput.toolCall).input);
|
||||
const input = Object.keys(nestedInput).length > 0 ? nestedInput : rootInput;
|
||||
const action = recordValue(input.action);
|
||||
return summaryCandidate(
|
||||
"tool.input",
|
||||
firstNonEmptyString(
|
||||
stringValue(input.summary),
|
||||
stringValue(input.command),
|
||||
stringValue(input.cmd),
|
||||
stringValue(input.path),
|
||||
stringValue(input.file_path),
|
||||
stringValue(input.fileName),
|
||||
stringValue(input.filename),
|
||||
stringValue(input.query),
|
||||
stringArrayFirstValue(input.search_query),
|
||||
stringArrayFirstValue(input.searchQuery),
|
||||
stringValue(input.url),
|
||||
stringValue(input.pattern),
|
||||
stringValue(input.glob),
|
||||
stringValue(input.regex),
|
||||
stringValue(input.prompt),
|
||||
stringValue(action.query),
|
||||
stringValue(action.url)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function isGenericToolLabelSummary(
|
||||
candidate: MessageSummaryCandidate,
|
||||
context: {
|
||||
hasToolDigestSignal: boolean;
|
||||
isToolMessage: boolean;
|
||||
structuralLabelTokens: Set<string> | null;
|
||||
}
|
||||
): boolean {
|
||||
if (!context.isToolMessage) {
|
||||
return false;
|
||||
}
|
||||
const normalizedSummary = normalizeToken(candidate.summary);
|
||||
if (!normalizedSummary) {
|
||||
return true;
|
||||
}
|
||||
if (context.structuralLabelTokens?.has(normalizedSummary)) {
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
candidate.source === "payload.title" &&
|
||||
!context.hasToolDigestSignal &&
|
||||
looksLikeBareLabel(candidate.summary)
|
||||
);
|
||||
}
|
||||
|
||||
function decorateMessageSummary(
|
||||
message: AgentActivityMessage,
|
||||
candidate: MessageSummaryCandidate,
|
||||
isToolMessage: boolean
|
||||
): string {
|
||||
if (!isToolMessage) {
|
||||
return candidate.summary;
|
||||
}
|
||||
const mcpTarget = extractAgentMcpToolTarget({ payload: message.payload });
|
||||
if (!mcpTarget) {
|
||||
return candidate.summary;
|
||||
}
|
||||
if (candidate.summary.includes(mcpTarget.displayName)) {
|
||||
return candidate.summary;
|
||||
}
|
||||
return `${mcpTarget.displayName}: ${candidate.summary}`;
|
||||
}
|
||||
|
||||
function isToolLikeMessage(
|
||||
message: AgentActivityMessage,
|
||||
payload = recordValue(message.payload)
|
||||
): boolean {
|
||||
const normalizedKind = normalizeToken(message.kind);
|
||||
const normalizedType = [
|
||||
stringValue(payload.type),
|
||||
stringValue(payload.action),
|
||||
stringValue(payload.requestType)
|
||||
]
|
||||
.map((value) => normalizeToken(value ?? ""))
|
||||
.join(" ");
|
||||
return (
|
||||
includesAny(normalizedKind, ["call", "tool"]) ||
|
||||
includesAny(normalizedType, ["call", "tool"]) ||
|
||||
Boolean(
|
||||
stringValue(payload.callType) ||
|
||||
stringValue(payload.toolName) ||
|
||||
stringValue(payload.tool) ||
|
||||
hasRecordValue(payload.input) ||
|
||||
hasRecordValue(payload.output) ||
|
||||
hasRecordValue(payload.error)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function structuralToolLabelTokens(
|
||||
message: AgentActivityMessage,
|
||||
payload = recordValue(message.payload)
|
||||
): Set<string> {
|
||||
const metadata = recordValue(payload.metadata);
|
||||
return new Set(
|
||||
[
|
||||
message.kind,
|
||||
message.status,
|
||||
stringValue(payload.type),
|
||||
stringValue(payload.action),
|
||||
stringValue(payload.requestType),
|
||||
stringValue(payload.callType),
|
||||
stringValue(payload.toolName),
|
||||
stringValue(payload.name),
|
||||
stringValue(payload.tool),
|
||||
stringValue(metadata.callType),
|
||||
stringValue(metadata.toolName),
|
||||
stringValue(metadata.name),
|
||||
stringValue(metadata.tool)
|
||||
]
|
||||
.map((value) => normalizeToken(value ?? ""))
|
||||
.filter(Boolean)
|
||||
);
|
||||
}
|
||||
|
||||
function includesAny(value: string, needles: readonly string[]): boolean {
|
||||
return needles.some((needle) => value.includes(normalizeToken(needle)));
|
||||
}
|
||||
|
||||
function isToolDigestSignalCandidate(
|
||||
candidate: MessageSummaryCandidate
|
||||
): boolean {
|
||||
return (
|
||||
candidate.source === "tool.error" ||
|
||||
candidate.source === "tool.output" ||
|
||||
candidate.source === "tool.input"
|
||||
);
|
||||
}
|
||||
|
||||
function looksLikeBareLabel(value: string): boolean {
|
||||
const trimmed = value.trim();
|
||||
return (
|
||||
trimmed.length > 0 &&
|
||||
trimmed.length <= 40 &&
|
||||
!/\s/u.test(trimmed) &&
|
||||
!/[/:\\.[\]{}()=#?&]/u.test(trimmed)
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeToken(value: string): string {
|
||||
return value.replace(/[_\s.-]+/g, "").toLowerCase();
|
||||
}
|
||||
|
||||
function firstNonEmptyString(...values: Array<string | null>): string {
|
||||
return (
|
||||
values.find((value) => value !== null && value.trim().length > 0) ?? ""
|
||||
);
|
||||
}
|
||||
|
||||
function stringValue(value: unknown): string | null {
|
||||
return typeof value === "string" && value.trim() ? value.trim() : null;
|
||||
}
|
||||
|
||||
function stringArrayFirstValue(value: unknown): string | null {
|
||||
if (!Array.isArray(value)) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
value.map(stringValue).find((item): item is string => Boolean(item)) ?? null
|
||||
);
|
||||
}
|
||||
|
||||
function textFromContentValue(value: unknown): string | null {
|
||||
if (typeof value === "string") {
|
||||
return stringValue(value);
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return (
|
||||
value
|
||||
.map(textFromContentValue)
|
||||
.find((item): item is string => Boolean(item)) ?? null
|
||||
);
|
||||
}
|
||||
if (!value || typeof value !== "object") {
|
||||
return null;
|
||||
}
|
||||
const record = value as Record<string, unknown>;
|
||||
return firstNonEmptyString(
|
||||
stringValue(record.text),
|
||||
"content" in record ? textFromContentValue(record.content) : null
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeSummaryText(value: string | null): string | null {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
return value
|
||||
.replace(/^#{1,6}\s+error\s*\n+/iu, "")
|
||||
.replace(/\s+/gu, " ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function recordValue(value: unknown): Record<string, unknown> {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: {};
|
||||
}
|
||||
|
||||
function hasRecordValue(value: unknown): boolean {
|
||||
return Object.keys(recordValue(value)).length > 0;
|
||||
}
|
||||
|
||||
function positiveNumber(value: unknown): number | null {
|
||||
return typeof value === "number" && Number.isFinite(value) && value > 0
|
||||
? value
|
||||
: null;
|
||||
}
|
||||
Reference in New Issue
Block a user