tags:
- opencode/permission
- opencode/proposal
- opencode/guide
- opencode/english
Adding reject reason input to CodeNomad's permission UI
Overview
CodeNomad currently allows users to "Deny" a permission request, but the rejection is sent without any explanation. The underlying opencode SDK and HTTP API already support an optional message field with the "reject" reply type. When a message is provided, the agent receives a CorrectedError with the feedback text — enabling it to understand why it was denied and adjust its behavior accordingly.
This guide shows the minimal changes needed to add a reject-reason text input to CodeNomad's permission approval modal.
Current behavior
User clicks "Deny" → sendPermissionResponse(id, sessionId, permId, "reject")
→ client.permission.reply({ requestID, reply: "reject" })
→ Agent receives RejectedError (generic message)
Target behavior
User clicks "Deny" → UI shows a textarea
User types reason → "Please use a different directory"
User confirms → sendPermissionResponse(id, sessionId, permId, "reject", "Please use a different directory")
→ client.permission.reply({ requestID, reply: "reject", message: "Please use a different directory" })
→ Agent receives CorrectedError({ feedback: "Please use a different directory" })
Changes needed
1. Update sendPermissionResponse to accept and pass a message
File: packages/ui/src/stores/instances.ts
Current (line 1087-1091):
async function sendPermissionResponse(
instanceId: string,
sessionId: string,
requestId: string,
reply: PermissionReply
): Promise<void> {
Change: Add an optional message parameter and pass it to the SDK:
async function sendPermissionResponse(
instanceId: string,
sessionId: string,
requestId: string,
reply: PermissionReply,
message?: string // ← add this
): Promise<void> {
Then update the SDK call (line 1104-1108):
await client.permission.reply({
requestID: requestId,
reply,
...(message ? { message } : {}), // ← add message if present
})
2. Update handlePermissionDecision to handle message
File: packages/ui/src/components/permission-approval-modal.tsx
Current (line 158):
async function handlePermissionDecision(
permission: PermissionRequestLike,
response: "once" | "always" | "reject"
) {
Change: Add an optional message parameter:
async function handlePermissionDecision(
permission: PermissionRequestLike,
response: "once" | "always" | "reject",
message?: string
) {
// ...
await sendPermissionResponse(props.instanceId, sessionId, permissionId, response, message)
}
3. Add a reject-reason state and text input in the modal
File: packages/ui/src/components/permission-approval-modal.tsx
When the user clicks "Deny", instead of immediately calling handlePermissionDecision, transition to a "reject reason" state that shows a text input (similar to how opencode's TUI RejectPrompt works).
You can either:
Option A — Two-step flow (like opencode TUI):
- User clicks "Deny"
- UI switches to show a textarea + "Confirm" / "Cancel" buttons
- User types reason and clicks "Confirm"
- Call
handlePermissionDecision(item, "reject", reason)
Option B — Inline input (simpler):
- User clicks "Deny"
- A textarea slides in below the permission patterns
- User types reason (optional)
- User clicks "Confirm" (or the original "Deny" becomes "Confirm")
- Call
handlePermissionDecision(item, "reject", reason)
Reference implementation (opencode TUI):
// From: packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx:444-519
function RejectPrompt(props: { onConfirm: (message: string) => void; onCancel: () => void }) {
// Renders:
// - A header: "△ Reject permission"
// - A hint: "Tell OpenCode what to do differently"
// - A textarea for input
// - Enter to confirm, Escape to cancel
}
4. (Optional) Add a signal that a reason is expected
You may want to show a subtle hint like "Tell the AI why this was denied — it will see your feedback and adjust" below the textarea to encourage users to provide useful feedback.
How it affects the agent
With a message:
CorrectedError({ feedback: "Please use a different directory" })
↓
error.message = "The user rejected permission to use this specific tool call
with the following feedback: Please use a different directory"
↓
Tool part state.error = the above message string
↓
LLM sees this string in the tool result message
→ Can adjust its next action based on the feedback
Without a message:
RejectedError()
↓
error.message = "The user rejected permission to use this specific tool call."
↓
LLM sees a generic rejection, may retry the same action with different parameters
Error propagation path (complete)
User inputs reason in textarea
│
▼
handlePermissionDecision(perm, "reject", reason)
│
▼
sendPermissionResponse(instanceId, sessionId, permId, "reject", reason)
│
▼
client.permission.reply({ requestID, reply: "reject", message: reason })
│ POST /permission/{requestID}/reply { reply: "reject", message: reason }
▼
Permission.reply() core service
│ CorrectedError({ feedback: reason }) (instead of RejectedError)
▼
Deferred.fail(deferred, CorrectedError)
│
▼
ctx.ask() → Effect.orDie → EffectBridge.runPromise
│ AI SDK catches "tool-error"
▼
LLM receives tool error with feedback text
Reference links
- Opencode TUI RejectPrompt:
packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx (lines 444-519)
- Permission core service:
packages/opencode/src/permission/index.ts (lines 213-269)
- CorrectedError definition:
packages/opencode/src/permission/index.ts (lines 87-93)
- SDK permission.reply:
packages/sdk/js/src/v2/gen/sdk.gen.ts (lines 2810-2831)
tags:
Adding reject reason input to CodeNomad's permission UI
Overview
CodeNomad currently allows users to "Deny" a permission request, but the rejection is sent without any explanation. The underlying opencode SDK and HTTP API already support an optional
messagefield with the"reject"reply type. When a message is provided, the agent receives aCorrectedErrorwith the feedback text — enabling it to understand why it was denied and adjust its behavior accordingly.This guide shows the minimal changes needed to add a reject-reason text input to CodeNomad's permission approval modal.
Current behavior
Target behavior
Changes needed
1. Update
sendPermissionResponseto accept and pass a messageFile:
packages/ui/src/stores/instances.tsCurrent (line 1087-1091):
Change: Add an optional
messageparameter and pass it to the SDK:Then update the SDK call (line 1104-1108):
2. Update
handlePermissionDecisionto handle messageFile:
packages/ui/src/components/permission-approval-modal.tsxCurrent (line 158):
Change: Add an optional
messageparameter:3. Add a reject-reason state and text input in the modal
File:
packages/ui/src/components/permission-approval-modal.tsxWhen the user clicks "Deny", instead of immediately calling
handlePermissionDecision, transition to a "reject reason" state that shows a text input (similar to how opencode's TUIRejectPromptworks).You can either:
Option A — Two-step flow (like opencode TUI):
handlePermissionDecision(item, "reject", reason)Option B — Inline input (simpler):
handlePermissionDecision(item, "reject", reason)Reference implementation (opencode TUI):
4. (Optional) Add a signal that a reason is expected
You may want to show a subtle hint like "Tell the AI why this was denied — it will see your feedback and adjust" below the textarea to encourage users to provide useful feedback.
How it affects the agent
With a message:
Without a message:
Error propagation path (complete)
Reference links
packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx(lines 444-519)packages/opencode/src/permission/index.ts(lines 213-269)packages/opencode/src/permission/index.ts(lines 87-93)packages/sdk/js/src/v2/gen/sdk.gen.ts(lines 2810-2831)