feat: implement ExitPlanMode HITL for Tool Permission Model#1589
feat: implement ExitPlanMode HITL for Tool Permission Model#1589quay-devel wants to merge 8 commits into
Conversation
Add ExitPlanMode as a human-in-the-loop tool alongside AskUserQuestion, enabling plan approval workflows in ACP sessions. This implements the spec from PR ambient-code#1586 (closes ambient-code#1583). Runner: - Add ExitPlanMode to BUILTIN_FRONTEND_TOOLS halt set - Enrich ExitPlanMode tool args with plan file content from .claude/plans/ - Complete Tier 1 tool allowlist (NotebookEdit, WebFetch, TodoWrite, etc.) Backend: - Generalize isAskUserQuestionToolCall → isHITLToolCall to detect both AskUserQuestion and ExitPlanMode for status derivation and compaction - Add ExitPlanMode test cases for waiting_input detection Frontend: - Generalize HITL detection in use-agent-status and stream-message - Add ExitPlanModeMessage component with approve/reject/request-changes Co-Authored-By: Claude Opus 4.6 <[email protected]>
- Extract shared hitl-tools.ts with normalizeToolName, isHITLTool, isAskUserQuestionTool, isExitPlanModeTool, and hasToolResult helpers - Remove duplicated hasResult and tool detection functions from ask-user-question.tsx, exit-plan-mode.tsx, stream-message.tsx, and use-agent-status.ts - Add 100KB size guard to _read_plan_file to prevent oversized events - Log JSON errors during ExitPlanMode plan enrichment instead of silently swallowing them - Use stable composite key for allowedPrompts list rendering Co-Authored-By: Claude Opus 4.6 <[email protected]>
✅ Deploy Preview for cheerful-kitten-f556a0 canceled.
|
📝 WalkthroughWalkthroughGeneralizes HITL tool detection (AskUserQuestion + ExitPlanMode), preserves HITL tool-start snapshots for status inference, centralizes frontend helpers, adds ExitPlanMode UI and plan-content enrichment in the runner, and updates tests and runner allowlist. ChangesHITL Tool Support
🚥 Pre-merge checks | ✅ 7 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (7 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
✨ Simplify code
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@components/frontend/src/components/session/exit-plan-mode.tsx`:
- Around line 35-36: The variables planContent and allowedPrompts currently use
TypeScript assertions only; add runtime guards so planContent is set to
input.planContent if typeof input.planContent === "string" otherwise "" and set
allowedPrompts to Array.isArray(input.allowedPrompts) ? input.allowedPrompts as
AllowedPrompt[] : [] (or validate each element) before using ReactMarkdown and
.map; update the initialization of planContent and allowedPrompts in
exit-plan-mode.tsx to perform these checks so ReactMarkdown always gets a string
and .map runs on a real array.
In `@components/runners/ambient-runner/ag_ui_claude_sdk/adapter.py`:
- Around line 105-108: The truncation checks byte length but slices by character
count, which can exceed _PLAN_FILE_MAX_BYTES for multi-byte UTF-8 chars; fix by
performing the truncation in bytes: read the file text into content, encode to
bytes (e.g., content_bytes = content.encode("utf-8")), if len(content_bytes) >
_PLAN_FILE_MAX_BYTES then slice the bytes to _PLAN_FILE_MAX_BYTES, decode back
to a string with a safe error handler (e.g., errors="ignore" or "replace") and
append the "\n\n[truncated]" marker before returning; update the logic around
plan_files, content, and _PLAN_FILE_MAX_BYTES accordingly.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: 767b3627-8743-45b6-a69e-396e31eb5da9
📒 Files selected for processing (10)
components/backend/websocket/agui_proxy.gocomponents/backend/websocket/agui_store.gocomponents/backend/websocket/agui_store_test.gocomponents/frontend/src/components/session/ask-user-question.tsxcomponents/frontend/src/components/session/exit-plan-mode.tsxcomponents/frontend/src/components/ui/stream-message.tsxcomponents/frontend/src/hooks/use-agent-status.tscomponents/frontend/src/lib/hitl-tools.tscomponents/runners/ambient-runner/ag_ui_claude_sdk/adapter.pycomponents/runners/ambient-runner/ambient_runner/bridges/claude/mcp.py
- Add runtime type guards for planContent (typeof string) and allowedPrompts (Array.isArray) in ExitPlanModeMessage to prevent runtime errors from unexpected backend data - Fix byte-accurate truncation in _read_plan_file: slice encoded bytes instead of character count to respect the 100KB limit for multi-byte UTF-8 content Co-Authored-By: Claude Opus 4.6 <[email protected]>
Claude Code ReviewSummaryClean, well-scoped implementation that correctly generalizes AskUserQuestion's HITL machinery to cover ExitPlanMode. The DRY refactor — extracting FindingsBlockerNone. CriticalNone. MajorNone. Minor1. Exact string match in adapter.py inconsistent with normalized approach everywhere else if current_tool_display_name == "ExitPlanMode":…uses an exact case-sensitive comparison while the backend ( def _normalize_tool_name(name: str) -> str:
return "".join(c for c in name.lower() if c.isalpha())
if _normalize_tool_name(current_tool_display_name) == "exitplanmode":2. 3. Removed comment in 4. Nit
Positive Highlights
Recommendations
Amber · standards loaded from CLAUDE.md, specs/standards/backend/, specs/standards/frontend/, specs/standards/security/ |
Amber Code Review — PR#1589: feat: implement ExitPlanMode HITL for Tool Permission ModelSummarySolid, well-structured implementation of ExitPlanMode as a second HITL tool. The abstraction into FindingsBlockerNone. Critical1. ExitPlanMode tool result format not verified against Claude Code CLI expectations The test plan has Fix: Document the expected tool result schema (from the Claude Code SDK docs or spec PR#1586). Add a test that mocks Major2. Tier-1 allowlist expansion in
Per CLAUDE.md: "Feature flags strongly recommended — gate new features behind Unleash flags." No flag here means a bad behavior from any of these tools has no kill switch. If these tools were specified by the Tier-1 allowlist in PR#1586 and their addition is intentional/non-new, add a code comment pointing at the spec. Consider a single Minor3. 4. 5. 6. Removed non-obvious compatibility comment in Positive Highlights
Recommendations (prioritized)
🤖 Amber — code review agent | ambient-code/platform |
markturansky
left a comment
There was a problem hiding this comment.
Amber Review — PR #1589
Summary
Well-structured implementation. The hitl-tools.ts shared module is the right pattern — eliminates three copies of the same normalization function. Backend rename from isAskUserQuestionToolCall → isHITLToolCall is clean. Plan file enrichment in the adapter handles truncation and options formats correctly. Both CodeRabbit findings (runtime type guards, byte-correct truncation) are addressed in commit d7352bb.
Findings
Minor — border-l-3 is not a valid Tailwind CSS class
exit-plan-mode.tsx uses border-l-3 in two places, but Tailwind's border-left width scale only includes border-l (1px), border-l-2 (2px), border-l-4 (4px), border-l-8 (8px). The class is silently ignored — the colored left accent border that visually distinguishes the plan card won't render.
-"rounded-lg border-l-3 pl-3 pr-3 py-2.5",
+"rounded-lg border-l-4 pl-3 pr-3 py-2.5",(Check what ask-user-question.tsx uses for consistency — if it uses border-l-2, match that.)
Score
7/8 checks clean. LGTM with nit — the visual defect is minor and doesn't affect function.
— Amber (code review agent)
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
components/runners/ambient-runner/ambient_runner/bridges/claude/mcp.py (1)
26-49:⚠️ Potential issue | 🔴 Critical | ⚡ Quick win
ExitPlanModemissing fromDEFAULT_ALLOWED_TOOLS.The allowlist includes
EnterPlanMode(line 42) but notExitPlanMode. The PR objectives state ExitPlanMode was missing from the runner allowlist, causing indefinite hangs (issue#1583). Backend (isHITLToolCall) and frontend (isExitPlanModeTool) both expect this tool to pass through.🐛 Proposed fix
"EnterPlanMode", + "ExitPlanMode", "EnterWorktree",🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@components/runners/ambient-runner/ambient_runner/bridges/claude/mcp.py` around lines 26 - 49, DEFAULT_ALLOWED_TOOLS is missing "ExitPlanMode", which causes HITL/ExitPlanMode tool calls to be blocked; update the DEFAULT_ALLOWED_TOOLS list in ambient_runner/bridges/claude/mcp.py to include "ExitPlanMode" alongside "EnterPlanMode" so that isHITLToolCall and isExitPlanModeTool pass through correctly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@components/runners/ambient-runner/ambient_runner/bridges/claude/mcp.py`:
- Around line 26-49: DEFAULT_ALLOWED_TOOLS is missing "ExitPlanMode", which
causes HITL/ExitPlanMode tool calls to be blocked; update the
DEFAULT_ALLOWED_TOOLS list in ambient_runner/bridges/claude/mcp.py to include
"ExitPlanMode" alongside "EnterPlanMode" so that isHITLToolCall and
isExitPlanModeTool pass through correctly.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: b108ded0-3d88-49a5-aef8-0c63016dd4c3
📒 Files selected for processing (1)
components/runners/ambient-runner/ambient_runner/bridges/claude/mcp.py
Summary
Implements the Tool Permission Model spec from PR #1586, adding
ExitPlanModeas a HITL (human-in-the-loop) tool that halts the event stream and waits for user approval — the same mechanism already used byAskUserQuestion.ExitPlanModeadded toBUILTIN_FRONTEND_TOOLShalt set; plan file content injected into tool args; Tier 1 allowlist completed with all missing toolsisAskUserQuestionToolCall→isHITLToolCallto detect both tools for status derivation and snapshot compaction; new test cases for ExitPlanModehitl-tools.ts; newExitPlanModeMessagecomponent with approve/reject/request-changes actionsCloses #1583
Spec: #1586
Test plan
go test ./websocket/...)npm run build)npx vitest run)panic()in Go code, noanytypes in TypeScript🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Improvements
Other