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
486 lines
14 KiB
TypeScript
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()
|
|
};
|
|
}
|