You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The permission/approval system in forestage's custom TUI (--mode forestage, the default) is wired visually but has three functional gaps that together mean the user has no reliable way to approve or deny tool-use requests while running in the forestage TUI.
1. Permission mode toggle (Shift+Tab) is display-only
src/tui/app.rs:213-266 defines PermissionMode (Default / AcceptEdits / Plan / Auto / Bypass). src/tui/input.rs:420-421 dispatches InputAction::CyclePermissionMode on Shift+BackTab. src/tui/mod.rs:291-296 handles the action by calling state.permission_mode = state.permission_mode.next() and updating the status-bar label.
No code path sends the new mode to the Claude Code subprocess. The status bar shows [acceptEdits] but Claude Code is still in whatever mode it was initialized with via system/init (src/protocol_ext.rs:187, src/tui/app.rs:345-350). Cycling is a lie — it mutates forestage's display and nothing else.
2. The permission dialog does not command attention
When a PermissionRequest hook event arrives (src/protocol_ext.rs:315-336 → BridgeEvent::PermissionRequest), it lands on AppState.pending_permission (src/tui/app.rs:475) and src/tui/layout.rs:79-110 reserves a 6-row slot above the input area. The rendering is passive — it just appears where the input moves up a bit. There is:
no focus pull
no bell / terminal alert
no color flash or animation
no conversation-area dim or modal overlay
no keypress queue interception explaining what to do
no timeout indicator, no "Claude is waiting" hint
If the user is reading earlier output when a prompt arrives, nothing signals that the agent is blocked on them. The prompt quietly appears in the layout and stays there.
3. The approve/deny response is explicitly unvalidated and may hang
src/bridge.rs:174-203 contains this WARNING, added in PR #24 review fix 0f4dff1:
WARNING: This protocol format is unvalidated against Claude Code's actual hook response protocol. The hook system may expect a different JSON shape. If the format is wrong, Claude Code will hang waiting for a valid response. Needs integration testing before relying on permission prompt functionality.
No one has confirmed Claude Code accepts that shape. Pressing a or d may leave the subprocess hung forever with no user-visible feedback beyond "Permission: allowed" in the status bar.
Expected behavior
The user sees a clear, attention-commanding prompt when Claude Code asks for permission. Pressing a key produces a response that Claude Code actually accepts, and the session proceeds. Cycling permission modes with Shift+Tab changes the actual permission posture of the running session, matching Claude Code's own Shift+Tab behavior.
Concretely:
Mode cycling is wired through.Shift+Tab in forestage produces the same effect as Shift+Tab in vanilla Claude Code — the subprocess's permission mode actually changes. Mechanism is TBD: a control message on the NDJSON stream, a SIGHUP-style reload, or (failing that) a restart with the new --permission-mode flag.
The dialog commands attention. Options to consider (not all mutually exclusive):
Render as a true modal overlay (centered, dimmed background) rather than a layout slot
Visual/audio alert on arrival (terminal bell, color flash, border animation)
Keyboard input is routed to the dialog until resolved (no accidental typing into the input box)
Status bar shows "Claude is waiting for approval" with the pending tool name
Optional: allow /permissions slash command or /approve, /deny typed commands as alternatives to a / d
The response protocol is validated. Integration test against a live Claude Code subprocess:
Confirm the JSON shape Claude Code actually expects for hook permission responses
Confirm whether it's type: permission_response or something else (maybe the hook system uses a different envelope)
Confirm whether behavior: allow is correct vs. action: allow, decision: allow, permission: grant, etc.
Add a regression test (VCR cassette or mock subprocess) that captures the working shape
Remove the WARNING comment once validated
Failure is visible. If the subprocess does not acknowledge the response within N seconds, surface a warning: "Claude Code did not respond to permission; the protocol may be wrong. Fall back to `--mode claude` for this session." Better a loud failure than a silent hang.
Reproduction
forestage (launches default --mode forestage TUI)
Prompt Claude to do something that triggers a tool permission check, e.g. run \ls /etc` via bash`
Observe the prompt appears quietly above the input area
Press Shift+Tab a few times; note that the status bar label changes but the actual permission posture of the subprocess does not (reproducible by comparing to --mode claude behavior)
Press a on a permission prompt; note: response may or may not actually unblock the subprocess, depending on whether the unvalidated JSON shape happens to match
Scope
This is one meta-issue because the three gaps are not independently fixable — a working UX requires all three. If this turns out to be too large for one PR, split after the response-protocol integration test clarifies what the correct shape is (that test is the critical path; everything else is UI work).
References
src/bridge.rs:174-203 — send_permission_response with the WARNING
src/tui/app.rs:213-266 — PermissionMode enum and cycle
src/tui/app.rs:268-275 — PermissionPrompt struct
src/tui/app.rs:475 — where pending_permission is set
What happened
The permission/approval system in forestage's custom TUI (
--mode forestage, the default) is wired visually but has three functional gaps that together mean the user has no reliable way to approve or deny tool-use requests while running in the forestage TUI.1. Permission mode toggle (
Shift+Tab) is display-onlysrc/tui/app.rs:213-266definesPermissionMode(Default / AcceptEdits / Plan / Auto / Bypass).src/tui/input.rs:420-421dispatchesInputAction::CyclePermissionModeonShift+BackTab.src/tui/mod.rs:291-296handles the action by callingstate.permission_mode = state.permission_mode.next()and updating the status-bar label.No code path sends the new mode to the Claude Code subprocess. The status bar shows
[acceptEdits]but Claude Code is still in whatever mode it was initialized with viasystem/init(src/protocol_ext.rs:187,src/tui/app.rs:345-350). Cycling is a lie — it mutates forestage's display and nothing else.2. The permission dialog does not command attention
When a
PermissionRequesthook event arrives (src/protocol_ext.rs:315-336→BridgeEvent::PermissionRequest), it lands onAppState.pending_permission(src/tui/app.rs:475) andsrc/tui/layout.rs:79-110reserves a 6-row slot above the input area. The rendering is passive — it just appears where the input moves up a bit. There is:If the user is reading earlier output when a prompt arrives, nothing signals that the agent is blocked on them. The prompt quietly appears in the layout and stays there.
3. The approve/deny response is explicitly unvalidated and may hang
src/bridge.rs:174-203contains this WARNING, added in PR #24 review fix0f4dff1:send_permission_response()sends:{"type":"permission_response","permission_response":{"behavior":"allow"|"deny"}}No one has confirmed Claude Code accepts that shape. Pressing
aordmay leave the subprocess hung forever with no user-visible feedback beyond"Permission: allowed"in the status bar.Expected behavior
The user sees a clear, attention-commanding prompt when Claude Code asks for permission. Pressing a key produces a response that Claude Code actually accepts, and the session proceeds. Cycling permission modes with
Shift+Tabchanges the actual permission posture of the running session, matching Claude Code's ownShift+Tabbehavior.Concretely:
Mode cycling is wired through.
Shift+Tabin forestage produces the same effect asShift+Tabin vanilla Claude Code — the subprocess's permission mode actually changes. Mechanism is TBD: a control message on the NDJSON stream, a SIGHUP-style reload, or (failing that) a restart with the new--permission-modeflag.The dialog commands attention. Options to consider (not all mutually exclusive):
/permissionsslash command or/approve,/denytyped commands as alternatives toa/dThe response protocol is validated. Integration test against a live Claude Code subprocess:
type: permission_responseor something else (maybe the hook system uses a different envelope)behavior: allowis correct vs.action: allow,decision: allow,permission: grant, etc.Failure is visible. If the subprocess does not acknowledge the response within N seconds, surface a warning: "Claude Code did not respond to permission; the protocol may be wrong. Fall back to `--mode claude` for this session." Better a loud failure than a silent hang.
Reproduction
forestage(launches default--mode forestageTUI)run \ls /etc` via bash`--mode claudebehavior)aon a permission prompt; note: response may or may not actually unblock the subprocess, depending on whether the unvalidated JSON shape happens to matchScope
This is one meta-issue because the three gaps are not independently fixable — a working UX requires all three. If this turns out to be too large for one PR, split after the response-protocol integration test clarifies what the correct shape is (that test is the critical path; everything else is UI work).
References
src/bridge.rs:174-203—send_permission_responsewith the WARNINGsrc/tui/app.rs:213-266—PermissionModeenum and cyclesrc/tui/app.rs:268-275—PermissionPromptstructsrc/tui/app.rs:475— wherepending_permissionis setsrc/tui/app.rs:1076-1120—render_permission_promptsrc/tui/input.rs:420-425—Shift+Tab/a/dbindingssrc/tui/layout.rs:79-110— layout with permission prompt slotsrc/tui/mod.rs:291-312— action handlers for cycle/allow/denysrc/protocol_ext.rs:315-336—PermissionRequestevent parsing0f4dff1— C1 finding that produced the WARNING