Files
tutti/packages/agent/gui/agent-gui/agentGuiNode/controller/agentGuiController.draftMessageHelpers.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

486 lines
14 KiB
TypeScript

import type {
AgentActivityComposerOptions,
AgentActivityDisplayStatus,
AgentActivityMessage,
AgentActivitySession
} from "@tutti-os/agent-activity-core";
import { translate } from "../../../i18n/index";
import type {
AgentSessionComposerSettings,
AgentSessionReasoningEffort
} from "../../../shared/agentSessionTypes";
import type { AgentPromptContentBlock } from "../../../shared/contracts/dto";
import type {
AgentGUINodeData,
AgentGUIProvider,
AgentGUIAgentTarget
} from "../../../types";
import {
agentComposerDraftPreservingConnectors,
emptyAgentComposerDraft,
materializePastedTextInstructions
} from "../model/agentComposerDraft";
import { materializeAgentCustomMentionPromptText } from "../agentRichText/agentMentionMarkdown";
import { type AgentGUIConversationSummary } from "../model/agentGuiConversationModel";
import type {
AgentComposerDraft,
AgentGUIQueuedPromptVM,
SubmittedDraftSnapshot
} from "../model/agentGuiNodeTypes";
import {
isAgentComposerSessionDraftScope,
resolveAgentComposerDraftScopeKey
} from "../model/agentComposerDraftScope";
import {
normalizePermissionModeId,
readNodeDefaultDraftSettings,
resolveEffectiveComposerSettings
} from "./agentGuiController.composerHelpers";
import type { AgentGUIComposerTargetData } from "./agentGuiController.composerPresentation";
import {
normalizeOptionalText,
stringPayloadValue
} from "./agentGuiController.promptHelpers";
export {
normalizePermissionModeSemantic,
permissionConfigFromComposerOptions,
permissionModeDescription,
permissionModeLabel,
permissionModeOptions
} from "./agentGuiController.composerHelpers";
export {
agentGUIConversationDiagnosticDetails,
agentGUIRuntimeSessionDiagnosticDetails,
agentGUISessionStateDiagnosticDetails,
agentGUIToolCallStatusIsWaiting,
promptRequestId
} from "./agentGuiController.diagnostics";
export * from "./agentGuiController.errors";
export {
createAgentGUIConversationId,
normalizeOptionalPrompt,
normalizeOptionalText,
projectAgentGUIMessagesToTimelineItems,
recordValue,
stringPayloadValue
} from "./agentGuiController.promptHelpers";
export * from "./agentGuiController.providerHelpers";
export {
messageFromMessageUpdate,
normalizedPositiveNumber,
normalizeTimelineStatus,
timelineItemTime
} from "./agentGuiController.sessionHelpers";
export {
filterMessagesForDetailWindowOverlay,
maxFiniteMessageVersion,
minFiniteMessageVersion,
sessionHasRenderableMessages,
windowHasTurnMissingUserPrompt
} from "./useAgentConversationMessagePaging";
export interface AgentGUIOpenSessionRequest {
agentSessionId: string;
sequence: number;
}
export function createPendingOptimisticTurnId(clientSubmitId: string): string {
return `pending:${clientSubmitId}`;
}
export function composerSettingsFromPendingRecord(
value: Readonly<Record<string, unknown>>
): AgentSessionComposerSettings {
const settings: AgentSessionComposerSettings = {};
if (typeof value.model === "string" || value.model === null) {
settings.model = value.model;
}
if (
typeof value.reasoningEffort === "string" ||
value.reasoningEffort === null
) {
settings.reasoningEffort = value.reasoningEffort;
}
if (typeof value.speed === "string" || value.speed === null) {
settings.speed = value.speed;
}
for (const key of [
"codexSaverMode",
"rtkSaverMode",
"planMode",
"browserUse",
"computerUse"
] as const) {
if (typeof value[key] === "boolean") settings[key] = value[key];
}
if (
typeof value.permissionModeId === "string" ||
value.permissionModeId === null
) {
settings.permissionModeId = value.permissionModeId;
}
return settings;
}
export function normalizeAgentGUIOpenSessionRequest(
request: AgentGUIOpenSessionRequest | null | undefined
): AgentGUIOpenSessionRequest | null {
const agentSessionId = request?.agentSessionId.trim() ?? "";
if (!agentSessionId || typeof request?.sequence !== "number") {
return null;
}
return {
agentSessionId,
sequence: request.sequence
};
}
export function numberValue(value: unknown): number | null {
if (typeof value === "number" && Number.isFinite(value)) {
return value;
}
if (typeof value === "string" && value.trim() !== "") {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : null;
}
return null;
}
export function conversationBusyStatus(
status: AgentGUIConversationSummary["status"] | null
): boolean {
return status === "working" || status === "waiting";
}
export function resolveUnreadCompletionAfterStatusPatch(input: {
status: AgentGUIConversationSummary["status"];
hasUnreadCompletion: boolean | undefined;
}): boolean {
if (input.status === "completed" || input.status === "ready") {
return input.hasUnreadCompletion ?? false;
}
return false;
}
export function resolveUnreadCompletionKeyAfterStatusPatch(input: {
status: AgentGUIConversationSummary["status"];
currentKey: string | null | undefined;
nextKey: string | null | undefined;
hasUnreadCompletion: boolean | undefined;
}): string | null {
if (input.status === "completed" || input.nextKey) {
return input.currentKey ?? input.nextKey ?? null;
}
if (input.status === "ready" && input.hasUnreadCompletion === true) {
return input.currentKey ?? null;
}
return null;
}
export function agentActivityDisplayStatusBusy(
status: AgentActivityDisplayStatus | null | undefined
): boolean {
return status === "working" || status === "waiting";
}
export function conversationStatusFromAgentActivityDisplayStatus(
status: AgentActivityDisplayStatus | null | undefined
): AgentGUIConversationSummary["status"] | null {
return status === "idle" ? "ready" : (status ?? null);
}
export function agentActivitySessionHasLiveTurn(
session: AgentActivitySession | null | undefined
): boolean {
return Boolean(
session?.activeTurnId?.trim() ||
(session?.activeTurn && session.activeTurn.phase !== "settled")
);
}
export function reuseAgentActivityDisplayStatusesIfUnchanged(
previous: ReadonlyMap<string, AgentActivityDisplayStatus> | null,
next: Map<string, AgentActivityDisplayStatus>
): Map<string, AgentActivityDisplayStatus> {
if (!previous || previous.size !== next.size) {
return next;
}
for (const [sessionId, status] of next) {
if (previous.get(sessionId) !== status) {
return next;
}
}
return previous as Map<string, AgentActivityDisplayStatus>;
}
export const EMPTY_AGENT_COMPOSER_DRAFT = emptyAgentComposerDraft();
export const EMPTY_QUEUED_PROMPTS: readonly AgentGUIQueuedPromptVM[] =
Object.freeze([]);
export function areAgentComposerDraftsEqual(
left: AgentComposerDraft,
right: AgentComposerDraft
): boolean {
return areDraftValuesStructurallyEqual(left, right);
}
function areDraftValuesStructurallyEqual(
left: unknown,
right: unknown
): boolean {
if (Object.is(left, right)) return true;
if (Array.isArray(left) || Array.isArray(right)) {
return (
Array.isArray(left) &&
Array.isArray(right) &&
left.length === right.length &&
left.every((value, index) =>
areDraftValuesStructurallyEqual(value, right[index])
)
);
}
if (
left === null ||
right === null ||
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);
const rightKeys = Object.keys(rightRecord);
return (
leftKeys.length === rightKeys.length &&
leftKeys.every(
(key) =>
Object.hasOwn(rightRecord, key) &&
areDraftValuesStructurallyEqual(leftRecord[key], rightRecord[key])
)
);
}
// Upload completion replaces the local image bytes with a provider locator and
// updates presentation state. Those transitions do not mean the user edited
// the submitted image. Keep uploadError and unknown fields comparable so a
// failed upload or a future user-owned field still prevents clearing.
const IMAGE_SUBMISSION_TRANSIENT_FIELDS = new Set([
"attachmentId",
"data",
"path",
"previewUrl",
"uploading",
"url"
]);
function submissionComparableDraftBlock(
block: AgentComposerDraft[number]
): unknown {
if (block.type !== "image") {
return block;
}
return Object.fromEntries(
Object.entries(block).filter(
([key]) => !IMAGE_SUBMISSION_TRANSIENT_FIELDS.has(key)
)
);
}
function areAgentComposerDraftsEquivalentForSubmission(
left: AgentComposerDraft,
right: AgentComposerDraft
): boolean {
return (
left.length === right.length &&
left.every((block, index) => {
const other = right[index];
if (!other || block.type !== other.type) return false;
return areDraftValuesStructurallyEqual(
submissionComparableDraftBlock(block),
submissionComparableDraftBlock(other)
);
})
);
}
export function deleteSubmittedDraftSnapshotsForScopes(input: {
snapshots: Record<string, SubmittedDraftSnapshot>;
scopeKeys: ReadonlySet<string>;
targetAgentSessionIds?: ReadonlySet<string>;
}): void {
for (const [clientSubmitId, snapshot] of Object.entries(input.snapshots)) {
if (
input.scopeKeys.has(snapshot.sourceScopeKey) ||
(snapshot.targetAgentSessionId != null &&
input.targetAgentSessionIds?.has(snapshot.targetAgentSessionId))
) {
delete input.snapshots[clientSubmitId];
}
}
}
export function deleteUnacceptedSubmittedDraftSnapshot(input: {
snapshots: Record<string, SubmittedDraftSnapshot>;
clientSubmitId: string;
accepted: boolean;
queued: boolean;
}): void {
if (!input.accepted && !input.queued) {
delete input.snapshots[input.clientSubmitId];
}
}
export function readAgentComposerDraftContent(input: {
drafts: Record<string, AgentComposerDraft>;
}): AgentComposerDraft {
return (
input.drafts[resolveAgentComposerDraftScopeKey({})] ??
EMPTY_AGENT_COMPOSER_DRAFT
);
}
export function resolvePromptImageSelectedModel(input: {
activeConversationId: string | null;
activeSessionPermissionModeId?: string | null;
activeSessionRuntimeContext?: Record<string, unknown> | null;
activeSessionSettings: AgentSessionComposerSettings | null;
data: AgentGUINodeData;
defaultReasoningEffort: AgentSessionReasoningEffort | null;
draftSettingsBySessionId: Record<string, AgentSessionComposerSettings>;
providerComposerOptions: AgentActivityComposerOptions | null;
selectedComposerTargetData: AgentGUIComposerTargetData;
}): string | null {
const storedNodeDefaultSettings = readNodeDefaultDraftSettings({
data:
input.activeConversationId === null
? input.selectedComposerTargetData.data
: input.data,
defaultReasoningEffort: input.defaultReasoningEffort,
drafts: input.draftSettingsBySessionId
});
const homeComposerSettings = resolveEffectiveComposerSettings({
settings: storedNodeDefaultSettings
});
const activeConversationDraftSettings = input.activeConversationId
? (input.draftSettingsBySessionId[input.activeConversationId] ?? null)
: null;
const defaultConversationDraftSettings = {
...(activeConversationDraftSettings ?? homeComposerSettings),
permissionModeId:
normalizePermissionModeId(input.activeSessionPermissionModeId) ??
normalizePermissionModeId(
(activeConversationDraftSettings ?? homeComposerSettings)
.permissionModeId
)
};
const draftSettings = input.activeConversationId
? (input.activeSessionSettings ?? defaultConversationDraftSettings)
: homeComposerSettings;
const persistedDraftModel = normalizeOptionalText(draftSettings.model);
return persistedDraftModel;
}
export function completionKeyFromMessage(
message: AgentActivityMessage
): string | null {
const agentSessionId = message.agentSessionId.trim();
if (!agentSessionId) {
return null;
}
if ((message.role ?? "").trim().toLowerCase() !== "assistant") {
return null;
}
const kind = (message.kind ?? "").trim().toLowerCase();
if (kind !== "message" && kind !== "text") {
return null;
}
const payload =
message.payload && typeof message.payload === "object"
? message.payload
: {};
const status =
message.status?.trim().toLowerCase() ||
(stringPayloadValue(payload, "status") ?? "").toLowerCase();
if (!isCompletedOutcomeToken(status)) {
return null;
}
const subject = message.turnId?.trim() || message.messageId.trim();
return subject ? `turn:${agentSessionId}:${subject}:completed` : null;
}
export function isCompletedOutcomeToken(
value: string | null | undefined
): boolean {
return value?.trim().toLowerCase() === "completed";
}
export const emptyComingSoonProviders: readonly AgentGUIProvider[] = [];
export function applyComingSoonProviderTargets(
targets: AgentGUIAgentTarget[],
comingSoonProviders: readonly AgentGUIProvider[]
): AgentGUIAgentTarget[] {
if (comingSoonProviders.length === 0) {
return targets;
}
const comingSoon = new Set(comingSoonProviders);
return targets.map((target) =>
comingSoon.has(target.provider) && target.disabled !== true
? { ...target, disabled: true }
: target
);
}
export function toRuntimeSendContent(
content: readonly AgentPromptContentBlock[]
): AgentPromptContentBlock[] {
return materializePastedTextInstructions(content, {
header: () => translate("agentHost.agentGui.pastedTextFilesHeader"),
line: (preview, path) =>
translate("agentHost.agentGui.pastedTextFileLine", { preview, path })
}).map((block) =>
block.type === "text"
? {
...block,
text: materializeAgentCustomMentionPromptText(block.text ?? "").trim()
}
: block
);
}
export function shouldClearSubmittedDraft(input: {
currentDraft: AgentComposerDraft | undefined;
submittedDraft: AgentComposerDraft;
}): boolean {
return Boolean(
input.currentDraft &&
areAgentComposerDraftsEquivalentForSubmission(
input.currentDraft,
input.submittedDraft
)
);
}
export function clearSubmittedDraftIfUnchanged(input: {
drafts: Record<string, AgentComposerDraft>;
snapshot: SubmittedDraftSnapshot;
}): Record<string, AgentComposerDraft> {
if (
!shouldClearSubmittedDraft({
currentDraft: input.drafts[input.snapshot.sourceScopeKey],
submittedDraft: input.snapshot.content
})
) {
return input.drafts;
}
return {
...input.drafts,
[input.snapshot.sourceScopeKey]: isAgentComposerSessionDraftScope(
input.snapshot.sourceScopeKey
)
? agentComposerDraftPreservingConnectors(
input.drafts[input.snapshot.sourceScopeKey]
)
: emptyAgentComposerDraft()
};
}