From fab485d2db01951b98eb3abd2f939ec922df281c Mon Sep 17 00:00:00 2001 From: Ariane Emory Date: Thu, 27 Nov 2025 09:02:35 -0500 Subject: [PATCH 01/14] ... --- .../src/cli/cmd/tui/routes/session/index.tsx | 25 ++++++++++++++++++- packages/opencode/src/permission/index.ts | 21 +++++++++++++--- packages/opencode/src/server/server.ts | 12 +++++++-- packages/sdk/go/sessionpermission.go | 14 ++++++----- packages/sdk/js/src/gen/types.gen.ts | 3 ++- 5 files changed, 61 insertions(+), 14 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index fdbcb34f9df..f8ea9aade91 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -188,11 +188,30 @@ export function Session() { if (evt.ctrl || evt.meta) return if (evt.name === "return") return "once" if (evt.name === "a") return "always" + if (evt.name === "i") return "interject" if (evt.name === "d") return "reject" if (evt.name === "escape") return "reject" return }) - if (response) { + if (response === "interject") { + // Show interjection dialog + DialogPrompt.show(dialog, "What should the model do instead?", { + placeholder: "Enter your suggestion for the model...", + }).then((interjection) => { + if (interjection !== null) { + sdk.client.postSessionIdPermissionsPermissionId({ + path: { + permissionID: first.id, + id: route.sessionID, + }, + body: { + response: "interject", + interjection: interjection, + }, + }) + } + }) + } else if (response) { sdk.client.postSessionIdPermissionsPermissionId({ path: { permissionID: first.id, @@ -1193,6 +1212,10 @@ function ToolPart(props: { last: boolean; part: ToolPart; message: AssistantMess a accept always + + i + interject + d deny diff --git a/packages/opencode/src/permission/index.ts b/packages/opencode/src/permission/index.ts index 32dbd5a0370..012497f0ba4 100644 --- a/packages/opencode/src/permission/index.ts +++ b/packages/opencode/src/permission/index.ts @@ -45,6 +45,7 @@ export namespace Permission { sessionID: z.string(), permissionID: z.string(), response: z.string(), + interjection: z.string().optional(), }), ), } @@ -140,10 +141,15 @@ export namespace Permission { }) } - export const Response = z.enum(["once", "always", "reject"]) + export const Response = z.enum(["once", "always", "reject", "interject"]) export type Response = z.infer - export function respond(input: { sessionID: Info["sessionID"]; permissionID: Info["id"]; response: Response }) { + export function respond(input: { + sessionID: Info["sessionID"] + permissionID: Info["id"] + response: Response + interjection?: string + }) { log.info("response", input) const { pending, approved } = state() const match = pending[input.sessionID]?.[input.permissionID] @@ -153,9 +159,16 @@ export namespace Permission { sessionID: input.sessionID, permissionID: input.permissionID, response: input.response, + interjection: input.interjection, }) - if (input.response === "reject") { - match.reject(new RejectedError(input.sessionID, input.permissionID, match.info.callID, match.info.metadata)) + if (input.response === "reject" || input.response === "interject") { + const reason = + input.response === "interject" && input.interjection + ? `The user rejected this action and suggests: ${input.interjection}` + : undefined + match.reject( + new RejectedError(input.sessionID, input.permissionID, match.info.callID, match.info.metadata, reason), + ) return } match.resolve() diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index 7743e3dbb83..7bd71308f83 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -1186,15 +1186,23 @@ export namespace Server { permissionID: z.string(), }), ), - validator("json", z.object({ response: Permission.Response })), + validator( + "json", + z.object({ + response: Permission.Response, + interjection: z.string().optional(), + }), + ), async (c) => { const params = c.req.valid("param") const id = params.id const permissionID = params.permissionID + const json = c.req.valid("json") Permission.respond({ sessionID: id, permissionID, - response: c.req.valid("json").response, + response: json.response, + interjection: json.interjection, }) return c.json(true) }, diff --git a/packages/sdk/go/sessionpermission.go b/packages/sdk/go/sessionpermission.go index 51d18e22815..a1f0ae529d3 100644 --- a/packages/sdk/go/sessionpermission.go +++ b/packages/sdk/go/sessionpermission.go @@ -136,8 +136,9 @@ type PermissionPatternArray []string func (r PermissionPatternArray) ImplementsPermissionPatternUnion() {} type SessionPermissionRespondParams struct { - Response param.Field[SessionPermissionRespondParamsResponse] `json:"response,required"` - Directory param.Field[string] `query:"directory"` + Response param.Field[SessionPermissionRespondParamsResponse] `json:"response,required"` + Interjection param.Field[string] `json:"interjection,omitempty"` + Directory param.Field[string] `query:"directory"` } func (r SessionPermissionRespondParams) MarshalJSON() (data []byte, err error) { @@ -156,14 +157,15 @@ func (r SessionPermissionRespondParams) URLQuery() (v url.Values) { type SessionPermissionRespondParamsResponse string const ( - SessionPermissionRespondParamsResponseOnce SessionPermissionRespondParamsResponse = "once" - SessionPermissionRespondParamsResponseAlways SessionPermissionRespondParamsResponse = "always" - SessionPermissionRespondParamsResponseReject SessionPermissionRespondParamsResponse = "reject" + SessionPermissionRespondParamsResponseOnce SessionPermissionRespondParamsResponse = "once" + SessionPermissionRespondParamsResponseAlways SessionPermissionRespondParamsResponse = "always" + SessionPermissionRespondParamsResponseReject SessionPermissionRespondParamsResponse = "reject" + SessionPermissionRespondParamsResponseInterject SessionPermissionRespondParamsResponse = "interject" ) func (r SessionPermissionRespondParamsResponse) IsKnown() bool { switch r { - case SessionPermissionRespondParamsResponseOnce, SessionPermissionRespondParamsResponseAlways, SessionPermissionRespondParamsResponseReject: + case SessionPermissionRespondParamsResponseOnce, SessionPermissionRespondParamsResponseAlways, SessionPermissionRespondParamsResponseReject, SessionPermissionRespondParamsResponseInterject: return true } return false diff --git a/packages/sdk/js/src/gen/types.gen.ts b/packages/sdk/js/src/gen/types.gen.ts index bf23f77ecad..e221d4b8349 100644 --- a/packages/sdk/js/src/gen/types.gen.ts +++ b/packages/sdk/js/src/gen/types.gen.ts @@ -440,6 +440,7 @@ export type EventPermissionReplied = { sessionID: string permissionID: string response: string + interjection?: string } } @@ -2549,7 +2550,7 @@ export type SessionUnrevertResponse = SessionUnrevertResponses[keyof SessionUnre export type PostSessionIdPermissionsPermissionIdData = { body?: { - response: "once" | "always" | "reject" + response: "once" | "always" | "reject" | "interject" } path: { id: string From dfdbe5546b6594f6c3cf0bea7bdf05e7057f52f8 Mon Sep 17 00:00:00 2001 From: Ariane Emory Date: Thu, 27 Nov 2025 15:58:28 -0500 Subject: [PATCH 02/14] feat : work towards interjections, not yet tested --- packages/sdk/js/src/gen/types.gen.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/sdk/js/src/gen/types.gen.ts b/packages/sdk/js/src/gen/types.gen.ts index e221d4b8349..00c0573466e 100644 --- a/packages/sdk/js/src/gen/types.gen.ts +++ b/packages/sdk/js/src/gen/types.gen.ts @@ -2551,6 +2551,7 @@ export type SessionUnrevertResponse = SessionUnrevertResponses[keyof SessionUnre export type PostSessionIdPermissionsPermissionIdData = { body?: { response: "once" | "always" | "reject" | "interject" + interjection?: string } path: { id: string From b2fb2260751fd4a4408070eee442d09662b47aa7 Mon Sep 17 00:00:00 2001 From: Ariane Emory Date: Thu, 27 Nov 2025 17:32:31 -0500 Subject: [PATCH 03/14] ... --- packages/opencode/src/permission/index.ts | 10 +++++++++- packages/opencode/src/session/processor.ts | 6 +++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/permission/index.ts b/packages/opencode/src/permission/index.ts index 012497f0ba4..862620ba64e 100644 --- a/packages/opencode/src/permission/index.ts +++ b/packages/opencode/src/permission/index.ts @@ -167,7 +167,14 @@ export namespace Permission { ? `The user rejected this action and suggests: ${input.interjection}` : undefined match.reject( - new RejectedError(input.sessionID, input.permissionID, match.info.callID, match.info.metadata, reason), + new RejectedError( + input.sessionID, + input.permissionID, + match.info.callID, + match.info.metadata, + reason, + input.response === "interject", + ), ) return } @@ -200,6 +207,7 @@ export namespace Permission { public readonly toolCallID?: string, public readonly metadata?: Record, public readonly reason?: string, + public readonly isInterjection: boolean = false, ) { super( reason !== undefined diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index 5bd833c0f90..237b01d00da 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -219,7 +219,11 @@ export namespace SessionProcessor { }) if (value.error instanceof Permission.RejectedError) { - blocked = true + // Only block processing for actual denials, not interjections + // Interjections contain user suggestions and should allow model to continue + if (!value.error.isInterjection) { + blocked = true + } } delete toolcalls[value.toolCallId] } From 50be3d3edff95cfe79918a3836ec2a077b796cf1 Mon Sep 17 00:00:00 2001 From: Ariane Emory Date: Thu, 27 Nov 2025 17:58:37 -0500 Subject: [PATCH 04/14] fix: close interjection modal properly after the user's interjection is entered. --- packages/opencode/src/cli/cmd/tui/ui/dialog-prompt.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-prompt.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-prompt.tsx index 9ae370658b6..86235c5bb56 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-prompt.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-prompt.tsx @@ -21,6 +21,7 @@ export function DialogPrompt(props: DialogPromptProps) { useKeyboard((evt) => { if (evt.name === "return") { props.onConfirm?.(textarea.plainText) + dialog.clear() } }) @@ -43,6 +44,7 @@ export function DialogPrompt(props: DialogPromptProps) {