summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDaniel Polito <[email protected]>2026-01-13 15:28:08 -0300
committerGitHub <[email protected]>2026-01-13 12:28:08 -0600
commit3600bd27f481c461734e517a40e01cd4e899e10f (patch)
treee8d1a426a81ff04a4dfc2fb88ab925e4977bd936
parent92089bb295ffc62e681baf5c93336e97a052b26e (diff)
downloadopencode-3600bd27f481c461734e517a40e01cd4e899e10f.tar.gz
opencode-3600bd27f481c461734e517a40e01cd4e899e10f.zip
feat(desktop): Ask Question Tool Support (#8232)
-rw-r--r--packages/app/src/context/global-sync.tsx75
-rw-r--r--packages/app/src/pages/directory-layout.tsx8
-rw-r--r--packages/opencode/src/tool/registry.ts2
-rw-r--r--packages/ui/src/components/basic-tool.tsx10
-rw-r--r--packages/ui/src/components/message-part.css193
-rw-r--r--packages/ui/src/components/message-part.tsx319
-rw-r--r--packages/ui/src/context/data.tsx22
7 files changed, 622 insertions, 7 deletions
diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/global-sync.tsx
index ddac1f228..c11edd292 100644
--- a/packages/app/src/context/global-sync.tsx
+++ b/packages/app/src/context/global-sync.tsx
@@ -16,6 +16,7 @@ import {
type LspStatus,
type VcsInfo,
type PermissionRequest,
+ type QuestionRequest,
createOpencodeClient,
} from "@opencode-ai/sdk/v2/client"
import { createStore, produce, reconcile } from "solid-js/store"
@@ -49,6 +50,9 @@ type State = {
permission: {
[sessionID: string]: PermissionRequest[]
}
+ question: {
+ [sessionID: string]: QuestionRequest[]
+ }
mcp: {
[name: string]: McpStatus
}
@@ -98,6 +102,7 @@ function createGlobalSync() {
session_diff: {},
todo: {},
permission: {},
+ question: {},
mcp: {},
lsp: [],
vcs: undefined,
@@ -208,6 +213,38 @@ function createGlobalSync() {
}
})
}),
+ sdk.question.list().then((x) => {
+ const grouped: Record<string, QuestionRequest[]> = {}
+ for (const question of x.data ?? []) {
+ if (!question?.id || !question.sessionID) continue
+ const existing = grouped[question.sessionID]
+ if (existing) {
+ existing.push(question)
+ continue
+ }
+ grouped[question.sessionID] = [question]
+ }
+
+ batch(() => {
+ for (const sessionID of Object.keys(store.question)) {
+ if (grouped[sessionID]) continue
+ setStore("question", sessionID, [])
+ }
+ for (const [sessionID, questions] of Object.entries(grouped)) {
+ setStore(
+ "question",
+ sessionID,
+ reconcile(
+ questions
+ .filter((q) => !!q?.id)
+ .slice()
+ .sort((a, b) => a.id.localeCompare(b.id)),
+ { key: "id" },
+ ),
+ )
+ }
+ })
+ }),
]).then(() => {
setStore("status", "complete")
})
@@ -396,6 +433,44 @@ function createGlobalSync() {
)
break
}
+ case "question.asked": {
+ const sessionID = event.properties.sessionID
+ const questions = store.question[sessionID]
+ if (!questions) {
+ setStore("question", sessionID, [event.properties])
+ break
+ }
+
+ const result = Binary.search(questions, event.properties.id, (q) => q.id)
+ if (result.found) {
+ setStore("question", sessionID, result.index, reconcile(event.properties))
+ break
+ }
+
+ setStore(
+ "question",
+ sessionID,
+ produce((draft) => {
+ draft.splice(result.index, 0, event.properties)
+ }),
+ )
+ break
+ }
+ case "question.replied":
+ case "question.rejected": {
+ const questions = store.question[event.properties.sessionID]
+ if (!questions) break
+ const result = Binary.search(questions, event.properties.requestID, (q) => q.id)
+ if (!result.found) break
+ setStore(
+ "question",
+ event.properties.sessionID,
+ produce((draft) => {
+ draft.splice(result.index, 1)
+ }),
+ )
+ break
+ }
case "lsp.updated": {
const sdk = createOpencodeClient({
baseUrl: globalSDK.url,
diff --git a/packages/app/src/pages/directory-layout.tsx b/packages/app/src/pages/directory-layout.tsx
index 39124637c..dca02489a 100644
--- a/packages/app/src/pages/directory-layout.tsx
+++ b/packages/app/src/pages/directory-layout.tsx
@@ -7,6 +7,7 @@ import { LocalProvider } from "@/context/local"
import { base64Decode } from "@opencode-ai/util/encode"
import { DataProvider } from "@opencode-ai/ui/context"
import { iife } from "@opencode-ai/util/iife"
+import type { QuestionAnswer } from "@opencode-ai/sdk/v2"
export default function Layout(props: ParentProps) {
const params = useParams()
@@ -27,6 +28,11 @@ export default function Layout(props: ParentProps) {
response: "once" | "always" | "reject"
}) => sdk.client.permission.respond(input)
+ const replyToQuestion = (input: { requestID: string; answers: QuestionAnswer[] }) =>
+ sdk.client.question.reply(input)
+
+ const rejectQuestion = (input: { requestID: string }) => sdk.client.question.reject(input)
+
const navigateToSession = (sessionID: string) => {
navigate(`/${params.dir}/session/${sessionID}`)
}
@@ -36,6 +42,8 @@ export default function Layout(props: ParentProps) {
data={sync.data}
directory={directory()}
onPermissionRespond={respond}
+ onQuestionReply={replyToQuestion}
+ onQuestionReject={rejectQuestion}
onNavigateToSession={navigateToSession}
>
<LocalProvider>{props.children}</LocalProvider>
diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts
index 82bf7f563..24faed7f0 100644
--- a/packages/opencode/src/tool/registry.ts
+++ b/packages/opencode/src/tool/registry.ts
@@ -93,7 +93,7 @@ export namespace ToolRegistry {
return [
InvalidTool,
- ...(Flag.OPENCODE_CLIENT === "cli" ? [QuestionTool] : []),
+ ...(["app", "cli", "desktop"].includes(Flag.OPENCODE_CLIENT) ? [QuestionTool] : []),
BashTool,
ReadTool,
GlobTool,
diff --git a/packages/ui/src/components/basic-tool.tsx b/packages/ui/src/components/basic-tool.tsx
index 15b5d4867..725a7d0d6 100644
--- a/packages/ui/src/components/basic-tool.tsx
+++ b/packages/ui/src/components/basic-tool.tsx
@@ -25,6 +25,7 @@ export interface BasicToolProps {
hideDetails?: boolean
defaultOpen?: boolean
forceOpen?: boolean
+ locked?: boolean
onSubtitleClick?: () => void
}
@@ -35,8 +36,13 @@ export function BasicTool(props: BasicToolProps) {
if (props.forceOpen) setOpen(true)
})
+ const handleOpenChange = (value: boolean) => {
+ if (props.locked && !value) return
+ setOpen(value)
+ }
+
return (
- <Collapsible open={open()} onOpenChange={setOpen}>
+ <Collapsible open={open()} onOpenChange={handleOpenChange}>
<Collapsible.Trigger>
<div data-component="tool-trigger">
<div data-slot="basic-tool-tool-trigger-content">
@@ -95,7 +101,7 @@ export function BasicTool(props: BasicToolProps) {
</Switch>
</div>
</div>
- <Show when={props.children && !props.hideDetails}>
+ <Show when={props.children && !props.hideDetails && !props.locked}>
<Collapsible.Arrow />
</Show>
</div>
diff --git a/packages/ui/src/components/message-part.css b/packages/ui/src/components/message-part.css
index b087b59e1..71d33de31 100644
--- a/packages/ui/src/components/message-part.css
+++ b/packages/ui/src/components/message-part.css
@@ -405,7 +405,8 @@
[data-component="tool-part-wrapper"] {
width: 100%;
- &[data-permission="true"] {
+ &[data-permission="true"],
+ &[data-question="true"] {
position: sticky;
top: calc(2px + var(--sticky-header-height, 40px));
bottom: 0px;
@@ -490,3 +491,193 @@
justify-content: flex-end;
}
}
+
+[data-component="question-prompt"] {
+ display: flex;
+ flex-direction: column;
+ padding: 12px;
+ background-color: var(--surface-inset-base);
+ border-radius: 0 0 6px 6px;
+ gap: 12px;
+
+ [data-slot="question-tabs"] {
+ display: flex;
+ gap: 4px;
+ flex-wrap: wrap;
+
+ [data-slot="question-tab"] {
+ padding: 4px 12px;
+ font-size: 13px;
+ border-radius: 4px;
+ background-color: var(--surface-base);
+ color: var(--text-base);
+ border: none;
+ cursor: pointer;
+ transition:
+ color 0.15s,
+ background-color 0.15s;
+
+ &:hover {
+ background-color: var(--surface-base-hover);
+ }
+
+ &[data-active="true"] {
+ background-color: var(--surface-raised-base);
+ }
+
+ &[data-answered="true"] {
+ color: var(--text-strong);
+ }
+ }
+ }
+
+ [data-slot="question-content"] {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+
+ [data-slot="question-text"] {
+ font-size: 14px;
+ color: var(--text-base);
+ line-height: 1.5;
+ }
+ }
+
+ [data-slot="question-options"] {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+
+ [data-slot="question-option"] {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 2px;
+ padding: 8px 12px;
+ background-color: var(--surface-base);
+ border: 1px solid var(--border-weaker-base);
+ border-radius: 6px;
+ cursor: pointer;
+ text-align: left;
+ width: 100%;
+ transition:
+ background-color 0.15s,
+ border-color 0.15s;
+ position: relative;
+
+ &:hover {
+ background-color: var(--surface-base-hover);
+ border-color: var(--border-default);
+ }
+
+ &[data-picked="true"] {
+ [data-component="icon"] {
+ position: absolute;
+ right: 12px;
+ top: 50%;
+ transform: translateY(-50%);
+ color: var(--text-strong);
+ }
+ }
+
+ [data-slot="option-label"] {
+ font-size: 14px;
+ color: var(--text-base);
+ font-weight: 500;
+ }
+
+ [data-slot="option-description"] {
+ font-size: 12px;
+ color: var(--text-weak);
+ }
+ }
+
+ [data-slot="custom-input-form"] {
+ display: flex;
+ gap: 8px;
+ padding: 8px 0;
+ align-items: stretch;
+
+ [data-slot="custom-input"] {
+ flex: 1;
+ padding: 8px 12px;
+ font-size: 14px;
+ border: 1px solid var(--border-default);
+ border-radius: 6px;
+ background-color: var(--surface-base);
+ color: var(--text-base);
+ outline: none;
+
+ &:focus {
+ border-color: var(--border-focus);
+ }
+
+ &::placeholder {
+ color: var(--text-weak);
+ }
+ }
+
+ [data-component="button"] {
+ height: auto;
+ }
+ }
+ }
+
+ [data-slot="question-review"] {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+
+ [data-slot="review-title"] {
+ display: none;
+ }
+
+ [data-slot="review-item"] {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ font-size: 13px;
+
+ [data-slot="review-label"] {
+ color: var(--text-weak);
+ }
+
+ [data-slot="review-value"] {
+ color: var(--text-strong);
+
+ &[data-answered="false"] {
+ color: var(--text-weak);
+ }
+ }
+ }
+ }
+
+ [data-slot="question-actions"] {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ justify-content: flex-end;
+ }
+}
+
+[data-component="question-answers"] {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ padding: 8px 12px;
+
+ [data-slot="question-answer-item"] {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ font-size: 13px;
+
+ [data-slot="question-text"] {
+ color: var(--text-weak);
+ }
+
+ [data-slot="answer-text"] {
+ color: var(--text-strong);
+ }
+ }
+}
diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx
index 71ff37161..e1a34a324 100644
--- a/packages/ui/src/components/message-part.tsx
+++ b/packages/ui/src/components/message-part.tsx
@@ -22,7 +22,11 @@ import {
ToolPart,
UserMessage,
Todo,
+ QuestionRequest,
+ QuestionAnswer,
+ QuestionInfo,
} from "@opencode-ai/sdk/v2"
+import { createStore } from "solid-js/store"
import { useData } from "../context"
import { useDiffComponent } from "../context/diff"
import { useCodeComponent } from "../context/code"
@@ -238,6 +242,11 @@ export function getToolInfo(tool: string, input: any = {}): ToolInfo {
icon: "checklist",
title: "Read to-dos",
}
+ case "question":
+ return {
+ icon: "bubble-5",
+ title: "Questions",
+ }
default:
return {
icon: "mcp",
@@ -438,6 +447,7 @@ export interface ToolProps {
hideDetails?: boolean
defaultOpen?: boolean
forceOpen?: boolean
+ locked?: boolean
}
export type ToolComponent = Component<ToolProps>
@@ -475,7 +485,15 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
return next
})
+ const questionRequest = createMemo(() => {
+ const next = data.store.question?.[props.message.sessionID]?.[0]
+ if (!next || !next.tool) return undefined
+ if (next.tool!.callID !== part.callID) return undefined
+ return next
+ })
+
const [showPermission, setShowPermission] = createSignal(false)
+ const [showQuestion, setShowQuestion] = createSignal(false)
createEffect(() => {
const perm = permission()
@@ -487,9 +505,19 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
}
})
+ createEffect(() => {
+ const question = questionRequest()
+ if (question) {
+ const timeout = setTimeout(() => setShowQuestion(true), 50)
+ onCleanup(() => clearTimeout(timeout))
+ } else {
+ setShowQuestion(false)
+ }
+ })
+
const [forceOpen, setForceOpen] = createSignal(false)
createEffect(() => {
- if (permission()) setForceOpen(true)
+ if (permission() || questionRequest()) setForceOpen(true)
})
const respond = (response: "once" | "always" | "reject") => {
@@ -512,7 +540,7 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
const render = ToolRegistry.render(part.tool) ?? GenericTool
return (
- <div data-component="tool-part-wrapper" data-permission={showPermission()}>
+ <div data-component="tool-part-wrapper" data-permission={showPermission()} data-question={showQuestion()}>
<Switch>
<Match when={part.state.status === "error" && part.state.error}>
{(error) => {
@@ -549,6 +577,7 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
status={part.state.status}
hideDetails={props.hideDetails}
forceOpen={forceOpen()}
+ locked={showPermission() || showQuestion()}
defaultOpen={props.defaultOpen}
/>
</Match>
@@ -568,6 +597,7 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
</div>
</div>
</Show>
+ <Show when={showQuestion() && questionRequest()}>{(request) => <QuestionPrompt request={request()} />}</Show>
</div>
)
}
@@ -1042,3 +1072,288 @@ ToolRegistry.register({
)
},
})
+
+ToolRegistry.register({
+ name: "question",
+ render(props) {
+ const questions = createMemo(() => (props.input.questions ?? []) as QuestionInfo[])
+ const answers = createMemo(() => (props.metadata.answers ?? []) as QuestionAnswer[])
+ const completed = createMemo(() => answers().length > 0)
+
+ const subtitle = createMemo(() => {
+ const count = questions().length
+ if (count === 0) return ""
+ if (completed()) return `${count} answered`
+ return `${count} question${count > 1 ? "s" : ""}`
+ })
+
+ return (
+ <BasicTool
+ {...props}
+ defaultOpen={completed()}
+ icon="bubble-5"
+ trigger={{
+ title: "Questions",
+ subtitle: subtitle(),
+ }}
+ >
+ <Show when={completed()}>
+ <div data-component="question-answers">
+ <For each={questions()}>
+ {(q, i) => {
+ const answer = () => answers()[i()] ?? []
+ return (
+ <div data-slot="question-answer-item">
+ <div data-slot="question-text">{q.question}</div>
+ <div data-slot="answer-text">{answer().join(", ") || "(no answer)"}</div>
+ </div>
+ )
+ }}
+ </For>
+ </div>
+ </Show>
+ </BasicTool>
+ )
+ },
+})
+
+function QuestionPrompt(props: { request: QuestionRequest }) {
+ const data = useData()
+ const questions = createMemo(() => props.request.questions)
+ const single = createMemo(() => questions().length === 1 && questions()[0]?.multiple !== true)
+
+ const [store, setStore] = createStore({
+ tab: 0,
+ answers: [] as QuestionAnswer[],
+ custom: [] as string[],
+ editing: false,
+ })
+
+ const question = createMemo(() => questions()[store.tab])
+ const confirm = createMemo(() => !single() && store.tab === questions().length)
+ const options = createMemo(() => question()?.options ?? [])
+ const input = createMemo(() => store.custom[store.tab] ?? "")
+ const multi = createMemo(() => question()?.multiple === true)
+ const customPicked = createMemo(() => {
+ const value = input()
+ if (!value) return false
+ return store.answers[store.tab]?.includes(value) ?? false
+ })
+
+ function submit() {
+ const answers = questions().map((_, i) => store.answers[i] ?? [])
+ data.replyToQuestion?.({
+ requestID: props.request.id,
+ answers,
+ })
+ }
+
+ function reject() {
+ data.rejectQuestion?.({
+ requestID: props.request.id,
+ })
+ }
+
+ function pick(answer: string, custom: boolean = false) {
+ const answers = [...store.answers]
+ answers[store.tab] = [answer]
+ setStore("answers", answers)
+ if (custom) {
+ const inputs = [...store.custom]
+ inputs[store.tab] = answer
+ setStore("custom", inputs)
+ }
+ if (single()) {
+ data.replyToQuestion?.({
+ requestID: props.request.id,
+ answers: [[answer]],
+ })
+ return
+ }
+ setStore("tab", store.tab + 1)
+ }
+
+ function toggle(answer: string) {
+ const existing = store.answers[store.tab] ?? []
+ const next = [...existing]
+ const index = next.indexOf(answer)
+ if (index === -1) next.push(answer)
+ if (index !== -1) next.splice(index, 1)
+ const answers = [...store.answers]
+ answers[store.tab] = next
+ setStore("answers", answers)
+ }
+
+ function selectTab(index: number) {
+ setStore("tab", index)
+ setStore("editing", false)
+ }
+
+ function selectOption(optIndex: number) {
+ if (optIndex === options().length) {
+ setStore("editing", true)
+ return
+ }
+ const opt = options()[optIndex]
+ if (!opt) return
+ if (multi()) {
+ toggle(opt.label)
+ return
+ }
+ pick(opt.label)
+ }
+
+ function handleCustomSubmit(e: Event) {
+ e.preventDefault()
+ const value = input().trim()
+ if (!value) {
+ setStore("editing", false)
+ return
+ }
+ if (multi()) {
+ const existing = store.answers[store.tab] ?? []
+ const next = [...existing]
+ if (!next.includes(value)) next.push(value)
+ const answers = [...store.answers]
+ answers[store.tab] = next
+ setStore("answers", answers)
+ setStore("editing", false)
+ return
+ }
+ pick(value, true)
+ setStore("editing", false)
+ }
+
+ return (
+ <div data-component="question-prompt">
+ <Show when={!single()}>
+ <div data-slot="question-tabs">
+ <For each={questions()}>
+ {(q, index) => {
+ const active = () => index() === store.tab
+ const answered = () => (store.answers[index()]?.length ?? 0) > 0
+ return (
+ <button
+ data-slot="question-tab"
+ data-active={active()}
+ data-answered={answered()}
+ onClick={() => selectTab(index())}
+ >
+ {q.header}
+ </button>
+ )
+ }}
+ </For>
+ <button data-slot="question-tab" data-active={confirm()} onClick={() => selectTab(questions().length)}>
+ Confirm
+ </button>
+ </div>
+ </Show>
+
+ <Show when={!confirm()}>
+ <div data-slot="question-content">
+ <div data-slot="question-text">
+ {question()?.question}
+ {multi() ? " (select all that apply)" : ""}
+ </div>
+ <div data-slot="question-options">
+ <For each={options()}>
+ {(opt, i) => {
+ const picked = () => store.answers[store.tab]?.includes(opt.label) ?? false
+ return (
+ <button data-slot="question-option" data-picked={picked()} onClick={() => selectOption(i())}>
+ <span data-slot="option-label">{opt.label}</span>
+ <Show when={opt.description}>
+ <span data-slot="option-description">{opt.description}</span>
+ </Show>
+ <Show when={picked()}>
+ <Icon name="check-small" size="normal" />
+ </Show>
+ </button>
+ )
+ }}
+ </For>
+ <button
+ data-slot="question-option"
+ data-picked={customPicked()}
+ onClick={() => selectOption(options().length)}
+ >
+ <span data-slot="option-label">Type your own answer</span>
+ <Show when={!store.editing && input()}>
+ <span data-slot="option-description">{input()}</span>
+ </Show>
+ <Show when={customPicked()}>
+ <Icon name="check-small" size="normal" />
+ </Show>
+ </button>
+ <Show when={store.editing}>
+ <form data-slot="custom-input-form" onSubmit={handleCustomSubmit}>
+ <input
+ ref={(el) => setTimeout(() => el.focus(), 0)}
+ type="text"
+ data-slot="custom-input"
+ placeholder="Type your answer..."
+ value={input()}
+ onInput={(e) => {
+ const inputs = [...store.custom]
+ inputs[store.tab] = e.currentTarget.value
+ setStore("custom", inputs)
+ }}
+ />
+ <Button type="submit" variant="primary" size="small">
+ {multi() ? "Add" : "Submit"}
+ </Button>
+ <Button type="button" variant="ghost" size="small" onClick={() => setStore("editing", false)}>
+ Cancel
+ </Button>
+ </form>
+ </Show>
+ </div>
+ </div>
+ </Show>
+
+ <Show when={confirm()}>
+ <div data-slot="question-review">
+ <div data-slot="review-title">Review your answers</div>
+ <For each={questions()}>
+ {(q, index) => {
+ const value = () => store.answers[index()]?.join(", ") ?? ""
+ const answered = () => Boolean(value())
+ return (
+ <div data-slot="review-item">
+ <span data-slot="review-label">{q.question}</span>
+ <span data-slot="review-value" data-answered={answered()}>
+ {answered() ? value() : "(not answered)"}
+ </span>
+ </div>
+ )
+ }}
+ </For>
+ </div>
+ </Show>
+
+ <div data-slot="question-actions">
+ <Button variant="ghost" size="small" onClick={reject}>
+ Dismiss
+ </Button>
+ <Show when={!single()}>
+ <Show when={confirm()}>
+ <Button variant="primary" size="small" onClick={submit}>
+ Submit
+ </Button>
+ </Show>
+ <Show when={!confirm() && multi()}>
+ <Button
+ variant="secondary"
+ size="small"
+ onClick={() => selectTab(store.tab + 1)}
+ disabled={(store.answers[store.tab]?.length ?? 0) === 0}
+ >
+ Next
+ </Button>
+ </Show>
+ </Show>
+ </div>
+ </div>
+ )
+}
diff --git a/packages/ui/src/context/data.tsx b/packages/ui/src/context/data.tsx
index acab99fe8..dcb9adb39 100644
--- a/packages/ui/src/context/data.tsx
+++ b/packages/ui/src/context/data.tsx
@@ -1,4 +1,13 @@
-import type { Message, Session, Part, FileDiff, SessionStatus, PermissionRequest } from "@opencode-ai/sdk/v2"
+import type {
+ Message,
+ Session,
+ Part,
+ FileDiff,
+ SessionStatus,
+ PermissionRequest,
+ QuestionRequest,
+ QuestionAnswer,
+} from "@opencode-ai/sdk/v2"
import { createSimpleContext } from "./helper"
import { PreloadMultiFileDiffResult } from "@pierre/diffs/ssr"
@@ -16,6 +25,9 @@ type Data = {
permission?: {
[sessionID: string]: PermissionRequest[]
}
+ question?: {
+ [sessionID: string]: QuestionRequest[]
+ }
message: {
[sessionID: string]: Message[]
}
@@ -30,6 +42,10 @@ export type PermissionRespondFn = (input: {
response: "once" | "always" | "reject"
}) => void
+export type QuestionReplyFn = (input: { requestID: string; answers: QuestionAnswer[] }) => void
+
+export type QuestionRejectFn = (input: { requestID: string }) => void
+
export type NavigateToSessionFn = (sessionID: string) => void
export const { use: useData, provider: DataProvider } = createSimpleContext({
@@ -38,6 +54,8 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
data: Data
directory: string
onPermissionRespond?: PermissionRespondFn
+ onQuestionReply?: QuestionReplyFn
+ onQuestionReject?: QuestionRejectFn
onNavigateToSession?: NavigateToSessionFn
}) => {
return {
@@ -48,6 +66,8 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
return props.directory
},
respondToPermission: props.onPermissionRespond,
+ replyToQuestion: props.onQuestionReply,
+ rejectQuestion: props.onQuestionReject,
navigateToSession: props.onNavigateToSession,
}
},