Files
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

675 lines
22 KiB
TypeScript

import type { AgentActivityComposerOptions } from "@tutti-os/agent-activity-core";
import { useCallback, useRef } from "react";
import { AGENT_PROVIDER_LABEL } from "../../../contexts/settings/domain/agentSettings";
import type {
AgentSessionComposerSettings,
AgentSessionPermissionConfig,
AgentSessionPermissionModeOption
} from "../../../shared/agentSessionTypes";
import type { AgentProviderId } from "../../../shared/contracts/dto";
import type { WorkspaceAgentActivityCard } from "../../../shared/workspaceAgentActivityListViewModel";
import { isWorkspaceAgentUntitledConversation } from "../../../shared/workspaceAgentLatestActivitySummary";
import type { WorkspaceAgentSessionDetailViewModel } from "../../../shared/workspaceAgentSessionDetailViewModel";
import { type AgentGUIConversationSummary } from "../model/agentGuiConversationModel";
import type {
AgentGUIComposerSettingOption,
AgentGUIComposerSettingsVM,
AgentGUIProviderSkillOption
} from "../model/agentGuiNodeTypes";
import {
areProviderSkillOptionListsEqual,
normalizeConfigOptionValue,
sameComposerSettings,
slashCommandPoliciesEqual as sameSlashPolicy
} from "./agentGuiController.composerHelpers";
import { hasPromptConversationTitle } from "./agentGuiController.conversationHelpers";
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,
normalizeTimelineStatus,
normalizedPositiveNumber,
timelineItemTime
} from "./agentGuiController.sessionHelpers";
export {
filterMessagesForDetailWindowOverlay,
maxFiniteMessageVersion,
minFiniteMessageVersion,
sessionHasRenderableMessages,
windowHasTurnMissingUserPrompt
} from "./useAgentConversationMessagePaging";
export function stableConversationSummaryList(
previous: readonly AgentGUIConversationSummary[] | null,
next: AgentGUIConversationSummary[]
): AgentGUIConversationSummary[] {
if (previous?.length !== next.length) {
const previousById = new Map(
(previous ?? []).map((conversation) => [conversation.id, conversation])
);
return next.map((conversation) => {
const previousConversation = previousById.get(conversation.id);
return previousConversation &&
conversationSummariesRenderEqual(previousConversation, conversation)
? previousConversation
: conversation;
});
}
let hasRenderChange = false;
const stable = next.map((conversation, index) => {
const previousConversation = previous[index];
if (
previousConversation &&
conversationSummariesRenderEqual(previousConversation, conversation)
) {
return previousConversation;
}
hasRenderChange = true;
return conversation;
});
return hasRenderChange ? stable : (previous as AgentGUIConversationSummary[]);
}
export function useStableConversationDetail(
detail: WorkspaceAgentSessionDetailViewModel | null
): WorkspaceAgentSessionDetailViewModel | null {
const detailRef = useRef<WorkspaceAgentSessionDetailViewModel | null>(null);
detailRef.current = stabilizeConversationDetail(detailRef.current, detail);
return detailRef.current;
}
export function stabilizeConversationDetail(
previous: WorkspaceAgentSessionDetailViewModel | null,
next: WorkspaceAgentSessionDetailViewModel | null
): WorkspaceAgentSessionDetailViewModel | null {
if (!previous || !next) {
return next;
}
const session = conversationDetailSessionsEqual(
previous.session,
next.session
)
? previous.session
: next.session;
const activity = stabilizeConversationDetailActivity(
previous.activity,
next.activity
);
if (
previous.cwd === next.cwd &&
previous.workspaceRoot === next.workspaceRoot &&
previous.showProcessingIndicator === next.showProcessingIndicator &&
previous.goalControls === next.goalControls &&
previous.turns === next.turns &&
previous.sessionTurns === next.sessionTurns &&
previous.session === session &&
previous.activity === activity
) {
return previous;
}
return {
...next,
activity,
session
};
}
export function stabilizeConversationDetailActivity(
previous: WorkspaceAgentActivityCard,
next: WorkspaceAgentActivityCard
): WorkspaceAgentActivityCard {
const changedFiles = conversationDetailChangedFilesEqual(
previous.changedFiles,
next.changedFiles
)
? previous.changedFiles
: next.changedFiles;
if (
previous.id === next.id &&
previous.sessionId === next.sessionId &&
previous.userId === next.userId &&
previous.userName === next.userName &&
previous.userAvatarUrl === next.userAvatarUrl &&
previous.agentProvider === next.agentProvider &&
previous.agentName === next.agentName &&
previous.title === next.title &&
previous.status === next.status &&
previous.latestActivitySummary === next.latestActivitySummary &&
previous.conversationPreview === next.conversationPreview &&
previous.latestActivityActorName === next.latestActivityActorName &&
previous.toolCalls === next.toolCalls &&
previous.changedFiles === changedFiles &&
previous.sortTimeUnixMs === next.sortTimeUnixMs &&
previous.readTimeUnixMs === next.readTimeUnixMs
) {
return previous;
}
return {
...next,
changedFiles
};
}
export function conversationDetailChangedFilesEqual(
left: WorkspaceAgentActivityCard["changedFiles"],
right: WorkspaceAgentActivityCard["changedFiles"]
): boolean {
return (
left.length === right.length &&
left.every(
(file, index) =>
file.path === right[index]?.path && file.label === right[index]?.label
)
);
}
export function conversationDetailSessionsEqual(
left: WorkspaceAgentSessionDetailViewModel["session"],
right: WorkspaceAgentSessionDetailViewModel["session"]
): boolean {
return (
left.workspaceId === right.workspaceId &&
left.agentSessionId === right.agentSessionId &&
left.userId === right.userId &&
left.kind === right.kind &&
left.agentTargetId === right.agentTargetId &&
left.provider === right.provider &&
left.providerSessionId === right.providerSessionId &&
left.model === right.model &&
left.resumable === right.resumable &&
left.endedAtUnixMs === right.endedAtUnixMs &&
left.title === right.title &&
left.pinnedAtUnixMs === right.pinnedAtUnixMs &&
left.createdAtUnixMs === right.createdAtUnixMs &&
left.updatedAtUnixMs === right.updatedAtUnixMs &&
left.cwd === right.cwd &&
left.isolation?.mode === right.isolation?.mode &&
left.activeTurnId === right.activeTurnId &&
left.activeTurn?.updatedAtUnixMs === right.activeTurn?.updatedAtUnixMs &&
left.latestTurn?.updatedAtUnixMs === right.latestTurn?.updatedAtUnixMs &&
left.lifecycleCapabilities.fork === right.lifecycleCapabilities.fork &&
left.lifecycleCapabilities.forkThroughTurn ===
right.lifecycleCapabilities.forkThroughTurn &&
left.forkedFrom?.sourceAgentSessionId ===
right.forkedFrom?.sourceAgentSessionId &&
left.forkedFrom?.sourceTurnId === right.forkedFrom?.sourceTurnId &&
left.forkedFrom?.targetTurnId === right.forkedFrom?.targetTurnId &&
left.forkedFrom?.operationId === right.forkedFrom?.operationId &&
left.forkedFrom?.forkedAtUnixMs === right.forkedFrom?.forkedAtUnixMs &&
left.pendingInteractions === right.pendingInteractions &&
left.latestTurnInteractions === right.latestTurnInteractions
);
}
export function conversationSummariesRenderEqual(
left: AgentGUIConversationSummary,
right: AgentGUIConversationSummary
): boolean {
return (
left.id === right.id &&
left.userId === right.userId &&
left.provider === right.provider &&
left.title === right.title &&
left.titleLeadingMentionKind === right.titleLeadingMentionKind &&
conversationTitleFallbacksRenderEqual(
left.titleFallback,
right.titleFallback
) &&
left.status === right.status &&
left.cwd === right.cwd &&
left.railSectionKey === right.railSectionKey &&
left.pinnedAtUnixMs === right.pinnedAtUnixMs &&
left.sortTimeUnixMs === right.sortTimeUnixMs &&
left.updatedAtUnixMs === right.updatedAtUnixMs &&
left.isTransient === right.isTransient &&
left.projectionSource === right.projectionSource &&
left.isImported === right.isImported &&
left.hasUnreadCompletion === right.hasUnreadCompletion &&
left.unreadCompletionKey === right.unreadCompletionKey &&
left.needsUserAction === right.needsUserAction &&
conversationProjectsRenderEqual(left.project, right.project)
);
}
export function conversationTitleFallbacksRenderEqual(
left: AgentGUIConversationSummary["titleFallback"],
right: AgentGUIConversationSummary["titleFallback"]
): boolean {
return (
left === right ||
JSON.stringify(left ?? null) === JSON.stringify(right ?? null)
);
}
export function conversationProjectsRenderEqual(
left: AgentGUIConversationSummary["project"],
right: AgentGUIConversationSummary["project"]
): boolean {
return (
left === right ||
(!left || !right
? !left && !right
: left.id === right.id &&
left.path === right.path &&
left.sectionKey === right.sectionKey &&
left.label === right.label &&
left.createdAtUnixMs === right.createdAtUnixMs &&
left.updatedAtUnixMs === right.updatedAtUnixMs &&
left.lastUsedAtUnixMs === right.lastUsedAtUnixMs)
);
}
export function mergeConversationTitleUpdateFields(
current: AgentGUIConversationSummary,
incomingTitle: string,
provider?: AgentProviderId
): Pick<AgentGUIConversationSummary, "title" | "titleFallback"> {
const title = incomingTitle.trim();
if (!title) {
return {
title: current.title,
titleFallback: current.titleFallback
};
}
const currentHasPromptTitle = hasPromptConversationTitle(current);
if (currentHasPromptTitle) {
return {
title: current.title,
titleFallback: current.titleFallback
};
}
if (
provider &&
shouldPreserveExistingConversationTitle(current, title, provider)
) {
return {
title: current.title,
titleFallback: current.titleFallback
};
}
return {
title,
titleFallback: null
};
}
export function shouldPreserveExistingConversationTitle(
current: AgentGUIConversationSummary,
incomingTitle: string,
provider: AgentProviderId
): boolean {
const normalizedIncoming = incomingTitle.trim();
if (!normalizedIncoming || !current.title.trim()) {
return false;
}
if (isWorkspaceAgentUntitledConversation(normalizedIncoming)) {
return true;
}
const providerLabel =
AGENT_PROVIDER_LABEL[provider as keyof typeof AGENT_PROVIDER_LABEL] ??
provider;
return (
normalizedIncoming === provider || normalizedIncoming === providerLabel
);
}
export function draftAgentSessionIdFromComposerOptions(
options: AgentActivityComposerOptions | null | undefined
): string | null {
return normalizeConfigOptionValue(options?.draftAgentSessionId);
}
export function areComposerSettingOptionsEqual(
left: AgentGUIComposerSettingOption,
right: AgentGUIComposerSettingOption
): boolean {
return (
left.value === right.value &&
left.label === right.label &&
left.description === right.description
);
}
export function areComposerSettingOptionListsEqual(
left: readonly AgentGUIComposerSettingOption[] | null | undefined,
right: readonly AgentGUIComposerSettingOption[] | null | undefined
): boolean {
const leftOptions = left ?? [];
const rightOptions = right ?? [];
return (
leftOptions.length === rightOptions.length &&
leftOptions.every((option, index) =>
areComposerSettingOptionsEqual(option, rightOptions[index]!)
)
);
}
function areComposerModelChoiceHistoriesEqual(
left: AgentGUIComposerSettingsVM["modelChoiceHistory"],
right: AgentGUIComposerSettingsVM["modelChoiceHistory"]
): boolean {
if (!left || !right) {
return left === right;
}
if (left.targetId !== right.targetId) {
return false;
}
const leftCatalog = left.catalog;
const rightCatalog = right.catalog;
if (!leftCatalog || !rightCatalog) {
return leftCatalog === rightCatalog;
}
return (
leftCatalog.authoritative === rightCatalog.authoritative &&
leftCatalog.loading === rightCatalog.loading &&
leftCatalog.effectiveModel === rightCatalog.effectiveModel &&
leftCatalog.models.length === rightCatalog.models.length &&
leftCatalog.models.every(
(model, index) =>
model.value === rightCatalog.models[index]!.value &&
Boolean(model.requested) ===
Boolean(rightCatalog.models[index]!.requested)
)
);
}
export function useStableComposerSettings(
settings: AgentSessionComposerSettings
): AgentSessionComposerSettings;
export function useStableComposerSettings(
settings: AgentSessionComposerSettings | null
): AgentSessionComposerSettings | null;
export function useStableComposerSettings(
settings: AgentSessionComposerSettings | null
): AgentSessionComposerSettings | null {
const settingsRef = useRef<{
value: AgentSessionComposerSettings | null;
} | null>(null);
if (
settingsRef.current === null ||
!sameComposerSettings(settingsRef.current.value, settings)
) {
settingsRef.current = { value: settings };
}
return settingsRef.current.value;
}
export function useStableProviderSkillOptions(
skills: AgentGUIProviderSkillOption[]
): AgentGUIProviderSkillOption[] {
const skillsRef = useRef<AgentGUIProviderSkillOption[] | null>(null);
if (
skillsRef.current === null ||
!areProviderSkillOptionListsEqual(skillsRef.current, skills)
) {
skillsRef.current = skills;
}
return skillsRef.current;
}
export function areComposerSettingsDraftsEqual(
left: AgentGUIComposerSettingsVM["draftSettings"],
right: AgentGUIComposerSettingsVM["draftSettings"]
): boolean {
return (
left.codexSaverMode === right.codexSaverMode &&
left.rtkSaverMode === right.rtkSaverMode &&
left.model === right.model &&
left.reasoningEffort === right.reasoningEffort &&
left.speed === right.speed &&
left.planMode === right.planMode &&
(left.browserUse ?? true) === (right.browserUse ?? true) &&
(left.computerUse ?? true) === (right.computerUse ?? true) &&
(left.permissionModeId ?? null) === (right.permissionModeId ?? null)
);
}
export function arePermissionModeOptionsEqual(
left: AgentSessionPermissionModeOption,
right: AgentSessionPermissionModeOption
): boolean {
return (
left.id === right.id &&
left.label === right.label &&
left.description === right.description &&
left.semantic === right.semantic
);
}
export function arePermissionConfigsEqual(
left: AgentSessionPermissionConfig | null | undefined,
right: AgentSessionPermissionConfig | null | undefined
): boolean {
if (!left || !right) {
return left === right;
}
return (
left.configurable === right.configurable &&
(left.defaultValue ?? null) === (right.defaultValue ?? null) &&
left.modes.length === right.modes.length &&
left.modes.every((mode, index) =>
arePermissionModeOptionsEqual(mode, right.modes[index]!)
)
);
}
export function areComposerSettingsVMsEqual(
left: AgentGUIComposerSettingsVM,
right: AgentGUIComposerSettingsVM
): boolean {
return (
sameComposerSettings(left.sessionSettings, right.sessionSettings) &&
areComposerSettingsDraftsEqual(left.draftSettings, right.draftSettings) &&
left.supportsModel === right.supportsModel &&
(left.supportsCodexSaverMode ?? false) ===
(right.supportsCodexSaverMode ?? false) &&
left.supportsReasoningEffort === right.supportsReasoningEffort &&
left.supportsSpeed === right.supportsSpeed &&
(left.supportsPermissionMode ?? false) ===
(right.supportsPermissionMode ?? false) &&
left.supportsPlanMode === right.supportsPlanMode &&
(left.supportsBrowser ?? false) === (right.supportsBrowser ?? false) &&
(left.supportsComputerUse ?? false) ===
(right.supportsComputerUse ?? false) &&
(left.permissionModeChangeDuringTurn ?? false) ===
(right.permissionModeChangeDuringTurn ?? false) &&
sameSlashPolicy(left.slashCommandPolicy, right.slashCommandPolicy) &&
left.isSettingsLoading === right.isSettingsLoading &&
Boolean(left.composerOptionsError) ===
Boolean(right.composerOptionsError) &&
(left.composerOptionsLoadStatus ?? null) ===
(right.composerOptionsLoadStatus ?? null) &&
!!left.isCapabilityOptionsLoading === !!right.isCapabilityOptionsLoading &&
!!left.isConnectorOptionsLoading === !!right.isConnectorOptionsLoading &&
!!left.isModelOptionsLoading === !!right.isModelOptionsLoading &&
areComposerModelChoiceHistoriesEqual(
left.modelChoiceHistory,
right.modelChoiceHistory
) &&
left.modelUnavailable === right.modelUnavailable &&
left.reasoningUnavailable === right.reasoningUnavailable &&
left.speedUnavailable === right.speedUnavailable &&
(left.permissionModeUnavailable ?? false) ===
(right.permissionModeUnavailable ?? false) &&
(left.planExclusiveWithPermissionMode ?? false) ===
(right.planExclusiveWithPermissionMode ?? false) &&
(left.selectedModelValue ?? null) === (right.selectedModelValue ?? null) &&
(left.effectiveModelValue ?? null) ===
(right.effectiveModelValue ?? null) &&
(left.selectedReasoningEffortValue ?? null) ===
(right.selectedReasoningEffortValue ?? null) &&
(left.selectedSpeedValue ?? null) === (right.selectedSpeedValue ?? null) &&
(left.selectedPermissionModeValue ?? null) ===
(right.selectedPermissionModeValue ?? null) &&
arePermissionConfigsEqual(left.permissionConfig, right.permissionConfig) &&
(left.selectedProjectPath ?? null) ===
(right.selectedProjectPath ?? null) &&
Boolean(left.shouldApplyPreparedProjectSelection) ===
Boolean(right.shouldApplyPreparedProjectSelection) &&
Boolean(left.projectLocked) === Boolean(right.projectLocked) &&
Boolean(left.projectPathIsRemote) === Boolean(right.projectPathIsRemote) &&
Boolean(left.collapseModelOptionsToLatest) ===
Boolean(right.collapseModelOptionsToLatest) &&
(left.modelPlan?.id ?? null) === (right.modelPlan?.id ?? null) &&
(left.modelPlan?.name ?? null) === (right.modelPlan?.name ?? null) &&
(left.modelPlan?.protocol ?? null) ===
(right.modelPlan?.protocol ?? null) &&
Boolean(left.modelSwitchTakesEffectNextTurn) ===
Boolean(right.modelSwitchTakesEffectNextTurn) &&
areComposerSettingOptionListsEqual(
left.availableModels,
right.availableModels
) &&
areComposerSettingOptionListsEqual(
left.availableReasoningEfforts,
right.availableReasoningEfforts
) &&
areComposerSettingOptionListsEqual(
left.availableSpeeds,
right.availableSpeeds
) &&
areComposerSettingOptionListsEqual(
left.availablePermissionModes,
right.availablePermissionModes
)
);
}
export function useStableComposerSettingsVM(
settings: AgentGUIComposerSettingsVM
): AgentGUIComposerSettingsVM {
const settingsRef = useRef<AgentGUIComposerSettingsVM | null>(null);
settingsRef.current = stabilizeComposerSettingsVM(
settingsRef.current,
settings
);
return settingsRef.current;
}
export function stabilizeComposerSettingsVM(
previous: AgentGUIComposerSettingsVM | null,
next: AgentGUIComposerSettingsVM
): AgentGUIComposerSettingsVM {
if (!previous) {
return next;
}
if (areComposerSettingsVMsEqual(previous, next)) {
return previous;
}
const sessionSettings = sameComposerSettings(
previous.sessionSettings,
next.sessionSettings
)
? previous.sessionSettings
: next.sessionSettings;
const draftSettings = areComposerSettingsDraftsEqual(
previous.draftSettings,
next.draftSettings
)
? previous.draftSettings
: next.draftSettings;
const permissionConfig = arePermissionConfigsEqual(
previous.permissionConfig,
next.permissionConfig
)
? previous.permissionConfig
: next.permissionConfig;
const modelChoiceHistory = areComposerModelChoiceHistoriesEqual(
previous.modelChoiceHistory,
next.modelChoiceHistory
)
? previous.modelChoiceHistory
: next.modelChoiceHistory;
const availableModels = areComposerSettingOptionListsEqual(
previous.availableModels,
next.availableModels
)
? previous.availableModels
: next.availableModels;
const availableReasoningEfforts = areComposerSettingOptionListsEqual(
previous.availableReasoningEfforts,
next.availableReasoningEfforts
)
? previous.availableReasoningEfforts
: next.availableReasoningEfforts;
const availableSpeeds = areComposerSettingOptionListsEqual(
previous.availableSpeeds,
next.availableSpeeds
)
? previous.availableSpeeds
: next.availableSpeeds;
const availablePermissionModes = areComposerSettingOptionListsEqual(
previous.availablePermissionModes ?? [],
next.availablePermissionModes ?? []
)
? previous.availablePermissionModes
: next.availablePermissionModes;
return {
...next,
sessionSettings,
draftSettings,
modelChoiceHistory,
permissionConfig,
availableModels,
availableReasoningEfforts,
availableSpeeds,
availablePermissionModes
};
}
export function useStableControllerEventCallback<
Args extends unknown[],
Result
>(callback: (...args: Args) => Result): (...args: Args) => Result {
const callbackRef = useRef(callback);
callbackRef.current = callback;
return useCallback((...args: Args) => callbackRef.current(...args), []);
}
export function stringArraysEqual(
first: string[] | null | undefined,
second: string[] | null | undefined
): boolean {
if (!first || !second) {
return first === second;
}
return (
first.length === second.length &&
first.every((value, index) => value === second[index])
);
}
export function useStableStringArrayByValue(values: string[]): string[] {
const valuesRef = useRef<string[] | null>(null);
const currentValues = valuesRef.current;
if (!stringArraysEqual(currentValues, values)) {
valuesRef.current = values;
return values;
}
return currentValues ?? values;
}