初步支持AskUserQuestion交互
This commit is contained in:
@@ -7,9 +7,13 @@ import type {
|
||||
SessionModeInfo,
|
||||
AvailableCommandInfo,
|
||||
} from "@/lib/types"
|
||||
import type { PendingPermission } from "@/contexts/acp-connections-context"
|
||||
import type {
|
||||
PendingPermission,
|
||||
PendingQuestion,
|
||||
} from "@/contexts/acp-connections-context"
|
||||
import { ChatInput } from "@/components/chat/chat-input"
|
||||
import { PermissionDialog } from "@/components/chat/permission-dialog"
|
||||
import { QuestionDialog } from "@/components/chat/question-dialog"
|
||||
|
||||
interface ConversationShellProps {
|
||||
status: ConnectionStatus | null
|
||||
@@ -17,10 +21,12 @@ interface ConversationShellProps {
|
||||
defaultPath?: string
|
||||
error: string | null
|
||||
pendingPermission: PendingPermission | null
|
||||
pendingQuestion: PendingQuestion | null
|
||||
onFocus: () => void
|
||||
onSend: (draft: PromptDraft, modeId?: string | null) => void
|
||||
onCancel: () => void
|
||||
onRespondPermission: (requestId: string, optionId: string) => void
|
||||
onAnswerQuestion: (answer: string) => void
|
||||
children: ReactNode
|
||||
modes?: SessionModeInfo[]
|
||||
configOptions?: SessionConfigOptionInfo[]
|
||||
@@ -42,10 +48,12 @@ export function ConversationShell({
|
||||
defaultPath,
|
||||
error,
|
||||
pendingPermission,
|
||||
pendingQuestion,
|
||||
onFocus,
|
||||
onSend,
|
||||
onCancel,
|
||||
onRespondPermission,
|
||||
onAnswerQuestion,
|
||||
children,
|
||||
modes,
|
||||
configOptions,
|
||||
@@ -69,6 +77,8 @@ export function ConversationShell({
|
||||
onRespond={onRespondPermission}
|
||||
/>
|
||||
|
||||
<QuestionDialog question={pendingQuestion} onAnswer={onAnswerQuestion} />
|
||||
|
||||
{!hideInput && (
|
||||
<ChatInput
|
||||
status={status}
|
||||
|
||||
86
src/components/chat/question-dialog.tsx
Normal file
86
src/components/chat/question-dialog.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useRef, useEffect, useCallback } from "react"
|
||||
import { useTranslations } from "next-intl"
|
||||
import { MessageCircleQuestion, SendHorizonal } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import type { PendingQuestion } from "@/contexts/acp-connections-context"
|
||||
|
||||
interface QuestionDialogProps {
|
||||
question: PendingQuestion | null
|
||||
onAnswer: (answer: string) => void
|
||||
}
|
||||
|
||||
export function QuestionDialog({ question, onAnswer }: QuestionDialogProps) {
|
||||
const t = useTranslations("Folder.chat.questionDialog")
|
||||
const [answer, setAnswer] = useState("")
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||
const prevQuestionIdRef = useRef<string | null>(null)
|
||||
|
||||
const questionId = question?.tool_call_id ?? null
|
||||
if (questionId !== prevQuestionIdRef.current) {
|
||||
prevQuestionIdRef.current = questionId
|
||||
if (questionId && answer !== "") {
|
||||
setAnswer("")
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (question) {
|
||||
textareaRef.current?.focus()
|
||||
}
|
||||
}, [question])
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
const trimmed = answer.trim()
|
||||
if (!trimmed) return
|
||||
onAnswer(trimmed)
|
||||
setAnswer("")
|
||||
}, [answer, onAnswer])
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
handleSubmit()
|
||||
}
|
||||
},
|
||||
[handleSubmit]
|
||||
)
|
||||
|
||||
if (!question) return null
|
||||
|
||||
return (
|
||||
<div className="mx-4 mb-3 rounded-xl border border-blue-500/30 bg-card/95 p-3 shadow-sm">
|
||||
<div className="flex items-center gap-1.5 text-sm font-medium">
|
||||
<MessageCircleQuestion className="h-4 w-4 shrink-0 text-blue-500" />
|
||||
<span>{t("title")}</span>
|
||||
</div>
|
||||
|
||||
<p className="mt-2 text-sm text-foreground/90 whitespace-pre-wrap">
|
||||
{question.question}
|
||||
</p>
|
||||
|
||||
<div className="mt-3 flex gap-2">
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
value={answer}
|
||||
onChange={(e) => setAnswer(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={t("placeholder")}
|
||||
rows={2}
|
||||
className="flex-1 resize-none rounded-md border border-border bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={!answer.trim()}
|
||||
onClick={handleSubmit}
|
||||
className="self-end"
|
||||
>
|
||||
<SendHorizonal className="mr-1.5 h-3.5 w-3.5" />
|
||||
{t("send")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -572,6 +572,36 @@ const ConversationTabView = memo(function ConversationTabView({
|
||||
[connConnect, connDisconnect, workingDirForConnection]
|
||||
)
|
||||
|
||||
const handleAnswerQuestion = useCallback(
|
||||
(answer: string) => {
|
||||
if (connStatus !== "connected") return
|
||||
const optimisticTurn: MessageTurn = {
|
||||
id: `optimistic-${crypto.randomUUID()}`,
|
||||
role: "user",
|
||||
blocks: [{ type: "text", text: answer }],
|
||||
timestamp: new Date().toISOString(),
|
||||
}
|
||||
appendOptimisticTurn(
|
||||
effectiveConversationId,
|
||||
optimisticTurn,
|
||||
optimisticTurn.id
|
||||
)
|
||||
setSendSignal((prev) => prev + 1)
|
||||
setSyncState(effectiveConversationId, "awaiting_persist")
|
||||
lifecycleSend(
|
||||
{ blocks: [{ type: "text", text: answer }], displayText: answer },
|
||||
null
|
||||
)
|
||||
},
|
||||
[
|
||||
appendOptimisticTurn,
|
||||
connStatus,
|
||||
effectiveConversationId,
|
||||
lifecycleSend,
|
||||
setSyncState,
|
||||
]
|
||||
)
|
||||
|
||||
const showDraftHeader = !hasPersistedConversation
|
||||
const isWelcomeMode = showDraftHeader && !hasSentMessage
|
||||
|
||||
@@ -596,10 +626,12 @@ const ConversationTabView = memo(function ConversationTabView({
|
||||
defaultPath={workingDirForConnection}
|
||||
error={conn.error}
|
||||
pendingPermission={conn.pendingPermission}
|
||||
pendingQuestion={conn.pendingQuestion}
|
||||
onFocus={handleFocus}
|
||||
onSend={handleSend}
|
||||
onCancel={handleCancel}
|
||||
onRespondPermission={handleRespondPermission}
|
||||
onAnswerQuestion={handleAnswerQuestion}
|
||||
modes={connectionModes}
|
||||
configOptions={connectionConfigOptions}
|
||||
modeLoading={modeLoading}
|
||||
|
||||
Reference in New Issue
Block a user