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

293 lines
12 KiB
TypeScript

import { memo } from "react";
import { ChevronsDown } from "lucide-react";
import { cn } from "@tutti-os/ui-system";
import styles from "../AgentGUINode.styles";
import { AgentInteractivePromptSurface } from "../AgentInteractivePromptSurface";
import { AgentComposerRegion } from "../AgentComposerRegion";
import { AgentSessionChrome } from "../AgentSessionChrome";
import { AgentComposer, type AgentComposerProps } from "../AgentComposer";
import {
AgentGoalBanner,
isGoalBannerVisible,
type AgentGoalBannerLabels
} from "../AgentGoalBanner";
import {
TuttiWorkflowDock,
type TuttiWorkflowDockLabels
} from "../TuttiWorkflowDock";
import { resolveAgentGUIInteractionDisabledReason } from "./agentGUIDetailModelHelpers";
import type {
TuttiModePlanPanelLabels,
TuttiPlanIssuePanelLabels
} from "../../../workspaceWorkflow";
import type {
AgentGUINodeViewModel,
AgentGUISessionChrome
} from "../model/agentGuiNodeTypes";
import type { AgentGUINodeViewProps } from "../AgentGUINodeView";
import type {
ChromeLabels,
InteractivePromptLabels
} from "./AgentGUINodeView.types";
import { numberValue, objectRecord, stringValue } from "./agentGUIViewUtils";
import type { AgentGUITuttiWorkflowDockController } from "./useAgentGUITuttiWorkflow";
interface AgentGUIBottomDockPaneProps {
bottomDockRef: React.RefObject<HTMLDivElement | null>;
showScrollToBottom: boolean;
scrollToBottomLabel: string;
onScrollToBottom: () => void;
// Approval / ask-user prompts lifted above the inline notice (composer stays
// visible below). Mutually exclusive with bottomDockReplacementPrompt.
bottomDockLiftedPrompt:
| AgentGUINodeViewModel["interaction"]["pendingApproval"]
| AgentGUINodeViewModel["interaction"]["pendingInteractivePrompt"];
// When set, this interactive prompt takes the composer's slot in the bottom
// dock (the composer is hidden). Closing the prompt returns the composer.
bottomDockReplacementPrompt:
| AgentGUINodeViewModel["interaction"]["pendingApproval"]
| AgentGUINodeViewModel["interaction"]["pendingInteractivePrompt"];
composerProps: AgentComposerProps;
approvalDisabledReason: string | null;
interactivePromptDisabledReason: string | null;
inlineNoticeChrome: AgentGUISessionChrome | null;
isRespondingApproval: boolean;
sessionChrome: AgentGUISessionChrome;
keyboardShortcutsEnabled: boolean;
chromeLabels: ChromeLabels;
goalBannerLabels: AgentGoalBannerLabels;
promptLabels: InteractivePromptLabels;
onSubmitApprovalOption: AgentGUINodeViewProps["actions"]["submitApprovalOption"];
onAuthLogin?: (provider?: string | null) => void;
onRetryActivation: AgentGUINodeViewProps["actions"]["retryActivation"];
onRetryInlineNotice: () => void;
onContinueInNewConversation: AgentGUINodeViewProps["actions"]["continueInNewConversation"];
onSubmitBottomDockInteractivePrompt: AgentGUINodeViewProps["actions"]["submitInteractivePrompt"];
onGoalControl: AgentGUINodeViewProps["actions"]["goalControl"];
goalPauseSupported: boolean;
tuttiWorkflowDock: AgentGUITuttiWorkflowDockController;
tuttiWorkflowDockLabels: TuttiWorkflowDockLabels;
tuttiPlanPanelLabels: TuttiModePlanPanelLabels;
tuttiPlanIssuePanelLabels: TuttiPlanIssuePanelLabels;
}
export const AgentGUIBottomDockPane = memo(function AgentGUIBottomDockPane({
bottomDockRef,
showScrollToBottom,
scrollToBottomLabel,
onScrollToBottom,
bottomDockLiftedPrompt,
bottomDockReplacementPrompt,
composerProps,
approvalDisabledReason,
interactivePromptDisabledReason,
inlineNoticeChrome,
isRespondingApproval,
sessionChrome,
keyboardShortcutsEnabled,
chromeLabels,
goalBannerLabels,
promptLabels,
onSubmitApprovalOption,
onAuthLogin,
onRetryActivation,
onRetryInlineNotice,
onContinueInNewConversation,
onSubmitBottomDockInteractivePrompt,
onGoalControl,
goalPauseSupported,
tuttiWorkflowDock,
tuttiWorkflowDockLabels,
tuttiPlanPanelLabels,
tuttiPlanIssuePanelLabels
}: AgentGUIBottomDockPaneProps): React.JSX.Element {
"use memo";
const liftedPromptDisabledReason = resolveAgentGUIInteractionDisabledReason({
promptKind: bottomDockLiftedPrompt?.kind,
approvalReason: approvalDisabledReason,
interactivePromptReason: interactivePromptDisabledReason
});
const replacementPromptDisabledReason =
resolveAgentGUIInteractionDisabledReason({
promptKind: bottomDockReplacementPrompt?.kind,
approvalReason: approvalDisabledReason,
interactivePromptReason: interactivePromptDisabledReason
});
// Active thread goal rides the same runtimeContext channel as account /
// rateLimits, so we read it straight off the session chrome's raw state.
const goal = objectRecord(sessionChrome.rawState?.goal);
const goalObjective = goal ? stringValue(goal.objective) : "";
const goalStatus = goal ? stringValue(goal.status) : "";
const goalTokenBudget = goal ? numberValue(goal.tokenBudget) : null;
const goalTokensUsed = goal ? numberValue(goal.tokens) : null;
const goalStartedAtUnixMs = goal ? numberValue(goal.startedAtUnixMs) : null;
const goalDurationMs = goal ? numberValue(goal.durationMs) : null;
const goalIsOptimistic = sessionChrome.rawState?.goalIsOptimistic === true;
const goalControlPending =
sessionChrome.rawState?.goalControlStatus === "pending" ||
sessionChrome.rawState?.goalControlStatus === "pending_create";
const showGoalBanner = isGoalBannerVisible(goalObjective, goalStatus);
const workflowPhase = tuttiWorkflowDock.phase;
return (
<AgentComposerRegion
regionRef={bottomDockRef}
floating={
showScrollToBottom ? (
<button
type="button"
className={cn(
styles.bottomDockScrollToBottom,
"nodrag tsh-desktop-no-drag [-webkit-app-region:no-drag]"
)}
data-testid="agent-gui-scroll-to-bottom"
aria-label={scrollToBottomLabel}
title={scrollToBottomLabel}
onPointerDown={(event) => event.stopPropagation()}
onClick={onScrollToBottom}
>
<ChevronsDown aria-hidden="true" size={15} strokeWidth={2.2} />
</button>
) : null
}
lifted={
bottomDockLiftedPrompt ? (
<div
className={styles.bottomDockPrompt}
data-testid="agent-gui-bottom-dock-active-prompt"
>
<AgentInteractivePromptSurface
prompt={bottomDockLiftedPrompt}
conversationReturn={promptLabels.conversationReturn}
embedded={true}
edgeGlow={true}
keyboardShortcuts={keyboardShortcutsEnabled}
isSubmitting={isRespondingApproval}
isInteractionDisabled={Boolean(liftedPromptDisabledReason)}
interactionDisabledReason={liftedPromptDisabledReason}
onSubmit={onSubmitBottomDockInteractivePrompt}
labels={promptLabels}
/>
</div>
) : null
}
accessories={
<>
{inlineNoticeChrome ? (
<AgentSessionChrome
chrome={inlineNoticeChrome}
approvalDisabledReason={approvalDisabledReason}
isRespondingApproval={isRespondingApproval}
onSubmitApprovalOption={onSubmitApprovalOption}
onAuthLogin={onAuthLogin}
onRetryActivation={onRetryInlineNotice}
onContinueInNewConversation={onContinueInNewConversation}
labels={chromeLabels}
/>
) : null}
<AgentSessionChrome
chrome={sessionChrome}
approvalDisabledReason={approvalDisabledReason}
isRespondingApproval={isRespondingApproval}
onSubmitApprovalOption={onSubmitApprovalOption}
onAuthLogin={onAuthLogin}
onRetryActivation={onRetryActivation}
onContinueInNewConversation={onContinueInNewConversation}
labels={chromeLabels}
/>
{showGoalBanner ? (
<AgentGoalBanner
objective={goalObjective}
status={goalStatus}
tokenBudget={goalTokenBudget ?? undefined}
tokensUsed={goalTokensUsed ?? undefined}
startedAtUnixMs={goalStartedAtUnixMs ?? undefined}
durationMs={goalDurationMs ?? undefined}
optimistic={goalIsOptimistic}
labels={goalBannerLabels}
onPauseGoal={
goalPauseSupported && !goalControlPending
? () => onGoalControl("pause")
: undefined
}
onResumeGoal={
goalPauseSupported && !goalControlPending
? () => onGoalControl("resume")
: undefined
}
onClearGoal={
goalControlPending ? undefined : () => onGoalControl("clear")
}
/>
) : null}
{workflowPhase ? (
<TuttiWorkflowDock
assignmentCatalog={tuttiWorkflowDock.assignmentCatalog}
assignmentDrafts={tuttiWorkflowDock.assignmentDrafts}
preferencePopoverLabels={{
title: composerProps.labels.tuttiBudgetTitle,
effectLabel: composerProps.labels.tuttiBudgetEffectLabel,
speedLabel: composerProps.labels.tuttiBudgetSpeedLabel,
previewHint: composerProps.labels.tuttiBudgetPreviewHint,
previewCost: composerProps.labels.tuttiBudgetPreviewCost,
previewBalance: composerProps.labels.tuttiBudgetPreviewBalance,
previewPowerful:
composerProps.labels.tuttiBudgetPreviewPowerful,
modelPreferenceLabel:
composerProps.labels.tuttiBudgetModelPreferenceLabel,
modelPreferenceCost:
composerProps.labels.tuttiBudgetModelPreferenceCost,
modelPreferenceBalance:
composerProps.labels.tuttiBudgetModelPreferenceBalance,
modelPreferencePowerful:
composerProps.labels.tuttiBudgetModelPreferencePowerful,
parallelismLabel:
composerProps.labels.tuttiBudgetParallelismLabel,
parallelismValue:
composerProps.labels.tuttiBudgetParallelismValue
}}
labels={tuttiWorkflowDockLabels}
phase={workflowPhase}
planPanelLabels={tuttiPlanPanelLabels}
planIssuePanelLabels={tuttiPlanIssuePanelLabels}
onAssignmentDraftChange={tuttiWorkflowDock.updateAssignment}
onCancelReview={tuttiWorkflowDock.cancelReview}
onTaskAction={tuttiWorkflowDock.taskAction}
onEffectChange={tuttiWorkflowDock.changeEffect}
onSpeedChange={tuttiWorkflowDock.changeSpeed}
onOpenIssue={tuttiWorkflowDock.openIssue}
onOpenTask={tuttiWorkflowDock.openTask}
onRetry={tuttiWorkflowDock.retry}
/>
) : null}
</>
}
primary={
bottomDockReplacementPrompt ? (
<div
className={styles.bottomDockPrompt}
data-testid="agent-gui-bottom-dock-active-prompt"
>
<AgentInteractivePromptSurface
prompt={bottomDockReplacementPrompt}
embedded={true}
edgeGlow={true}
keyboardShortcuts={keyboardShortcutsEnabled}
isSubmitting={isRespondingApproval}
isInteractionDisabled={Boolean(replacementPromptDisabledReason)}
interactionDisabledReason={replacementPromptDisabledReason}
onSubmit={onSubmitBottomDockInteractivePrompt}
labels={promptLabels}
/>
</div>
) : (
<AgentComposer {...composerProps} />
)
}
/>
);
});