summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src
diff options
context:
space:
mode:
authorAdam <[email protected]>2026-02-25 19:05:08 -0600
committerGitHub <[email protected]>2026-02-26 01:05:08 +0000
commitb8337cddc4269ba6e72f74c1e1f39aae41f56af3 (patch)
tree178a80a21af1a24f3568a0ef15e735a5d537c16f /packages/app/src
parent444178e079fb41ba2149a1cdfdd3040593715d70 (diff)
downloadopencode-b8337cddc4269ba6e72f74c1e1f39aae41f56af3.tar.gz
opencode-b8337cddc4269ba6e72f74c1e1f39aae41f56af3.zip
fix(app): permissions and questions from child sessions (#15105)
Co-authored-by: adamelmore <[email protected]>
Diffstat (limited to 'packages/app/src')
-rw-r--r--packages/app/src/pages/directory-layout.tsx11
-rw-r--r--packages/app/src/pages/session/composer/session-composer-state.test.ts83
-rw-r--r--packages/app/src/pages/session/composer/session-composer-state.ts22
-rw-r--r--packages/app/src/pages/session/composer/session-request-tree.ts45
4 files changed, 143 insertions, 18 deletions
diff --git a/packages/app/src/pages/directory-layout.tsx b/packages/app/src/pages/directory-layout.tsx
index 4f1d93ab2..71b52180f 100644
--- a/packages/app/src/pages/directory-layout.tsx
+++ b/packages/app/src/pages/directory-layout.tsx
@@ -1,12 +1,11 @@
import { createEffect, createMemo, Show, type ParentProps } from "solid-js"
import { createStore } from "solid-js/store"
import { useNavigate, useParams } from "@solidjs/router"
-import { SDKProvider, useSDK } from "@/context/sdk"
+import { SDKProvider } from "@/context/sdk"
import { SyncProvider, useSync } from "@/context/sync"
import { LocalProvider } from "@/context/local"
import { DataProvider } from "@opencode-ai/ui/context"
-import type { QuestionAnswer } from "@opencode-ai/sdk/v2"
import { decode64 } from "@/utils/base64"
import { showToast } from "@opencode-ai/ui/toast"
import { useLanguage } from "@/context/language"
@@ -15,19 +14,11 @@ function DirectoryDataProvider(props: ParentProps<{ directory: string }>) {
const params = useParams()
const navigate = useNavigate()
const sync = useSync()
- const sdk = useSDK()
return (
<DataProvider
data={sync.data}
directory={props.directory}
- onPermissionRespond={(input: {
- sessionID: string
- permissionID: string
- response: "once" | "always" | "reject"
- }) => sdk.client.permission.respond(input)}
- onQuestionReply={(input: { requestID: string; answers: QuestionAnswer[] }) => sdk.client.question.reply(input)}
- onQuestionReject={(input: { requestID: string }) => sdk.client.question.reject(input)}
onNavigateToSession={(sessionID: string) => navigate(`/${params.dir}/session/${sessionID}`)}
onSessionHref={(sessionID: string) => `/${params.dir}/session/${sessionID}`}
>
diff --git a/packages/app/src/pages/session/composer/session-composer-state.test.ts b/packages/app/src/pages/session/composer/session-composer-state.test.ts
new file mode 100644
index 000000000..7b6029eb3
--- /dev/null
+++ b/packages/app/src/pages/session/composer/session-composer-state.test.ts
@@ -0,0 +1,83 @@
+import { describe, expect, test } from "bun:test"
+import type { PermissionRequest, QuestionRequest, Session } from "@opencode-ai/sdk/v2/client"
+import { sessionPermissionRequest, sessionQuestionRequest } from "./session-request-tree"
+
+const session = (input: { id: string; parentID?: string }) =>
+ ({
+ id: input.id,
+ parentID: input.parentID,
+ }) as Session
+
+const permission = (id: string, sessionID: string) =>
+ ({
+ id,
+ sessionID,
+ }) as PermissionRequest
+
+const question = (id: string, sessionID: string) =>
+ ({
+ id,
+ sessionID,
+ questions: [],
+ }) as QuestionRequest
+
+describe("sessionPermissionRequest", () => {
+ test("prefers the current session permission", () => {
+ const sessions = [session({ id: "root" }), session({ id: "child", parentID: "root" })]
+ const permissions = {
+ root: [permission("perm-root", "root")],
+ child: [permission("perm-child", "child")],
+ }
+
+ expect(sessionPermissionRequest(sessions, permissions, "root")?.id).toBe("perm-root")
+ })
+
+ test("returns a nested child permission", () => {
+ const sessions = [
+ session({ id: "root" }),
+ session({ id: "child", parentID: "root" }),
+ session({ id: "grand", parentID: "child" }),
+ session({ id: "other" }),
+ ]
+ const permissions = {
+ grand: [permission("perm-grand", "grand")],
+ other: [permission("perm-other", "other")],
+ }
+
+ expect(sessionPermissionRequest(sessions, permissions, "root")?.id).toBe("perm-grand")
+ })
+
+ test("returns undefined without a matching tree permission", () => {
+ const sessions = [session({ id: "root" }), session({ id: "child", parentID: "root" })]
+ const permissions = {
+ other: [permission("perm-other", "other")],
+ }
+
+ expect(sessionPermissionRequest(sessions, permissions, "root")).toBeUndefined()
+ })
+})
+
+describe("sessionQuestionRequest", () => {
+ test("prefers the current session question", () => {
+ const sessions = [session({ id: "root" }), session({ id: "child", parentID: "root" })]
+ const questions = {
+ root: [question("q-root", "root")],
+ child: [question("q-child", "child")],
+ }
+
+ expect(sessionQuestionRequest(sessions, questions, "root")?.id).toBe("q-root")
+ })
+
+ test("returns a nested child question", () => {
+ const sessions = [
+ session({ id: "root" }),
+ session({ id: "child", parentID: "root" }),
+ session({ id: "grand", parentID: "child" }),
+ ]
+ const questions = {
+ grand: [question("q-grand", "grand")],
+ }
+
+ expect(sessionQuestionRequest(sessions, questions, "root")?.id).toBe("q-grand")
+ })
+})
diff --git a/packages/app/src/pages/session/composer/session-composer-state.ts b/packages/app/src/pages/session/composer/session-composer-state.ts
index 04c6f7e69..ed65867ef 100644
--- a/packages/app/src/pages/session/composer/session-composer-state.ts
+++ b/packages/app/src/pages/session/composer/session-composer-state.ts
@@ -7,14 +7,20 @@ import { useGlobalSync } from "@/context/global-sync"
import { useLanguage } from "@/context/language"
import { useSDK } from "@/context/sdk"
import { useSync } from "@/context/sync"
+import { sessionPermissionRequest, sessionQuestionRequest } from "./session-request-tree"
export function createSessionComposerBlocked() {
const params = useParams()
const sync = useSync()
+ const permissionRequest = createMemo(() =>
+ sessionPermissionRequest(sync.data.session, sync.data.permission, params.id),
+ )
+ const questionRequest = createMemo(() => sessionQuestionRequest(sync.data.session, sync.data.question, params.id))
+
return createMemo(() => {
const id = params.id
if (!id) return false
- return !!sync.data.permission[id]?.[0] || !!sync.data.question[id]?.[0]
+ return !!permissionRequest() || !!questionRequest()
})
}
@@ -26,18 +32,18 @@ export function createSessionComposerState() {
const language = useLanguage()
const questionRequest = createMemo((): QuestionRequest | undefined => {
- const id = params.id
- if (!id) return
- return sync.data.question[id]?.[0]
+ return sessionQuestionRequest(sync.data.session, sync.data.question, params.id)
})
const permissionRequest = createMemo((): PermissionRequest | undefined => {
- const id = params.id
- if (!id) return
- return sync.data.permission[id]?.[0]
+ return sessionPermissionRequest(sync.data.session, sync.data.permission, params.id)
})
- const blocked = createSessionComposerBlocked()
+ const blocked = createMemo(() => {
+ const id = params.id
+ if (!id) return false
+ return !!permissionRequest() || !!questionRequest()
+ })
const todos = createMemo((): Todo[] => {
const id = params.id
diff --git a/packages/app/src/pages/session/composer/session-request-tree.ts b/packages/app/src/pages/session/composer/session-request-tree.ts
new file mode 100644
index 000000000..f9673e254
--- /dev/null
+++ b/packages/app/src/pages/session/composer/session-request-tree.ts
@@ -0,0 +1,45 @@
+import type { PermissionRequest, QuestionRequest, Session } from "@opencode-ai/sdk/v2/client"
+
+function sessionTreeRequest<T>(session: Session[], request: Record<string, T[] | undefined>, sessionID?: string) {
+ if (!sessionID) return
+
+ const map = session.reduce((acc, item) => {
+ if (!item.parentID) return acc
+ const list = acc.get(item.parentID)
+ if (list) list.push(item.id)
+ if (!list) acc.set(item.parentID, [item.id])
+ return acc
+ }, new Map<string, string[]>())
+
+ const seen = new Set([sessionID])
+ const ids = [sessionID]
+ for (const id of ids) {
+ const list = map.get(id)
+ if (!list) continue
+ for (const child of list) {
+ if (seen.has(child)) continue
+ seen.add(child)
+ ids.push(child)
+ }
+ }
+
+ const id = ids.find((id) => !!request[id]?.[0])
+ if (!id) return
+ return request[id]?.[0]
+}
+
+export function sessionPermissionRequest(
+ session: Session[],
+ request: Record<string, PermissionRequest[] | undefined>,
+ sessionID?: string,
+) {
+ return sessionTreeRequest(session, request, sessionID)
+}
+
+export function sessionQuestionRequest(
+ session: Session[],
+ request: Record<string, QuestionRequest[] | undefined>,
+ sessionID?: string,
+) {
+ return sessionTreeRequest(session, request, sessionID)
+}