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

801 lines
31 KiB
TypeScript

import { memo, useCallback, useMemo, useRef } from "react";
import type { AgentTranscriptVirtualScrollController } from "../../../shared/agentConversation/components/AgentTranscriptView";
import { latestAssistantMessageText } from "../../../shared/agentConversation/projection/agentConversationProjection";
import { AGENT_GUI_WORKBENCH_OPEN_EXTERNAL_IMPORT_EVENT } from "../../../workbench/contribution";
import type { AgentComposerProps } from "../AgentComposer";
import type { AgentHomeSuggestionAction } from "../model/agentGuiNodeTypes";
import {
emptyAgentComposerDraft,
updateAgentComposerDraft
} from "../model/agentComposerDraft";
import { resolveAgentComposerDraftScopeKey } from "../model/agentComposerDraftScope";
import {
agentGUIDetailBottomDockStoreRevision,
buildAgentConversationHandoffPrompt,
handoffProjectPathForConversation,
resolveAgentGUIComposerInteractionDisabledReason,
resolveAgentGUITuttiStopTargets
} from "./agentGUIDetailModelHelpers";
import { EMPTY_WORKSPACE_APP_ICONS } from "./agentGUIDetailConstants";
import { AgentGUIBottomDockPane } from "./AgentGUIBottomDockPane";
import {
AgentGUIEmptyHomePane,
EMPTY_HOME_SUGGESTIONS
} from "./AgentGUIEmptyState";
import { AgentGUIContentToast } from "./AgentGUIContentToast";
import { AgentGUIDetailTimeline } from "./AgentGUIDetailTimeline";
import {
useOptionalStableEventCallback,
useStableEventCallback
} from "./agentGUIViewUtils";
import styles from "../AgentGUINode.styles";
import { useAgentGUIDetailScroll } from "./useAgentGUIDetailScroll";
import { useAgentGUIDetailModel } from "./useAgentGUIDetailModel";
import { useAgentGUIComposerInputHistoryProps } from "./useAgentGUIComposerInputHistoryProps";
import { useAgentGUITuttiWorkflow } from "./useAgentGUITuttiWorkflow";
import { AgentGUITuttiPlanReviewActionSlot } from "./AgentGUITuttiPlanReviewAction";
import type { AgentGUIDetailPaneProps } from "./AgentGUIDetailPane.types";
import { useAgentGUIDetailEditRetry } from "./useAgentGUIDetailEditRetry";
import { useAgentGUIDetailSideConversation } from "./useAgentGUIDetailSideConversation";
import { useAgentGUIDetailSideChrome as useSideChrome } from "./useAgentGUIDetailSideChrome";
import type { TimelineScrollAnchor } from "./agentGUIScrollMemory";
import { useBottomDockInteractionSubmission } from "./useBottomDockInteractionSubmission";
import type { AgentGUIPendingPrependScrollAnchor } from "./agentGUIDetailScrollTypes";
export const AgentGUIDetailPane = memo(function AgentGUIDetailPane({
shell,
rail,
detail,
composer,
interaction,
readiness,
operations,
homeTargetProjection,
referenceProvenanceFilters = null,
hiddenMentionFilterIds = [],
sessionInputHistoryEnabled = false,
sideConversationEnabled = false,
sideConversationPresentation = null,
sessionWorktreeEnabled = false,
sessionLaunchModesByProjectSectionKey,
onSessionLaunchModePreferenceChange,
composerEngagement,
actions,
labels,
workspaceUserProjectI18n,
uiLanguage,
isActive,
isVisible,
workspaceReferencePickerOpen,
composerFocusRequestSequence,
slashStatusLimits,
slashStatusLimitsLoading,
slashStatusLimitsUnavailable,
slashStatusOverride,
onSlashStatusOpen,
onSlashStatusClose,
onSlashStatusRefresh,
onLinkAction,
onHandoffConversation,
showHandoffTargetOwnershipLabels = false,
capabilityMenuState,
capabilityControlsReadOnly = false,
onCapabilitySettingsRequest,
onAgentProviderLogin,
onRequestWorkspaceReferences,
resolveExternalPromptEntries = null,
prepareExternalPromptFiles = null,
resolvePastedPath = null,
promptAssetLimit = null,
selectProjectDirectory,
projectSelectOptions,
onRequestGitBranches,
onRequestComposerFocus,
workspaceAppIcons = EMPTY_WORKSPACE_APP_ICONS,
renderComposerFooterAccessory
}: AgentGUIDetailPaneProps): React.JSX.Element {
"use memo";
const viewModel = {
shell,
rail,
detail,
composer,
interaction,
readiness,
operations
};
const bottomDockRef = useRef<HTMLDivElement | null>(null);
const pendingPrependScrollAnchorRef =
useRef<AgentGUIPendingPrependScrollAnchor | null>(null);
const submittedPromptScrollConversationRef = useRef<string | null>(null);
const timelineContentRef = useRef<HTMLDivElement | null>(null);
const timelineRef = useRef<HTMLDivElement | null>(null);
const timelineScrollAnchorRef = useRef<TimelineScrollAnchor | null>(null);
const virtualScrollControllerRef =
useRef<AgentTranscriptVirtualScrollController | null>(null);
const {
activePromptResponsePending,
bottomDockLiftedPrompt,
bottomDockReplacementPrompt,
chromeLabels,
composerActivePrompt,
composerDisabledReason,
composerGate,
composerLabels,
conversation,
conversationFlowEmpty,
conversationFlowLabels,
dismissBottomDockPrompt,
emptyProviderReadinessGate,
goalBannerLabels,
hasActiveConversation,
homeNoticeChrome,
inlineNoticeChrome,
interactivePromptLabels,
isComposerSending,
sessionChrome,
showStopButton,
stopDisabled,
showTimelineSkeleton,
showUnavailableChatEmpty,
slashStatus: derivedSlashStatus,
timelineConversationId,
timelineInteractionLocked
} = useAgentGUIDetailModel({
labels,
slashStatusLimits,
slashStatusLimitsLoading,
slashStatusLimitsUnavailable,
viewModel
});
const slashStatus = slashStatusOverride ?? derivedSlashStatus;
const handleForkThroughTurn = useStableEventCallback((turnId: string) => {
const agentSessionId =
conversation?.sourceDetail.session.agentSessionId.trim() ?? "";
if (agentSessionId) {
void actions.forkConversationThroughTurn(agentSessionId, turnId);
}
});
const openForkSourceSession = useStableEventCallback(
actions.openForkSourceConversation
);
const editRetry = useAgentGUIDetailEditRetry({
agentSessionId: viewModel.rail.activeConversationId,
workspaceId: viewModel.shell.workspaceId
});
const submitApproval = useStableEventCallback(actions.submitApprovalOption);
const retryActivation = useStableEventCallback(actions.retryActivation);
const retryTuttiModeActivation = useStableEventCallback(
actions.retryTuttiModeActivation
);
const retryInlineNotice =
viewModel.composer.tuttiModeUpdateStatus === "failed"
? retryTuttiModeActivation
: retryActivation;
const continueInNewConversation = useStableEventCallback(
actions.continueInNewConversation
);
const updateDraftContent = useStableEventCallback(actions.updateDraftContent);
const clearMainDraft = useStableEventCallback(() =>
updateDraftContent(emptyAgentComposerDraft())
);
const updateSelectedProjectPath = useOptionalStableEventCallback(
actions.updateSelectedProjectPath
);
const updateComposerSettings = useStableEventCallback(
actions.updateComposerSettings
);
const retryComposerOptions = useStableEventCallback(
actions.retryComposerOptions
);
const setTuttiModeEffect = useStableEventCallback(actions.setTuttiModeEffect);
const setTuttiModeSpeed = useStableEventCallback(actions.setTuttiModeSpeed);
const updatePlanIssueBudgetPreset = useStableEventCallback(
actions.updatePlanIssueBudgetPreset
);
const selectHomeComposerAgentTarget = useStableEventCallback(
actions.selectHomeComposerAgentTarget
);
const selectHomeComposerAgentTargetAndFocus = useCallback(
(input: Parameters<typeof selectHomeComposerAgentTarget>[0]) => {
selectHomeComposerAgentTarget(input);
onRequestComposerFocus();
},
[onRequestComposerFocus, selectHomeComposerAgentTarget]
);
const handleSelectHomeSuggestion = useCallback(
(prompt: string) => {
updateDraftContent(
updateAgentComposerDraft(viewModel.composer.draftContent, { prompt })
);
},
[updateDraftContent, viewModel.composer.draftContent]
);
const handleHomeSuggestionAction = useCallback(
(action: AgentHomeSuggestionAction) => {
if (action === "import-session") {
window.dispatchEvent(
new CustomEvent(AGENT_GUI_WORKBENCH_OPEN_EXTERNAL_IMPORT_EVENT)
);
}
},
[]
);
const submitPrompt = useStableEventCallback(actions.submitPrompt);
const goalControl = useStableEventCallback(actions.goalControl);
const submitGuidancePrompt = useStableEventCallback(
actions.submitGuidancePrompt
);
const requestSubmittedPromptScrollToBottom = useStableEventCallback(() => {
const activeConversationId = viewModel.rail.activeConversationId;
if (!activeConversationId) {
return;
}
submittedPromptScrollConversationRef.current = activeConversationId;
pendingPrependScrollAnchorRef.current = null;
});
const submitPromptAndScrollToBottom = useStableEventCallback(
(...args: Parameters<typeof submitPrompt>): void => {
requestSubmittedPromptScrollToBottom();
submitPrompt(...args);
}
);
const submitGuidancePromptAndScrollToBottom = useStableEventCallback(
(...args: Parameters<typeof submitGuidancePrompt>): void => {
requestSubmittedPromptScrollToBottom();
submitGuidancePrompt(...args);
}
);
const showPromptImagesUnsupported = useStableEventCallback(
actions.showPromptImagesUnsupported
);
const sendQueuedPromptNext = useStableEventCallback(
actions.sendQueuedPromptNext
);
const removeQueuedPrompt = useStableEventCallback(actions.removeQueuedPrompt);
const editQueuedPrompt = useStableEventCallback(actions.editQueuedPrompt);
const submitInteractivePrompt = useStableEventCallback(
actions.submitInteractivePrompt
);
const stableLinkAction = useOptionalStableEventCallback(onLinkAction);
const tuttiWorkflow = useAgentGUITuttiWorkflow({
viewModel,
labels,
stableLinkAction,
setTuttiModeActive: actions.setTuttiModeActive,
setTuttiModeEffect: actions.setTuttiModeEffect,
setTuttiModeSpeed: actions.setTuttiModeSpeed,
updateDraftContent: actions.updateDraftContent,
submitPromptPassthrough: submitPromptAndScrollToBottom
});
const tuttiWorkflowComposer = tuttiWorkflow.composer;
const tuttiWorkflowDock = tuttiWorkflow.workflowDock;
const sourceActiveTurn =
viewModel.detail.conversationDetail?.session.activeTurn ?? null;
const sourceHasStoppableWork =
Boolean(sourceActiveTurn && sourceActiveTurn.phase !== "settled") ||
viewModel.composer.isCreatingConversation;
const handleInterruptCurrentTurn = useStableEventCallback(() => {
const targets = resolveAgentGUITuttiStopTargets({
executionActive: tuttiWorkflowComposer.tuttiExecutionActive,
sourceHasStoppableWork
});
if (targets.stopExecution) {
void tuttiWorkflowComposer
.stopTuttiExecution()
.catch((error: unknown) => {
console.error("tutti plan execution stop failed", error);
});
}
if (targets.stopSession) {
actions.interruptCurrentTurn(labels.noRunningResponse);
}
});
const stableRequestWorkspaceReferences = useOptionalStableEventCallback(
onRequestWorkspaceReferences
);
const stableSelectProjectDirectory = useOptionalStableEventCallback(
selectProjectDirectory
);
const stableRequestGitBranches =
useOptionalStableEventCallback(onRequestGitBranches);
const authLogin = useOptionalStableEventCallback(onAgentProviderLogin);
const submitBottomDockInteractivePrompt = useBottomDockInteractionSubmission(
submitInteractivePrompt,
dismissBottomDockPrompt
);
const composerActivePromptDisabledReason =
resolveAgentGUIComposerInteractionDisabledReason(
composerActivePrompt?.kind,
viewModel.interaction
);
const homeComposerProviderTargets = homeTargetProjection.agentTargets;
const selectedHomeComposerTarget = homeTargetProjection.selectedAgentTarget;
const composerProviderTargets =
viewModel.rail.activeConversationId === null
? homeComposerProviderTargets
: viewModel.rail.agentTargets;
const composerHandoffProviderTargets = viewModel.composer.handoffAgentTargets;
const composerProvider =
viewModel.rail.activeConversationId === null
? (selectedHomeComposerTarget?.provider ?? viewModel.shell.data.provider)
: viewModel.shell.data.provider;
const composerSelectedProviderTarget =
viewModel.rail.activeConversationId === null
? selectedHomeComposerTarget
: (viewModel.rail.agentTargets.find((target) => {
if (target.provider !== viewModel.shell.data.provider) {
return false;
}
const agentTargetId = viewModel.shell.data.agentTargetId;
return (
!agentTargetId ||
target.targetId === agentTargetId ||
target.agentTargetId === agentTargetId
);
}) ?? viewModel.rail.selectedAgentTarget);
const handoffSourceSessionId = viewModel.rail.activeConversationId;
const stableHandoffConversation = useOptionalStableEventCallback(
onHandoffConversation && handoffSourceSessionId !== null
? (target: (typeof composerHandoffProviderTargets)[number]) =>
onHandoffConversation({
agentTargetId: target.agentTargetId ?? target.targetId,
draftPrompt: buildAgentConversationHandoffPrompt({
activeConversation: viewModel.rail.activeConversation,
currentUserId: viewModel.shell.currentUserId,
labels,
selectedAgentTarget: composerSelectedProviderTarget,
uiLanguage,
workspaceId: viewModel.shell.workspaceId
}),
provider: target.provider,
sourceAgentSessionId: handoffSourceSessionId,
userProjectPath: handoffProjectPathForConversation(
viewModel.rail.activeConversation
)
})
: undefined
);
const composerInputHistoryProps = useAgentGUIComposerInputHistoryProps({
actions,
conversation,
enabled: sessionInputHistoryEnabled,
pendingPrependScrollAnchorRef,
timelineRef,
viewModel
});
const sideConversation = useAgentGUIDetailSideConversation({
enabled: sideConversationEnabled,
workspaceId: viewModel.shell.workspaceId,
sourceAgentSessionId: viewModel.rail.activeConversationId,
provider: composerProvider,
cwd: viewModel.shell.workspacePath ?? null,
capabilityRevision: `${viewModel.detail.conversationDetail?.session.providerSessionId ?? ""}:${sourceActiveTurn?.turnId ?? ""}:${sourceActiveTurn?.phase ?? ""}`,
availableCommands: viewModel.composer.availableCommands,
slashCommandPolicy: viewModel.composer.composerSettings.slashCommandPolicy,
clearMainDraft,
submitPrompt: tuttiWorkflowComposer.submitPromptOrDecidePlan
});
const sideComposerFocused = sideConversation.focused;
const baseComposerProps = useMemo<AgentComposerProps>(
() => ({
workspaceId: viewModel.shell.workspaceId,
agentSessionId: viewModel.rail.activeConversationId,
workspacePath: viewModel.shell.workspacePath,
currentUserId: viewModel.shell.currentUserId,
provider: composerProvider,
slashStatus,
onSlashStatusOpen,
onSlashStatusClose,
onSlashStatusRefresh,
usage: viewModel.detail.usage,
draftContent: viewModel.composer.draftContent,
engagement: composerEngagement,
draftScopeKey: resolveAgentComposerDraftScopeKey({
agentSessionId: viewModel.rail.activeConversationId
}),
...{ ...composerInputHistoryProps, hiddenMentionFilterIds },
availableCommands: sideConversation.commands,
hasCompactableContext: viewModel.detail.hasSentUserMessage,
compactSupported: viewModel.composer.compactSupported,
availableSkills: viewModel.composer.availableSkills,
selectedAgentTarget: composerSelectedProviderTarget,
sessionWorktreeEnabled:
sessionWorktreeEnabled &&
sessionLaunchModesByProjectSectionKey !== undefined,
sessionLaunchMode:
sessionLaunchModesByProjectSectionKey?.[
viewModel.composer.composerSettings.selectedProjectSectionKey?.trim() ??
""
] ?? "local",
onSessionLaunchModeChange:
onSessionLaunchModePreferenceChange &&
viewModel.composer.composerSettings.selectedProjectSectionKey?.trim()
? (mode) =>
onSessionLaunchModePreferenceChange({
mode,
projectSectionKey:
viewModel.composer.composerSettings.selectedProjectSectionKey!.trim()
})
: undefined,
agentTargets: composerProviderTargets,
handoffAgentTargets: composerHandoffProviderTargets,
showHandoffTargetOwnershipLabels,
providerSelectReadonly: viewModel.rail.activeConversationId !== null,
onProviderSelect:
viewModel.rail.activeConversationId === null
? selectHomeComposerAgentTargetAndFocus
: undefined,
gate: composerGate,
presentationEditorDisabled: timelineInteractionLocked,
disabledReason: composerDisabledReason,
presentationSubmitDisabled:
timelineInteractionLocked ||
tuttiWorkflowDock.phase?.kind === "materializing",
tuttiModeActive: viewModel.composer.isTuttiModeActive,
tuttiModeUpdating: viewModel.composer.isTuttiModeUpdating,
tuttiModeEffect: viewModel.composer.tuttiModeEffect,
tuttiModeSpeed: viewModel.composer.tuttiModeSpeed,
composerSettings: {
...viewModel.composer.composerSettings,
slashCommandPolicy: sideConversation.slashCommandPolicy
},
queueStatus: viewModel.composer.queueStatus,
queuedPrompts: viewModel.composer.queuedPrompts,
drainingQueuedPromptId: viewModel.composer.drainingQueuedPromptId,
workspaceAppIcons,
placeholder: viewModel.detail.hasSentUserMessage
? labels.followupPlaceholder
: labels.initialPlaceholder,
showStopButton:
showStopButton || tuttiWorkflowComposer.tuttiExecutionActive,
activeTurnId: sourceActiveTurn?.turnId ?? null,
draftOverridesStopButton: tuttiWorkflowComposer.tuttiExecutionActive,
stopDisabled:
stopDisabled ||
timelineInteractionLocked ||
tuttiWorkflowComposer.tuttiExecutionStopping,
workspaceReferencePickerOpen,
...{ referenceProvenanceFilters, activePrompt: composerActivePrompt },
activePromptKeyboardShortcutsEnabled: isActive && !sideComposerFocused,
activePromptDisabledReason: composerActivePromptDisabledReason,
promptTips: labels.promptTips,
composerFocusRequestSequence,
isActive: isActive && !sideComposerFocused,
promptImagesSupported: viewModel.composer.promptImagesSupported,
providerSelectLabel: labels.providerSwitchLabel,
handoffLabel: labels.handoffConversation,
handoffMenuLabel: labels.handoffConversationMenu,
isInterrupting:
viewModel.composer.isInterrupting ||
viewModel.composer.isCancelPending ||
tuttiWorkflowComposer.tuttiExecutionStopping,
modelConsult:
viewModel.rail.activeConversationId !== null
? {
agentSessionId: viewModel.rail.activeConversationId,
lastAssistantMessageText: latestAssistantMessageText(conversation)
}
: null,
isSendingTurn: isComposerSending,
isSubmittingPrompt: activePromptResponsePending,
uiLanguage,
labels: composerLabels,
workspaceUserProjectI18n,
capabilityMenuState,
capabilityControlsReadOnly,
onDraftContentChange: updateDraftContent,
onProjectPathChange: updateSelectedProjectPath,
onSettingsChange: updateComposerSettings,
onRetryComposerOptions: retryComposerOptions,
onTuttiModeChange:
capabilityMenuState?.tuttiMode?.enabled === true
? tuttiWorkflowComposer.setTuttiModeActiveAndSettleReview
: undefined,
onTuttiModeEffectChange:
capabilityMenuState?.tuttiMode?.enabled === true
? setTuttiModeEffect
: undefined,
onTuttiModeSpeedChange:
capabilityMenuState?.tuttiMode?.enabled === true
? setTuttiModeSpeed
: undefined,
onPlanIssueBudgetPresetChange: updatePlanIssueBudgetPreset,
onSubmit: sideConversation.submitMain,
composerActionAccessory: (
<AgentGUITuttiPlanReviewActionSlot
controller={tuttiWorkflowComposer}
label={labels.tuttiModePlanSendRequestChanges}
/>
),
onSubmitEmpty: tuttiWorkflowComposer.planReviewSendActive
? tuttiWorkflowComposer.acceptPendingPlan
: undefined,
onSubmitGuidance: submitGuidancePromptAndScrollToBottom,
onPromptImagesUnsupported: showPromptImagesUnsupported,
onSendQueuedPromptNext: sendQueuedPromptNext,
onRemoveQueuedPrompt: removeQueuedPrompt,
onEditQueuedPrompt: editQueuedPrompt,
onInterruptCurrentTurn: handleInterruptCurrentTurn,
onSubmitInteractivePrompt: submitInteractivePrompt,
onCapabilitySettingsRequest,
onLinkAction: stableLinkAction,
onHandoffConversation: stableHandoffConversation,
onRequestWorkspaceReferences: stableRequestWorkspaceReferences,
resolveExternalPromptEntries,
prepareExternalPromptFiles,
resolvePastedPath,
promptAssetLimit,
selectProjectDirectory: stableSelectProjectDirectory,
projectSelectOptions,
onRequestGitBranches: stableRequestGitBranches
}),
[
capabilityMenuState,
capabilityControlsReadOnly,
composerDisabledReason,
composerGate,
composerFocusRequestSequence,
composerEngagement,
composerInputHistoryProps,
composerHandoffProviderTargets,
composerLabels,
conversation,
composerProviderTargets,
composerSelectedProviderTarget,
timelineInteractionLocked,
handleInterruptCurrentTurn,
isActive,
sideComposerFocused,
isComposerSending,
labels,
stableHandoffConversation,
onSlashStatusOpen,
onSlashStatusClose,
onSlashStatusRefresh,
workspaceReferencePickerOpen,
composerActivePrompt,
composerActivePromptDisabledReason,
editQueuedPrompt,
onCapabilitySettingsRequest,
removeQueuedPrompt,
resolveExternalPromptEntries,
prepareExternalPromptFiles,
resolvePastedPath,
promptAssetLimit,
projectSelectOptions,
sendQueuedPromptNext,
showPromptImagesUnsupported,
showStopButton,
sourceActiveTurn?.turnId,
showHandoffTargetOwnershipLabels,
sessionWorktreeEnabled,
sessionLaunchModesByProjectSectionKey,
onSessionLaunchModePreferenceChange,
stopDisabled,
slashStatus,
setTuttiModeEffect,
setTuttiModeSpeed,
submitInteractivePrompt,
sideConversation.submitMain,
sideConversation.slashCommandPolicy,
tuttiWorkflowComposer.planReviewSendActive,
tuttiWorkflowComposer.tuttiExecutionActive,
tuttiWorkflowComposer.tuttiExecutionStopping,
tuttiWorkflowComposer.planReviewDraftHasContent,
tuttiWorkflowComposer.planReviewPreferencesDiverged,
tuttiWorkflowDock.phase?.kind,
tuttiWorkflowComposer.acceptPendingPlan,
tuttiWorkflowComposer.requestPendingPlanChanges,
submitGuidancePromptAndScrollToBottom,
uiLanguage,
stableLinkAction,
stableRequestGitBranches,
stableSelectProjectDirectory,
stableRequestWorkspaceReferences,
updateComposerSettings,
retryComposerOptions,
updatePlanIssueBudgetPreset,
updateDraftContent,
updateSelectedProjectPath,
viewModel.rail.activeConversationId,
sideConversation.commands,
viewModel.composer.availableSkills,
viewModel.composer.compactSupported,
viewModel.composer.composerSettings,
viewModel.shell.currentUserId,
viewModel.rail.activeConversation,
composerProvider,
viewModel.composer.draftContent,
viewModel.composer.draftPrompt,
viewModel.composer.drainingQueuedPromptId,
viewModel.detail.hasSentUserMessage,
viewModel.composer.isInterrupting,
viewModel.composer.isTuttiModeActive,
viewModel.composer.isTuttiModeUpdating,
viewModel.composer.tuttiModeEffect,
viewModel.composer.tuttiModeSpeed,
activePromptResponsePending,
viewModel.composer.promptImagesSupported,
viewModel.composer.queueStatus,
viewModel.composer.queuedPrompts,
viewModel.detail.usage,
viewModel.shell.workspaceId,
viewModel.shell.workspacePath,
referenceProvenanceFilters,
workspaceUserProjectI18n,
workspaceAppIcons,
selectHomeComposerAgentTargetAndFocus
]
);
const { bottomDockComposerProps, selectionProps, sidePane } = useSideChrome({
availableSkills: viewModel.composer.availableSkills,
baseComposerProps,
controller: sideConversation,
conversationFlowLabels,
isVisible,
presentation: sideConversationPresentation,
textSelectionActionsEnabled: sideConversationEnabled,
onRequestComposerFocus,
renderComposerFooterAccessory
});
const emptyHeroComposerProps = useMemo<AgentComposerProps>(
() => ({
...bottomDockComposerProps,
layoutMode: "hero"
}),
[bottomDockComposerProps]
);
const emptyHeroProvider =
composerSelectedProviderTarget?.provider ?? viewModel.shell.data.provider;
const bottomDockStoreRevision = agentGUIDetailBottomDockStoreRevision({
activePromptResponsePending,
bottomDockLiftedPrompt,
bottomDockReplacementPrompt,
inlineNoticeChrome,
sessionChrome,
viewModel
});
const {
followEndMode,
isTimelineScrolledToBottom,
isTimelineScrolledToTop,
setVirtualScrollController,
scrollTimelineToBottom
} = useAgentGUIDetailScroll({
actions,
bottomDockRef,
bottomDockStoreRevision,
conversation,
isVisible,
pendingPrependScrollAnchorRef,
showTimelineSkeleton,
submittedPromptScrollConversationRef,
timelineConversationId,
timelineContentRef,
timelineRef,
timelineScrollAnchorRef,
virtualScrollControllerRef,
viewModel
});
const homeContent = !hasActiveConversation ? (
<AgentGUIEmptyHomePane
isActive={isActive}
isVisible={isVisible}
provider={emptyHeroProvider}
providerReadinessGate={emptyProviderReadinessGate}
showAllProviders={viewModel.rail.conversationFilter.kind === "all"}
agentTargets={composerProviderTargets}
selectedAgentTarget={composerSelectedProviderTarget}
onProviderSelect={
viewModel.rail.activeConversationId === null
? selectHomeComposerAgentTargetAndFocus
: undefined
}
noticeChrome={homeNoticeChrome}
isRespondingApproval={activePromptResponsePending}
onSubmitApprovalOption={submitApproval}
onRetryActivation={retryActivation}
onAuthLogin={authLogin}
onContinueInNewConversation={continueInNewConversation}
chromeLabels={chromeLabels}
composerProps={emptyHeroComposerProps}
labels={labels}
suggestions={labels.homeSuggestions ?? EMPTY_HOME_SUGGESTIONS}
suggestionsCloseLabel={labels.homeSuggestionsClose}
onSelectSuggestion={handleSelectHomeSuggestion}
onSelectSuggestionAction={handleHomeSuggestionAction}
/>
) : null;
const forkedFrom = detail.conversationDetail?.session.forkedFrom ?? null;
return (
<main
className={styles.detail}
aria-busy={timelineInteractionLocked || undefined}
data-agent-session-id={viewModel.rail.activeConversationId ?? undefined}
>
{viewModel.operations.goalClearNoticeSequence > 0 ? (
<AgentGUIContentToast
key={viewModel.operations.goalClearNoticeSequence}
insetTopPx={16}
message={labels.goalRemoved}
/>
) : null}
<div className={styles.detailWorkbench}>
<section
className={styles.detailPrimary}
inert={timelineInteractionLocked}
>
<AgentGUIDetailTimeline
availableSkills={viewModel.composer.availableSkills}
conversation={conversation}
editRetry={editRetry}
conversationFlowEmpty={conversationFlowEmpty}
conversationFlowLabels={conversationFlowLabels}
followEndMode={followEndMode}
forkedFrom={forkedFrom}
hasActiveConversation={hasActiveConversation}
homeContent={homeContent}
isLoadingOlderMessages={viewModel.detail.isLoadingOlderMessages}
isVisible={isVisible}
isTimelineScrolledToTop={isTimelineScrolledToTop}
labels={labels}
onAuthLogin={authLogin}
onForkThroughTurn={handleForkThroughTurn}
onOpenForkSourceSession={openForkSourceSession}
forkThroughTurnPendingTurnIds={
viewModel.operations.forkThroughTurnPendingTurnIds
}
onLinkAction={stableLinkAction}
{...selectionProps}
showTimelineSkeleton={showTimelineSkeleton}
showUnavailableChatEmpty={showUnavailableChatEmpty}
timelineContentRef={timelineContentRef}
timelineRef={timelineRef}
virtualScrollControllerRef={setVirtualScrollController}
workspaceAppIcons={workspaceAppIcons}
/>
{hasActiveConversation ? (
<AgentGUIBottomDockPane
bottomDockRef={bottomDockRef}
showScrollToBottom={!isTimelineScrolledToBottom}
scrollToBottomLabel={labels.scrollToBottom}
onScrollToBottom={scrollTimelineToBottom}
bottomDockLiftedPrompt={bottomDockLiftedPrompt}
bottomDockReplacementPrompt={bottomDockReplacementPrompt}
composerProps={bottomDockComposerProps}
approvalDisabledReason={
viewModel.interaction.approvalDisabledReason
}
interactivePromptDisabledReason={
viewModel.interaction.interactivePromptDisabledReason
}
inlineNoticeChrome={inlineNoticeChrome}
isRespondingApproval={activePromptResponsePending}
sessionChrome={sessionChrome}
keyboardShortcutsEnabled={isActive && !sideComposerFocused}
chromeLabels={chromeLabels}
goalBannerLabels={goalBannerLabels}
promptLabels={interactivePromptLabels}
onSubmitApprovalOption={submitApproval}
onRetryActivation={retryActivation}
onRetryInlineNotice={retryInlineNotice}
onAuthLogin={authLogin}
onContinueInNewConversation={continueInNewConversation}
onSubmitBottomDockInteractivePrompt={
submitBottomDockInteractivePrompt
}
onGoalControl={goalControl}
goalPauseSupported={viewModel.composer.goalPauseSupported}
tuttiWorkflowDock={tuttiWorkflowDock}
tuttiWorkflowDockLabels={labels.tuttiWorkflowDock}
tuttiPlanPanelLabels={labels.tuttiModePlanPanel}
tuttiPlanIssuePanelLabels={labels.tuttiModePlanIssuePanel}
/>
) : null}
</section>
{hasActiveConversation ? sidePane : null}
</div>
</main>
);
});