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

402 lines
13 KiB
TypeScript

import {
useCallback,
useEffect,
type Dispatch,
type RefObject,
type SetStateAction
} from "react";
import type { AgentGUIRuntime } from "../../../agentActivityRuntime";
import type { PendingActivationIntentRecord } from "@tutti-os/agent-activity-core";
import type { AgentGUIConversationFilter } from "../model/agentGuiConversationFilter";
import type { AgentComposerDraft } from "../model/agentGuiNodeTypes";
import { buildAgentComposerDraft } from "../model/agentComposerDraft";
import {
normalizeAgentComposerDraftProjectPath,
resolveAgentComposerDraftScopeKey
} from "../model/agentComposerDraftScope";
import type {
AgentGUINodeData,
AgentGUIProvider,
AgentGUIAgentTarget
} from "../../../types";
import { resolveAgentGUIAgentTarget } from "../../../agentTargets";
import type { AgentGUIComposerTargetData } from "./agentGuiController.composerPresentation";
import { isPendingNewConversationActivationForSession } from "./useAgentGUIActivation";
import {
resolveAgentGUIComposerAppendRequest,
type AgentGUIComposerAppendRequest
} from "./useAgentGUIComposerAppendRequest";
import {
buildNodeDefaultComposerSettings,
nodeDataFromComposerSettings
} from "./agentGuiController.composerHelpers";
export interface AgentGUIPrefillPromptRequest {
agentTargetId?: string | null;
autoSubmit?: boolean;
draftPrompt: string;
model?: string | null;
modelPlanId?: string | null;
provider?: AgentGUIProvider;
sequence: number;
userProjectPath?: string | null;
}
export interface UseAgentGUIConversationHomeInput {
activeConversationId: string | null;
activeConversationIdRef: RefObject<string | null>;
activePendingActivation: PendingActivationIntentRecord | null;
agentActivityRuntime: AgentGUIRuntime;
currentProvider: AgentGUIProvider;
composerTargetDataFromProviderTarget: (input: {
current: AgentGUINodeData;
isExplicit: boolean;
target: AgentGUIAgentTarget;
}) => AgentGUIComposerTargetData;
composerAppendRequest: AgentGUIComposerAppendRequest | null;
conversationFilterRef: RefObject<AgentGUIConversationFilter>;
dataRef: RefObject<AgentGUINodeData>;
defaultAgentTargetId: string | null;
handledPrefillPromptSequenceRef: RefObject<number | null>;
handledComposerAppendSequenceRef: RefObject<number | null>;
isComposerHomeRef: RefObject<boolean>;
isExplicitAgentGUIAgentTarget: (
target: AgentGUIAgentTarget,
explicitTargets: readonly AgentGUIAgentTarget[]
) => boolean;
loadDraftComposerOptions: () => void;
normalizedExplicitProviderTargets: readonly AgentGUIAgentTarget[];
normalizedProviderTargets: readonly AgentGUIAgentTarget[];
onDataChangeRef: RefObject<
(updater: (current: AgentGUINodeData) => AgentGUINodeData) => void
>;
onComposerAppendHandled?: (sequence: number) => void;
submitPrefillPrompt: (prompt: string) => void;
persistActiveConversation: (agentSessionId: string | null) => void;
prefillPromptRequest: AgentGUIPrefillPromptRequest | null;
reportActiveConversationCleared: (input: {
details: Record<string, unknown>;
previousAgentSessionId: string | null;
reason: "create_conversation" | "prefill_prompt";
runtime: AgentGUIRuntime;
workspaceId: string;
}) => void;
selectedComposerTargetDataRef: RefObject<AgentGUIComposerTargetData>;
selectedProjectPathRef: RefObject<string | null>;
draftByScopeKeyRef: RefObject<Record<string, AgentComposerDraft>>;
setActiveConversationId: Dispatch<SetStateAction<string | null>>;
setConversationFilter: Dispatch<SetStateAction<AgentGUIConversationFilter>>;
setDetailError: Dispatch<SetStateAction<string | null>>;
setDraftByScopeKey: Dispatch<
SetStateAction<Record<string, AgentComposerDraft>>
>;
setHomeComposerTargetOverride: Dispatch<
SetStateAction<AgentGUIAgentTarget | null>
>;
setIntent: (intent: { tag: "home" }) => void;
setIsComposerHome: Dispatch<SetStateAction<boolean>>;
setIsLoadingMessages: Dispatch<SetStateAction<boolean>>;
setSelectedProjectPath: Dispatch<SetStateAction<string | null>>;
shouldUseStaticProviderTargets: boolean;
unactivate: (agentSessionId: string) => Promise<unknown>;
workspaceId: string;
}
/** Owns transitions from an active conversation back to the home composer. */
export function useAgentGUIConversationHome({
activeConversationId,
activeConversationIdRef,
activePendingActivation,
agentActivityRuntime,
composerTargetDataFromProviderTarget,
composerAppendRequest,
conversationFilterRef,
currentProvider,
dataRef,
defaultAgentTargetId,
handledPrefillPromptSequenceRef,
handledComposerAppendSequenceRef,
isComposerHomeRef,
isExplicitAgentGUIAgentTarget,
loadDraftComposerOptions,
normalizedExplicitProviderTargets,
normalizedProviderTargets,
onDataChangeRef,
onComposerAppendHandled,
submitPrefillPrompt,
persistActiveConversation,
prefillPromptRequest,
reportActiveConversationCleared,
selectedComposerTargetDataRef,
selectedProjectPathRef,
draftByScopeKeyRef,
setActiveConversationId,
setConversationFilter,
setDetailError,
setDraftByScopeKey,
setHomeComposerTargetOverride,
setIntent,
setIsComposerHome,
setIsLoadingMessages,
setSelectedProjectPath,
shouldUseStaticProviderTargets,
unactivate,
workspaceId
}: UseAgentGUIConversationHomeInput) {
const enterHome = useCallback(
(
reason: "create_conversation" | "prefill_prompt",
details: Record<string, unknown>
) => {
const previous = activeConversationIdRef.current;
reportActiveConversationCleared({
details,
previousAgentSessionId: previous,
reason,
runtime: agentActivityRuntime,
workspaceId
});
if (
previous &&
!isPendingNewConversationActivationForSession(
activePendingActivation,
previous
)
) {
void unactivate(previous);
}
setIntent({ tag: "home" });
isComposerHomeRef.current = true;
setIsComposerHome(true);
activeConversationIdRef.current = null;
setActiveConversationId(null);
setIsLoadingMessages(false);
setDetailError(null);
},
[
activeConversationIdRef,
activePendingActivation,
agentActivityRuntime,
isComposerHomeRef,
reportActiveConversationCleared,
setActiveConversationId,
setDetailError,
setIntent,
setIsComposerHome,
setIsLoadingMessages,
unactivate,
workspaceId
]
);
const createConversation = useCallback(
(options?: { projectPath?: string | null; source?: string }) => {
if (options && "projectPath" in options) {
const projectPath = normalizeAgentComposerDraftProjectPath(
options.projectPath
);
selectedProjectPathRef.current = projectPath;
setSelectedProjectPath(projectPath);
}
enterHome("create_conversation", {
hasProjectPathOption: Boolean(options && "projectPath" in options),
isComposerHome: isComposerHomeRef.current,
projectPathPresent: Boolean(
options &&
"projectPath" in options &&
normalizeAgentComposerDraftProjectPath(options.projectPath)
),
source: options?.source ?? "controller"
});
persistActiveConversation(null);
const filter = conversationFilterRef.current;
if (filter.kind === "agentTarget") {
const target = resolveAgentGUIAgentTarget({
agentTargetId: filter.agentTargetId,
defaultAgentTargetId,
provider: currentProvider,
agentTargets: normalizedProviderTargets,
useStaticCatalog: false
});
if (
target &&
target.disabled !== true &&
(target.agentTargetId?.trim() ?? "") === filter.agentTargetId
) {
setHomeComposerTargetOverride(target);
}
}
loadDraftComposerOptions();
},
[
currentProvider,
conversationFilterRef,
dataRef,
defaultAgentTargetId,
enterHome,
isComposerHomeRef,
loadDraftComposerOptions,
normalizedProviderTargets,
persistActiveConversation,
selectedProjectPathRef,
setHomeComposerTargetOverride,
setSelectedProjectPath
]
);
useEffect(() => {
const resolvedAppendRequest = resolveAgentGUIComposerAppendRequest({
activeConversationId,
draftByScopeKey: draftByScopeKeyRef.current,
handledSequence: handledComposerAppendSequenceRef.current,
request: composerAppendRequest
});
if (resolvedAppendRequest) {
handledComposerAppendSequenceRef.current = resolvedAppendRequest.sequence;
draftByScopeKeyRef.current = {
...draftByScopeKeyRef.current,
[resolvedAppendRequest.draftKey]: resolvedAppendRequest.nextDraft
};
setDraftByScopeKey((current) => ({
...current,
[resolvedAppendRequest.draftKey]: resolvedAppendRequest.nextDraft
}));
onComposerAppendHandled?.(resolvedAppendRequest.sequence);
}
if (
!prefillPromptRequest ||
handledPrefillPromptSequenceRef.current === prefillPromptRequest.sequence
) {
return;
}
handledPrefillPromptSequenceRef.current = prefillPromptRequest.sequence;
const draftPrompt = prefillPromptRequest.draftPrompt.trim();
if (!draftPrompt) return;
const projectPath = normalizeAgentComposerDraftProjectPath(
prefillPromptRequest.userProjectPath
);
selectedProjectPathRef.current = projectPath;
setSelectedProjectPath(projectPath);
enterHome("prefill_prompt", {
autoSubmit: prefillPromptRequest.autoSubmit === true,
sequence: prefillPromptRequest.sequence
});
const selectedTargetData = selectedComposerTargetDataRef.current;
const target =
prefillPromptRequest.provider || prefillPromptRequest.agentTargetId
? resolveAgentGUIAgentTarget({
agentTargetId: prefillPromptRequest.agentTargetId,
defaultAgentTargetId,
provider:
prefillPromptRequest.provider ?? selectedTargetData.provider,
agentTargets: normalizedProviderTargets,
useStaticCatalog: shouldUseStaticProviderTargets
})
: null;
const targetData = target
? composerTargetDataFromProviderTarget({
current: dataRef.current,
isExplicit: isExplicitAgentGUIAgentTarget(
target,
normalizedExplicitProviderTargets
),
target
})
: selectedTargetData;
if (target) {
setHomeComposerTargetOverride(target);
setConversationFilter(
targetData.agentTargetId
? { kind: "agentTarget", agentTargetId: targetData.agentTargetId }
: { kind: "all" }
);
onDataChangeRef.current((current) => {
const nextTargetData = composerTargetDataFromProviderTarget({
current,
isExplicit: isExplicitAgentGUIAgentTarget(
target,
normalizedExplicitProviderTargets
),
target
});
const nextTargetNodeData = {
...nextTargetData.data,
lastActiveAgentSessionId: null
};
const nextData = applyAgentGUIPrefillComposerAssignment(
nextTargetNodeData,
prefillPromptRequest
);
dataRef.current = nextData;
return nextData;
});
} else if (prefillPromptRequest.model || prefillPromptRequest.modelPlanId) {
onDataChangeRef.current((current) => {
const nextData = applyAgentGUIPrefillComposerAssignment(
{ ...current, lastActiveAgentSessionId: null },
prefillPromptRequest
);
dataRef.current = nextData;
return nextData;
});
}
const sourceScopeKey = resolveAgentComposerDraftScopeKey({});
const prefilledDraft = buildAgentComposerDraft({ prompt: draftPrompt });
draftByScopeKeyRef.current = {
...draftByScopeKeyRef.current,
[sourceScopeKey]: prefilledDraft
};
setDraftByScopeKey((current) => ({
...current,
[sourceScopeKey]: prefilledDraft
}));
if (prefillPromptRequest.autoSubmit) {
submitPrefillPrompt(draftPrompt);
}
persistActiveConversation(null);
loadDraftComposerOptions();
}, [
activeConversationId,
dataRef,
draftByScopeKeyRef,
composerAppendRequest,
defaultAgentTargetId,
enterHome,
handledPrefillPromptSequenceRef,
handledComposerAppendSequenceRef,
loadDraftComposerOptions,
normalizedExplicitProviderTargets,
normalizedProviderTargets,
onComposerAppendHandled,
onDataChangeRef,
persistActiveConversation,
prefillPromptRequest,
selectedComposerTargetDataRef,
selectedProjectPathRef,
setConversationFilter,
setDraftByScopeKey,
setHomeComposerTargetOverride,
setSelectedProjectPath,
submitPrefillPrompt,
shouldUseStaticProviderTargets
]);
return { createConversation, enterHome };
}
export function applyAgentGUIPrefillComposerAssignment(
data: AgentGUINodeData,
request: Pick<AgentGUIPrefillPromptRequest, "model" | "modelPlanId">
): AgentGUINodeData {
const model = request.model?.trim() || null;
const modelPlanId = request.modelPlanId?.trim() || null;
if (!model && !modelPlanId) {
return data;
}
return nodeDataFromComposerSettings(data, {
...buildNodeDefaultComposerSettings(data),
model,
modelPlanId
});
}