mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-08 03:53:22 +00:00
fix(tui): dismiss stale permission prompts (#40960)
This commit is contained in:
parent
d7651519f3
commit
047d434aa2
4 changed files with 92 additions and 33 deletions
|
|
@ -15,6 +15,7 @@ import type {
|
|||
ModelInfo,
|
||||
PermissionSavedInfo,
|
||||
PermissionRequest,
|
||||
PermissionReplyInput,
|
||||
Project,
|
||||
ProviderInfo,
|
||||
ReferenceInfo,
|
||||
|
|
@ -31,6 +32,7 @@ import type {
|
|||
OpenCodeEvent,
|
||||
WebSearchProvider,
|
||||
} from "@opencode-ai/client"
|
||||
import { isPermissionNotFoundError } from "@opencode-ai/client"
|
||||
import type { Plugin } from "@opencode-ai/plugin/tui"
|
||||
import { createStore, produce, reconcile } from "solid-js/store"
|
||||
import { createSimpleContext } from "./helper"
|
||||
|
|
@ -176,6 +178,17 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
)
|
||||
}
|
||||
|
||||
function removePermission(sessionID: string, requestID: string) {
|
||||
const requests = store.session.permission[sessionID]
|
||||
if (!requests?.some((request) => request.id === requestID)) return
|
||||
setStore(
|
||||
"session",
|
||||
"permission",
|
||||
sessionID,
|
||||
requests.filter((request) => request.id !== requestID),
|
||||
)
|
||||
}
|
||||
|
||||
const message = {
|
||||
update(sessionID: string, fn: (messages: SessionMessageInfo[], index: Map<string, number>) => void) {
|
||||
setStore(
|
||||
|
|
@ -840,14 +853,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
])
|
||||
break
|
||||
case "permission.replied":
|
||||
setStore(
|
||||
"session",
|
||||
"permission",
|
||||
event.data.sessionID,
|
||||
(store.session.permission[event.data.sessionID] ?? []).filter(
|
||||
(request) => request.id !== event.data.requestID,
|
||||
),
|
||||
)
|
||||
removePermission(event.data.sessionID, event.data.requestID)
|
||||
break
|
||||
case "form.created":
|
||||
if (store.session.form[event.data.form.sessionID]?.some((form) => form.id === event.data.form.id)) break
|
||||
|
|
@ -1036,6 +1042,12 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||
invalidate(sessionID: string) {
|
||||
sync.invalidate(`session.permission:${sessionID}`)
|
||||
},
|
||||
async reply(input: PermissionReplyInput) {
|
||||
await client.api.permission.reply(input).catch((error: unknown) => {
|
||||
if (!isPermissionNotFoundError(error)) throw error
|
||||
})
|
||||
removePermission(input.sessionID, input.requestID)
|
||||
},
|
||||
},
|
||||
form: {
|
||||
list(sessionID: string, ref?: LocationRef) {
|
||||
|
|
|
|||
|
|
@ -227,7 +227,7 @@ export function Session() {
|
|||
permissions().forEach((request) => {
|
||||
if (autoApproved.has(request.id)) return
|
||||
autoApproved.add(request.id)
|
||||
void client.api.permission
|
||||
void data.session.permission
|
||||
.reply({
|
||||
sessionID: request.sessionID,
|
||||
reply: "once",
|
||||
|
|
|
|||
|
|
@ -3,8 +3,7 @@ import { createMemo, For, Match, Show, Switch } from "solid-js"
|
|||
import { Portal, useRenderer, useTerminalDimensions, type JSX } from "@opentui/solid"
|
||||
import type { TextareaRenderable } from "@opentui/core"
|
||||
import { useTheme, useThemes } from "../../context/theme"
|
||||
import type { PermissionRequest } from "@opencode-ai/client"
|
||||
import { useClient } from "../../context/client"
|
||||
import type { PermissionReply, PermissionRequest } from "@opencode-ai/client"
|
||||
import { SplitBorder } from "../../ui/border"
|
||||
import { useData } from "../../context/data"
|
||||
import { filetype } from "../../util/filetype"
|
||||
|
|
@ -15,6 +14,7 @@ import { Keymap } from "../../context/keymap"
|
|||
import { usePathFormatter } from "../../context/path-format"
|
||||
import { SimulationSemantics } from "../../simulation/semantics"
|
||||
import { PatchDiff } from "../../component/patch-diff"
|
||||
import { useToast } from "../../ui/toast"
|
||||
|
||||
type PermissionStage = "permission" | "always" | "reject"
|
||||
|
||||
|
|
@ -110,8 +110,8 @@ function EditBody(props: { file?: string; diff?: string; patch?: string }) {
|
|||
}
|
||||
|
||||
export function PermissionPrompt(props: { request: PermissionRequest; directory?: string }) {
|
||||
const client = useClient()
|
||||
const data = useData()
|
||||
const toast = useToast()
|
||||
const [store, setStore] = createStore({
|
||||
stage: "permission" as PermissionStage,
|
||||
})
|
||||
|
|
@ -132,6 +132,12 @@ export function PermissionPrompt(props: { request: PermissionRequest; directory?
|
|||
|
||||
const theme = useTheme()
|
||||
|
||||
function reply(value: PermissionReply, message?: string) {
|
||||
void data.session.permission
|
||||
.reply({ sessionID: props.request.sessionID, requestID: props.request.id, reply: value, message })
|
||||
.catch((error: unknown) => toast.error(error))
|
||||
}
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Match when={store.stage === "always"}>
|
||||
|
|
@ -151,11 +157,7 @@ export function PermissionPrompt(props: { request: PermissionRequest; directory?
|
|||
onSelect={(option) => {
|
||||
setStore("stage", "permission")
|
||||
if (option === "cancel") return
|
||||
void client.api.permission.reply({
|
||||
sessionID: props.request.sessionID,
|
||||
reply: "always",
|
||||
requestID: props.request.id,
|
||||
})
|
||||
reply("always")
|
||||
}}
|
||||
/>
|
||||
</Match>
|
||||
|
|
@ -164,12 +166,7 @@ export function PermissionPrompt(props: { request: PermissionRequest; directory?
|
|||
action={props.request.action}
|
||||
instance={props.request.id}
|
||||
onConfirm={(message) => {
|
||||
void client.api.permission.reply({
|
||||
sessionID: props.request.sessionID,
|
||||
reply: "reject",
|
||||
requestID: props.request.id,
|
||||
message: message || undefined,
|
||||
})
|
||||
reply("reject", message || undefined)
|
||||
}}
|
||||
onCancel={() => {
|
||||
setStore("stage", "permission")
|
||||
|
|
@ -265,18 +262,10 @@ export function PermissionPrompt(props: { request: PermissionRequest; directory?
|
|||
setStore("stage", "reject")
|
||||
return
|
||||
}
|
||||
void client.api.permission.reply({
|
||||
sessionID: props.request.sessionID,
|
||||
reply: "reject",
|
||||
requestID: props.request.id,
|
||||
})
|
||||
reply("reject")
|
||||
return
|
||||
}
|
||||
void client.api.permission.reply({
|
||||
sessionID: props.request.sessionID,
|
||||
reply: "once",
|
||||
requestID: props.request.id,
|
||||
})
|
||||
reply("once")
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2067,6 +2067,64 @@ test("reconciles active session permissions when the event stream reconnects", a
|
|||
}
|
||||
})
|
||||
|
||||
test("dismisses a permission that expired before its reply", async () => {
|
||||
const events = createEventStream()
|
||||
const request = { id: "per_stale", sessionID: "ses_active", action: "read", resources: ["old.txt"] }
|
||||
let replies = 0
|
||||
const calls = createFetch((url, init) => {
|
||||
if (url.pathname === "/api/session/ses_active/permission/per_stale/reply" && init.method === "POST") {
|
||||
replies++
|
||||
return json(
|
||||
{
|
||||
_tag: "PermissionNotFoundError",
|
||||
requestID: request.id,
|
||||
message: `Permission request not found: ${request.id}`,
|
||||
},
|
||||
{ status: 404 },
|
||||
)
|
||||
}
|
||||
}, events)
|
||||
let data!: ReturnType<typeof useData>
|
||||
|
||||
function Probe() {
|
||||
data = useData()
|
||||
return <box />
|
||||
}
|
||||
|
||||
const app = await testRender(() => (
|
||||
<TestTuiContexts>
|
||||
<ClientProvider api={createApi(calls.fetch)}>
|
||||
<ProjectProvider>
|
||||
<DataProvider>
|
||||
<Probe />
|
||||
</DataProvider>
|
||||
</ProjectProvider>
|
||||
</ClientProvider>
|
||||
</TestTuiContexts>
|
||||
))
|
||||
|
||||
try {
|
||||
emitEvent(events, {
|
||||
id: "evt_permission_asked_stale",
|
||||
created: 0,
|
||||
type: "permission.asked",
|
||||
data: request,
|
||||
})
|
||||
await wait(() => data.session.permission.list(request.sessionID)?.length === 1)
|
||||
|
||||
await data.session.permission.reply({
|
||||
sessionID: request.sessionID,
|
||||
requestID: request.id,
|
||||
reply: "once",
|
||||
})
|
||||
|
||||
expect(replies).toBe(1)
|
||||
expect(data.session.permission.list(request.sessionID)).toEqual([])
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
test("adds, dismisses, and refreshes form requests", async () => {
|
||||
const events = createEventStream()
|
||||
const calls = createFetch((url) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue