Skip to content

Commit fd40184

Browse files
dbpolitofwang
authored andcommitted
feat(desktop): Ask Question Tool Support (anomalyco#8232)
1 parent b494e76 commit fd40184

7 files changed

Lines changed: 622 additions & 7 deletions

File tree

packages/app/src/context/global-sync.tsx

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
type LspStatus,
1717
type VcsInfo,
1818
type PermissionRequest,
19+
type QuestionRequest,
1920
createOpencodeClient,
2021
} from "@opencode-ai/sdk/v2/client"
2122
import { createStore, produce, reconcile } from "solid-js/store"
@@ -49,6 +50,9 @@ type State = {
4950
permission: {
5051
[sessionID: string]: PermissionRequest[]
5152
}
53+
question: {
54+
[sessionID: string]: QuestionRequest[]
55+
}
5256
mcp: {
5357
[name: string]: McpStatus
5458
}
@@ -98,6 +102,7 @@ function createGlobalSync() {
98102
session_diff: {},
99103
todo: {},
100104
permission: {},
105+
question: {},
101106
mcp: {},
102107
lsp: [],
103108
vcs: undefined,
@@ -208,6 +213,38 @@ function createGlobalSync() {
208213
}
209214
})
210215
}),
216+
sdk.question.list().then((x) => {
217+
const grouped: Record<string, QuestionRequest[]> = {}
218+
for (const question of x.data ?? []) {
219+
if (!question?.id || !question.sessionID) continue
220+
const existing = grouped[question.sessionID]
221+
if (existing) {
222+
existing.push(question)
223+
continue
224+
}
225+
grouped[question.sessionID] = [question]
226+
}
227+
228+
batch(() => {
229+
for (const sessionID of Object.keys(store.question)) {
230+
if (grouped[sessionID]) continue
231+
setStore("question", sessionID, [])
232+
}
233+
for (const [sessionID, questions] of Object.entries(grouped)) {
234+
setStore(
235+
"question",
236+
sessionID,
237+
reconcile(
238+
questions
239+
.filter((q) => !!q?.id)
240+
.slice()
241+
.sort((a, b) => a.id.localeCompare(b.id)),
242+
{ key: "id" },
243+
),
244+
)
245+
}
246+
})
247+
}),
211248
]).then(() => {
212249
setStore("status", "complete")
213250
})
@@ -396,6 +433,44 @@ function createGlobalSync() {
396433
)
397434
break
398435
}
436+
case "question.asked": {
437+
const sessionID = event.properties.sessionID
438+
const questions = store.question[sessionID]
439+
if (!questions) {
440+
setStore("question", sessionID, [event.properties])
441+
break
442+
}
443+
444+
const result = Binary.search(questions, event.properties.id, (q) => q.id)
445+
if (result.found) {
446+
setStore("question", sessionID, result.index, reconcile(event.properties))
447+
break
448+
}
449+
450+
setStore(
451+
"question",
452+
sessionID,
453+
produce((draft) => {
454+
draft.splice(result.index, 0, event.properties)
455+
}),
456+
)
457+
break
458+
}
459+
case "question.replied":
460+
case "question.rejected": {
461+
const questions = store.question[event.properties.sessionID]
462+
if (!questions) break
463+
const result = Binary.search(questions, event.properties.requestID, (q) => q.id)
464+
if (!result.found) break
465+
setStore(
466+
"question",
467+
event.properties.sessionID,
468+
produce((draft) => {
469+
draft.splice(result.index, 1)
470+
}),
471+
)
472+
break
473+
}
399474
case "lsp.updated": {
400475
const sdk = createOpencodeClient({
401476
baseUrl: globalSDK.url,

packages/app/src/pages/directory-layout.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { LocalProvider } from "@/context/local"
77
import { base64Decode } from "@opencode-ai/util/encode"
88
import { DataProvider } from "@opencode-ai/ui/context"
99
import { iife } from "@opencode-ai/util/iife"
10+
import type { QuestionAnswer } from "@opencode-ai/sdk/v2"
1011

1112
export default function Layout(props: ParentProps) {
1213
const params = useParams()
@@ -27,6 +28,11 @@ export default function Layout(props: ParentProps) {
2728
response: "once" | "always" | "reject"
2829
}) => sdk.client.permission.respond(input)
2930

31+
const replyToQuestion = (input: { requestID: string; answers: QuestionAnswer[] }) =>
32+
sdk.client.question.reply(input)
33+
34+
const rejectQuestion = (input: { requestID: string }) => sdk.client.question.reject(input)
35+
3036
const navigateToSession = (sessionID: string) => {
3137
navigate(`/${params.dir}/session/${sessionID}`)
3238
}
@@ -36,6 +42,8 @@ export default function Layout(props: ParentProps) {
3642
data={sync.data}
3743
directory={directory()}
3844
onPermissionRespond={respond}
45+
onQuestionReply={replyToQuestion}
46+
onQuestionReject={rejectQuestion}
3947
onNavigateToSession={navigateToSession}
4048
>
4149
<LocalProvider>{props.children}</LocalProvider>

packages/opencode/src/tool/registry.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ export namespace ToolRegistry {
9393

9494
return [
9595
InvalidTool,
96-
...(Flag.OPENCODE_CLIENT === "cli" ? [QuestionTool] : []),
96+
...(["app", "cli", "desktop"].includes(Flag.OPENCODE_CLIENT) ? [QuestionTool] : []),
9797
BashTool,
9898
ReadTool,
9999
GlobTool,

packages/ui/src/components/basic-tool.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export interface BasicToolProps {
2525
hideDetails?: boolean
2626
defaultOpen?: boolean
2727
forceOpen?: boolean
28+
locked?: boolean
2829
onSubtitleClick?: () => void
2930
}
3031

@@ -35,8 +36,13 @@ export function BasicTool(props: BasicToolProps) {
3536
if (props.forceOpen) setOpen(true)
3637
})
3738

39+
const handleOpenChange = (value: boolean) => {
40+
if (props.locked && !value) return
41+
setOpen(value)
42+
}
43+
3844
return (
39-
<Collapsible open={open()} onOpenChange={setOpen}>
45+
<Collapsible open={open()} onOpenChange={handleOpenChange}>
4046
<Collapsible.Trigger>
4147
<div data-component="tool-trigger">
4248
<div data-slot="basic-tool-tool-trigger-content">
@@ -95,7 +101,7 @@ export function BasicTool(props: BasicToolProps) {
95101
</Switch>
96102
</div>
97103
</div>
98-
<Show when={props.children && !props.hideDetails}>
104+
<Show when={props.children && !props.hideDetails && !props.locked}>
99105
<Collapsible.Arrow />
100106
</Show>
101107
</div>

packages/ui/src/components/message-part.css

Lines changed: 192 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -405,7 +405,8 @@
405405
[data-component="tool-part-wrapper"] {
406406
width: 100%;
407407

408-
&[data-permission="true"] {
408+
&[data-permission="true"],
409+
&[data-question="true"] {
409410
position: sticky;
410411
top: calc(2px + var(--sticky-header-height, 40px));
411412
bottom: 0px;
@@ -490,3 +491,193 @@
490491
justify-content: flex-end;
491492
}
492493
}
494+
495+
[data-component="question-prompt"] {
496+
display: flex;
497+
flex-direction: column;
498+
padding: 12px;
499+
background-color: var(--surface-inset-base);
500+
border-radius: 0 0 6px 6px;
501+
gap: 12px;
502+
503+
[data-slot="question-tabs"] {
504+
display: flex;
505+
gap: 4px;
506+
flex-wrap: wrap;
507+
508+
[data-slot="question-tab"] {
509+
padding: 4px 12px;
510+
font-size: 13px;
511+
border-radius: 4px;
512+
background-color: var(--surface-base);
513+
color: var(--text-base);
514+
border: none;
515+
cursor: pointer;
516+
transition:
517+
color 0.15s,
518+
background-color 0.15s;
519+
520+
&:hover {
521+
background-color: var(--surface-base-hover);
522+
}
523+
524+
&[data-active="true"] {
525+
background-color: var(--surface-raised-base);
526+
}
527+
528+
&[data-answered="true"] {
529+
color: var(--text-strong);
530+
}
531+
}
532+
}
533+
534+
[data-slot="question-content"] {
535+
display: flex;
536+
flex-direction: column;
537+
gap: 8px;
538+
539+
[data-slot="question-text"] {
540+
font-size: 14px;
541+
color: var(--text-base);
542+
line-height: 1.5;
543+
}
544+
}
545+
546+
[data-slot="question-options"] {
547+
display: flex;
548+
flex-direction: column;
549+
gap: 4px;
550+
551+
[data-slot="question-option"] {
552+
display: flex;
553+
flex-direction: column;
554+
align-items: flex-start;
555+
gap: 2px;
556+
padding: 8px 12px;
557+
background-color: var(--surface-base);
558+
border: 1px solid var(--border-weaker-base);
559+
border-radius: 6px;
560+
cursor: pointer;
561+
text-align: left;
562+
width: 100%;
563+
transition:
564+
background-color 0.15s,
565+
border-color 0.15s;
566+
position: relative;
567+
568+
&:hover {
569+
background-color: var(--surface-base-hover);
570+
border-color: var(--border-default);
571+
}
572+
573+
&[data-picked="true"] {
574+
[data-component="icon"] {
575+
position: absolute;
576+
right: 12px;
577+
top: 50%;
578+
transform: translateY(-50%);
579+
color: var(--text-strong);
580+
}
581+
}
582+
583+
[data-slot="option-label"] {
584+
font-size: 14px;
585+
color: var(--text-base);
586+
font-weight: 500;
587+
}
588+
589+
[data-slot="option-description"] {
590+
font-size: 12px;
591+
color: var(--text-weak);
592+
}
593+
}
594+
595+
[data-slot="custom-input-form"] {
596+
display: flex;
597+
gap: 8px;
598+
padding: 8px 0;
599+
align-items: stretch;
600+
601+
[data-slot="custom-input"] {
602+
flex: 1;
603+
padding: 8px 12px;
604+
font-size: 14px;
605+
border: 1px solid var(--border-default);
606+
border-radius: 6px;
607+
background-color: var(--surface-base);
608+
color: var(--text-base);
609+
outline: none;
610+
611+
&:focus {
612+
border-color: var(--border-focus);
613+
}
614+
615+
&::placeholder {
616+
color: var(--text-weak);
617+
}
618+
}
619+
620+
[data-component="button"] {
621+
height: auto;
622+
}
623+
}
624+
}
625+
626+
[data-slot="question-review"] {
627+
display: flex;
628+
flex-direction: column;
629+
gap: 12px;
630+
631+
[data-slot="review-title"] {
632+
display: none;
633+
}
634+
635+
[data-slot="review-item"] {
636+
display: flex;
637+
flex-direction: column;
638+
gap: 2px;
639+
font-size: 13px;
640+
641+
[data-slot="review-label"] {
642+
color: var(--text-weak);
643+
}
644+
645+
[data-slot="review-value"] {
646+
color: var(--text-strong);
647+
648+
&[data-answered="false"] {
649+
color: var(--text-weak);
650+
}
651+
}
652+
}
653+
}
654+
655+
[data-slot="question-actions"] {
656+
display: flex;
657+
align-items: center;
658+
gap: 8px;
659+
justify-content: flex-end;
660+
}
661+
}
662+
663+
[data-component="question-answers"] {
664+
display: flex;
665+
flex-direction: column;
666+
gap: 12px;
667+
padding: 8px 12px;
668+
669+
[data-slot="question-answer-item"] {
670+
display: flex;
671+
flex-direction: column;
672+
gap: 2px;
673+
font-size: 13px;
674+
675+
[data-slot="question-text"] {
676+
color: var(--text-weak);
677+
}
678+
679+
[data-slot="answer-text"] {
680+
color: var(--text-strong);
681+
}
682+
}
683+
}

0 commit comments

Comments
 (0)