From b8337cddc4269ba6e72f74c1e1f39aae41f56af3 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Wed, 25 Feb 2026 19:05:08 -0600 Subject: fix(app): permissions and questions from child sessions (#15105) Co-authored-by: adamelmore <2363879+adamdottv@users.noreply.github.com> --- packages/ui/src/components/message-part.css | 101 +-------- packages/ui/src/components/message-part.tsx | 325 +--------------------------- 2 files changed, 3 insertions(+), 423 deletions(-) (limited to 'packages/ui/src/components') diff --git a/packages/ui/src/components/message-part.css b/packages/ui/src/components/message-part.css index f063076e0..bea33ff54 100644 --- a/packages/ui/src/components/message-part.css +++ b/packages/ui/src/components/message-part.css @@ -660,105 +660,6 @@ [data-component="tool-part-wrapper"] { width: 100%; - - &[data-permission="true"], - &[data-question="true"] { - position: sticky; - top: calc(2px + var(--sticky-header-height, 40px)); - bottom: 0px; - z-index: 20; - border-radius: 6px; - border: none; - box-shadow: var(--shadow-xs-border-base); - background-color: var(--surface-raised-base); - overflow: visible; - overflow-anchor: none; - - & > *:first-child { - border-top-left-radius: 6px; - border-top-right-radius: 6px; - overflow: hidden; - } - - & > *:last-child { - border-bottom-left-radius: 6px; - border-bottom-right-radius: 6px; - overflow: hidden; - } - - [data-component="collapsible"] { - border: none; - } - - [data-component="card"] { - border: none; - } - } - - &[data-permission="true"] { - &::before { - content: ""; - position: absolute; - inset: -1.5px; - top: -5px; - border-radius: 7.5px; - border: 1.5px solid transparent; - background: - linear-gradient(var(--background-base) 0 0) padding-box, - conic-gradient( - from var(--border-angle), - transparent 0deg, - transparent 0deg, - var(--border-warning-strong, var(--border-warning-selected)) 300deg, - var(--border-warning-base) 360deg - ) - border-box; - animation: chase-border 2.5s linear infinite; - pointer-events: none; - z-index: -1; - } - } - - &[data-question="true"] { - background: var(--background-base); - border: 1px solid var(--border-weak-base); - } -} - -@property --border-angle { - syntax: ""; - initial-value: 0deg; - inherits: false; -} - -@keyframes chase-border { - from { - --border-angle: 0deg; - } - - to { - --border-angle: 360deg; - } -} - -[data-component="permission-prompt"] { - display: flex; - flex-direction: column; - padding: 8px 12px; - background-color: var(--surface-raised-strong); - border-radius: 0 0 6px 6px; - - [data-slot="permission-actions"] { - display: flex; - align-items: center; - gap: 8px; - justify-content: flex-end; - - [data-component="button"] { - padding-left: 12px; - padding-right: 12px; - } - } } [data-component="dock-prompt"][data-kind="permission"] { @@ -873,7 +774,7 @@ } } -:is([data-component="question-prompt"], [data-component="dock-prompt"][data-kind="question"]) { +[data-component="dock-prompt"][data-kind="question"] { position: relative; display: flex; flex-direction: column; diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx index adba42ce9..8fbad45bd 100644 --- a/packages/ui/src/components/message-part.tsx +++ b/packages/ui/src/components/message-part.tsx @@ -23,11 +23,9 @@ 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" @@ -37,7 +35,6 @@ import { BasicTool } from "./basic-tool" import { GenericTool } from "./basic-tool" import { Accordion } from "./accordion" import { StickyAccordionHeader } from "./sticky-accordion-header" -import { Button } from "./button" import { Card } from "./card" import { Collapsible } from "./collapsible" import { FileIcon } from "./file-icon" @@ -950,7 +947,6 @@ function ToolFileAccordion(props: { path: string; actions?: JSX.Element; childre } PART_MAPPING["tool"] = function ToolPartDisplay(props) { - const data = useData() const i18n = useI18n() const part = props.part as ToolPart if (part.tool === "todowrite" || part.tool === "todoread") return null @@ -959,75 +955,18 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) { () => part.tool === "question" && (part.state.status === "pending" || part.state.status === "running"), ) - const permission = createMemo(() => { - const next = data.store.permission?.[props.message.sessionID]?.[0] - if (!next || !next.tool) return undefined - if (next.tool!.callID !== part.callID) return undefined - 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() - if (perm) { - const timeout = setTimeout(() => setShowPermission(true), 50) - onCleanup(() => clearTimeout(timeout)) - } else { - setShowPermission(false) - } - }) - - 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() || questionRequest()) setForceOpen(true) - }) - - const respond = (response: "once" | "always" | "reject") => { - const perm = permission() - if (!perm || !data.respondToPermission) return - data.respondToPermission({ - sessionID: perm.sessionID, - permissionID: perm.id, - response, - }) - } - const emptyInput: Record = {} const emptyMetadata: Record = {} const input = () => part.state?.input ?? emptyInput // @ts-expect-error const partMetadata = () => part.state?.metadata ?? emptyMetadata - const metadata = () => { - const perm = permission() - if (perm?.metadata) return { ...perm.metadata, ...partMetadata() } - return partMetadata() - } const render = ToolRegistry.render(part.tool) ?? GenericTool return ( -
+
{(error) => { @@ -1067,33 +1006,15 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) { component={render} input={input()} tool={part.tool} - metadata={metadata()} + metadata={partMetadata()} // @ts-expect-error output={part.state.output} status={part.state.status} hideDetails={props.hideDetails} - forceOpen={forceOpen()} - locked={showPermission() || showQuestion()} defaultOpen={props.defaultOpen} /> - -
-
- - - -
-
-
- {(request) => }
) @@ -1963,245 +1884,3 @@ ToolRegistry.register({ ) }, }) - -function QuestionPrompt(props: { request: QuestionRequest }) { - const data = useData() - const i18n = useI18n() - 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 ( -
- -
- - {(q, index) => { - const active = () => index() === store.tab - const answered = () => (store.answers[index()]?.length ?? 0) > 0 - return ( - - ) - }} - - -
-
- - -
-
- {question()?.question} - {multi() ? " " + i18n.t("ui.question.multiHint") : ""} -
-
- - {(opt, i) => { - const picked = () => store.answers[store.tab]?.includes(opt.label) ?? false - return ( - - ) - }} - - - -
- setTimeout(() => el.focus(), 0)} - type="text" - data-slot="custom-input" - placeholder={i18n.t("ui.question.custom.placeholder")} - value={input()} - onInput={(e) => { - const inputs = [...store.custom] - inputs[store.tab] = e.currentTarget.value - setStore("custom", inputs) - }} - /> - - -
-
-
-
-
- - -
-
{i18n.t("ui.messagePart.review.title")}
- - {(q, index) => { - const value = () => store.answers[index()]?.join(", ") ?? "" - const answered = () => Boolean(value()) - return ( -
- {q.question} - - {answered() ? value() : i18n.t("ui.question.review.notAnswered")} - -
- ) - }} -
-
-
- -
- - - - - - - - - -
-
- ) -} -- cgit v1.2.3