feat(ui): support question tool requests

Add question queue hydration, inline answering UI, and unify pending requests with permissions.
This commit is contained in:
Shantur Rathore
2026-01-10 09:46:23 +00:00
parent 147c9e3e4b
commit 72f420b6f6
22 changed files with 1098 additions and 96 deletions

View File

@@ -1,7 +1,15 @@
import { For, Show, createMemo, createSignal, createEffect, onCleanup, type Component } from "solid-js"
import type { PermissionRequestLike } from "../types/permission"
import { getPermissionCallId, getPermissionDisplayTitle, getPermissionKind, getPermissionMessageId, getPermissionSessionId } from "../types/permission"
import { activePermissionId, getPermissionQueue } from "../stores/instances"
import { getQuestionCallId, getQuestionMessageId, getQuestionSessionId, type QuestionRequest } from "../types/question"
import {
activeInterruption,
getPermissionQueue,
getQuestionQueue,
getQuestionEnqueuedAtForInstance,
setActivePermissionIdForInstance,
setActiveQuestionIdForInstance,
} from "../stores/instances"
import { loadMessages, setActiveSession } from "../stores/sessions"
import { messageStoreBus } from "../stores/message-v2/bus"
import ToolCall from "./tool-call"
@@ -88,24 +96,72 @@ function resolveToolCallFromPermission(
return null
}
function resolveToolCallFromQuestion(instanceId: string, request: QuestionRequest): ResolvedToolCall | null {
const sessionId = getQuestionSessionId(request)
const messageId = getQuestionMessageId(request)
if (!sessionId || !messageId) return null
const store = messageStoreBus.getInstance(instanceId)
if (!store) return null
const record = store.getMessage(messageId)
if (!record) return null
const callId = getQuestionCallId(request)
if (!callId) return null
for (const partId of record.partIds) {
const partRecord = record.parts?.[partId]
const part = partRecord?.data as any
if (!part || part.type !== "tool") continue
const partCallId = part.callID ?? part.callId ?? part.toolCallID ?? part.toolCallId ?? undefined
if (partCallId !== callId) continue
if (typeof part.id !== "string" || part.id.length === 0) continue
return {
messageId,
sessionId,
toolPart: part as ResolvedToolCall["toolPart"],
messageVersion: record.revision,
partVersion: partRecord?.revision ?? 0,
}
}
return null
}
const PermissionApprovalModal: Component<PermissionApprovalModalProps> = (props) => {
const [loadingSession, setLoadingSession] = createSignal<string | null>(null)
const queue = createMemo(() => getPermissionQueue(props.instanceId))
const activePermId = createMemo(() => activePermissionId().get(props.instanceId) ?? null)
const permissionQueue = createMemo(() => getPermissionQueue(props.instanceId))
const questionQueue = createMemo(() => getQuestionQueue(props.instanceId))
const active = createMemo(() => activeInterruption().get(props.instanceId) ?? null)
const orderedQueue = createMemo(() => {
const current = queue()
const activeId = activePermId()
if (!activeId) return current
const index = current.findIndex((entry) => entry.id === activeId)
if (index <= 0) return current
const active = current[index]
if (!active) return current
return [active, ...current.slice(0, index), ...current.slice(index + 1)]
type InterruptionItem =
| { kind: "permission"; id: string; sessionId: string; createdAt: number; payload: PermissionRequestLike }
| { kind: "question"; id: string; sessionId: string; createdAt: number; payload: QuestionRequest }
const orderedQueue = createMemo<InterruptionItem[]>(() => {
const permissions = permissionQueue().map((permission) => ({
kind: "permission" as const,
id: permission.id,
sessionId: getPermissionSessionId(permission) || "",
createdAt: (permission as any)?.time?.created ?? Date.now(),
payload: permission,
}))
const questions = questionQueue().map((question) => ({
kind: "question" as const,
id: question.id,
sessionId: getQuestionSessionId(question) || "",
createdAt: getQuestionEnqueuedAtForInstance(props.instanceId, question.id),
payload: question,
}))
return [...permissions, ...questions].sort((a, b) => a.createdAt - b.createdAt)
})
const hasPermissions = createMemo(() => queue().length > 0)
const hasRequests = createMemo(() => orderedQueue().length > 0)
const closeOnEscape = (event: KeyboardEvent) => {
if (event.key === "Escape") {
@@ -122,7 +178,7 @@ const PermissionApprovalModal: Component<PermissionApprovalModalProps> = (props)
createEffect(() => {
if (!props.isOpen) return
if (queue().length === 0) {
if (orderedQueue().length === 0) {
props.onClose()
}
})
@@ -156,10 +212,10 @@ const PermissionApprovalModal: Component<PermissionApprovalModalProps> = (props)
<div class="permission-center-modal-header">
<div class="permission-center-modal-title-row">
<h2 id="permission-center-title" class="permission-center-modal-title">
Permissions
Requests
</h2>
<Show when={queue().length > 0}>
<span class="permission-center-modal-count">{queue().length}</span>
<Show when={orderedQueue().length > 0}>
<span class="permission-center-modal-count">{orderedQueue().length}</span>
</Show>
</div>
<button type="button" class="permission-center-modal-close" onClick={props.onClose} aria-label="Close">
@@ -168,24 +224,58 @@ const PermissionApprovalModal: Component<PermissionApprovalModalProps> = (props)
</div>
<div class="permission-center-modal-body">
<Show when={hasPermissions()} fallback={<div class="permission-center-empty">No pending permissions.</div>}>
<Show when={hasRequests()} fallback={<div class="permission-center-empty">No pending requests.</div>}>
<div class="permission-center-list" role="list">
<For each={orderedQueue()}>
{(permission) => {
const sessionId = getPermissionSessionId(permission) || ""
const isActive = () => permission.id === activePermId()
const resolved = createMemo(() => resolveToolCallFromPermission(props.instanceId, permission))
{(item) => {
const isActive = () => active()?.kind === item.kind && active()?.id === item.id
const sessionId = () => item.sessionId
const resolved = createMemo(() => {
if (item.kind === "permission") {
return resolveToolCallFromPermission(props.instanceId, item.payload)
}
return resolveToolCallFromQuestion(props.instanceId, item.payload)
})
const showFallback = () => !resolved()
const kindLabel = () => (item.kind === "permission" ? "Permission" : "Question")
const primaryTitle = () => {
if (item.kind === "permission") {
return getPermissionDisplayTitle(item.payload)
}
const first = item.payload.questions?.[0]?.question
return typeof first === "string" && first.trim().length > 0 ? first : "Question"
}
const secondaryTitle = () => {
if (item.kind === "permission") {
return getPermissionKind(item.payload)
}
const count = item.payload.questions?.length ?? 0
return count === 1 ? "1 question" : `${count} questions`
}
const handleActivate = () => {
if (item.kind === "permission") {
setActivePermissionIdForInstance(props.instanceId, item.id)
} else {
setActiveQuestionIdForInstance(props.instanceId, item.id)
}
}
return (
<div
class={`permission-center-item${isActive() ? " permission-center-item-active" : ""}`}
role="listitem"
onClick={handleActivate}
>
<div class="permission-center-item-header">
<div class="permission-center-item-heading">
<span class="permission-center-item-kind">{getPermissionKind(permission)}</span>
<span class={`permission-center-item-chip permission-center-item-chip-${item.kind}`}>{kindLabel()}</span>
<span class="permission-center-item-kind">{secondaryTitle()}</span>
<Show when={isActive()}>
<span class="permission-center-item-chip">Active</span>
</Show>
@@ -195,7 +285,10 @@ const PermissionApprovalModal: Component<PermissionApprovalModalProps> = (props)
<button
type="button"
class="permission-center-item-action"
onClick={() => handleGoToSession(sessionId)}
onClick={(e) => {
e.stopPropagation()
handleGoToSession(sessionId())
}}
>
Go to Session
</button>
@@ -203,10 +296,13 @@ const PermissionApprovalModal: Component<PermissionApprovalModalProps> = (props)
<button
type="button"
class="permission-center-item-action"
disabled={loadingSession() === sessionId}
onClick={() => handleLoadSession(sessionId)}
disabled={loadingSession() === sessionId()}
onClick={(e) => {
e.stopPropagation()
handleLoadSession(sessionId())
}}
>
{loadingSession() === sessionId ? "Loading…" : "Load Session"}
{loadingSession() === sessionId() ? "Loading…" : "Load Session"}
</button>
</Show>
</div>
@@ -217,7 +313,7 @@ const PermissionApprovalModal: Component<PermissionApprovalModalProps> = (props)
fallback={
<div class="permission-center-fallback">
<div class="permission-center-fallback-title">
<code>{getPermissionDisplayTitle(permission)}</code>
<code>{primaryTitle()}</code>
</div>
<div class="permission-center-fallback-hint">Load session for more information.</div>
</div>

View File

@@ -1,6 +1,6 @@
import { Show, createMemo, type Component } from "solid-js"
import { ShieldAlert } from "lucide-solid"
import { getPermissionQueueLength } from "../stores/instances"
import { getPermissionQueueLength, getQuestionQueueLength } from "../stores/instances"
interface PermissionNotificationBannerProps {
instanceId: string
@@ -8,15 +8,21 @@ interface PermissionNotificationBannerProps {
}
const PermissionNotificationBanner: Component<PermissionNotificationBannerProps> = (props) => {
const queueLength = createMemo(() => getPermissionQueueLength(props.instanceId))
const hasPermissions = createMemo(() => queueLength() > 0)
const permissionCount = createMemo(() => getPermissionQueueLength(props.instanceId))
const questionCount = createMemo(() => getQuestionQueueLength(props.instanceId))
const queueLength = createMemo(() => permissionCount() + questionCount())
const hasRequests = createMemo(() => queueLength() > 0)
const label = createMemo(() => {
const count = queueLength()
return `${count} permission${count === 1 ? "" : "s"} pending approval`
const total = queueLength()
const parts: string[] = []
if (permissionCount() > 0) parts.push(`${permissionCount()} permission${permissionCount() === 1 ? "" : "s"}`)
if (questionCount() > 0) parts.push(`${questionCount()} question${questionCount() === 1 ? "" : "s"}`)
const detail = parts.length ? ` (${parts.join(", ")})` : ""
return `${total} pending request${total === 1 ? "" : "s"}${detail}`
})
return (
<Show when={hasPermissions()}>
<Show when={hasRequests()}>
<button
type="button"
class="permission-center-trigger"

View File

@@ -175,9 +175,11 @@ const SessionList: Component<SessionListProps> = (props) => {
const title = () => session()?.title || "Untitled"
const status = () => getSessionStatus(props.instanceId, rowProps.sessionId)
const statusLabel = () => formatSessionStatus(status())
const pendingPermission = () => Boolean(session()?.pendingPermission)
const statusClassName = () => (pendingPermission() ? "session-permission" : `session-${status()}`)
const statusText = () => (pendingPermission() ? "Needs Permission" : statusLabel())
const needsPermission = () => Boolean(session()?.pendingPermission)
const needsQuestion = () => Boolean((session() as any)?.pendingQuestion)
const needsInput = () => needsPermission() || needsQuestion()
const statusClassName = () => (needsInput() ? "session-permission" : `session-${status()}`)
const statusText = () => (needsPermission() ? "Needs Permission" : needsQuestion() ? "Needs Input" : statusLabel())
return (
<div class="session-list-item group">
@@ -224,7 +226,7 @@ const SessionList: Component<SessionListProps> = (props) => {
</span>
</Show>
<span class={`status-indicator session-status session-status-list ${statusClassName()}`}>
{pendingPermission() ? (
{needsInput() ? (
<ShieldAlert class="w-3.5 h-3.5" aria-hidden="true" />
) : (
<span class="status-dot" />

View File

@@ -39,6 +39,12 @@ export const SessionView: Component<SessionViewProps> = (props) => {
if (!currentSession) return false
return getSessionBusyStatus(props.instanceId, currentSession.id)
})
const sessionNeedsInput = createMemo(() => {
const currentSession = session()
if (!currentSession) return false
return Boolean(currentSession.pendingPermission || (currentSession as any).pendingQuestion)
})
let scrollToBottomHandle: (() => void) | undefined
let rootRef: HTMLDivElement | undefined
function scheduleScrollToBottom() {
@@ -224,17 +230,18 @@ export const SessionView: Component<SessionViewProps> = (props) => {
/>
<PromptInput
instanceId={props.instanceId}
instanceFolder={props.instanceFolder}
sessionId={activeSession.id}
onSend={handleSendMessage}
onRunShell={handleRunShell}
escapeInDebounce={props.escapeInDebounce}
isSessionBusy={sessionBusy()}
onAbortSession={handleAbortSession}
registerQuoteHandler={registerQuoteHandler}
/>
<PromptInput
instanceId={props.instanceId}
instanceFolder={props.instanceFolder}
sessionId={activeSession.id}
onSend={handleSendMessage}
onRunShell={handleRunShell}
escapeInDebounce={props.escapeInDebounce}
isSessionBusy={sessionBusy()}
disabled={sessionNeedsInput()}
onAbortSession={handleAbortSession}
registerQuoteHandler={registerQuoteHandler}
/>
</div>
)
}}

View File

@@ -6,8 +6,9 @@ import { useTheme } from "../lib/theme"
import { useGlobalCache } from "../lib/hooks/use-global-cache"
import { useConfig } from "../stores/preferences"
import type { DiffViewMode } from "../stores/preferences"
import { sendPermissionResponse } from "../stores/instances"
import { activeInterruption, sendPermissionResponse, sendQuestionReject, sendQuestionReply } from "../stores/instances"
import { getPermissionDisplayTitle, getPermissionKind, getPermissionSessionId } from "../types/permission"
import type { QuestionRequest } from "@opencode-ai/sdk/v2"
import type { TextPart, RenderCache } from "../types/message"
import { resolveToolRenderer } from "./tool-call/renderers"
import type {
@@ -239,6 +240,7 @@ export default function ToolCall(props: ToolCallProps) {
}))
const store = createMemo(() => messageStoreBus.getOrCreate(props.instanceId))
const activeRequest = createMemo(() => activeInterruption().get(props.instanceId) ?? null)
const cacheVersion = createMemo(() => {
if (typeof props.partVersion === "number") {
@@ -278,6 +280,16 @@ export default function ToolCall(props: ToolCallProps) {
}
return toolCallMemo()?.pendingPermission
})
const questionState = createMemo(() => store().getQuestionState(props.messageId, toolCallIdentifier()))
const pendingQuestion = createMemo(() => {
const state = questionState()
if (state) {
return { request: state.entry.request as QuestionRequest, active: state.active }
}
return undefined
})
const toolOutputDefaultExpanded = createMemo(() => (preferences().toolOutputExpansion || "expanded") === "expanded")
const diagnosticsDefaultExpanded = createMemo(() => (preferences().diagnosticsExpansion || "expanded") === "expanded")
@@ -292,27 +304,45 @@ export default function ToolCall(props: ToolCallProps) {
const [userExpanded, setUserExpanded] = createSignal<boolean | null>(null)
const isPermissionActive = createMemo(() => {
const pending = pendingPermission()
if (!pending?.permission) return false
const active = activeRequest()
return active?.kind === "permission" && active.id === pending.permission.id
})
const isQuestionActive = createMemo(() => {
const pending = pendingQuestion()
if (!pending?.request) return false
const active = activeRequest()
return active?.kind === "question" && active.id === pending.request.id
})
const expanded = () => {
const permission = pendingPermission()
if (permission?.active) return true
if (isPermissionActive() || isQuestionActive()) return true
const override = userExpanded()
if (override !== null) return override
return defaultExpandedForTool()
}
const permissionDetails = createMemo(() => pendingPermission()?.permission)
const isPermissionActive = createMemo(() => pendingPermission()?.active === true)
const questionDetails = createMemo(() => pendingQuestion()?.request)
const activePermissionKey = createMemo(() => {
const permission = permissionDetails()
return permission && isPermissionActive() ? permission.id : ""
})
const activeQuestionKey = createMemo(() => {
const request = questionDetails()
return request && isQuestionActive() ? request.id : ""
})
const [permissionSubmitting, setPermissionSubmitting] = createSignal(false)
const [permissionError, setPermissionError] = createSignal<string | null>(null)
const [diagnosticsOverride, setDiagnosticsOverride] = createSignal<boolean | undefined>(undefined)
const diagnosticsExpanded = () => {
const permission = pendingPermission()
if (permission?.active) return true
if (isPermissionActive() || isQuestionActive()) return true
const override = diagnosticsOverride()
if (override !== undefined) return override
return diagnosticsDefaultExpanded()
@@ -513,7 +543,7 @@ export default function ToolCall(props: ToolCallProps) {
})
createEffect(() => {
const activeKey = activePermissionKey()
const activeKey = activePermissionKey() || activeQuestionKey()
if (!activeKey) return
requestAnimationFrame(() => {
toolCallRootRef?.scrollIntoView({ block: "center", behavior: "smooth" })
@@ -539,6 +569,81 @@ export default function ToolCall(props: ToolCallProps) {
onCleanup(() => document.removeEventListener("keydown", handler))
})
const [questionSubmitting, setQuestionSubmitting] = createSignal(false)
const [questionError, setQuestionError] = createSignal<string | null>(null)
const [questionDraftAnswers, setQuestionDraftAnswers] = createSignal<Record<string, string[][]>>({})
const [questionCustomDraft, setQuestionCustomDraft] = createSignal<Record<string, string[]>>({})
function isTextInputFocused() {
const active = document.activeElement
return (
active?.tagName === "TEXTAREA" ||
active?.tagName === "INPUT" ||
(active?.hasAttribute("contenteditable") ?? false)
)
}
async function handleQuestionSubmit() {
const request = questionDetails()
if (!request || !isQuestionActive()) {
return
}
const answers = (questionDraftAnswers()[request.id] ?? []).map((x) => (Array.isArray(x) ? x : []))
const normalized = request.questions.map((_, index) => answers[index] ?? [])
if (normalized.some((item) => (item?.length ?? 0) === 0)) {
setQuestionError("Please answer all questions before submitting.")
return
}
setQuestionSubmitting(true)
setQuestionError(null)
try {
const sessionId = (request as any).sessionID ?? (request as any).sessionId ?? props.sessionId
await sendQuestionReply(props.instanceId, sessionId, request.id, normalized)
} catch (error) {
log.error("Failed to send question reply", error)
setQuestionError(error instanceof Error ? error.message : "Unable to reply")
} finally {
setQuestionSubmitting(false)
}
}
async function handleQuestionDismiss() {
const request = questionDetails()
if (!request || !isQuestionActive()) {
return
}
setQuestionSubmitting(true)
setQuestionError(null)
try {
const sessionId = (request as any).sessionID ?? (request as any).sessionId ?? props.sessionId
await sendQuestionReject(props.instanceId, sessionId, request.id)
} catch (error) {
log.error("Failed to reject question", error)
setQuestionError(error instanceof Error ? error.message : "Unable to dismiss")
} finally {
setQuestionSubmitting(false)
}
}
createEffect(() => {
const activeKey = activeQuestionKey()
if (!activeKey) return
const handler = (event: KeyboardEvent) => {
if (isTextInputFocused()) return
if (event.key === "Enter") {
event.preventDefault()
void handleQuestionSubmit()
} else if (event.key === "Escape") {
event.preventDefault()
void handleQuestionDismiss()
}
}
document.addEventListener("keydown", handler)
onCleanup(() => document.removeEventListener("keydown", handler))
})
const statusIcon = () => {
const status = toolState()?.status || ""
@@ -563,7 +668,7 @@ export default function ToolCall(props: ToolCallProps) {
const combinedStatusClass = () => {
const base = statusClass()
return pendingPermission() ? `${base} tool-call-awaiting-permission` : base
return pendingPermission() || pendingQuestion() ? `${base} tool-call-awaiting-permission` : base
}
function toggle() {
@@ -950,6 +1055,218 @@ export default function ToolCall(props: ToolCallProps) {
)
}
const renderQuestionBlock = () => {
const state = toolState()
const request = questionDetails()
const isQuestionTool = toolName() === "question"
if (!request && !isQuestionTool) return null
const questionsSource = request?.questions ?? ((state as any)?.input?.questions as any[] | undefined) ?? []
const questions = Array.isArray(questionsSource) ? questionsSource : []
if (questions.length === 0) return null
const requestId = request?.id ?? (state as any)?.input?.requestID ?? `question-${toolCallMemo()?.id ?? "unknown"}`
const active = Boolean(request && isQuestionActive())
const completedAnswers = Array.isArray((state as any)?.metadata?.answers) ? ((state as any).metadata.answers as string[][]) : undefined
const answers = completedAnswers ?? questionDraftAnswers()[requestId] ?? []
const customInputs = questionCustomDraft()[requestId] ?? []
const updateAnswer = (questionIndex: number, next: string[]) => {
if (!active) return
setQuestionDraftAnswers((prev) => {
const current = prev[requestId] ?? []
const updated = [...current]
updated[questionIndex] = next
return { ...prev, [requestId]: updated }
})
}
const updateCustom = (questionIndex: number, value: string) => {
if (!active) return
setQuestionCustomDraft((prev) => {
const current = prev[requestId] ?? []
const updated = [...current]
updated[questionIndex] = value
return { ...prev, [requestId]: updated }
})
}
const toggleOption = (questionIndex: number, label: string) => {
const info = questions[questionIndex]
const multi = info?.multiple === true
const existing = answers[questionIndex] ?? []
if (multi) {
const next = existing.includes(label) ? existing.filter((x) => x !== label) : [...existing, label]
updateAnswer(questionIndex, next)
return
}
updateAnswer(questionIndex, [label])
}
const submitDisabled = () => {
if (!active) return true
if (questionSubmitting()) return true
return questions.some((_, index) => (answers[index]?.length ?? 0) === 0)
}
const showButtons = () => active
return (
<div class={`tool-call-permission ${active ? "tool-call-permission-active" : "tool-call-permission-queued"}`}>
<div class="tool-call-permission-header">
<span class="tool-call-permission-label">
{active ? "Question Required" : request ? "Question Queued" : "Questions"}
</span>
<span class="tool-call-permission-type">{questions.length === 1 ? "Question" : "Questions"}</span>
</div>
<div class="tool-call-permission-body">
<div class="flex flex-col gap-4">
<For each={questions}>
{(q, index) => {
const i = () => index()
const multi = () => q?.multiple === true
const selected = () => answers[i()] ?? []
const customValue = () => customInputs[i()] ?? ""
const inputType = () => (multi() ? "checkbox" : "radio")
const groupName = () => `question-${requestId}-${i()}`
return (
<div class="rounded-md border border-base/60 bg-surface/30 p-3">
<div class="flex items-baseline justify-between gap-2">
<div class="text-xs">
Q{i() + 1}: <span class="font-semibold">{q?.header}</span>
</div>
<Show when={multi()}>
<div class="text-xs text-muted">Multiple</div>
</Show>
</div>
<div class="mt-1 text-sm font-medium">{q?.question}</div>
<div class="mt-3 flex flex-col gap-1">
<For each={q?.options ?? []}>
{(opt) => {
const checked = () => selected().includes(opt.label)
return (
<label
class={`flex items-start gap-2 py-1 ${active ? "cursor-pointer" : request ? "opacity-80" : ""}`}
title={opt.description}
>
<input
type={inputType()}
name={groupName()}
checked={checked()}
disabled={!active || questionSubmitting()}
onChange={() => toggleOption(i(), opt.label)}
/>
<div class="flex flex-col">
<div class="text-sm leading-tight">{opt.label}</div>
<div class="text-xs text-muted leading-tight">{opt.description}</div>
</div>
</label>
)
}}
</For>
<Show when={active}>
<div class="mt-2 flex items-center gap-2">
<input
class="flex-1 rounded-md border border-base/50 bg-surface px-2 py-1 text-sm"
type="text"
placeholder="Type your own answer"
value={customValue()}
disabled={!active || questionSubmitting()}
onInput={(e) => updateCustom(i(), e.currentTarget.value)}
/>
<button
type="button"
class="tool-call-permission-button"
disabled={!active || questionSubmitting() || !customValue().trim()}
onClick={() => {
const value = customValue().trim()
if (!value) return
updateCustom(i(), value)
toggleOption(i(), value)
}}
>
{multi() ? "Toggle" : "Select"}
</button>
</div>
</Show>
</div>
</div>
)
}}
</For>
<Show when={showButtons()}>
<div class="tool-call-permission-actions">
<div class="tool-call-permission-buttons">
<button
type="button"
class="tool-call-permission-button"
disabled={submitDisabled()}
onClick={() => handleQuestionSubmit()}
>
Submit
</button>
<button
type="button"
class="tool-call-permission-button"
disabled={questionSubmitting()}
onClick={() => handleQuestionDismiss()}
>
Dismiss
</button>
</div>
<div class="tool-call-permission-shortcuts">
<kbd class="kbd">Enter</kbd>
<span>Submit</span>
<kbd class="kbd">Esc</kbd>
<span>Dismiss</span>
</div>
<Show when={questionError()}>
<div class="tool-call-permission-error">{questionError()}</div>
</Show>
</div>
</Show>
<Show when={!active && request}>
<p class="tool-call-permission-queued-text">Waiting for earlier responses.</p>
</Show>
</div>
</div>
</div>
)
}
createEffect(() => {
const request = questionDetails()
if (!request) {
setQuestionSubmitting(false)
setQuestionError(null)
return
}
setQuestionError(null)
const requestId = request.id
setQuestionDraftAnswers((prev) => {
if (prev[requestId]) return prev
const initial = request.questions.map(() => [])
return { ...prev, [requestId]: initial }
})
setQuestionCustomDraft((prev) => {
if (prev[requestId]) return prev
const initial = request.questions.map(() => "")
return { ...prev, [requestId]: initial }
})
})
const status = () => toolState()?.status || ""
onCleanup(() => {
@@ -993,6 +1310,7 @@ export default function ToolCall(props: ToolCallProps) {
{renderError()}
{renderPermissionBlock()}
{renderQuestionBlock()}
<Show when={status() === "pending" && !pendingPermission()}>
<div class="tool-call-pending-message">

View File

@@ -9,6 +9,7 @@ import { todoRenderer } from "./todo"
import { webfetchRenderer } from "./webfetch"
import { writeRenderer } from "./write"
import { invalidRenderer } from "./invalid"
import { questionRenderer } from "./question"
const TOOL_RENDERERS: ToolRenderer[] = [
bashRenderer,
@@ -19,6 +20,7 @@ const TOOL_RENDERERS: ToolRenderer[] = [
webfetchRenderer,
todoRenderer,
taskRenderer,
questionRenderer,
invalidRenderer,
]

View File

@@ -0,0 +1,17 @@
import type { ToolRenderer } from "../types"
export const questionRenderer: ToolRenderer = {
tools: ["question"],
getAction: () => "Awaiting answers...",
getTitle({ toolState }) {
const state = toolState()
if (!state) return "Questions"
if (state.status === "completed") return "Questions"
return "Asking questions"
},
renderBody() {
// The question tool UI is rendered by ToolCall itself so
// it can share the same layout for pending/completed.
return null
},
}

View File

@@ -45,6 +45,8 @@ export function getToolIcon(tool: string): string {
case "todowrite":
case "todoread":
return "📋"
case "question":
return "❓"
case "list":
return "📁"
case "patch":