Skip to content

feat: show intent summary before file approval prompt (#2381)#2389

Merged
Hmbown merged 3 commits into
Hmbown:mainfrom
HUQIANTAO:feat/intent-summary-before-approval
May 31, 2026
Merged

feat: show intent summary before file approval prompt (#2381)#2389
Hmbown merged 3 commits into
Hmbown:mainfrom
HUQIANTAO:feat/intent-summary-before-approval

Conversation

@HUQIANTAO

@HUQIANTAO HUQIANTAO commented May 31, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements #2381show intent summary before file approval prompt.

When the model invokes write/modify/delete tools, its preceding text content is extracted as an "intent summary" and displayed in the approval view. Users can now see why a change is being made before reviewing what will change, addressing the "blind approval" problem described in the issue.

Before (current flow)

Agent analyzes → triggers write_file → approval prompt appears → user approves blind

After (new flow)

Agent analyzes → explains intent in text → approval prompt shows intent + diff → user decides with context

Motivation

The current approval prompt shows the diff (what will change) but not the reasoning behind it. Users are asked to approve changes they haven't been given context for. This PR adds a lightweight, non-blocking intent summary layer:

  1. Do I agree with this approach? (intent summary)
  2. Does this diff match what I agreed to? (existing approval)

Changes

File Change
core/events.rs Add intent_summary: Option<String> to ApprovalRequired event
core/engine/turn_loop.rs After building ToolExecutionPlans, detect write tools and extract current_text_visible as intent summary
tui/approval.rs Add intent_summary field to ApprovalRequest; add new_with_intent() constructor; gate new() behind #[cfg(test)]
tui/ui.rs Pass intent_summary through event handler to push_approval_request_view()
tui/widgets/mod.rs Render intent summary in approval card: up to 3 lines, truncated to card width, i18n labels
runtime_threads.rs Adapt 4 test constructions to new event field
tui/ui/tests.rs Adapt test call to new function signature

How it works

Engine layer (turn_loop.rs)

After building all ToolExecutionPlans for a turn, the engine checks:

  • Does this turn contain write tools? (!read_only && approval_required)
  • Did the model produce text content before the tool calls? (current_text_visible)

If both are true, the text is passed as intent_summary in the ApprovalRequired event. If the model jumped straight to tool calls without explanation, intent_summary is None and the approval proceeds normally.

TUI layer (widgets/mod.rs)

The approval widget renders the intent summary above the tool parameters:

  [DESTRUCTIVE]  write_file
  Type: File modification
  About: Write content to src/main.rs
  Impact: Overwrites existing file

  Intent: I'm adding a new error handler for the timeout case.
  The current code silently drops the error, which makes
  debugging difficult in production.

  Params: {"path": "src/main.rs", "content": "..."}

  [1] Approve once
  [2] Approve for session
  [3] Deny

i18n

  • English: Intent:
  • Chinese (zh-Hans): 意图:
  • Remaining lines indicator: … (+N lines) / … (还有 N 行)

Design decisions

  • Non-blocking: If the model has no explanation, the approval still proceeds. No extra round-trip, no token cost, no latency.
  • Zero overhead: No additional LLM calls. Reuses the text the model already generated.
  • Backward compatible: YOLO mode (auto-approve) bypasses the approval view entirely, so intent summary is never shown. Approval cache works as-is.
  • Graceful degradation: In narrow terminals where max_width == 0, the intent section is skipped entirely.
  • No new dependencies: Uses existing truncate_with_ellipsis utility.

Testing

  • All existing approval tests pass
  • Build passes with RUSTFLAGS="-Dwarnings" (no dead code warnings)
  • cargo fmt --check passes
  • Pre-existing test failures (non_recoverable_engine_error_enters_offline_mode, session_id_captured_from_post_response_and_replayed) are unrelated and fail on main

Closes #2381

Greptile Summary

This PR implements feature #2381, showing the model's preceding text explanation ("intent summary") in the approval widget before the user approves write/modify/delete tool calls. The extraction is bounded, non-blocking, and falls back gracefully when no explanation exists.

  • Engine layer (turn_loop.rs): after building tool plans, checks for write-tool approvals and captures up to 2 000 chars of current_text_visible as the intent summary, passing it through the ApprovalRequired event; read-only plans explicitly receive None.
  • TUI layer (widgets/mod.rs, approval.rs, ui.rs): ApprovalRequest gains an intent_summary field populated via the new new_with_intent() constructor; the widget renders up to 3 lines with a "+N lines" overflow indicator and Chinese i18n.
  • API/runtime path (runtime_threads.rs): intent_summary is now included in the external approval.required JSON event payload.

Confidence Score: 5/5

Safe to merge — the feature is additive, all existing approval paths are preserved, and the only finding is a cosmetic edge case in very narrow terminals.

The change is purely additive: a new optional field flows through the event pipeline without altering any existing approval decision logic. The extraction is correctly bounded at 2 000 chars with a unit test, read-only plans are explicitly excluded, and all test call sites compile under #[cfg(test)]. The one finding is that intent lines lack the .max(20) floor that the adjacent params rendering uses, which only manifests on terminals narrower than ~17 columns.

crates/tui/src/tui/widgets/mod.rs — intent line truncation is missing the minimum-width floor present in the params rendering path.

Important Files Changed

Filename Overview
crates/tui/src/core/engine/turn_loop.rs Adds approval_intent_summary helper (char-bounded, trimmed, tested) and wires intent_summary into the serial approval path; read-only plans correctly receive None.
crates/tui/src/tui/widgets/mod.rs Renders intent summary above tool params; truncate_with_ellipsis is called without the .max(N) floor used by the params path, risking an ellipsis-only line in very narrow terminals.
crates/tui/src/tui/approval.rs Adds intent_summary to ApprovalRequest; new() correctly gated behind #[cfg(test)] (all call sites are inside test modules); new_with_intent() trims and normalises the incoming value.
crates/tui/src/runtime_threads.rs Destructures intent_summary from the event and serialises it into the external JSON payload; test updated with a round-trip assertion.
crates/tui/src/tui/ui.rs Threads intent_summary through the event handler and into push_approval_request_view; no logic changes to existing approval flow.
crates/tui/src/tui/ui/tests.rs Single-line adaptation: passes None as the new intent_summary argument to push_approval_request_view.
crates/tui/src/core/events.rs Adds intent_summary: Option<String> to the ApprovalRequired variant with appropriate doc comment.

Sequence Diagram

sequenceDiagram
    participant M as Model
    participant E as Engine (turn_loop.rs)
    participant EV as Event Bus (events.rs)
    participant RT as RuntimeThreads
    participant UI as TUI UI (ui.rs)
    participant W as ApprovalWidget (widgets/mod.rs)

    M->>E: Text content (explains intent)
    Note over E: current_text_visible += text
    M->>E: Tool call (write_file / exec_command)
    Note over E: has_write_tools = true
    E->>E: approval_intent_summary(current_text_visible) cap at 2000 chars
    E->>EV: "ApprovalRequired { intent_summary: Some("...") }"
    EV->>RT: "approval.required JSON { intent_summary: "..." }"
    EV->>UI: EngineEvent::ApprovalRequired
    UI->>UI: push_approval_request_view(..., intent_summary)
    UI->>W: ApprovalRequest::new_with_intent(..., intent_summary)
    Note over W: Render intent up to 3 lines + N more indicator
    W-->>M: User approves / denies
Loading

Fix All in Codex Fix All in Claude Code Fix All in Cursor

Reviews (3): Last reviewed commit: "fix(tui): harden approval intent summari..." | Re-trigger Greptile

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces an "intent summary" feature for write tools. When a model invokes write tools, its preceding text is extracted and propagated to the approval view so users can understand the intent behind the changes. The feedback highlights two issues in the UI rendering widget: first, using a hard floor of 20 characters for truncation can cause layout overflow in narrow terminals, so it is recommended to truncate directly to the available width; second, the remaining lines indicator is hardcoded in English and should be localized to support other locales like Chinese.

Comment thread crates/tui/src/tui/widgets/mod.rs Outdated
Comment on lines +1181 to +1182
let truncated =
crate::utils::truncate_with_ellipsis(sline, max_width.max(20), "...");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using max_width.max(20) sets a hard floor of 20 characters for the truncated text. If the terminal window or pane is very narrow (e.g., card_area.width is less than 30), this floor will exceed the actual available width, causing the text to overflow the card area and break the layout. Instead, we should truncate directly to max_width to ensure it scales correctly with the terminal size.

Suggested change
let truncated =
crate::utils::truncate_with_ellipsis(sline, max_width.max(20), "...");
let truncated =
crate::utils::truncate_with_ellipsis(sline, max_width, "...");

Comment on lines +1196 to +1204
if summary_lines.len() > 3 {
lines.push(Line::from(vec![
Span::raw(" "),
Span::styled(
format!(" ... (+{} lines)", summary_lines.len() - 3),
Style::default().fg(palette::TEXT_HINT),
),
]));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The string " ... (+{} lines)" is hardcoded in English and does not respect the user's locale (e.g., Locale::ZhHans). To ensure proper internationalization (i18n), this message should be localized based on the locale variable, similar to how the intent_label is handled.

                if summary_lines.len() > 3 {
                    let remaining = summary_lines.len() - 3;
                    let more_text = match locale {
                        Locale::ZhHans => format!("  ... (还有 {} 行)", remaining),
                        _ => format!("  ... (+{} lines)", remaining),
                    };
                    lines.push(Line::from(vec![
                        Span::raw("  "),
                        Span::styled(
                            more_text,
                            Style::default().fg(palette::TEXT_HINT),
                        ),
                    ]));
                }

Comment thread crates/tui/src/core/engine/turn_loop.rs
Comment thread crates/tui/src/core/engine/turn_loop.rs
Comment thread crates/tui/src/tui/widgets/mod.rs
When the model invokes write/modify/delete tools, extract its preceding
text content as an 'intent summary' and pass it to the approval view.
This gives users context about why a change is being made before they
review what will change.

Changes:
- Add intent_summary field to ApprovalRequired event (events.rs)
- Extract model text from current_text_visible when write tools
  are detected in the turn loop (turn_loop.rs)
- Add ApprovalRequest::new_with_intent constructor with
  intent_summary parameter (approval.rs)
- Pass intent_summary through TUI event handler to approval view
  (ui.rs)
- Render intent summary in approval widget: up to 3 lines of the
  model explanation, truncated to available card width, with
  i18n labels for zh-Hans locale (widgets/mod.rs)
- Adapt existing tests to new event field (runtime_threads.rs,
  ui/tests.rs)

Design decisions:
- Non-blocking: if the model provides no explanation, the approval
  still proceeds normally (no extra round-trip or token cost)
- Backward compatible: YOLO mode and approval cache unaffected
- The new() constructor is gated behind #[cfg(test)] since
  production code now uses new_with_intent()
@HUQIANTAO HUQIANTAO force-pushed the feat/intent-summary-before-approval branch from 81d06a6 to ea7dffa Compare May 31, 2026 06:57
@Hmbown

Hmbown commented May 31, 2026

Copy link
Copy Markdown
Owner

Thanks @HUQIANTAO and @Dr3259 — I pulled this onto current main and tightened the approval-intent path before merging consideration:

  • bounded the stored intent summary so long pre-tool explanations are not cloned unboundedly across approvals
  • only attach the intent summary to write approvals, not read-only approvals in mixed batches
  • carry intent_summary through the runtime approval.required event for API/headless consumers
  • normalize empty summaries at construction and kept the widget rendering bounded/localized

Local verification on the refreshed branch passed: cargo fmt --all -- --check, git diff --check origin/main..HEAD, python3 scripts/check-provider-registry.py, cargo test -p codewhale-tui approval_intent_summary_trims_and_bounds_text -- --nocapture, cargo test -p codewhale-tui approval_required_awaits_external_decision_allow -- --nocapture, and cargo check -p codewhale-tui --all-features --locked.

@Hmbown Hmbown merged commit 72ad833 into Hmbown:main May 31, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature Request: Show intent summary before file approval prompt

2 participants