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

169 lines
5.4 KiB
TypeScript

import { useCallback, useEffect, useRef } from "react";
import { useOptionalAgentHostApi } from "../../../agentActivityHost";
import type { UiLanguage } from "../../../contexts/settings/domain/agentSettings";
import type { AgentGUINodeViewModel } from "../model/agentGuiNodeTypes";
import type {
AgentGUINodeViewProps,
AgentGUIViewLabels
} from "./AgentGUINodeView.types";
import { useAgentGUIConversationCopyAction } from "./AgentGUIConversationActionsMenu";
import {
AGENT_GUI_WORKBENCH_COMMAND_EVENT,
normalizeAgentGuiWorkbenchCommand
} from "../../../workbench/commands";
type Conversation = AgentGUINodeViewModel["rail"]["conversations"][number];
function resolveSessionActionConversation(
viewModel: AgentGUINodeViewModel,
agentSessionId: string | null
): Conversation | null {
const active = viewModel.rail.activeConversation;
if (!agentSessionId) {
return active;
}
if (active?.id === agentSessionId) {
return active;
}
return (
viewModel.rail.conversations.find(
(conversation) => conversation.id === agentSessionId
) ?? null
);
}
/**
* Handles requests dispatched by the host chrome (workbench header) into the
* AgentGUI node: creating a conversation and session actions (rename, copy
* variants) targeting the conversation the header menu was rendered for.
*
* The header chrome cannot see the rail interaction lock or whether the
* target conversation is loaded, so unlike the rail menus (which disable or
* suppress actions up front) failures here surface as an error toast rather
* than a silent drop.
*
* The rail query controller owns the interaction lock and lives inside the
* rail subtree; it registers a probe through the returned callback so session
* actions honor the same lock as the rail's own menus.
*/
export function useAgentGUIExternalRequests(input: {
createConversationDisabled: boolean;
labels: Pick<
AgentGUIViewLabels,
| "conversationCopyFile"
| "conversationCopyImage"
| "conversationCopyImagesOmitted"
| "conversationCopyInProgress"
| "conversationCopyMentionPrefix"
| "conversationCopyPreviousMessages"
| "copiedToClipboard"
| "copyFailed"
| "sessionActionUnavailable"
| "untitledConversationTitle"
>;
requestCreateConversation: (options?: { source?: string }) => void;
requestRenameConversation: (conversation: Conversation) => void;
uiLanguage: UiLanguage;
viewModel: AgentGUINodeViewModel;
workbenchCommandBridge: NonNullable<
AgentGUINodeViewProps["workbenchCommandBridge"]
> | null;
}): {
registerRailInteractionLockProbe: (probe: (() => boolean) | null) => void;
} {
const {
createConversationDisabled,
labels,
requestCreateConversation,
requestRenameConversation,
uiLanguage,
viewModel,
workbenchCommandBridge
} = input;
const agentHostApi = useOptionalAgentHostApi();
const railInteractionLockProbeRef = useRef<(() => boolean) | null>(null);
const registerRailInteractionLockProbe = useCallback(
(probe: (() => boolean) | null) => {
railInteractionLockProbeRef.current = probe;
},
[]
);
const copyConversationValue = useAgentGUIConversationCopyAction(labels);
const requestContextRef = useRef({
agentHostApi,
copyConversationValue,
createConversationDisabled,
labels,
requestCreateConversation,
requestRenameConversation,
uiLanguage,
viewModel,
workbenchCommandBridge
});
requestContextRef.current = {
agentHostApi,
copyConversationValue,
createConversationDisabled,
labels,
requestCreateConversation,
requestRenameConversation,
uiLanguage,
viewModel,
workbenchCommandBridge
};
useEffect(() => {
if (!workbenchCommandBridge) {
return;
}
const instanceId = workbenchCommandBridge.instanceId;
const handleCommand = (event: Event): void => {
const command = normalizeAgentGuiWorkbenchCommand(
(event as CustomEvent<unknown>).detail
);
if (!command || command.instanceId !== instanceId) {
return;
}
const current = requestContextRef.current;
if (command.type === "conversation-rail-toggle") {
current.workbenchCommandBridge?.onConversationRailToggle?.(
command.conversationRailCollapsed
);
return;
}
if (command.type === "new-conversation") {
if (!current.createConversationDisabled) {
current.requestCreateConversation({ source: "external_request" });
}
return;
}
const conversation = resolveSessionActionConversation(
current.viewModel,
command.agentSessionId
);
const railInteractionLocked =
railInteractionLockProbeRef.current?.() ?? false;
if (!conversation || railInteractionLocked) {
current.agentHostApi?.toast?.error(
current.labels.sessionActionUnavailable
);
} else if (command.action === "rename") {
current.requestRenameConversation(conversation);
} else {
current.copyConversationValue(command.action, {
conversation,
uiLanguage: current.uiLanguage,
workspaceId: current.viewModel.shell.workspaceId
});
}
};
window.addEventListener(AGENT_GUI_WORKBENCH_COMMAND_EVENT, handleCommand);
return () => {
window.removeEventListener(
AGENT_GUI_WORKBENCH_COMMAND_EVENT,
handleCommand
);
};
}, [workbenchCommandBridge?.instanceId]);
return { registerRailInteractionLockProbe };
}