mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-21 02:06:00 +00:00
fix(tui): follow the current permission request in usePermissionInput
The prompt stays mounted while requests queue, so the hook must take a request accessor; capturing the first request left the input subscribed to the resolved tool call. Regression test swaps the mounted request.
This commit is contained in:
@@ -19,14 +19,20 @@ import { useSlot } from "@opencode-ai/quark/solid"
|
||||
|
||||
type PermissionStage = "permission" | "always" | "reject"
|
||||
|
||||
/** Resolved tool input for a permission request, once input streaming settles. */
|
||||
export function usePermissionInput(request: PermissionV2Request): () => Record<string, unknown> {
|
||||
/**
|
||||
* Resolved tool input for a permission request, once input streaming settles.
|
||||
* Takes an accessor: the prompt stays mounted across queued requests, so the
|
||||
* slot subscription must follow the current request.
|
||||
*/
|
||||
export function usePermissionInput(request: () => PermissionV2Request): () => Record<string, unknown> {
|
||||
const data = useData()
|
||||
const tool = request.source
|
||||
if (!tool) return () => ({})
|
||||
const part = useSlot(data.session.message.parts(request.sessionID, tool.messageID), tool.callID)
|
||||
const part = createMemo(() => {
|
||||
const tool = request().source
|
||||
if (!tool) return
|
||||
return useSlot(data.session.message.parts(request().sessionID, tool.messageID), tool.callID)
|
||||
})
|
||||
return createMemo(() => {
|
||||
const item = part()
|
||||
const item = part()?.()
|
||||
if (item?.type === "tool" && item.state.status !== "streaming") return item.state.input
|
||||
return {}
|
||||
})
|
||||
@@ -157,7 +163,7 @@ export function PermissionPrompt(props: { request: PermissionV2Request; director
|
||||
const pathFormatter = usePathFormatter()
|
||||
const session = createMemo(() => data.session.get(props.request.sessionID))
|
||||
|
||||
const input = usePermissionInput(props.request)
|
||||
const input = usePermissionInput(() => props.request)
|
||||
|
||||
const { themeV2 } = useTheme()
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import { testRender } from "@opentui/solid"
|
||||
import type { OpenCodeEvent, PermissionV2Request } from "@opencode-ai/client"
|
||||
import { createEffect, type ParentProps } from "solid-js"
|
||||
import { createEffect, createSignal, type ParentProps } from "solid-js"
|
||||
import { ClientProvider, useClient } from "../../../src/context/client"
|
||||
import { DataProvider as DataProviderBase, useData } from "../../../src/context/data"
|
||||
import { LocationProvider, useLocation } from "../../../src/context/location"
|
||||
@@ -50,15 +50,18 @@ test("permission input tracks the tool part slot as input settles", async () =>
|
||||
let client!: ReturnType<typeof useClient>
|
||||
let input!: () => unknown
|
||||
|
||||
const request = {
|
||||
id: "perm_1",
|
||||
sessionID,
|
||||
permission: "shell",
|
||||
resources: [],
|
||||
metadata: {},
|
||||
source: { messageID: "message-assistant", callID: "call-permission" },
|
||||
time: { created: 1 },
|
||||
} as unknown as PermissionV2Request
|
||||
const permissionRequest = (id: string, callID: string) =>
|
||||
({
|
||||
id,
|
||||
sessionID,
|
||||
permission: "shell",
|
||||
resources: [],
|
||||
metadata: {},
|
||||
source: { messageID: "message-assistant", callID },
|
||||
time: { created: 1 },
|
||||
}) as unknown as PermissionV2Request
|
||||
|
||||
const [request, setRequest] = createSignal(permissionRequest("perm_1", "call-permission"))
|
||||
|
||||
function Probe() {
|
||||
data = useData()
|
||||
@@ -109,6 +112,36 @@ test("permission input tracks the tool part slot as input settles", async () =>
|
||||
const value = input()
|
||||
return typeof value === "object" && value !== null && (value as { command?: string }).command === "rm -rf ./dist"
|
||||
})
|
||||
|
||||
// The prompt stays mounted while requests queue: when the next request
|
||||
// becomes current, the input must re-resolve against its tool call.
|
||||
emitEvent(events, {
|
||||
id: "evt_perm_tool_started_2",
|
||||
created: 3,
|
||||
type: "session.tool.input.started",
|
||||
durable: { aggregateID: `session_${sessionID}`, seq: 2, version: 1 },
|
||||
data: { sessionID, assistantMessageID: "message-assistant", callID: "call-permission-2", name: "shell" },
|
||||
} as unknown as OpenCodeEvent)
|
||||
emitEvent(events, {
|
||||
id: "evt_perm_tool_called_2",
|
||||
created: 4,
|
||||
type: "session.tool.called",
|
||||
durable: { aggregateID: `session_${sessionID}`, seq: 3, version: 1 },
|
||||
data: {
|
||||
sessionID,
|
||||
assistantMessageID: "message-assistant",
|
||||
callID: "call-permission-2",
|
||||
input: { command: "git push" },
|
||||
timestamp: 4,
|
||||
},
|
||||
} as unknown as OpenCodeEvent)
|
||||
await wait(() => data.session.message.parts(sessionID, "message-assistant").has("call-permission-2"))
|
||||
|
||||
setRequest(permissionRequest("perm_2", "call-permission-2"))
|
||||
await wait(() => {
|
||||
const value = input()
|
||||
return typeof value === "object" && value !== null && (value as { command?: string }).command === "git push"
|
||||
})
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user