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

352 lines
11 KiB
TypeScript

import { useState, type JSX } from "react";
import { canonicalInteractionKey } from "@tutti-os/agent-activity-core";
import { Button } from "@tutti-os/ui-system";
import { MessageSquareMoreIcon } from "../../../app/renderer/components/icons/MessageSquareMoreIcon";
import styles from "../../../agent-gui/agentGuiNode/AgentGUIConversation.styles";
import type { AgentConversationPromptVM } from "../contracts/agentConversationVM";
import { buildAskUserAnswerPayload } from "../interactiveAnswerPayload";
import type {
AgentInteractivePromptConversationReturn,
AgentInteractivePromptSurfaceProps,
AgentInteractivePromptVariant
} from "./AgentInteractivePromptSurface";
import {
interactiveOptionLabel,
interactivePromptCardClassName,
interactivePromptClassName,
stripPromptTitlePunctuation
} from "./interactivePromptPresentation";
import { useAskUserAnswerFlow } from "./useAskUserAnswerFlow";
type AskUserPrompt = Extract<AgentConversationPromptVM, { kind: "ask-user" }>;
type SharedAskUserSurfaceProps = Pick<
AgentInteractivePromptSurfaceProps,
"edgeGlow" | "isInteractionDisabled" | "isSubmitting" | "labels" | "onSubmit"
> & {
embedded?: boolean;
prompt: AskUserPrompt;
};
export function AgentAskUserPromptSurface({
prompt,
variant,
conversationReturn,
...props
}: SharedAskUserSurfaceProps & {
variant: AgentInteractivePromptVariant;
conversationReturn?: AgentInteractivePromptConversationReturn;
}): JSX.Element {
"use memo";
const question = prompt.questions[0] ?? null;
const useCompactQuickAnswer =
variant === "compact" &&
prompt.questions.length === 1 &&
question !== null &&
!question.multiSelect &&
question.options.length > 0;
if (useCompactQuickAnswer) {
return (
<CompactQuickAnswerSurface
{...props}
prompt={prompt}
question={question}
/>
);
}
return (
<AskUserAnswerFlowSurface
key={canonicalInteractionKey(
prompt.agentSessionId ?? "",
prompt.turnId ?? "",
prompt.requestId
)}
{...props}
prompt={prompt}
conversationReturn={variant === "full" ? conversationReturn : undefined}
/>
);
}
function CompactQuickAnswerSurface({
prompt,
question,
embedded = false,
edgeGlow = false,
isSubmitting,
isInteractionDisabled = false,
onSubmit
}: Omit<SharedAskUserSurfaceProps, "labels"> & {
question: AskUserPrompt["questions"][number];
}): JSX.Element {
return (
<section
className={interactivePromptClassName(embedded)}
data-agent-interaction-id={prompt.requestId}
data-agent-interaction-kind="ask-user"
data-agent-question-id={question.id}
data-testid={`agent-question-${prompt.requestId}-${question.id}`}
>
<div className={interactivePromptCardClassName(edgeGlow)}>
<div className={styles.interactivePromptHeader}>
<span className={styles.interactivePromptLead}>
{stripPromptTitlePunctuation(question.header)}
</span>
</div>
<div className={styles.interactivePromptQuestion}>
{question.question}
</div>
<div className={styles.interactivePromptOptions}>
{question.options.map((option) => (
<button
key={option.label}
type="button"
className={styles.interactiveOptionButton}
aria-label={interactiveOptionLabel(
option.label,
option.description
)}
data-agent-question-option-id={option.id}
data-testid={
option.id
? `agent-question-${prompt.requestId}-${question.id}-option-${option.id}`
: undefined
}
disabled={isSubmitting || isInteractionDisabled}
onClick={() =>
onSubmit({
requestId: prompt.requestId,
action: "submit",
payload: {
...buildAskUserAnswerPayload({
[question.id]: option.label
})
}
})
}
>
<span className={styles.interactiveOptionContent}>
<span className={styles.interactiveOptionTitle}>
{option.label}
</span>
{option.description ? (
<span className={styles.interactiveOptionDescription}>
{option.description}
</span>
) : null}
</span>
</button>
))}
</div>
</div>
</section>
);
}
function AskUserAnswerFlowSurface({
prompt,
conversationReturn,
embedded = false,
edgeGlow = false,
isSubmitting,
isInteractionDisabled = false,
onSubmit,
labels
}: SharedAskUserSurfaceProps & {
conversationReturn?: AgentInteractivePromptConversationReturn;
}): JSX.Element {
"use memo";
const [collapsed, setCollapsed] = useState(false);
const flow = useAskUserAnswerFlow({
isSubmitting: isSubmitting || isInteractionDisabled,
questions: prompt.questions
});
const question = flow.currentQuestion;
if (!question) {
return (
<section className={interactivePromptClassName(embedded)}>
<div className={interactivePromptCardClassName(edgeGlow)}>
<div
className={`${styles.interactivePromptLead} inline-flex items-center gap-1.5`}
>
<MessageSquareMoreIcon
size={15}
active
aria-hidden="true"
className="shrink-0"
/>
{stripPromptTitlePunctuation(labels.waitingForAnswer)}
</div>
</div>
</section>
);
}
if (collapsed && conversationReturn) {
return (
<section
className={interactivePromptClassName(embedded)}
data-agent-interaction-id={prompt.requestId}
data-agent-interaction-kind="ask-user"
data-testid={`agent-question-${prompt.requestId}-collapsed`}
>
<div
className={`${interactivePromptCardClassName(edgeGlow)} ${styles.interactivePromptCollapsed}`}
>
<span className={styles.interactivePromptLead} role="status">
{stripPromptTitlePunctuation(labels.waitingForAnswer)}
</span>
<Button
type="button"
variant="secondary"
size="sm"
disabled={isSubmitting}
onClick={() => setCollapsed(false)}
>
{conversationReturn.continueAnswering}
</Button>
</div>
</section>
);
}
return (
<section
className={interactivePromptClassName(embedded)}
data-agent-interaction-id={prompt.requestId}
data-agent-interaction-kind="ask-user"
data-agent-question-id={question.id}
data-testid={`agent-question-${prompt.requestId}-${question.id}`}
>
<div className={interactivePromptCardClassName(edgeGlow)}>
<div className={styles.interactivePromptHeader}>
<span className={styles.interactivePromptLead}>
{stripPromptTitlePunctuation(question.header)}
</span>
<span className={styles.interactivePromptMeta}>
{flow.currentIndex + 1}/{prompt.questions.length}
</span>
</div>
<div className={styles.interactivePromptQuestion}>
{question.question}
</div>
{question.options.length > 0 ? (
<div className={styles.interactivePromptOptions}>
{question.options.map((option) => {
const active = flow.selectedOptions.includes(option.label);
return (
<button
key={option.label}
type="button"
className={styles.interactiveOptionButton}
data-active={active}
aria-pressed={active}
aria-label={interactiveOptionLabel(
option.label,
option.description
)}
data-agent-question-option-id={option.id}
data-testid={
option.id
? `agent-question-${prompt.requestId}-${question.id}-option-${option.id}`
: undefined
}
disabled={isSubmitting || isInteractionDisabled}
onClick={() => flow.toggleOption(option.label)}
>
<span className={styles.interactiveOptionContent}>
<span className={styles.interactiveOptionTitle}>
{option.label}
</span>
{option.description ? (
<span className={styles.interactiveOptionDescription}>
{option.description}
</span>
) : null}
</span>
</button>
);
})}
</div>
) : null}
{question.allowFreeText !== false &&
(!conversationReturn || question.options.length === 0) ? (
<textarea
value={flow.freeText}
placeholder={labels.answerPlaceholder}
disabled={isSubmitting || isInteractionDisabled}
className={styles.interactivePromptTextarea}
data-testid={`agent-question-${prompt.requestId}-${question.id}-custom-answer`}
onChange={(event) => flow.setFreeText(event.currentTarget.value)}
/>
) : null}
<div className={styles.interactivePromptActions}>
{conversationReturn ? (
<Button
type="button"
variant="secondary"
size="sm"
disabled={isSubmitting}
onClick={() => setCollapsed(true)}
>
{conversationReturn.returnToConversation}
</Button>
) : null}
{prompt.questions.length > 1 ? (
<Button
type="button"
variant="secondary"
size="sm"
disabled={
isSubmitting || isInteractionDisabled || flow.currentIndex === 0
}
onClick={flow.goToPreviousQuestion}
>
{labels.previousQuestion}
</Button>
) : null}
{flow.isLastQuestion ? (
<Button
type="button"
variant="default"
size="sm"
disabled={
isSubmitting ||
isInteractionDisabled ||
!flow.allQuestionsAnswered
}
data-testid={`agent-question-${prompt.requestId}-${question.id}-submit`}
onClick={() =>
onSubmit({
requestId: prompt.requestId,
action: "submit",
payload: { ...flow.answerPayload }
})
}
>
{labels.submitAnswers}
</Button>
) : (
<Button
type="button"
variant="default"
size="sm"
disabled={
isSubmitting ||
isInteractionDisabled ||
!flow.currentQuestionAnswered
}
onClick={flow.goToNextQuestion}
>
{labels.nextQuestion}
</Button>
)}
</div>
</div>
</section>
);
}