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

288 lines
9.7 KiB
TypeScript

import { useCallback, useMemo, type ReactNode } from "react";
import type { AgentGUISideConversationPresentation } from "../../../agentSideConversationPresentation";
import type { AgentComposerProps } from "../AgentComposer";
import type { AgentGUIDetailPaneProps } from "./AgentGUINodeView.types";
import type { useAgentGUIDetailSideConversation } from "./useAgentGUIDetailSideConversation";
import {
projectAgentSideCapabilityMenuState,
projectAgentSideComposerGate,
projectAgentSideComposerSettings
} from "../model/agentGuiSideComposerPolicy";
import { useTranslation } from "../../../i18n/index";
import {
AgentGUISideConversationPane,
type AgentGUISideConversationPaneProps,
type AgentGUISideConversationSurfaceProps
} from "./AgentGUISideConversationPane";
import { appendAgentComposerDraftQuote } from "../model/agentComposerDraft";
import { AgentGUISideConversationPresentationPublisher } from "./AgentGUISideConversationPresentationPublisher";
const EMPTY_WORKSPACE_APP_ICONS: NonNullable<
AgentComposerProps["workspaceAppIcons"]
> = [];
interface UseAgentGUIDetailSideChromeInput {
availableSkills: AgentGUISideConversationPaneProps["availableSkills"];
baseComposerProps: AgentComposerProps;
controller: ReturnType<typeof useAgentGUIDetailSideConversation>;
conversationFlowLabels: AgentGUISideConversationPaneProps["conversationFlowLabels"];
isVisible: boolean;
presentation?: AgentGUISideConversationPresentation | null;
textSelectionActionsEnabled: boolean;
onRequestComposerFocus: () => void;
renderComposerFooterAccessory: AgentGUIDetailPaneProps["renderComposerFooterAccessory"];
}
export function useAgentGUIDetailSideChrome({
availableSkills,
baseComposerProps,
controller,
conversationFlowLabels,
isVisible,
presentation = null,
textSelectionActionsEnabled,
onRequestComposerFocus,
renderComposerFooterAccessory
}: UseAgentGUIDetailSideChromeInput): {
bottomDockComposerProps: AgentComposerProps;
selectionProps: {
onAddSelectionToConversation: (text: string) => void;
onAskSelectionInSide?: (text: string) => void;
textSelectionActionsEnabled: boolean;
};
sidePane: ReactNode;
} {
const { t } = useTranslation();
const {
active,
close,
draftContent,
entryError,
focused,
focusRequestSequence,
interactionSubmitting,
interactivePrompt,
interrupt,
setDraftContent,
setFocused,
sourceAgentSessionId,
stageSelection,
submitInteraction,
submitSide
} = controller;
const hostFooterAccessory = useMemo(
() =>
renderComposerFooterAccessory?.({
agentSessionId: baseComposerProps.agentSessionId,
isActive: baseComposerProps.isActive,
isSendingTurn: baseComposerProps.isSendingTurn,
isSubmittingPrompt: baseComposerProps.isSubmittingPrompt,
composerSettings: baseComposerProps.composerSettings,
selectedAgentTarget: baseComposerProps.selectedAgentTarget
}) ?? null,
[
baseComposerProps.agentSessionId,
baseComposerProps.composerSettings,
baseComposerProps.isActive,
baseComposerProps.isSendingTurn,
baseComposerProps.isSubmittingPrompt,
baseComposerProps.selectedAgentTarget,
renderComposerFooterAccessory
]
);
const footerAccessory = useMemo(() => {
if (!entryError && hostFooterAccessory === null) return null;
return (
<>
{entryError ? (
<span className="text-xs text-destructive">
{entryError === "content_unsupported"
? t("agentHost.agentGui.sideContentUnsupported")
: t("agentHost.agentGui.sideOperationFailed")}
</span>
) : null}
{hostFooterAccessory}
</>
);
}, [entryError, hostFooterAccessory, t]);
const bottomDockComposerProps = useMemo<AgentComposerProps>(
() => ({ ...baseComposerProps, footerAccessory }),
[baseComposerProps, footerAccessory]
);
const addSelectionToConversation = useCallback(
(text: string) => {
baseComposerProps.onDraftContentChange(
appendAgentComposerDraftQuote(baseComposerProps.draftContent, {
type: "quote",
id: crypto.randomUUID(),
text
})
);
onRequestComposerFocus();
},
[baseComposerProps, onRequestComposerFocus]
);
const askSelectionInSide = useCallback(
(text: string) => {
void stageSelection(text).catch(() => {});
},
[stageSelection]
);
const selectionProps = useMemo(
() => ({
onAddSelectionToConversation: addSelectionToConversation,
onAskSelectionInSide: controller.canOpen ? askSelectionInSide : undefined,
textSelectionActionsEnabled
}),
[
addSelectionToConversation,
askSelectionInSide,
controller.canOpen,
textSelectionActionsEnabled
]
);
const sideComposerProps = useMemo<AgentComposerProps | null>(() => {
if (!active) return null;
return {
workspaceId: baseComposerProps.workspaceId,
agentSessionId: active.sideAgentSessionId,
workspacePath: baseComposerProps.workspacePath,
currentUserId: baseComposerProps.currentUserId,
provider: baseComposerProps.provider,
draftContent,
draftScopeKey: `side:${active.sideAgentSessionId}`,
inputHistory: [],
availableCommands: [],
hasCompactableContext: false,
compactSupported: false,
availableSkills: [],
gate: projectAgentSideComposerGate(active),
presentationEditorDisabled: false,
presentationSubmitDisabled: false,
placeholder: t("agentHost.agentGui.sideInputPlaceholder"),
composerSettings: projectAgentSideComposerSettings(
baseComposerProps.composerSettings
),
queuedPrompts: [],
drainingQueuedPromptId: null,
workspaceAppIcons: baseComposerProps.workspaceAppIcons,
selectedAgentTarget: baseComposerProps.selectedAgentTarget,
agentTargets: [],
handoffAgentTargets: [],
providerSelectReadonly: true,
showStopButton: Boolean(active.activeTurnId),
stopDisabled: false,
activePrompt: interactivePrompt,
activePromptKeyboardShortcutsEnabled:
baseComposerProps.isActive && focused,
composerFocusRequestSequence: focusRequestSequence,
promptTips: [],
isInterrupting: false,
isSendingTurn: Boolean(active.activeTurnId),
isSubmittingPrompt: interactionSubmitting,
projectMissingProbeEnabled: false,
uiLanguage: baseComposerProps.uiLanguage,
// isActive controls whether the editor installs its focus and input
// behavior. It cannot depend on focus itself or the first click has no
// active editor with which to establish that focus.
isActive: baseComposerProps.isActive && isVisible,
workspaceReferencePickerOpen: false,
promptImagesSupported: false,
canGoalControl: false,
canUploadAttachment: false,
labels: {
...baseComposerProps.labels,
conversationReturn: undefined
},
workspaceUserProjectI18n: baseComposerProps.workspaceUserProjectI18n,
capabilityMenuState: projectAgentSideCapabilityMenuState(
baseComposerProps.capabilityMenuState
),
capabilityControlsReadOnly: true,
onDraftContentChange: setDraftContent,
onSettingsChange: () => {},
onSubmit: submitSide,
onSendQueuedPromptNext: () => {},
onRemoveQueuedPrompt: () => {},
onEditQueuedPrompt: () => {},
onInterruptCurrentTurn: interrupt,
onSubmitInteractivePrompt: (input) => {
void submitInteraction(input).catch(() => {});
// Side is event-confirmed rather than optimistically dismissed. Its
// transient runtime keeps the prompt live on transport failure and
// clears it when the Side event stream confirms the response.
return false;
},
onLinkAction: baseComposerProps.onLinkAction
};
}, [
active,
baseComposerProps,
draftContent,
focused,
focusRequestSequence,
interactionSubmitting,
interactivePrompt,
interrupt,
isVisible,
setDraftContent,
submitInteraction,
submitSide,
t
]);
const sideMatchesSource =
active !== null && active.sourceAgentSessionId === sourceAgentSessionId;
const sideSurfaceProps = useMemo<AgentGUISideConversationSurfaceProps | null>(
() =>
active && sideComposerProps && sideMatchesSource
? {
active,
availableSkills,
composerProps: sideComposerProps,
conversationFlowLabels,
isVisible,
loadingLabel: t("agentHost.agentGui.loadingConversation"),
workspaceAppIcons:
baseComposerProps.workspaceAppIcons ?? EMPTY_WORKSPACE_APP_ICONS,
onFocusChange: setFocused,
onLinkAction: baseComposerProps.onLinkAction
}
: null,
[
active,
availableSkills,
baseComposerProps.onLinkAction,
baseComposerProps.workspaceAppIcons,
conversationFlowLabels,
isVisible,
setFocused,
sideComposerProps,
sideMatchesSource,
t
]
);
const projection = useMemo(
() =>
sideSurfaceProps && active
? {
close,
sideAgentSessionId: active.sideAgentSessionId,
sourceAgentSessionId: active.sourceAgentSessionId,
surfaceProps: sideSurfaceProps
}
: null,
[active, close, sideSurfaceProps]
);
const sidePane = presentation ? (
<AgentGUISideConversationPresentationPublisher
presentation={presentation}
projection={projection}
sourceMismatch={active !== null && !sideMatchesSource}
onClose={close}
/>
) : sideSurfaceProps ? (
<AgentGUISideConversationPane {...sideSurfaceProps} onClose={close} />
) : null;
return { bottomDockComposerProps, selectionProps, sidePane };
}