feat: show intent summary before file approval prompt (#2381)#2389
Conversation
There was a problem hiding this comment.
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.
| let truncated = | ||
| crate::utils::truncate_with_ellipsis(sline, max_width.max(20), "..."); |
There was a problem hiding this comment.
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.
| let truncated = | |
| crate::utils::truncate_with_ellipsis(sline, max_width.max(20), "..."); | |
| let truncated = | |
| crate::utils::truncate_with_ellipsis(sline, max_width, "..."); |
| 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), | ||
| ), | ||
| ])); | ||
| } |
There was a problem hiding this comment.
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),
),
]));
}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()
81d06a6 to
ea7dffa
Compare
|
Thanks @HUQIANTAO and @Dr3259 — I pulled this onto current
Local verification on the refreshed branch passed: |
Summary
Implements #2381 — show 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)
After (new flow)
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:
Changes
core/events.rsintent_summary: Option<String>toApprovalRequiredeventcore/engine/turn_loop.rsToolExecutionPlans, detect write tools and extractcurrent_text_visibleas intent summarytui/approval.rsintent_summaryfield toApprovalRequest; addnew_with_intent()constructor; gatenew()behind#[cfg(test)]tui/ui.rsintent_summarythrough event handler topush_approval_request_view()tui/widgets/mod.rsruntime_threads.rstui/ui/tests.rsHow it works
Engine layer (
turn_loop.rs)After building all
ToolExecutionPlans for a turn, the engine checks:!read_only && approval_required)current_text_visible)If both are true, the text is passed as
intent_summaryin theApprovalRequiredevent. If the model jumped straight to tool calls without explanation,intent_summaryisNoneand the approval proceeds normally.TUI layer (
widgets/mod.rs)The approval widget renders the intent summary above the tool parameters:
i18n
Intent:意图:… (+N lines)/… (还有 N 行)Design decisions
max_width == 0, the intent section is skipped entirely.truncate_with_ellipsisutility.Testing
RUSTFLAGS="-Dwarnings"(no dead code warnings)cargo fmt --checkpassesnon_recoverable_engine_error_enters_offline_mode,session_id_captured_from_post_response_and_replayed) are unrelated and fail on mainCloses #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.
turn_loop.rs): after building tool plans, checks for write-tool approvals and captures up to 2 000 chars ofcurrent_text_visibleas the intent summary, passing it through theApprovalRequiredevent; read-only plans explicitly receiveNone.widgets/mod.rs,approval.rs,ui.rs):ApprovalRequestgains anintent_summaryfield populated via the newnew_with_intent()constructor; the widget renders up to 3 lines with a "+N lines" overflow indicator and Chinese i18n.runtime_threads.rs):intent_summaryis now included in the externalapproval.requiredJSON 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
approval_intent_summaryhelper (char-bounded, trimmed, tested) and wiresintent_summaryinto the serial approval path; read-only plans correctly receiveNone.truncate_with_ellipsisis called without the.max(N)floor used by the params path, risking an ellipsis-only line in very narrow terminals.intent_summarytoApprovalRequest;new()correctly gated behind#[cfg(test)](all call sites are inside test modules);new_with_intent()trims and normalises the incoming value.intent_summaryfrom the event and serialises it into the external JSON payload; test updated with a round-trip assertion.intent_summarythrough the event handler and intopush_approval_request_view; no logic changes to existing approval flow.Noneas the newintent_summaryargument topush_approval_request_view.intent_summary: Option<String>to theApprovalRequiredvariant 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 / deniesReviews (3): Last reviewed commit: "fix(tui): harden approval intent summari..." | Re-trigger Greptile