Files
tutti/packages/agent/gui/shared/agentConversationTitleProjection.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

525 lines
15 KiB
TypeScript

import { AGENT_PROVIDER_LABEL } from "../contexts/settings/domain/agentSettings.providerMeta.ts";
import {
isPendingActivationViable,
type PendingActivationStatus,
type AgentActivityMessage,
type AgentPromptContentBlock
} from "@tutti-os/agent-activity-core";
import {
extractRichTextLinksFromContent,
extractRichTextMentionsFromContent,
removeRichTextMentionFromContent
} from "@tutti-os/ui-rich-text/core";
import { translateInUiLanguage } from "../i18n/runtime.ts";
import { resolveAgentGUIProviderCatalogIdentity } from "../providerIdentityCatalog.ts";
import type { AgentGUIProvider } from "../types.ts";
import type { WorkspaceAgentActivityTimelineItem } from "./workspaceAgentTimelineTypes.ts";
import { normalizeAgentTitleText } from "./utils/agentTitleText.ts";
export type AgentGUIResolvedProvider = AgentGUIProvider | "unknown";
export type AgentGUIConversationTitleFallback = "untitled-conversation" | null;
export type AgentGUIConversationTitleLeadingMentionKind =
| "agent"
| "app"
| "file"
| "session"
| "task";
const AGENT_GUI_UNRESOLVED_PROVIDER: AgentGUIResolvedProvider = "unknown";
const AGENT_GUI_MAX_OPTIMISTIC_TITLE_CODE_POINTS = 120;
const AGENT_GUI_TRUNCATED_TITLE_SUFFIX = "...";
export function isAgentGUIProviderUnresolved(
value: AgentGUIResolvedProvider
): value is "unknown" {
return value === AGENT_GUI_UNRESOLVED_PROVIDER;
}
export interface AgentGUIConversationTitleMessage {
id?: number | string | null;
messageId?: string | null;
version?: number | null;
role?: string | null;
kind?: string | null;
payload?: {
content?: unknown;
displayPrompt?: unknown;
text?: unknown;
} | null;
occurredAtUnixMs?: number | null;
completedAtUnixMs?: number | null;
startedAtUnixMs?: number | null;
}
export function normalizeAgentGUIProviderIdentity(
provider: string | null | undefined
): AgentGUIResolvedProvider {
const normalized = provider?.trim().toLowerCase() ?? "";
const catalogIdentity = resolveAgentGUIProviderCatalogIdentity(normalized);
if (catalogIdentity) {
return catalogIdentity.providerId as AgentGUIProvider;
}
if (!/^[a-z][a-z0-9._:-]{0,127}$/.test(normalized)) {
return "unknown";
}
return normalized as AgentGUIProvider;
}
function providerLabel(provider: AgentGUIProvider): string {
return (AGENT_PROVIDER_LABEL as Record<string, string>)[provider] ?? provider;
}
export function resolveAgentGUIProviderIdentity(input: {
sessionProvider?: string | null;
workspaceSessionProvider?: string | null;
conversationProvider?: string | null;
timelineItems?: readonly WorkspaceAgentActivityTimelineItem[];
}): AgentGUIResolvedProvider {
const candidates = [
input.sessionProvider,
input.workspaceSessionProvider,
input.conversationProvider,
timelineProviderHint(input.timelineItems ?? [])
];
for (const candidate of candidates) {
const normalized = normalizeAgentGUIProviderIdentity(candidate);
if (normalized !== "unknown") {
return normalized;
}
}
return "unknown";
}
export function resolveAgentGUIConversationTitle(
title: string | null | undefined
): {
title: string;
titleFallback: AgentGUIConversationTitleFallback;
} {
const normalizedTitle = stripAgentGUITitleTrailingPeriod(title?.trim() ?? "");
if (normalizedTitle) {
return {
title: normalizedTitle,
titleFallback: null
};
}
return {
title: "",
titleFallback: "untitled-conversation"
};
}
export function deriveAgentGUIOptimisticConversationTitle(
visiblePrompt: string | null | undefined
): string {
const normalizedTitle = normalizeAgentTitleText(visiblePrompt);
const codePoints = Array.from(normalizedTitle);
if (codePoints.length <= AGENT_GUI_MAX_OPTIMISTIC_TITLE_CODE_POINTS) {
return normalizedTitle;
}
return `${codePoints
.slice(
0,
AGENT_GUI_MAX_OPTIMISTIC_TITLE_CODE_POINTS -
AGENT_GUI_TRUNCATED_TITLE_SUFFIX.length
)
.join("")
.trimEnd()}${AGENT_GUI_TRUNCATED_TITLE_SUFFIX}`;
}
export function resolveAgentGUIConversationTitleDisplayPrompt(input: {
activation?: {
content: readonly AgentPromptContentBlock[];
displayPrompt?: string;
mode: "existing" | "new";
status: PendingActivationStatus;
} | null;
allowEmptyTitle?: boolean;
firstUserDisplayPrompt?: string | null;
messages?: readonly AgentActivityMessage[];
title: string | null;
}): string | null {
const prompt = resolveAgentGUIConversationTitlePrompt(input);
if (
!prompt ||
(extractRichTextMentionsFromContent(prompt).length === 0 &&
extractRichTextLinksFromContent(prompt).length === 0) ||
!agentGUITitleMatchesDerivedPrompt(
input.title,
prompt,
input.allowEmptyTitle
)
) {
return null;
}
// Titles are presentation text, not an interactive rich-text surface. Keep
// the mention label but discard its Markdown href before handing it to the
// header/rail consumers so mention SVGs cannot leak into the chrome.
return normalizeAgentTitleText(prompt);
}
export function resolveAgentGUIConversationBrowserFreeTitle(input: {
activation?: {
content: readonly AgentPromptContentBlock[];
displayPrompt?: string;
mode: "existing" | "new";
status: PendingActivationStatus;
} | null;
allowEmptyTitle?: boolean;
firstUserDisplayPrompt?: string | null;
messages?: readonly AgentActivityMessage[];
title: string | null;
}): string | null {
const prompt = resolveAgentGUIConversationTitlePrompt(input);
if (
!prompt ||
!agentGUITitleMatchesDerivedPrompt(
input.title,
prompt,
input.allowEmptyTitle
)
) {
return input.title;
}
const presentationPrompt = removeAgentGUIBrowserElementMentions(prompt);
return presentationPrompt === prompt
? input.title
: deriveAgentGUIOptimisticConversationTitle(presentationPrompt);
}
export function resolveAgentGUIConversationTitleLeadingMentionKind(
_displayPrompt: string | null | undefined
): AgentGUIConversationTitleLeadingMentionKind | null {
return null;
}
function resolveAgentGUIConversationTitlePrompt(input: {
activation?: {
content: readonly AgentPromptContentBlock[];
displayPrompt?: string;
mode: "existing" | "new";
status: PendingActivationStatus;
} | null;
firstUserDisplayPrompt?: string | null;
messages?: readonly AgentActivityMessage[];
}): string {
const activationPrompt =
input.activation?.mode === "new" &&
isPendingActivationViable(input.activation)
? agentGUIActivationPromptText(
input.activation.content,
input.activation.displayPrompt ?? null
)
: "";
return (
activationPrompt ||
input.firstUserDisplayPrompt?.trim() ||
resolveAgentGUIFirstUserMessageDisplayPrompt(input.messages ?? [])
);
}
function removeAgentGUIBrowserElementMentions(prompt: string): string {
return extractRichTextMentionsFromContent(prompt)
.filter((mention) => mention.providerId === "browser-element")
.reduce(
(content, mention) => removeRichTextMentionFromContent(content, mention),
prompt
);
}
export function resolveAgentGUIFirstUserMessageDisplayPrompt(
messages: readonly AgentActivityMessage[]
): string {
const message = messages.find(
(candidate) =>
candidate.role.trim().toLowerCase() === "user" &&
candidate.kind.trim().toLowerCase() === "text"
);
if (!message) {
return "";
}
return (
agentGUIStringValue(message.payload.displayPrompt) ||
agentGUIPromptTextFromUnknownContent(message.payload.content) ||
agentGUIStringValue(message.payload.text) ||
agentGUIStringValue(message.payload.content)
);
}
function agentGUIPromptTextFromUnknownContent(content: unknown): string {
if (!Array.isArray(content)) {
return "";
}
return content
.flatMap((value) => {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return [];
}
const block = value as Record<string, unknown>;
return block.type === "text" && typeof block.text === "string"
? [block.text.trim()]
: [];
})
.filter(Boolean)
.join("\n");
}
function agentGUIActivationPromptText(
content: readonly AgentPromptContentBlock[],
displayPrompt: string | null
): string {
const display = displayPrompt?.trim() ?? "";
if (display) {
return display;
}
return content
.filter((block) => block.type === "text")
.map((block) => block.text?.trim() ?? "")
.filter(Boolean)
.join("\n");
}
function agentGUITitleMatchesDerivedPrompt(
title: string | null,
prompt: string,
allowEmptyTitle = false
): boolean {
const canonicalTitle = title?.trim() ?? "";
if (!canonicalTitle) {
return allowEmptyTitle;
}
const derivedTitle = normalizeAgentTitleText(prompt);
if (canonicalTitle === derivedTitle) {
return true;
}
const runes = Array.from(derivedTitle);
const truncatedDerivedTitle =
runes.length > AGENT_GUI_MAX_OPTIMISTIC_TITLE_CODE_POINTS
? `${runes
.slice(
0,
AGENT_GUI_MAX_OPTIMISTIC_TITLE_CODE_POINTS -
AGENT_GUI_TRUNCATED_TITLE_SUFFIX.length
)
.join("")
.trim()}${AGENT_GUI_TRUNCATED_TITLE_SUFFIX}`
: derivedTitle;
return canonicalTitle === truncatedDerivedTitle;
}
function agentGUIStringValue(value: unknown): string {
return typeof value === "string" ? value.trim() : "";
}
export function resolveAgentGUIConversationDisplayTitle(
input: {
title: string;
titleFallback?: AgentGUIConversationTitleFallback;
},
untitledConversationLabel: string
): string {
if (input.title) {
return stripAgentGUITitleTrailingPeriod(input.title.trim());
}
if (input.titleFallback === "untitled-conversation") {
return stripAgentGUITitleTrailingPeriod(untitledConversationLabel);
}
return "";
}
export function resolveAgentGUIDockConversationTitle(input: {
provider: AgentGUIResolvedProvider;
title: string;
titleFallback?: AgentGUIConversationTitleFallback;
}): string | null {
return resolveAgentGUIExplicitConversationTitle(input);
}
export function resolveAgentGUIExplicitConversationTitle(input: {
provider: AgentGUIResolvedProvider;
title: string;
titleFallback?: AgentGUIConversationTitleFallback;
}): string | null {
if (input.titleFallback) {
return null;
}
const title = stripAgentGUITitleTrailingPeriod(input.title.trim());
if (!title) {
return null;
}
if (isAgentGUIUntitledTaskTitle(title)) {
return null;
}
if (input.provider !== "unknown" && title === providerLabel(input.provider)) {
return null;
}
return title;
}
export function resolveAgentGUIExplicitConversationTitleFromMessages(input: {
messages: readonly AgentGUIConversationTitleMessage[];
provider: AgentGUIResolvedProvider;
title: string | null | undefined;
}): string | null {
const explicitTitle = resolveAgentGUIExplicitConversationTitle({
provider: input.provider,
title: input.title?.trim() ?? ""
});
if (explicitTitle) {
return explicitTitle;
}
return resolveAgentGUIExplicitConversationTitle({
provider: input.provider,
title: firstAgentGUIUserMessageTitle(input.messages)
});
}
export function resolveAgentGUIProviderDisplayLabel(
provider: string | null | undefined,
fallbackAgentLabel: string,
exactAgentTargetLabel?: string | null
): string {
const normalizedAgentTargetLabel = exactAgentTargetLabel?.trim() ?? "";
if (normalizedAgentTargetLabel) {
return normalizedAgentTargetLabel;
}
const resolvedProvider = normalizeAgentGUIProviderIdentity(provider);
if (resolvedProvider === "unknown") {
return fallbackAgentLabel;
}
return providerLabel(resolvedProvider);
}
export function firstAgentGUIUserMessageTitle(
messages: readonly AgentGUIConversationTitleMessage[]
): string {
const userMessage = [...messages]
.filter(
(message) =>
messageRole(message) === "user" && messageText(message).length > 0
)
.sort(compareMessagesAscending)[0];
return userMessage ? messageText(userMessage) : "";
}
function messageRole(
message: AgentGUIConversationTitleMessage
): "user" | "agent" | null {
const role = message.role?.trim().toLowerCase();
if (role === "user") {
return "user";
}
if (role === "assistant" || role === "agent") {
return "agent";
}
const kind = message.kind?.trim().toLowerCase() ?? "";
if (kind === "message.user") {
return "user";
}
if (kind === "message.assistant" || kind === "message.agent") {
return "agent";
}
return null;
}
function messageText(message: AgentGUIConversationTitleMessage): string {
const payload = message.payload;
const displayPrompt =
typeof payload?.displayPrompt === "string" ? payload.displayPrompt : "";
const text = typeof payload?.text === "string" ? payload.text : "";
const content = typeof payload?.content === "string" ? payload.content : "";
return normalizeAgentTitleText(displayPrompt || text || content);
}
function compareMessagesAscending(
left: AgentGUIConversationTitleMessage,
right: AgentGUIConversationTitleMessage
): number {
const leftTime =
left.occurredAtUnixMs ??
left.completedAtUnixMs ??
left.startedAtUnixMs ??
0;
const rightTime =
right.occurredAtUnixMs ??
right.completedAtUnixMs ??
right.startedAtUnixMs ??
0;
const timeDiff = leftTime - rightTime;
if (timeDiff !== 0) {
return timeDiff;
}
const sequenceDiff =
messageSequence(left) - messageSequence(right) ||
(left.messageId ?? "").localeCompare(right.messageId ?? "");
return sequenceDiff;
}
function messageSequence(message: AgentGUIConversationTitleMessage): number {
const numericId =
typeof message.id === "number" && Number.isFinite(message.id)
? message.id
: 0;
return message.version ?? numericId;
}
function stripAgentGUITitleTrailingPeriod(title: string): string {
return title
.trimEnd()
.replace(/[.。]+$/u, "")
.trimEnd();
}
function isAgentGUIUntitledTaskTitle(title: string): boolean {
return localizedAgentGUIUntitledTaskLabels().has(compactTitleText(title));
}
function localizedAgentGUIUntitledTaskLabels(): Set<string> {
return new Set(
(["en", "zh-CN"] as const)
.map((language) =>
compactTitleText(
translateInUiLanguage(
language,
"agentHost.workspaceAgentsUntitledConversation"
)
)
)
.filter(Boolean)
);
}
function timelineProviderHint(
timelineItems: readonly WorkspaceAgentActivityTimelineItem[]
): string | null {
for (const item of timelineItems) {
if (isUserTimelineItem(item)) {
continue;
}
const normalized = normalizeAgentGUIProviderIdentity(item.actorId);
if (normalized !== "unknown") {
return normalized;
}
}
return null;
}
function isUserTimelineItem(item: WorkspaceAgentActivityTimelineItem): boolean {
const role = item.role?.trim().toLowerCase();
if (role === "user") {
return true;
}
const actorType = item.actorType.trim().toLowerCase();
if (actorType === "user") {
return true;
}
return item.itemType.trim().toLowerCase() === "message.user";
}
function compactTitleText(value: string): string {
return value.trim().replace(/\s+/g, " ");
}