perf(tui): classify approval stakes once per request, not on every render (CW-7236)

Follow-up to a5326d10db after re-review (nit, confirmed from code).
ApprovalRequest::stakes() called classify_stakes on every call. Since
53ae15645c and a5326d10db, that runs credential_upload_texts, twice for a
non-Benign shell call because ToolActionKind::from_tool_call scans as well.
stakes() is on the render path: widgets/mod.rs kit and body_facts,
approval_band_legacy.rs, and can_save_allow_rule. So a pending card whose
input names curl/wget re-parsed it several times per frame, up to the
4,096-word cap.

new_with_intent already called classify_stakes to decide
persistent_allow_rules. It now keeps the result in a private `stakes`
field, and stakes() returns it. The params, category and risk the
classification reads are never changed after construction (git grep finds
no assignment), and the private field means every request is built through
new_with_intent.

Verification (source: a5326d10db plus this diff; rustfmt applied after
the build, whitespace only)
- scripts/dev-cargo.sh clippy -p codewhale-tui --all-targets --all-features
  --locked -- -D warnings -A clippy::uninlined_format_args
  -A clippy::too_many_arguments -A clippy::unnecessary_map_or: clean.
- scripts/dev-test.sh tui tui::approval tui::widgets tools::approval_summary
  core::authority:: — 490 tests run: 490 passed, 14861 skipped.
- cargo fmt --all -- --check: clean.
- npm test and check:web were last run on a5326d10db (101/19/420(413+7)/7,
  vitest 772/772, check:web exit 0). This commit changes only
  crates/tui/src/tui/approval.rs, which neither gate exercises, so they were
  not rerun.

Not changed (known limit): summary_for_locale still rebuilds the localized
summary, including its credential scan, on each render for a non-English
locale. The English summary is computed once at construction. That costs
one scan per frame, bounded by the same caps.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NBKbhx19BRbZtvWK8rVKjz
This commit is contained in:
CodeWhale BotandClaude Opus 5.5 committed 2026-10-16 10:46:09 -07:00
1 parent a5326d10db
commit 41ef8e0eb4
1 file changed
+15 -14
+15 -14
View File
@@ -141,6 +141,7 @@ pub struct ApprovalRequest {
/// This file write asks only because the folder has no git repository;
/// in one it would run without a card. The card says so in one line.
pub asks_without_git: bool,
stakes: ApprovalStakes,
}
/// Key approval details rendered prominently in the approval card.
@@ -166,7 +167,7 @@ impl ApprovalRequest {
/// Presentation stakes for this request (see [`ApprovalStakes`]).
#[must_use]
pub fn stakes(&self) -> ApprovalStakes {
classify_stakes(&self.tool_name, self.category, self.risk, &self.params)
self.stakes
}
#[cfg(test)]
@@ -204,19 +205,18 @@ impl ApprovalRequest {
crate::tools::approval_cache::build_approval_grouping_key(tool_name, params).0;
let persistent_ask_rules =
build_persistent_ask_rules(semantic_tool_name, params, workspace);
let persistent_allow_rules = if classify_stakes(tool_name, category, risk, params)
== ApprovalStakes::Critical
|| description_is_repo_law_prompt(description)
{
Vec::new()
} else {
build_persistent_allow_rules(
semantic_tool_name,
params,
workspace,
&persistent_ask_rules,
)
};
let stakes = classify_stakes(tool_name, category, risk, params);
let persistent_allow_rules =
if stakes == ApprovalStakes::Critical || description_is_repo_law_prompt(description) {
Vec::new()
} else {
build_persistent_allow_rules(
semantic_tool_name,
params,
workspace,
&persistent_ask_rules,
)
};
Self {
id: id.to_string(),
@@ -246,6 +246,7 @@ impl ApprovalRequest {
persistent_allow_rules,
owner: None,
asks_without_git: false,
stakes,
}
}