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

219 lines
6.7 KiB
TypeScript

import type { JSX } from "react";
import { canonicalInteractionKey } from "@tutti-os/agent-activity-core";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger
} from "@tutti-os/ui-system";
import type {
AgentConversationPromptVM,
AgentInteractionResponseInput
} from "../contracts/agentConversationVM";
import { AgentAskUserPromptSurface } from "./AgentAskUserPromptSurface";
import {
ApprovalPromptSurface,
ExitPlanPromptSurface,
PlanImplementationSurface
} from "./AgentInteractiveDecisionPromptSurfaces";
/**
* Where the prompt is rendered, which sets its interaction budget:
* - "full" (conversation / composer): the user is focused here, so every action
* is shown — primary decisions plus rich follow-ups (feedback textareas,
* multi-step wizards, "stay in plan").
* - "compact" (message-center attention deck): a glanceable needs-attention card
* across many sessions. Ask-user prompts retain the full answer flow in a
* denser layout; other prompt kinds may intentionally limit secondary actions.
*/
export type AgentInteractivePromptVariant = "full" | "compact";
export interface AgentInteractivePromptConversationReturn {
continueAnswering: string;
returnToConversation: string;
}
export interface AgentInteractivePromptSurfaceProps {
prompt: AgentConversationPromptVM;
variant?: AgentInteractivePromptVariant;
/**
* Opt in only when the host Composer can answer this exact pending
* Interaction. The surface also fails closed for non-canonical and
* fixed-choice prompts.
*/
conversationReturn?: AgentInteractivePromptConversationReturn;
edgeGlow?: boolean;
keyboardShortcuts?: boolean;
isSubmitting: boolean;
isInteractionDisabled?: boolean;
interactionDisabledReason?: string | null;
onSubmit: (input: AgentInteractionResponseInput) => boolean | void;
labels: {
approvalLead: string;
fileChangeApprovalLead: string;
planLead: string;
planModes: Array<{ id: string; label: string; description: string }>;
stayInPlan: string;
sendFeedback: string;
feedbackPlaceholder: string;
previousQuestion: string;
nextQuestion: string;
submitAnswers: string;
answerPlaceholder: string;
waitingForAnswer: string;
planImplementationLead: string;
planImplementationConfirm: string;
planImplementationFeedbackPlaceholder: string;
planImplementationSend: string;
planImplementationSkip: string;
};
}
export function AgentInteractivePromptSurface({
prompt,
variant = "full",
conversationReturn,
edgeGlow = false,
embedded = false,
keyboardShortcuts = true,
isSubmitting,
isInteractionDisabled = false,
interactionDisabledReason = null,
onSubmit,
labels
}: AgentInteractivePromptSurfaceProps & {
embedded?: boolean;
}): JSX.Element | null {
"use memo";
const submitPrompt = (
input: AgentInteractionResponseInput
): boolean | void => {
const agentSessionId =
"agentSessionId" in prompt ? prompt.agentSessionId?.trim() : "";
const turnId = "turnId" in prompt ? prompt.turnId?.trim() : "";
return onSubmit({
...input,
...(agentSessionId && turnId ? { agentSessionId, turnId } : {}),
requestId: prompt.requestId
});
};
const isComposerAnswerableAskUser =
prompt.kind === "ask-user" &&
Boolean(prompt.agentSessionId?.trim()) &&
Boolean(prompt.turnId?.trim()) &&
prompt.questions.length > 0 &&
prompt.questions.every((question) => question.allowFreeText !== false);
const askUserConversationReturn = isComposerAnswerableAskUser
? conversationReturn
: undefined;
let promptSurface: JSX.Element;
if (prompt.kind === "approval") {
promptSurface = (
<ApprovalPromptSurface
prompt={prompt}
embedded={embedded}
edgeGlow={edgeGlow}
keyboardShortcuts={keyboardShortcuts}
isSubmitting={isSubmitting}
isInteractionDisabled={isInteractionDisabled}
onSubmit={submitPrompt}
labels={labels}
/>
);
} else if (prompt.kind === "exit-plan") {
promptSurface = (
<ExitPlanPromptSurface
prompt={prompt}
variant={variant}
embedded={embedded}
edgeGlow={edgeGlow}
isSubmitting={isSubmitting}
isInteractionDisabled={isInteractionDisabled}
onSubmit={submitPrompt}
labels={labels}
/>
);
} else if (prompt.kind === "plan-implementation") {
promptSurface = (
<PlanImplementationSurface
prompt={prompt}
variant={variant}
embedded={embedded}
edgeGlow={edgeGlow}
isSubmitting={isSubmitting}
isInteractionDisabled={isInteractionDisabled}
onSubmit={submitPrompt}
labels={labels}
/>
);
} else {
promptSurface = (
<AgentAskUserPromptSurface
key={canonicalInteractionKey(
prompt.agentSessionId ?? "",
prompt.turnId ?? "",
prompt.requestId
)}
prompt={prompt}
variant={variant}
conversationReturn={askUserConversationReturn}
embedded={embedded}
edgeGlow={edgeGlow}
isSubmitting={isSubmitting}
isInteractionDisabled={isInteractionDisabled}
onSubmit={submitPrompt}
labels={labels}
/>
);
}
const normalizedReason = interactionDisabledReason?.trim() ?? "";
if (!isInteractionDisabled || !normalizedReason) {
return promptSurface;
}
const hasPresentationNavigation =
prompt.kind === "ask-user" &&
variant === "full" &&
askUserConversationReturn !== undefined;
return (
<TooltipProvider delayDuration={120} skipDelayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<div
aria-disabled={hasPresentationNavigation ? undefined : "true"}
aria-label={normalizedReason}
className={
hasPresentationNavigation
? "rounded-md outline-none"
: "cursor-not-allowed rounded-md outline-none"
}
data-agent-interaction-disabled="true"
role="group"
tabIndex={0}
>
{/* Without a presentation-only escape action, keep disabled
controls out of hit testing so the wrapper owns the tooltip. */}
<div
className={
hasPresentationNavigation ? undefined : "pointer-events-none"
}
>
{promptSurface}
</div>
</div>
</TooltipTrigger>
<TooltipContent
side="top"
align="start"
className="max-w-[min(360px,calc(100vw-32px))] whitespace-normal text-left [overflow-wrap:anywhere]"
>
{normalizedReason}
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}