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

654 lines
23 KiB
TypeScript

import { memo, useCallback, useMemo, useRef } from "react";
import { createWorkspaceUserProjectI18nRuntime } from "@tutti-os/workspace-user-project/i18n";
import { createWorkspaceFileManagerI18nRuntime } from "@tutti-os/workspace-file-manager";
import type { WorkspaceFileReference } from "@tutti-os/workspace-file-reference/contracts";
import { useTranslation } from "../../i18n/index";
import type { WorkspaceLinkAction } from "../../actions/workspaceLinkActions";
import type { AgentGUIAgentTarget, AgentGUINodeData } from "../../types";
import { resolveCanonicalNodeMinSize } from "../../utils/workspaceNodeSizing";
import { WorkspaceNodeWindow } from "../shared/WorkspaceNodeWindow";
import { CanvasNodeGhostIconButton } from "../shared/CanvasNodeGhostIconButton";
import { CanvasNodePanelLinedIcon } from "../shared/canvasNodeChromeIcons";
import { useAgentGUINodeController } from "./controller/useAgentGUINodeController";
import { useAgentGUIStatus } from "./controller/useAgentGUIStatus";
import { agentTargetForConversation } from "./controller/agentGuiController.providerHelpers";
import { AgentGUINodeView } from "./AgentGUINodeView";
import {
normalizeAgentGUIProviderIdentity,
resolveAgentGUIProviderDisplayLabel
} from "./model/agentGuiProviderIdentity";
import { AgentProbeInfoPopover } from "../workspaceDesktop/view/AgentProbeInfoPopover";
import styles from "./AgentGUINode.styles";
import {
AGENT_GUI_COLLAPSED_MIN_WIDTH_PX,
AGENT_GUI_CONVERSATION_RAIL_MIN_WIDTH_PX,
AGENT_GUI_DETAIL_MIN_WIDTH_PX,
clampAgentGUIConversationRailWidthPx,
resolveAgentGUIConversationRailPresentation,
resolveAgentGUIExpandedWindowFrame,
resolveNextAgentGUIConversationRailWidthPx,
resolveAgentGUIConversationRailMaxWidthPx
} from "./model/agentGuiRailLayout";
import type {
AgentGUIAgentConfigMenuContext,
AgentGUINodeProps
} from "./AgentGUINode.types";
import { areAgentGUINodePropsEqual } from "./AgentGUINode.types";
import { AgentGUIMentionServiceBoundary } from "./AgentGUIMentionServiceBoundary";
import {
useAgentGUIViewLabels,
useAgentGUIConversationRailLabels,
useAgentGUIWorkspaceFileReferenceCopy
} from "./AgentGUINode.labels";
import { useAgentMentionProvenanceFilters } from "./composer/useAgentMentionProvenanceFilters";
export type { AgentGUINodeProps } from "./AgentGUINode.types";
export const AgentGUINode = memo(function AgentGUINode({
identity,
workspace,
frame,
state,
runtimeRequests,
hostCapabilities,
hostActions,
renderSlots
}: AgentGUINodeProps): React.JSX.Element {
"use memo";
const { nodeId, workspaceId, currentUserId, title } = identity;
const {
path: workspacePath,
fileReferenceAdapter: workspaceFileReferenceAdapter = null,
onRequestGitBranches = null,
selectProjectDirectory,
resolveExternalPromptEntries = null,
prepareExternalPromptFiles = null,
resolvePastedPath = null,
promptAssetLimit = null,
projectDirectorySourceAggregator = null,
referenceSourceAggregator = null,
resolveReferenceContentErrorAction:
resolveWorkspaceReferenceContentErrorAction,
resolveReferenceEntryIconUrl: resolveWorkspaceReferenceEntryIconUrl,
resolveMentionReferenceTarget = null,
resolveReferenceInitialTarget:
resolveWorkspaceReferenceInitialTarget = null,
onFileReferencesAdded: onWorkspaceFileReferencesAdded,
agentSettings
} = workspace;
const {
position,
width,
height,
desktopSize,
isMaximized = false,
isActive,
isVisible = true,
embedded = false
} = frame;
const widthRef = useRef(width);
widthRef.current = width;
const {
composerAppend: composerAppendRequest = null,
composerFocusSequence: composerFocusRequestSequence = null,
workbench: workbenchCommandBridge = null,
openSession: openSessionRequest = null,
prefillPrompt: prefillPromptRequest = null,
agentStatusController
} = runtimeRequests;
const {
capabilityMenuState,
capabilityControlsReadOnly = false,
agentTargets,
agentTargetsLoading = false,
mentionAgentTargets,
handoffAgentTargets,
handoffAgentTargetsLoading = false,
showHandoffTargetOwnershipLabels = false,
providerRailAllPresentation = null,
providerRailMode = "catalog",
comingSoonProviders,
providerReadinessGates = null,
accountUsageRefreshInline = false,
targetConnectionSource = null,
interactionReadinessSource = null,
observationGapSource = null,
defaultAgentTargetId = null,
providerAuthAccountLabels,
mentionService,
workspaceAppIcons,
hiddenMentionFilterIds,
disabledHomeSuggestions,
referenceProvenanceFilterCatalog: injectedReferenceProvenanceFilterCatalog,
referenceProvenanceFilterEnabled = false,
sessionInputHistoryEnabled = false,
sideConversationEnabled = false,
sideConversationPresentation = null,
sessionWorktreeEnabled = false,
sessionLaunchModesByProjectSectionKey,
codexSaverModeEntryEnabled = false,
rtkSaverModeEntryEnabled = false
} = hostCapabilities;
const referenceProvenanceFilters = useAgentMentionProvenanceFilters({
agentTargets,
injectedCatalog: injectedReferenceProvenanceFilterCatalog,
legacyAgentFilterEnabled: referenceProvenanceFilterEnabled
});
const {
onComposerAppendHandled,
onLinkAction,
onHandoffConversation,
onCapabilitySettingsRequest,
onAgentProviderLogin,
onAgentEnvPanelOpen,
onAgentConfigMenuOpen: onHostAgentConfigMenuOpen,
onOpenConversationWindow,
onClose,
onResize,
onUpdateNode,
onRememberComposerDefaults,
onSessionLaunchModePreferenceChange,
isMuted = false,
onMinimize,
onToggleMaximize,
onShowMessage,
onEngagementEvent,
onConversationRailLayoutChange
} = hostActions;
const {
agentConfigAccount: renderAgentConfigAccount,
agentConfigSystemActions: renderAgentConfigSystemActions,
agentTargetInfo: renderAgentTargetInfo,
composerFooterAccessory: renderComposerFooterAccessory,
projectDirectoryPickerHeaderActions:
renderProjectDirectoryPickerHeaderActions,
projectSelectOptions,
referencePickerSidebarActions: renderReferencePickerSidebarActions,
providerRailEmpty: renderProviderRailEmpty,
sidebarFooter: renderSidebarFooter
} = renderSlots;
const { i18n, locale, t } = useTranslation();
const workspaceUserProjectI18n = useMemo(
() => createWorkspaceUserProjectI18nRuntime(i18n),
[i18n]
);
const workspaceFileManagerI18n = useMemo(
() =>
typeof i18n?.t === "function"
? createWorkspaceFileManagerI18nRuntime(i18n)
: null,
[i18n]
);
const handleLinkAction = useCallback(
(action: WorkspaceLinkAction) => {
onLinkAction?.(action);
},
[onLinkAction]
);
const handleAgentProviderLogin = useCallback(
(provider?: string | null) => {
const resolvedProvider = normalizeAgentGUIProviderIdentity(provider);
onAgentProviderLogin?.(
resolvedProvider === "unknown" ? state.provider : resolvedProvider
);
},
[onAgentProviderLogin, state.provider]
);
const handleWorkspaceFileReferencesAdded = useCallback(
(references: readonly WorkspaceFileReference[]) => {
onWorkspaceFileReferencesAdded?.({
provider: state.provider,
references
});
},
[onWorkspaceFileReferencesAdded, state.provider]
);
const handleDataChange = useCallback(
(updater: (current: AgentGUINodeData) => AgentGUINodeData) => {
onUpdateNode(updater);
},
[onUpdateNode]
);
const handleConversationRailWidthChanged = useCallback(
(widthPx: number) => {
onUpdateNode((current) => {
const nextWidthPx = resolveNextAgentGUIConversationRailWidthPx({
currentWidthPx: current.conversationRailWidthPx,
requestedWidthPx: widthPx,
containerWidthPx: widthRef.current
});
if (current.conversationRailWidthPx === nextWidthPx) {
return current;
}
return {
...current,
conversationRailWidthPx: nextWidthPx
};
});
},
[onUpdateNode]
);
const conversationRailPresentation =
resolveAgentGUIConversationRailPresentation({
autoCollapseMode: frame.conversationRailAutoCollapseMode,
containerWidthPx: width,
conversationRailCollapsed: state.conversationRailCollapsed,
conversationRailWidthPx: state.conversationRailWidthPx
});
const isConversationRailAutoCollapsed =
conversationRailPresentation.isAutoCollapsed;
const isConversationRailCollapsed = conversationRailPresentation.isCollapsed;
const minSize = useMemo(
() => ({
...resolveCanonicalNodeMinSize("agentGui"),
width: AGENT_GUI_COLLAPSED_MIN_WIDTH_PX
}),
[]
);
const toggleConversationRailCollapsed = useCallback(() => {
onUpdateNode((current) => ({
...current,
conversationRailCollapsed: current.conversationRailCollapsed !== true
}));
}, [onUpdateNode]);
const handleConversationRailToggle = useCallback(() => {
if (!isConversationRailAutoCollapsed) {
toggleConversationRailCollapsed();
return;
}
onResize(
resolveAgentGUIExpandedWindowFrame({
position,
width,
height,
desktopSize,
conversationRailWidthPx: state.conversationRailWidthPx
})
);
onUpdateNode((current) => {
if (current.conversationRailCollapsed !== true) {
return current;
}
return {
...current,
conversationRailCollapsed: false
};
});
}, [
desktopSize,
height,
isConversationRailAutoCollapsed,
onResize,
onUpdateNode,
position,
state.conversationRailWidthPx,
toggleConversationRailCollapsed,
width
]);
const { viewModel, actions } = useAgentGUINodeController({
nodeId,
isSurfaceActive: isActive,
isSurfaceVisible: isVisible,
workspaceId,
currentUserId,
workspacePath,
avoidGroupingEdits: agentSettings.avoidGroupingEdits,
data: state,
composerAppendRequest,
openSessionRequest,
prefillPromptRequest,
codexSaverModeEntryEnabled,
rtkSaverModeEntryEnabled,
agentTargets,
agentTargetsLoading,
handoffAgentTargets,
handoffAgentTargetsLoading,
providerRailMode,
comingSoonProviders,
providerReadinessGates,
targetConnectionSource,
interactionReadinessSource,
observationGapSource,
defaultAgentTargetId,
onDataChange: handleDataChange,
onComposerAppendHandled,
onRememberComposerDefaults,
onShowMessage
});
const handleCreateConversation = useCallback(
(...args: Parameters<typeof actions.createConversation>) => {
{
onUpdateNode((current) =>
current.lastActiveAgentSessionId === null
? current
: {
...current,
lastActiveAgentSessionId: null
}
);
}
actions.createConversation(...args);
},
[actions, onUpdateNode]
);
const viewActions = useMemo(
() => ({
...actions,
createConversation: handleCreateConversation
}),
[actions, handleCreateConversation]
);
const fallbackAgentTitle = t("sidebar.fallbackAgentLabel");
const activeProvider =
viewModel.rail.activeConversation?.provider ?? state.provider;
const activeConversationAgentTarget = agentTargetForConversation(
viewModel.rail.activeConversation,
viewModel.rail.agentTargets
);
const displayProviderLabel = resolveAgentGUIProviderDisplayLabel(
activeProvider,
fallbackAgentTitle,
viewModel.rail.activeConversation
? activeConversationAgentTarget?.label
: viewModel.rail.selectedAgentTarget?.label
);
const conversationRailLabels = useAgentGUIConversationRailLabels(t);
const labels = useAgentGUIViewLabels({
disabledHomeSuggestions,
displayProviderLabel,
fallbackAgentTitle,
t,
workspaceAppIcons: workspaceAppIcons ?? [],
workspaceId
});
const workspaceFileReferenceCopy = useAgentGUIWorkspaceFileReferenceCopy(t);
const windowTitle = title;
const {
agentProbeLines,
controllerRailStatus,
handleAgentConfigMenuClose,
handleAgentConfigMenuOpen: handleStatusAgentConfigMenuOpen,
handleAgentProbeInfoClose,
handleAgentProbeInfoOpen,
handleAgentUsageRefresh,
handleSlashStatusClose,
handleSlashStatusOpen,
handleSlashStatusRefresh,
railStatusProvider,
slashStatusLimits,
slashStatusLimitsUnavailable,
slashStatusOverride
} = useAgentGUIStatus({
activeProvider,
agentStatusController,
t,
viewModel
});
const effectiveProviderAuthAccountLabels = projectProviderAccountLabel(
providerAuthAccountLabels,
railStatusProvider,
controllerRailStatus?.accountLabel
);
const agentConfigMenuContext =
viewModel.rail.conversationFilter.kind === "all"
? null
: resolveAgentConfigMenuContext(viewModel.rail.selectedAgentTarget);
const handleAgentConfigMenuOpen = () => {
handleStatusAgentConfigMenuOpen();
if (agentConfigMenuContext) {
onHostAgentConfigMenuOpen?.(agentConfigMenuContext);
}
};
const agentConfigAccountContent = agentConfigMenuContext
? (renderAgentConfigAccount?.(agentConfigMenuContext) ?? null)
: null;
const agentConfigSystemActionsContent =
renderAgentConfigSystemActions?.({ presentation: "menu" }) ?? null;
return (
<AgentGUIMentionServiceBoundary
service={mentionService}
observationGapSource={observationGapSource}
>
<WorkspaceNodeWindow
nodeId={nodeId}
kind="agentGui"
title={windowTitle}
titleIcon={null}
position={position}
width={width}
height={height}
desktopSize={desktopSize}
minSize={minSize}
appearance={embedded ? "embedded" : "window"}
className="size-full bg-transparent"
bodyClassName={`${styles.shell} nodrag size-full min-h-0 min-w-0 !bg-transparent p-0`}
hideHeader={embedded}
titleAccessory={
<span className="inline-flex flex-none items-center gap-1">
<AgentProbeInfoPopover
lines={agentProbeLines}
testId="agent-gui-window-agent-info"
className={styles.windowAgentInfo}
onOpen={handleAgentProbeInfoOpen}
onClose={handleAgentProbeInfoClose}
/>
<CanvasNodeGhostIconButton
aria-label={
isConversationRailCollapsed
? t("agentHost.agentGui.expandConversationRail")
: t("agentHost.agentGui.collapseConversationRail")
}
title={
isConversationRailCollapsed
? t("agentHost.agentGui.expandConversationRail")
: t("agentHost.agentGui.collapseConversationRail")
}
data-testid="agent-gui-toggle-conversation-rail"
data-agent-gui-conversation-rail-collapsed={
isConversationRailCollapsed ? "true" : "false"
}
data-agent-gui-conversation-rail-auto-collapsed={
isConversationRailAutoCollapsed ? "true" : "false"
}
onClick={(event) => {
event.stopPropagation();
handleConversationRailToggle();
}}
>
<CanvasNodePanelLinedIcon
width={18}
height={18}
aria-hidden="true"
/>
</CanvasNodeGhostIconButton>
</span>
}
onClose={onClose}
onResize={onResize}
isMaximized={isMaximized}
isMuted={isMuted}
hideMaximizeButton
onMinimize={onMinimize}
onToggleMaximize={onToggleMaximize}
>
{(renderFrame) => {
const renderedWidth = renderFrame.size.width;
const isRenderedConversationRailCollapsed =
isConversationRailCollapsed ||
resolveAgentGUIConversationRailPresentation({
autoCollapseMode: frame.conversationRailAutoCollapseMode,
containerWidthPx: renderedWidth,
conversationRailCollapsed: state.conversationRailCollapsed,
conversationRailWidthPx: state.conversationRailWidthPx
}).isCollapsed;
return (
<AgentGUINodeView
viewModel={viewModel}
mentionAgentTargets={mentionAgentTargets}
hiddenMentionFilterIds={hiddenMentionFilterIds}
renderAgentTargetInfo={renderAgentTargetInfo}
renderSidebarFooter={renderSidebarFooter}
renderProviderRailEmpty={renderProviderRailEmpty}
providerRailAllPresentation={providerRailAllPresentation}
actions={viewActions}
isActive={isActive}
isVisible={isVisible}
onEngagementEvent={onEngagementEvent}
composerFocusRequestSequence={composerFocusRequestSequence}
workbenchCommandBridge={workbenchCommandBridge}
slashStatusLimits={slashStatusLimits}
slashStatusLimitsLoading={controllerRailStatus?.loading ?? false}
slashStatusLimitsUnavailable={slashStatusLimitsUnavailable}
slashStatusOverride={slashStatusOverride}
railConfigProvider={railStatusProvider}
railSlashStatusLimits={controllerRailStatus?.limits ?? []}
slashStatusUsageCapturedAtUnixMs={
controllerRailStatus?.capturedAtUnixMs ?? null
}
slashStatusUsageDidFail={controllerRailStatus?.didFail ?? false}
slashStatusUsageErrorMessage={
controllerRailStatus?.errorMessage ?? null
}
slashStatusUsageAttempted={
controllerRailStatus?.attempted ?? false
}
slashStatusLimitsResolvedEmpty={
controllerRailStatus?.resolvedEmpty ?? false
}
agentConfigAccountContent={agentConfigAccountContent}
agentConfigSystemActionsContent={agentConfigSystemActionsContent}
providerAuthAccountLabels={effectiveProviderAuthAccountLabels}
onAgentConfigMenuClose={handleAgentConfigMenuClose}
onAgentConfigMenuOpen={handleAgentConfigMenuOpen}
onAgentUsageRefresh={handleAgentUsageRefresh}
accountUsageRefreshInline={accountUsageRefreshInline}
onSlashStatusOpen={handleSlashStatusOpen}
onSlashStatusClose={handleSlashStatusClose}
onSlashStatusRefresh={handleSlashStatusRefresh}
onLinkAction={handleLinkAction}
onHandoffConversation={onHandoffConversation}
showHandoffTargetOwnershipLabels={
showHandoffTargetOwnershipLabels
}
capabilityMenuState={capabilityMenuState}
capabilityControlsReadOnly={capabilityControlsReadOnly}
onCapabilitySettingsRequest={onCapabilitySettingsRequest}
onAgentProviderLogin={
onAgentProviderLogin ? handleAgentProviderLogin : undefined
}
onAgentEnvPanelOpen={onAgentEnvPanelOpen}
conversationRailCollapsed={isRenderedConversationRailCollapsed}
conversationRailWidthPx={clampAgentGUIConversationRailWidthPx(
state.conversationRailWidthPx,
renderedWidth
)}
conversationRailMinWidthPx={
AGENT_GUI_CONVERSATION_RAIL_MIN_WIDTH_PX
}
conversationRailMaxWidthPx={resolveAgentGUIConversationRailMaxWidthPx(
renderedWidth
)}
detailMinWidthPx={AGENT_GUI_DETAIL_MIN_WIDTH_PX}
uiLanguage={locale}
onWorkspaceFileReferencesAdded={
onWorkspaceFileReferencesAdded
? handleWorkspaceFileReferencesAdded
: undefined
}
resolveExternalPromptEntries={resolveExternalPromptEntries}
prepareExternalPromptFiles={prepareExternalPromptFiles}
resolvePastedPath={resolvePastedPath}
promptAssetLimit={promptAssetLimit}
onConversationRailWidthChanged={
handleConversationRailWidthChanged
}
onConversationRailLayoutChange={onConversationRailLayoutChange}
labels={labels}
conversationRailLabels={conversationRailLabels}
workspaceUserProjectI18n={workspaceUserProjectI18n}
workspaceFileManagerCopy={workspaceFileManagerI18n}
workspaceFileReferenceAdapter={workspaceFileReferenceAdapter}
onOpenConversationWindow={onOpenConversationWindow}
onRequestGitBranches={onRequestGitBranches}
selectProjectDirectory={selectProjectDirectory}
projectDirectorySourceAggregator={
projectDirectorySourceAggregator
}
referenceSourceAggregator={referenceSourceAggregator}
resolveReferenceContentErrorAction={
resolveWorkspaceReferenceContentErrorAction
}
resolveWorkspaceReferenceEntryIconUrl={
resolveWorkspaceReferenceEntryIconUrl
}
resolveMentionReferenceTarget={resolveMentionReferenceTarget}
resolveWorkspaceReferenceInitialTarget={
resolveWorkspaceReferenceInitialTarget
}
workspaceFileReferenceCopy={workspaceFileReferenceCopy}
workspaceAppIcons={workspaceAppIcons}
referenceProvenanceFilters={referenceProvenanceFilters}
sessionInputHistoryEnabled={sessionInputHistoryEnabled}
sideConversationEnabled={sideConversationEnabled}
sideConversationPresentation={sideConversationPresentation}
sessionWorktreeEnabled={sessionWorktreeEnabled}
sessionLaunchModesByProjectSectionKey={
sessionLaunchModesByProjectSectionKey
}
onSessionLaunchModePreferenceChange={
onSessionLaunchModePreferenceChange
}
renderProjectDirectoryPickerHeaderActions={
renderProjectDirectoryPickerHeaderActions
}
projectSelectOptions={projectSelectOptions}
renderReferencePickerSidebarActions={
renderReferencePickerSidebarActions
}
renderComposerFooterAccessory={renderComposerFooterAccessory}
/>
);
}}
</WorkspaceNodeWindow>
</AgentGUIMentionServiceBoundary>
);
}, areAgentGUINodePropsEqual);
function projectProviderAccountLabel(
labels: Partial<Record<string, string>> | undefined,
providerValue: string | null | undefined,
accountLabelValue: string | null | undefined
): Partial<Record<string, string>> | undefined {
const provider = providerValue?.trim();
const accountLabel = accountLabelValue?.trim();
if (!provider || !accountLabel || labels?.[provider]?.trim()) {
return labels;
}
return {
...labels,
[provider]: accountLabel
};
}
function resolveAgentConfigMenuContext(
target: AgentGUIAgentTarget
): AgentGUIAgentConfigMenuContext | null {
const agentTargetId = target.agentTargetId?.trim() || target.targetId.trim();
const provider = target.provider.trim();
if (!agentTargetId || !provider) {
return null;
}
return {
agentTargetId,
provider,
label: target.label,
presentation: "menu",
...(target.ownership ? { ownership: target.ownership } : {})
};
}