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

358 lines
14 KiB
TypeScript

import { useMemo } from "react";
import { selectPendingSessionForkThroughTurnIds } from "@tutti-os/agent-activity-core";
import { useAgentGUIViewModel } from "../model/useAgentGUIViewModel";
import type { AgentGUIProviderRailMode } from "../../../types";
import type { AgentGUIDetailViewModel } from "../model/agentGuiNodeTypes";
import { useAgentGUIComposerPresentation } from "./useAgentGUIComposerPresentation";
import { useAgentGUIControllerActions } from "./useAgentGUIControllerActions";
import { useAgentGUIConversationDetail } from "./useAgentGUIConversationDetail";
import { useAgentGUIConversationPresentation } from "./useAgentGUIConversationPresentation";
import { useAgentGUIProviderHome } from "./useAgentGUIProviderHome";
import { useAgentGUISessionPresentation } from "./useAgentGUISessionPresentation";
import type { useAgentGUIOperationActions } from "./useAgentGUIOperationActions";
import type { useAgentGUIProviderCatalogSelection } from "./useAgentGUIProviderCatalogSelection";
import type { useAgentGUILocalState } from "./useAgentGUILocalState";
import type { useAgentGUIComposerCapabilities } from "./useAgentGUIComposerCapabilities";
import type { useAgentGUISessionDetailTransport } from "./useAgentGUISessionDetailTransport";
import { resolveAgentGUIProviderReadinessGateForView } from "../model/agentGuiProviderReadiness";
import type { useAgentGUITuttiModeActivation } from "./useAgentGUITuttiModeActivation";
import { targetConnectionForAgentGUIView } from "./agentGuiController.providerHelpers";
import { isAgentGUIAgentTargetComingSoon } from "../../../agentTargets";
import { useEngineSelector } from "../../../shared/engine/useEngineSelector";
type ConversationPresentationInput = Parameters<
typeof useAgentGUIConversationPresentation
>[0];
type ConversationDetailInput = Omit<
Parameters<typeof useAgentGUIConversationDetail>[0],
"activeConversation" | "activeSessionView"
>;
type ActiveSessionViewProjection = Parameters<
typeof useAgentGUIConversationDetail
>[0]["activeSessionView"];
type ComposerPresentationInput = Omit<
Parameters<typeof useAgentGUIComposerPresentation>[0],
"activeConversation"
>;
type SessionPresentationInput = Omit<
Parameters<typeof useAgentGUISessionPresentation>[0],
| "activeConversation"
| "activeLiveState"
| "activationError"
| "activationErrorCode"
| "conversation"
| "isInterrupting"
| "pendingApproval"
| "providerReadinessGate"
| "selectedAgentTargetUnavailable"
| "selectedAgentTargetUnavailableReason"
| "selectedAgentTargetOwnerLabel"
| "serverInteractivePrompt"
>;
type ProviderHomeInput = Parameters<typeof useAgentGUIProviderHome>[0];
type OperationActions = ReturnType<typeof useAgentGUIOperationActions>;
type ProviderCatalog = ReturnType<typeof useAgentGUIProviderCatalogSelection>;
type LocalState = ReturnType<typeof useAgentGUILocalState>;
type ComposerCapabilities = ReturnType<typeof useAgentGUIComposerCapabilities>;
type SessionDetailTransport = ReturnType<
typeof useAgentGUISessionDetailTransport
>;
type UseAgentGUIViewAssemblyInput = ConversationPresentationInput &
ConversationDetailInput &
ComposerPresentationInput &
SessionPresentationInput &
ProviderHomeInput &
ProviderCatalog &
LocalState &
ComposerCapabilities &
SessionDetailTransport &
OperationActions & {
nodeId?: string | null;
operationActions: OperationActions;
detailAvailability: AgentGUIDetailViewModel["availability"];
updateSelectedProjectPath: Parameters<
typeof useAgentGUIControllerActions
>[0]["updateSelectedProjectPath"];
selectConversation: Parameters<
typeof useAgentGUIControllerActions
>[0]["selectConversation"];
providerRailMode: AgentGUIProviderRailMode | undefined;
tuttiModeActivation: ReturnType<typeof useAgentGUITuttiModeActivation>;
};
export function useAgentGUIViewAssembly(input: UseAgentGUIViewAssemblyInput) {
const { activeConversation, visibleConversations } =
useAgentGUIConversationPresentation(input);
const forkThroughTurnPendingTurnIds = useEngineSelector(
input.sessionEngine,
(state) =>
selectPendingSessionForkThroughTurnIds(state, {
sourceAgentSessionId: input.activeConversationId,
workspaceId: input.workspaceId
}),
equalStringArrays
);
const targetConnection = useMemo(
() =>
targetConnectionForAgentGUIView({
activeConversation,
selectedTarget: input.effectiveSelectedProviderTarget,
targets: input.normalizedProviderTargets
}),
[
activeConversation,
input.effectiveSelectedProviderTarget,
input.normalizedProviderTargets
]
);
const stableActiveSessionViewProjection =
useMemo<ActiveSessionViewProjection>(
() =>
input.activeSessionView
? {
hasOlderMessages: input.activeSessionView.hasOlderMessages,
isLoadingOlderMessages:
input.activeSessionView.isLoadingOlderMessages,
olderMessageCount: input.activeMessages.length,
oldestLoadedVersion: input.activeSessionView.oldestLoadedVersion
}
: null,
[
input.activeSessionView?.hasOlderMessages,
input.activeSessionView?.isLoadingOlderMessages,
input.activeSessionView?.oldestLoadedVersion,
input.activeMessages.length
]
);
const detail = useAgentGUIConversationDetail({
...input,
activeConversation,
activeSessionView: stableActiveSessionViewProjection
});
const { stableComposerSettings } = useAgentGUIComposerPresentation({
...input,
activeConversation
});
const providerReadinessGate = useMemo(
() =>
input.activeConversationId === null &&
isAgentGUIAgentTargetComingSoon(
input.effectiveSelectedProviderTarget,
input.normalizedComingSoonProviders
)
? ({ status: "coming_soon" } as const)
: resolveAgentGUIProviderReadinessGateForView({
activeConversationId: input.activeConversationId,
providerReadinessGates: input.providerReadinessGates,
selectedProvider: input.effectiveSelectedProviderTarget.provider
}),
[
input.activeConversationId,
input.effectiveSelectedProviderTarget,
input.normalizedComingSoonProviders,
input.providerReadinessGates
]
);
const selectedAgentTargetUnavailable =
input.activeConversationId === null &&
input.effectiveSelectedProviderTarget.disabled === true &&
!isAgentGUIAgentTargetComingSoon(
input.effectiveSelectedProviderTarget,
input.normalizedComingSoonProviders
);
const selectedAgentTargetUnavailableReason = selectedAgentTargetUnavailable
? input.effectiveSelectedProviderTarget.availability?.reason?.trim() ||
input.effectiveSelectedProviderTarget.unavailableReason?.trim() ||
null
: null;
const selectedAgentTargetOwnerLabel =
input.effectiveSelectedProviderTarget.ownerLabel?.trim() || null;
const session = useAgentGUISessionPresentation({
...input,
activeConversation,
currentUserId: input.currentUserId,
ownerDeviceLabel: targetConnection.ownerDeviceLabel,
providerReadinessGate,
selectedAgentTargetUnavailable,
selectedAgentTargetUnavailableReason,
selectedAgentTargetOwnerLabel,
targetConnectionAgentTargetId: targetConnection.agentTargetId,
activeLiveState: detail.activeLiveState,
activationError: detail.activationError,
activationErrorCode: detail.activationErrorCode,
conversation: detail.conversation,
isInterrupting: detail.isInterrupting,
pendingApproval: detail.pendingApproval,
serverInteractivePrompt: detail.serverInteractivePrompt
});
const pendingQuestionComposerTarget = detail.pendingQuestionComposerTarget;
const pendingInteractivePrompt = session.pendingInteractivePrompt;
const canAnswerPendingInteractivePromptFromComposer =
pendingInteractivePrompt?.kind === "ask-user" &&
pendingQuestionComposerTarget !== null &&
pendingInteractivePrompt.agentSessionId?.trim() ===
pendingQuestionComposerTarget.agentSessionId &&
pendingInteractivePrompt.turnId?.trim() ===
pendingQuestionComposerTarget.turnId &&
pendingInteractivePrompt.requestId ===
pendingQuestionComposerTarget.requestId;
const railConversations = useMemo(() => {
const prompt = session.pendingInteractivePrompt;
if (prompt?.kind !== "plan-implementation" || !input.activeConversationId) {
return visibleConversations;
}
let changed = false;
const next = visibleConversations.map((conversation) => {
if (
conversation.id !== input.activeConversationId ||
conversation.needsUserAction
) {
return conversation;
}
changed = true;
return { ...conversation, needsUserAction: true };
});
return changed ? next : visibleConversations;
}, [
input.activeConversationId,
session.pendingInteractivePrompt,
visibleConversations
]);
const railActiveConversation = useMemo(() => {
if (
!activeConversation ||
session.pendingInteractivePrompt?.kind !== "plan-implementation" ||
activeConversation.needsUserAction
) {
return activeConversation;
}
return { ...activeConversation, needsUserAction: true };
}, [activeConversation, session.pendingInteractivePrompt]);
const providerHome = useAgentGUIProviderHome(input);
const controllerActions = useAgentGUIControllerActions({
...input.operationActions,
...providerHome,
loadOlderConversationMessages: input.loadOlderConversationMessages,
selectConversation: input.selectConversation,
setTuttiModeActive: input.tuttiModeActivation.setActive,
setTuttiModeEffect: input.tuttiModeActivation.setEffect,
setTuttiModeSpeed: input.tuttiModeActivation.setSpeed,
retryTuttiModeActivation: input.tuttiModeActivation.retry,
updateSelectedProjectPath: input.updateSelectedProjectPath
});
const viewData =
input.activeConversationId === null
? input.selectedComposerTargetData.data
: input.data;
const viewModel = useAgentGUIViewModel({
shell: {
nodeId: input.nodeId?.trim() || null,
workspaceId: input.workspaceId,
workspacePath: input.workspacePath,
currentUserId: input.currentUserId,
data: viewData
},
rail: {
selectedAgentTarget: input.effectiveSelectedProviderTarget,
agentTargets: input.normalizedProviderTargets,
agentTargetsLoading: input.agentTargetsLoading,
providerRailMode: input.providerRailMode ?? "catalog",
comingSoonProviders: input.normalizedComingSoonProviders,
conversationFilter: input.conversationFilter,
conversations: railConversations,
userProjects: input.userProjects,
activeConversation: railActiveConversation,
activeConversationId: input.activeConversationId,
revealRequest: input.railRevealRequest,
isLoadingConversations: input.isLoadingConversations,
listError: input.listError
},
detail: {
availability: input.detailAvailability,
isLoadingMessages: input.isLoadingMessages,
isLoadingOlderMessages:
input.activeSessionView?.isLoadingOlderMessages ?? false,
hasOlderMessages: input.activeSessionView?.hasOlderMessages ?? false,
usage: input.usage,
hasSentUserMessage: session.hasSentUserMessage,
avoidGroupingEdits: input.avoidGroupingEdits,
conversation: detail.conversation,
conversationDetail: detail.conversationDetail
},
composer: {
handoffAgentTargets: input.handoffAgentTargets,
availableCommands: detail.availableCommands,
availableSkills: detail.availableSkills,
draftPrompt: detail.draftPrompt,
draftContent: detail.draftContent,
isCreatingConversation: input.isCreatingConversation,
isSubmitting: input.isSubmitting,
isInterrupting: detail.isInterrupting,
isCancelPending: detail.isCancelPending,
hasPendingSubmitStopTarget: session.hasPendingSubmitStopTarget,
promptImagesSupported: input.promptImagesSupported,
compactSupported: input.compactSupported,
goalPauseSupported: input.goalPauseSupported,
gate: session.composerGate,
isTuttiModeActive: input.tuttiModeActivation.active,
isTuttiModeUpdating: input.tuttiModeActivation.updatePending,
tuttiModeEffect:
input.tuttiModeActivation.effect ??
input.tuttiModeActivation.orchestrationIntensity,
tuttiModeSpeed: input.tuttiModeActivation.speed ?? 50,
tuttiModeUpdateStatus: input.tuttiModeActivation.updateStatus,
composerSettings: stableComposerSettings,
queueStatus: detail.queueStatus,
queuedPrompts: detail.queuedPrompts,
drainingQueuedPromptId: detail.drainingQueuedPromptId
},
interaction: {
approvalDisabledReason: session.approvalDisabledReason,
interactivePromptDisabledReason: session.interactivePromptDisabledReason,
isRespondingApproval: session.isRespondingApproval,
isRespondingInteractivePrompt: session.isRespondingInteractivePrompt,
canAnswerPendingInteractivePromptFromComposer,
pendingApproval: session.pendingApproval,
pendingInteractivePrompt: session.pendingInteractivePrompt,
sessionChrome: session.sessionChrome,
inlineNotice: detail.effectiveDetailError
? {
id: `agent-gui-detail-error:${input.activeConversationId ?? "current"}`,
message: detail.effectiveDetailError,
tone: "error" as const,
autoDismissMs: null
}
: null
},
readiness: {
activeLiveState: detail.activeLiveState,
activationError: detail.activationError,
providerReadinessGate
},
operations: {
forkThroughTurnPendingTurnIds,
goalClearNoticeSequence: input.goalClearNoticeSequence,
isDeletingConversation: input.isDeletingConversation,
isDeletingProjectConversations: input.isDeletingProjectConversations,
isUserProjectMutationPending: input.isUserProjectMutationPending,
pendingDeleteConversation: input.pendingDeleteConversation,
pendingDeleteProjectConversations: input.pendingDeleteProjectConversations
}
});
return useMemo(
() => ({ viewModel, actions: controllerActions }),
[controllerActions, viewModel]
);
}
function equalStringArrays(
previous: readonly string[],
next: readonly string[]
): boolean {
return (
previous.length === next.length &&
previous.every((value, index) => value === next[index])
);
}