Synced from monorepo

Changes:
- Persist submitter identity for /feedback
- Let custom models use rotating tokens from named auth providers
- Template stale tool/param name literals in server-native descriptions
- Minimal mode commits thinking in full, lookups as one-liners
- Tighten durable append internals
- Nudge model to end turn on no-op bash commands
- Per-fetch signing nonce in the managed-config envelope, with a server-side replay probe
- Include working tree in startup status
This commit is contained in:
grokkybara[bot] 2026-07-20 18:06:59 +01:00
commit a881e6703f
140 changed files with 6746 additions and 2377 deletions

View file

@ -157,9 +157,10 @@ pub(super) fn handle_mcp_tools_changed(notif: &acp::ExtNotification, app: &mut A
pub(super) fn agent_has_pending_mcps_fetch(app: &AppView, agent_id: AgentId) -> bool {
app.pending_effects.iter().any(|e| {
matches!(
e,
Effect::FetchMcpsList { agent_id: a, .. } if *a == agent_id
)
e,
Effect::FetchMcpsList { agent_id: a, .. }
if *a == agent_id
)
})
}

View file

@ -94,7 +94,8 @@
assert!(
app.pending_effects.iter().any(|e| matches!(
e,
Effect::PersistAnnouncementsHidden { hidden_ids } if hidden_ids == &expected
Effect::PersistAnnouncementsHidden { hidden_ids }
if hidden_ids == &expected
)),
"prune must persist the shrunken set, got {:?}",
app.pending_effects

View file

@ -25,9 +25,11 @@ impl AgentView {
// BEFORE the removal so a potential auto-hide pane switch can't hit
// the editing lock (see queue_edit.rs ordering invariant).
if matches!(
self.prompt_mode,
PromptMode::EditingQueued { id: editing_id, server_id: None, .. } if editing_id == id
) {
self.prompt_mode,
PromptMode::EditingQueued { id: editing_id, server_id: None, .. }
if editing_id == id
)
{
self.exit_editing_mode();
}
self.queue.select_after_delete(id);

View file

@ -343,8 +343,9 @@ impl VoiceState {
/// it). `/voice` and toggle-style starts leave this false.
pub(crate) fn hold(&self) -> bool {
matches!(
self, Self::ColdStart { hold, .. } | Self::Recording { hold, .. } if * hold
)
self, Self::ColdStart { hold, .. } | Self::Recording { hold, .. }
if * hold
)
}
}
/// Entry in the session picker list on the welcome screen.

View file

@ -965,7 +965,8 @@ mod tests {
// Turn ends → should drain "second" (front, not being edited) + FetchBilling.
let effects = dispatch(end_turn(), &mut app);
assert_eq!(effects.len(), 2);
assert!(matches!(&effects[0], Effect::SendPrompt { text, .. } if text == "second"));
assert!(matches!(&effects[0], Effect::SendPrompt { text, .. }
if text == "second"));
assert!(matches!(
&effects[1],
Effect::FetchBilling { silent: true, .. }
@ -991,7 +992,8 @@ mod tests {
// DrainQueue should pop and send.
let effects = dispatch(Action::DrainQueue, &mut app);
assert_eq!(effects.len(), 1);
assert!(matches!(&effects[0], Effect::SendPrompt { text, .. } if text == "queued"));
assert!(matches!(&effects[0], Effect::SendPrompt { text, .. }
if text == "queued"));
assert_eq!(app.agents[&id].session.queue_len(), 0);
}
@ -1960,7 +1962,8 @@ mod tests {
let effects = dispatch(Action::DrainQueue, &mut app);
assert_eq!(effects.len(), 1);
assert!(
matches!(&effects[0], Effect::SendPrompt { text, .. } if text == "p3-edited"),
matches!(&effects[0], Effect::SendPrompt { text, .. }
if text == "p3-edited"),
"should send the edited prompt, got: {:?}",
effects[0]
);

View file

@ -790,10 +790,6 @@ pub(in crate::app::dispatch) fn skip_picker_and_create_session(
model_id: None,
preferred_session_id,
chat_kind,
}]
}
pub(in crate::app::dispatch) fn handle_session_created(

View file

@ -492,7 +492,8 @@ fn show_usage_returns_fetch_billing_effect() {
// together and renders a single summary.
assert_eq!(effects.len(), 1, "got: {effects:?}");
assert!(
matches!(&effects[0], Effect::FetchBilling { agent_id, silent } if *agent_id == AgentId(0) && !*silent),
matches!(&effects[0], Effect::FetchBilling { agent_id, silent }
if *agent_id == AgentId(0) && !*silent),
"effect should be a non-silent FetchBilling, got: {effects:?}"
);
}
@ -859,7 +860,8 @@ fn free_usage_failure_opens_paywall_modal() {
// 1. Real send.
let effects = dispatch(Action::SendPrompt("draw me a cat".into()), &mut app);
assert!(
matches!(&effects[0], Effect::SendPrompt { text, .. } if text == "draw me a cat"),
matches!(&effects[0], Effect::SendPrompt { text, .. }
if text == "draw me a cat"),
"send must dispatch: {effects:?}"
);
let prompt_id = app.agents[&id].session.current_prompt_id.clone();
@ -1051,7 +1053,8 @@ fn unknown_non_restricted_command_still_passes_through() {
assert_eq!(effects.len(), 1);
assert!(
matches!(&effects[0], Effect::SendPrompt { text, .. } if text == "/frobnicate arg"),
matches!(&effects[0], Effect::SendPrompt { text, .. }
if text == "/frobnicate arg"),
"unknown command must still pass through: {effects:?}"
);
assert!(

View file

@ -218,9 +218,10 @@ fn plugin_cta_catalog_load_recomputes_match_for_typed_draft() {
&mut app,
);
assert!(matches!(
&app.agents[&id].plugin_cta.phase,
CtaPhase::Matched { name, .. } if name == "zzctaplugin"
));
&app.agents[&id].plugin_cta.phase,
CtaPhase::Matched { name, .. }
if name == "zzctaplugin"
));
}
#[test]

View file

@ -568,7 +568,8 @@ async fn dashboard_change_location_valid_updates_cwd_and_closes_modal() {
assert!(
effects
.iter()
.any(|e| matches!(e, Effect::SetWorkingDir { path } if path == &target))
.any(|e| matches!(e, Effect::SetWorkingDir { path }
if path == &target))
);
assert!(
app.dashboard.as_ref().unwrap().location_picker.is_none(),
@ -2187,9 +2188,10 @@ fn dashboard_dispatch_applies_pending_model_and_plan() {
let new_id = *app.agents.keys().next().unwrap();
// CreateSession carries the staged model id.
assert!(effects.iter().any(|e| matches!(
e,
Effect::CreateSession { model_id: Some(m), .. } if *m == model_id
)));
e,
Effect::CreateSession { model_id: Some(m), .. }
if *m == model_id
)));
let agent = &app.agents[&new_id];
assert_eq!(
agent.session.deferred_model_switch,
@ -2230,9 +2232,10 @@ fn dashboard_new_agent_button_applies_pending_model_and_plan() {
let new_id = *app.agents.keys().next().unwrap();
// CreateSession carries the staged model id.
assert!(effects.iter().any(|e| matches!(
e,
Effect::CreateSession { model_id: Some(m), .. } if *m == model_id
)));
e,
Effect::CreateSession { model_id: Some(m), .. }
if *m == model_id
)));
let agent = &app.agents[&new_id];
assert_eq!(
agent.session.deferred_model_switch,
@ -2277,7 +2280,8 @@ fn dashboard_deferred_plan_mode_applied_on_session_created() {
assert!(
effects
.iter()
.any(|e| matches!(e, Effect::SetSessionMode { session_id: s, .. } if *s == session_id)),
.any(|e| matches!(e, Effect::SetSessionMode { session_id: s, .. }
if *s == session_id)),
"SessionCreated must emit SetSessionMode for the deferred plan mode"
);
}
@ -5077,7 +5081,8 @@ fn dashboard_peek_reply_to_idle_agent_sends() {
/* attach */ false,
);
assert_eq!(effects.len(), 1);
assert!(matches!(&effects[0], Effect::SendPrompt { text, .. } if text == "please continue"));
assert!(matches!(&effects[0], Effect::SendPrompt { text, .. }
if text == "please continue"));
assert!(app.agents[&AgentId(0)].session.state.is_turn_running());
assert_eq!(app.agents[&AgentId(0)].session.queue_len(), 0);
// Reply draft cleared after sending.

View file

@ -161,7 +161,8 @@ fn slash_plan_no_args_not_in_plan_enters_plan_mode() {
// Should emit SetSessionMode to enter plan mode.
assert_eq!(effects.len(), 1);
assert!(
matches!(&effects[0], Effect::SetSessionMode { mode_id, .. } if &*mode_id.0 == "plan"),
matches!(&effects[0], Effect::SetSessionMode { mode_id, .. }
if &*mode_id.0 == "plan"),
"expected SetSessionMode(plan), got: {effects:?}"
);
// Optimistic pending state should be set.
@ -1932,9 +1933,10 @@ fn cycle_always_approve_with_nudge_jumps_to_plan() {
);
assert!(
effects.iter().any(|e| matches!(
e,
Effect::SetSessionMode { mode_id, .. } if &*mode_id.0 == "plan"
)),
e,
Effect::SetSessionMode { mode_id, .. }
if &*mode_id.0 == "plan"
)),
"expected SetSessionMode(plan), got {effects:?}"
);
assert!(
@ -1988,9 +1990,10 @@ fn cycle_auto_with_nudge_jumps_to_plan() {
);
assert!(
effects.iter().any(|e| matches!(
e,
Effect::SetSessionMode { mode_id, .. } if &*mode_id.0 == "plan"
)),
e,
Effect::SetSessionMode { mode_id, .. }
if &*mode_id.0 == "plan"
)),
"expected SetSessionMode(plan), got {effects:?}"
);
assert!(
@ -2312,7 +2315,8 @@ fn set_plan_mode_idempotency_uses_pending_over_active() {
"OFF from EFFECTIVE-ON must emit Effect::SetSessionMode (not idempotent)"
);
assert!(
matches!(&effects[0], Effect::SetSessionMode { mode_id, .. } if &*mode_id.0 == "default"),
matches!(&effects[0], Effect::SetSessionMode { mode_id, .. }
if &*mode_id.0 == "default"),
"OFF transition must emit SetSessionMode(default): {effects:?}"
);
let agent = app.agents.get(&AgentId(0)).unwrap();

View file

@ -534,7 +534,8 @@ fn send_prompt_produces_effect_and_clears_input() {
// Prompt is enqueued and immediately drained (agent was idle).
assert_eq!(effects.len(), 1);
assert!(matches!(&effects[0], Effect::SendPrompt { text, .. } if text == "hello"));
assert!(matches!(&effects[0], Effect::SendPrompt { text, .. }
if text == "hello"));
assert!(app.agents[&id].prompt.text().is_empty());
assert!(app.agents[&id].session.state.is_turn_running());
assert_eq!(app.agents[&id].scrollback.len(), 1);
@ -754,7 +755,8 @@ fn chip_submit_while_enqueued_clears_follow_up_chips() {
assert!(
!effects
.iter()
.any(|e| matches!(e, Effect::SendPrompt { text, .. } if text == "Summarize")),
.any(|e| matches!(e, Effect::SendPrompt { text, .. }
if text == "Summarize")),
"chip must be enqueued, not immediate-sent, got {effects:?}"
);
// The chips are cleared on the enqueue path too (the bug fix).
@ -979,7 +981,8 @@ fn multiple_queued_prompts_drain_one_per_turn() {
// Turn end → drain "b" + FetchBilling.
let effects = dispatch(end_turn(), &mut app);
assert!(matches!(&effects[0], Effect::SendPrompt { text, .. } if text == "b"));
assert!(matches!(&effects[0], Effect::SendPrompt { text, .. }
if text == "b"));
assert!(matches!(
&effects[1],
Effect::FetchBilling { silent: true, .. }
@ -988,7 +991,8 @@ fn multiple_queued_prompts_drain_one_per_turn() {
// Turn end → drain "c" + FetchBilling.
let effects = dispatch(end_turn(), &mut app);
assert!(matches!(&effects[0], Effect::SendPrompt { text, .. } if text == "c"));
assert!(matches!(&effects[0], Effect::SendPrompt { text, .. }
if text == "c"));
assert!(matches!(
&effects[1],
Effect::FetchBilling { silent: true, .. }
@ -1825,7 +1829,8 @@ fn cancel_with_queued_prompt_drains_on_completion() {
);
assert_eq!(effects.len(), 2);
assert!(matches!(&effects[0], Effect::SendPrompt { text, .. } if text == "queued"));
assert!(matches!(&effects[0], Effect::SendPrompt { text, .. }
if text == "queued"));
assert!(matches!(
&effects[1],
Effect::FetchBilling { silent: true, .. }
@ -1907,7 +1912,8 @@ fn cancel_with_multiple_queued_prompts_drains_only_front_prompt() {
);
assert_eq!(effects.len(), 2);
assert!(matches!(&effects[0], Effect::SendPrompt { text, .. } if text == "queued-1"));
assert!(matches!(&effects[0], Effect::SendPrompt { text, .. }
if text == "queued-1"));
assert!(matches!(
&effects[1],
Effect::FetchBilling { silent: true, .. }
@ -2172,7 +2178,8 @@ fn slash_unknown_command_passthrough_enqueues_prompt() {
let effects = dispatch(Action::SendPrompt("/unknown-cmd arg1".into()), &mut app);
// Unknown slash command → PassThrough → enqueue as prompt.
assert_eq!(effects.len(), 1);
assert!(matches!(&effects[0], Effect::SendPrompt { text, .. } if text == "/unknown-cmd arg1"));
assert!(matches!(&effects[0], Effect::SendPrompt { text, .. }
if text == "/unknown-cmd arg1"));
assert!(app.agents[&id].prompt.text().is_empty());
}
@ -2184,7 +2191,8 @@ fn non_slash_prompt_still_works() {
let effects = dispatch(Action::SendPrompt("hello world".into()), &mut app);
assert_eq!(effects.len(), 1);
assert!(matches!(&effects[0], Effect::SendPrompt { text, .. } if text == "hello world"));
assert!(matches!(&effects[0], Effect::SendPrompt { text, .. }
if text == "hello world"));
assert!(app.agents[&id].prompt.text().is_empty());
}

View file

@ -67,7 +67,8 @@ fn rewind_then_resubmit_drains_immediately_and_discards_orphan() {
// User edits and re-submits without waiting.
let effects = dispatch(Action::SendPrompt("second".into()), &mut app);
assert_eq!(effects.len(), 1);
assert!(matches!(&effects[0], Effect::SendPrompt { text, .. } if text == "second"));
assert!(matches!(&effects[0], Effect::SendPrompt { text, .. }
if text == "second"));
assert!(app.agents[&id].session.state.is_turn_running());
let second_pid = app.agents[&id].session.current_prompt_id.clone();
assert!(second_pid.is_some());
@ -379,9 +380,10 @@ fn inline_edit_conversation_only_success_resubmits_and_closes_editor() {
);
assert!(
effects.iter().any(
|e| matches!(e, Effect::SendPrompt { text, .. } if text == "fix the bug properly")
),
effects
.iter()
.any(|e| matches!(e, Effect::SendPrompt { text, .. }
if text == "fix the bug properly")),
"edited prompt must be sent, got {effects:?}"
);
let agent = &app.agents[&id];
@ -467,9 +469,10 @@ fn inline_edit_all_mode_previews_confirms_and_resubmits() {
&mut app,
);
assert!(
effects.iter().any(
|e| matches!(e, Effect::SendPrompt { text, .. } if text == "fix the bug properly")
),
effects
.iter()
.any(|e| matches!(e, Effect::SendPrompt { text, .. }
if text == "fix the bug properly")),
"got {effects:?}"
);
assert!(
@ -722,9 +725,10 @@ fn inline_edit_resubmit_sends_slash_text_literally() {
);
assert!(
effects.iter().any(
|e| matches!(e, Effect::SendPrompt { text, .. } if text == "/etc/hosts is wrong, fix it")
),
effects
.iter()
.any(|e| matches!(e, Effect::SendPrompt { text, .. }
if text == "/etc/hosts is wrong, fix it")),
"slash-lookalike edit must be sent as a prompt, got {effects:?}"
);
}

View file

@ -505,7 +505,8 @@ fn dispatch_send_prompt_announcements_via_registry() {
effects
.iter()
.any(|e| matches!(e, Effect::PersistAnnouncementsHidden {
hidden_ids } if hidden_ids.contains("crit-a"))),
hidden_ids }
if hidden_ids.contains("crit-a"))),
"expected persist effect carrying the hidden id, got {effects:?}"
);
assert!(app.hidden_announcement_ids.contains("crit-a"));
@ -569,7 +570,8 @@ fn announcements_show_clears_visible_critical_ids_only() {
effects
.iter()
.any(|e| matches!(e, Effect::PersistAnnouncementsHidden {
hidden_ids } if ! hidden_ids.contains("outage-a"))),
hidden_ids }
if ! hidden_ids.contains("outage-a"))),
"expected persist effect without the un-hidden id, got {effects:?}"
);
assert_eq!(shown_banner_id(&app).as_deref(), Some("outage-a"));
@ -661,7 +663,8 @@ fn announcements_show_clears_hidden_promo_ids() {
effects
.iter()
.any(|e| matches!(e, Effect::PersistAnnouncementsHidden {
hidden_ids } if ! hidden_ids.contains("promo-a"))),
hidden_ids }
if ! hidden_ids.contains("promo-a"))),
"expected persist effect without the un-hidden promo id, got {effects:?}"
);
assert_eq!(shown_banner_id(&app).as_deref(), Some("promo-a"));
@ -685,7 +688,8 @@ fn switch_model_dispatch_produces_effect_and_sets_pending() {
);
assert_eq!(effects.len(), 1);
assert!(
matches!(& effects[0], Effect::SwitchModel { model_id : mid, .. } if mid == &
matches!(& effects[0], Effect::SwitchModel { model_id : mid, .. }
if mid == &
model_id)
);
assert!(app.agents[&id].session.model_switch_pending);
@ -706,7 +710,8 @@ fn switch_model_allowed_when_agent_chat_kind() {
);
assert_eq!(effects.len(), 1);
assert!(
matches!(& effects[0], Effect::SwitchModel { model_id : mid, .. } if mid == &
matches!(& effects[0], Effect::SwitchModel { model_id : mid, .. }
if mid == &
model_id)
);
assert!(app.agents[&id].session.model_switch_pending);
@ -726,7 +731,8 @@ fn switch_model_allowed_when_app_chat_mode() {
);
assert_eq!(effects.len(), 1);
assert!(
matches!(& effects[0], Effect::SwitchModel { model_id : mid, .. } if mid == &
matches!(& effects[0], Effect::SwitchModel { model_id : mid, .. }
if mid == &
model_id)
);
assert!(app.agents[&id].session.model_switch_pending);
@ -946,7 +952,8 @@ fn acp_bootstrap_command_executes_as_passthrough() {
let effects = dispatch(Action::SendPrompt("/flush".into()), &mut app);
assert_eq!(effects.len(), 1);
assert!(
matches!(& effects[0], Effect::SendPrompt { text, .. } if text == "/flush"),
matches!(& effects[0], Effect::SendPrompt { text, .. }
if text == "/flush"),
"ACP command should passthrough, got: {effects:?}"
);
}
@ -1081,7 +1088,8 @@ fn acp_command_with_args_passthrough_includes_args() {
let effects = dispatch(Action::SendPrompt("/search find bugs".into()), &mut app);
assert_eq!(effects.len(), 1);
assert!(
matches!(& effects[0], Effect::SendPrompt { text, .. } if text ==
matches!(& effects[0], Effect::SendPrompt { text, .. }
if text ==
"/search find bugs"),
"ACP passthrough should preserve args, got: {effects:?}"
);
@ -1324,7 +1332,8 @@ fn view_catalog_entry_emits_fetch_effect() {
);
assert_eq!(effects.len(), 1);
assert!(
matches!(& effects[0], Effect::FetchCatalogEntry { kind, name } if kind ==
matches!(& effects[0], Effect::FetchCatalogEntry { kind, name }
if kind ==
"persona" && name == "researcher")
);
}

View file

@ -96,9 +96,10 @@ fn worktree_forked_with_restore_shows_summary_in_scrollback() {
// Should emit LoadSession.
assert_eq!(effects.len(), 1);
assert!(matches!(
&effects[0],
Effect::LoadSession { session_id, .. } if session_id == "forked-sess-2"
));
&effects[0],
Effect::LoadSession { session_id, .. }
if session_id == "forked-sess-2"
));
// Scrollback should contain the restore summary.
let has_restore_msg = app.agents[&id]
.scrollback

View file

@ -137,7 +137,8 @@ fn session_created_sets_session_id() {
);
assert_eq!(effects.len(), 7);
assert!(
matches!(& effects[0], Effect::FetchPromptHistory { session_id, .. } if
matches!(& effects[0], Effect::FetchPromptHistory { session_id, .. }
if
session_id == "new-session-123")
);
assert!(matches!(&effects[1], Effect::FetchSessionAgentName { .. }));
@ -491,7 +492,8 @@ fn worktree_session_created_drains_queued_prompts() {
assert!(
effects
.iter()
.any(|e| matches!(e, Effect::SendPrompt { text, .. } if text ==
.any(|e| matches!(e, Effect::SendPrompt { text, .. }
if text ==
"hello"))
);
assert!(
@ -525,7 +527,8 @@ fn session_created_drains_queued_prompts() {
assert!(
effects
.iter()
.any(|e| matches!(e, Effect::SendPrompt { text, .. } if text ==
.any(|e| matches!(e, Effect::SendPrompt { text, .. }
if text ==
"queued msg"))
);
assert!(
@ -825,7 +828,8 @@ fn deferred_model_switch_applied_on_session_created() {
effects
.iter()
.any(|e| matches!(e, Effect::SwitchModel { agent_id : a_id,
session_id : s_id, model_id : m_id, .. } if * a_id == id && * s_id == session_id
session_id : s_id, model_id : m_id, .. }
if * a_id == id && * s_id == session_id
&& * m_id == model_id))
);
}
@ -864,7 +868,8 @@ fn deferred_model_switch_applied_on_worktree_session_created() {
effects
.iter()
.any(|e| matches!(e, Effect::SwitchModel { agent_id : a_id,
session_id : s_id, model_id : m_id, .. } if * a_id == id && * s_id == session_id
session_id : s_id, model_id : m_id, .. }
if * a_id == id && * s_id == session_id
&& * m_id == model_id))
);
}
@ -1164,7 +1169,8 @@ fn deferred_worktree_ref_replays_through_gate() {
effects
.iter()
.any(|e| matches!(e, Effect::CreateWorktreeSession { git_ref :
Some(r), .. } if r == "feature-branch")),
Some(r), .. }
if r == "feature-branch")),
"the deferred --worktree <ref> replays with its git ref",
);
assert!(
@ -1225,7 +1231,8 @@ fn gated_worktree_without_load_id_preserves_stashed_resume() {
effects
.iter()
.any(|e| matches!(e, Effect::CreateWorktreeSession {
load_session_id : Some(id), .. } if id == "resume-me")),
load_session_id : Some(id), .. }
if id == "resume-me")),
"the deferred worktree replays with the preserved resume id",
);
assert!(app.deferred_startup.session.is_none());
@ -1297,7 +1304,8 @@ fn gated_worktree_with_none_companions_preserves_stashed_label_and_ref() {
effects
.iter()
.any(|e| matches!(e, Effect::CreateWorktreeSession {
load_session_id : Some(id), label : Some(l), git_ref : Some(r), .. } if id ==
load_session_id : Some(id), label : Some(l), git_ref : Some(r), .. }
if id ==
"mysess" && l == "mylabel" && r == "featbranch")),
"the deferred worktree replays with the preserved id, label, and ref",
);
@ -1411,7 +1419,8 @@ fn auth_complete_retries_stashed_prompt_after_mid_session_login() {
assert!(
effects
.iter()
.any(|e| matches!(e, Effect::SendPrompt { text, .. } if text ==
.any(|e| matches!(e, Effect::SendPrompt { text, .. }
if text ==
"retry me")),
"the stashed prompt must be auto-resubmitted, got: {effects:?}"
);
@ -1604,13 +1613,15 @@ async fn project_selected_creates_session_and_sends_prompt() {
assert!(
effects
.iter()
.any(|e| matches!(e, Effect::SetWorkingDir { path } if path == &
.any(|e| matches!(e, Effect::SetWorkingDir { path }
if path == &
selected))
);
assert!(
effects
.iter()
.any(|e| matches!(e, Effect::CreateSession { cwd, .. } if cwd ==
.any(|e| matches!(e, Effect::CreateSession { cwd, .. }
if cwd ==
& selected))
);
assert_eq!(app.agents[&id].session.queue_len(), 1);
@ -1837,7 +1848,8 @@ fn set_plan_mode_on_from_off_emits_set_session_mode() {
);
assert_eq!(effects.len(), 1);
assert!(
matches!(& effects[0], Effect::SetSessionMode { mode_id, .. } if &* mode_id.0 ==
matches!(& effects[0], Effect::SetSessionMode { mode_id, .. }
if &* mode_id.0 ==
"plan"),
"expected SetSessionMode(plan), got: {effects:?}"
);

View file

@ -629,7 +629,8 @@ fn resume_known_session_id_loads_not_creates() {
assert!(
effects
.iter()
.any(|e| matches!(e, Effect::LoadSession { session_id, .. } if
.any(|e| matches!(e, Effect::LoadSession { session_id, .. }
if
session_id == "resume-known-id")),
"expected LoadSession, got {effects:?}"
);
@ -964,7 +965,8 @@ fn resume_unknown_session_still_creates_new_agent() {
effects
.iter()
.any(|e| matches!(e, Effect::LoadSession { agent_id, session_id,
.. } if * agent_id == new_id && session_id == "sess-never-open"))
.. }
if * agent_id == new_id && session_id == "sess-never-open"))
);
}
/// Stale `attached_agent` (not equal to visible agent) must not re-arm overlay.
@ -1027,7 +1029,8 @@ fn resume_conversation_does_not_focus_build_id_collision() {
effects
.iter()
.any(|e| matches!(e, Effect::LoadSession { session_id, chat_kind
: true, .. } if session_id == "shared-id"))
: true, .. }
if session_id == "shared-id"))
);
assert!(!app.agents[&agent_0].chat_kind);
}
@ -1165,7 +1168,8 @@ fn resume_after_load_failed_reissues_load() {
assert!(
effects
.iter()
.any(|e| matches!(e, Effect::LoadSession { agent_id, .. } if *
.any(|e| matches!(e, Effect::LoadSession { agent_id, .. }
if *
agent_id == agent_0))
);
assert!(app.agents[&agent_0].loading_placeholder_id.is_some());
@ -1188,7 +1192,8 @@ fn resume_after_load_failed_reissues_load() {
effects
.iter()
.any(|e| matches!(e, Effect::LoadSession { agent_id, session_id,
.. } if * agent_id != agent_0 && session_id == "fail-then-retry")),
.. }
if * agent_id != agent_0 && session_id == "fail-then-retry")),
"retry after failure must emit LoadSession for a new agent, got {effects:?}"
);
assert_eq!(app.agents.len(), count_before + 1);

View file

@ -295,7 +295,8 @@ fn slash_model_valid_dispatches_set_default_model_with_switch_and_persist() {
effects[0],
);
assert!(
matches!(& effects[1], Effect::SwitchModel { model_id : mid, .. } if mid == &
matches!(& effects[1], Effect::SwitchModel { model_id : mid, .. }
if mid == &
model_id),
"second effect must be SwitchModel(<resolved id>), got {:?}",
effects[1],
@ -993,7 +994,8 @@ fn clear_default_model_persists_but_keeps_live_current() {
);
assert!(
matches!(& effects[0], Effect::PersistSetting { key : "default_model", value :
crate ::settings::SettingValue::String(s), .. } if s.is_empty()),
crate ::settings::SettingValue::String(s), .. }
if s.is_empty()),
"expected PersistSetting(default_model, ''), got {:?}",
effects[0],
);
@ -1028,9 +1030,13 @@ fn set_default_model_resolves_known_name() {
assert_eq!(effects.len(), 2);
assert!(
matches!(& effects[0], Effect::PersistSetting { key : "default_model", value :
crate ::settings::SettingValue::String(s), .. } if s == "grok-4.5")
crate ::settings::SettingValue::String(s), .. }
if s == "grok-4.5")
);
assert!(
matches!(& effects[1], Effect::SwitchModel { model_id : mid, .. }
if mid == & id)
);
assert!(matches!(& effects[1], Effect::SwitchModel { model_id : mid, .. } if mid == & id));
assert_eq!(app.agents[&agent_id].session.models.current, Some(id));
}
/// Re-dispatching the same model

View file

@ -660,9 +660,10 @@ fn switch_model_complete_success_updates_model_and_pushes_message() {
// PersistPreferredModel effect emitted.
assert_eq!(effects.len(), 1);
assert!(matches!(
&effects[0],
Effect::PersistPreferredModel { model_id: mid, .. } if *mid == model_id.clone()
));
&effects[0],
Effect::PersistPreferredModel { model_id: mid, .. }
if *mid == model_id.clone()
));
}
#[test]

View file

@ -249,7 +249,8 @@ fn parse_subagent_kill_outcome_reads_typed_outcome() {
);
assert!(
matches!(parse_subagent_kill_outcome(r#"{"result":{"subagentId":"sa-1","cancelled":false,"outcome":{"kind":"already_finished","status":"completed"}}}"#),
SubagentKillOutcome::NothingLive { status : Some(s) } if s == "completed")
SubagentKillOutcome::NothingLive { status : Some(s) }
if s == "completed")
);
assert!(
matches!(parse_subagent_kill_outcome(r#"{"result":{"subagentId":"sa-1","cancelled":false,"outcome":{"kind":"not_found"}}}"#),
@ -302,7 +303,8 @@ fn parse_subagent_kill_outcome_round_trips_agent_serialization() {
.unwrap();
assert!(
matches!(parse_subagent_kill_outcome(& wire), SubagentKillOutcome::NothingLive {
status : Some(s) } if s == "failed")
status : Some(s) }
if s == "failed")
);
}
/// A top-level payload (no `result` envelope), error envelopes, and
@ -1200,7 +1202,8 @@ async fn check_marketplace_updates_dispatches_update_and_skips_failed_notificati
MarketplaceAction::Update {
source_url_or_path,
plugin_relative_path,
} if source_url_or_path == "https://example.com/plugins.git"
}
if source_url_or_path == "https://example.com/plugins.git"
&& plugin_relative_path == "plugins/test-plugin" => {
saw_update_for_task.store(true, Ordering::SeqCst);
}

View file

@ -258,9 +258,11 @@ fn leader_kill_reconnect_reloads_without_duplicating_history() {
let deadline = tokio::time::Instant::now() + Duration::from_secs(30);
loop {
if matches!(
*status_rx.borrow_and_update(),
ConnectionStatus::Connected { generation } if generation >= 1
) {
*status_rx.borrow_and_update(),
ConnectionStatus::Connected { generation }
if generation >= 1
)
{
break;
}
assert!(

View file

@ -2086,9 +2086,10 @@ mod tests {
}),
);
assert!(matches!(
handle_ext_notification(&notif, OutputFormat::Plain),
ExtEvent::TaskBackgrounded { task_id, is_monitor: false } if task_id == "task-abc"
));
handle_ext_notification(&notif, OutputFormat::Plain),
ExtEvent::TaskBackgrounded { task_id, is_monitor: false }
if task_id == "task-abc"
));
}
#[test]
@ -2102,9 +2103,10 @@ mod tests {
}),
);
assert!(matches!(
handle_ext_notification(&notif, OutputFormat::Plain),
ExtEvent::TaskBackgrounded { task_id, is_monitor: true } if task_id == "mon-1"
));
handle_ext_notification(&notif, OutputFormat::Plain),
ExtEvent::TaskBackgrounded { task_id, is_monitor: true }
if task_id == "mon-1"
));
}
#[test]
@ -2122,9 +2124,10 @@ mod tests {
}),
);
assert!(matches!(
handle_ext_notification(&notif, OutputFormat::Plain),
ExtEvent::TaskCompleted { task_id } if task_id == "task-abc"
));
handle_ext_notification(&notif, OutputFormat::Plain),
ExtEvent::TaskCompleted { task_id }
if task_id == "task-abc"
));
}
#[test]
@ -2141,9 +2144,10 @@ mod tests {
}),
);
assert!(matches!(
handle_ext_notification(&spawned, OutputFormat::Plain),
ExtEvent::SubagentSpawned { subagent_id } if subagent_id == "sub-1"
));
handle_ext_notification(&spawned, OutputFormat::Plain),
ExtEvent::SubagentSpawned { subagent_id }
if subagent_id == "sub-1"
));
let finished = make_ext_notif(
"x.ai/session_notification",
serde_json::json!({
@ -2157,9 +2161,10 @@ mod tests {
}),
);
assert!(matches!(
handle_ext_notification(&finished, OutputFormat::Plain),
ExtEvent::SubagentFinished { subagent_id } if subagent_id == "sub-1"
));
handle_ext_notification(&finished, OutputFormat::Plain),
ExtEvent::SubagentFinished { subagent_id }
if subagent_id == "sub-1"
));
}
#[test]

View file

@ -888,9 +888,10 @@ fn marketplace_add(
if u.trim_end_matches(".git") == normalized)
})
}
MarketplaceAddInput::LocalPath(path) => sources
.iter()
.any(|s| matches!(&s.kind, SourceKind::Local { path: p } if p == path)),
MarketplaceAddInput::LocalPath(path) => sources.iter().any(|s| {
matches!(&s.kind, SourceKind::Local { path: p }
if p == path)
}),
};
if already_configured {
bail!("Marketplace source already configured: {identity}");

View file

@ -579,10 +579,13 @@ impl ExecuteToolCallBlock {
.with_joiner(joiner.clone()),
);
}
// Ellipsis (non-selectable, breaks range continuity)
let hidden = total - threshold;
lines.push(
BlockLine::separator(Line::from(Span::styled("\u{2026}", theme.muted())))
.with_panel_background(theme.bg_dark),
BlockLine::separator(Line::from(Span::styled(
format!("\u{2026} +{hidden} lines"),
theme.muted(),
)))
.with_panel_background(theme.bg_dark),
);
// Last M lines: range base + 1 (distinct from first chunk)
for (wrapped_line, joiner) in

View file

@ -97,9 +97,6 @@ impl ListDirToolCallBlock {
self.output = output.into();
}
/// Render collapsed line: `List path`.
///
/// When `width` is provided, the path is fish-shortened to fit.
fn collapsed_line(&self, theme: &Theme, muted: bool, width: Option<usize>) -> Line<'static> {
let text_style = if muted {
theme.muted()
@ -114,15 +111,32 @@ impl ListDirToolCallBlock {
};
let prefix = "List ";
let entry_count = self.output.lines().filter(|l| !l.trim().is_empty()).count();
let suffix = if self.error.is_none() && entry_count > 0 {
let s = if entry_count == 1 { "y" } else { "ies" };
format!(" ({entry_count} entr{s})")
} else {
String::new()
};
let suffix_fits = width.is_none_or(|w| prefix.len() + suffix.len() < w);
let effective_suffix = if suffix_fits { suffix.as_str() } else { "" };
let path_budget = width
.map(|w| w.saturating_sub(prefix.len()))
.map(|w| {
w.saturating_sub(prefix.len())
.saturating_sub(effective_suffix.len())
})
.unwrap_or(usize::MAX);
let path = crate::render::tool_paths::shorten_path(&self.path, path_budget);
Line::from(vec![
let mut spans = vec![
Span::styled(prefix, bold_style),
Span::styled(path, path_style),
])
];
if !effective_suffix.is_empty() {
spans.push(Span::styled(effective_suffix.to_string(), theme.muted()));
}
Line::from(spans)
}
/// Header line with only the path span selectable (exclude "List " prefix).
@ -224,3 +238,49 @@ impl BlockContent for ListDirToolCallBlock {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::scrollback::types::BlockContext;
fn ctx() -> BlockContext {
BlockContext {
width: 80,
mode: DisplayMode::Collapsed,
is_running: false,
raw: false,
max_lines: None,
appearance: Default::default(),
is_selected: false,
cwd: None,
}
}
fn header_text(block: &ListDirToolCallBlock) -> String {
block.output(&ctx()).lines[0]
.content
.spans
.iter()
.map(|s| s.content.as_ref())
.collect()
}
#[test]
fn collapsed_header_shows_entry_count() {
let block = ListDirToolCallBlock::new("src").with_output("a.rs\nb.rs\nsub/\n");
assert_eq!(header_text(&block), "List src (3 entries)");
let single = ListDirToolCallBlock::new("src").with_output("lonely.rs\n");
assert_eq!(header_text(&single), "List src (1 entry)");
}
#[test]
fn collapsed_header_omits_count_when_empty_or_failed() {
let empty = ListDirToolCallBlock::new("src");
assert_eq!(header_text(&empty), "List src");
let failed = ListDirToolCallBlock::new("gone").with_error("no such directory");
assert_eq!(header_text(&failed), "List gone");
}
}

View file

@ -342,6 +342,25 @@ impl ToolCallBlock {
}
}
/// Whether the tool call finished without an error.
pub fn is_success(&self) -> bool {
match self {
ToolCallBlock::Execute(b) => b.is_success(),
ToolCallBlock::Read(b) => b.is_success(),
ToolCallBlock::Edit(b) => b.is_success(),
ToolCallBlock::Search(b) => b.is_success(),
ToolCallBlock::ListDir(b) => b.is_success(),
ToolCallBlock::WebFetch(b) => b.is_success(),
ToolCallBlock::WebSearch(b) => b.is_success(),
ToolCallBlock::IntegrationSearch(b) => b.is_success(),
ToolCallBlock::UseTool(b) => b.is_success(),
ToolCallBlock::MemorySearch(b) => b.is_success(),
ToolCallBlock::Skill(b) => b.is_success(),
ToolCallBlock::Other(b) => b.is_success(),
ToolCallBlock::Lifecycle(_) => true,
}
}
/// Set `started_at` on the inner variant block.
///
/// Unlike `transfer_timing_from`, this works across variant boundaries

View file

@ -11,8 +11,8 @@ use crate::scrollback::types::{
};
use crate::theme::Theme;
/// Max lines of output shown inline before truncation.
const MAX_INLINE_LINES: usize = 10;
const TRUNCATED_INLINE_LINES: usize = 3;
/// Use tool call — dispatching to an MCP integration tool.
#[derive(Debug, Clone)]
@ -175,7 +175,11 @@ impl BlockContent for UseToolCallBlock {
}
}
// Output preview
let max_inline = if ctx.mode == DisplayMode::Truncated {
TRUNCATED_INLINE_LINES
} else {
MAX_INLINE_LINES
};
if let Some(ref output) = self.output {
lines.push(Line::from("").into());
lines
@ -185,8 +189,8 @@ impl BlockContent for UseToolCallBlock {
let content_lines: Vec<&str> = output.lines().collect();
for (i, line) in content_lines.iter().enumerate() {
if i >= MAX_INLINE_LINES {
let remaining = content_lines.len() - MAX_INLINE_LINES;
if i >= max_inline {
let remaining = content_lines.len() - max_inline;
lines.push(
BlockLine::from(Line::from(Span::styled(
format!(
@ -284,3 +288,58 @@ impl BlockContent for UseToolCallBlock {
Some(Text::from(vec![self.header_line(&theme, false, None)]))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::scrollback::types::BlockContext;
fn ctx(mode: DisplayMode) -> BlockContext {
BlockContext {
width: 80,
mode,
is_running: false,
raw: false,
max_lines: None,
appearance: Default::default(),
is_selected: false,
cwd: None,
}
}
fn rendered_text(block: &UseToolCallBlock, mode: DisplayMode) -> String {
block
.output(&ctx(mode))
.lines
.iter()
.map(|l| {
l.content
.spans
.iter()
.map(|s| s.content.as_ref())
.collect::<String>()
})
.collect::<Vec<_>>()
.join("\n")
}
#[test]
fn truncated_caps_inline_output_tighter_than_expanded() {
let mut block = UseToolCallBlock::new("linear__list_issues");
let content: Vec<String> = (1..=12).map(|i| format!("l{i:02} row")).collect();
block.output = Some(content.join("\n"));
let truncated = rendered_text(&block, DisplayMode::Truncated);
assert!(truncated.contains("l03"), "truncated:\n{truncated}");
assert!(!truncated.contains("l04"), "truncated:\n{truncated}");
assert!(
truncated.contains("(9 more lines"),
"truncated:\n{truncated}"
);
let expanded = rendered_text(&block, DisplayMode::Expanded);
assert!(expanded.contains("l10"), "expanded:\n{expanded}");
assert!(!expanded.contains("l11"), "expanded:\n{expanded}");
assert!(expanded.contains("(2 more lines"), "expanded:\n{expanded}");
}
}

View file

@ -11,8 +11,8 @@ use crate::scrollback::types::{
};
use crate::theme::Theme;
/// Max lines of content shown inline before truncation.
const MAX_INLINE_LINES: usize = 10;
const TRUNCATED_INLINE_LINES: usize = 3;
/// Web fetch tool call — fetching a URL and returning markdown content.
#[derive(Debug, Clone)]
@ -238,9 +238,11 @@ impl BlockContent for WebFetchToolCallBlock {
lines.push(BlockLine::separator(meta));
}
// Content preview with bg_dark background, capped at
// MAX_INLINE_LINES. Full content is available via the
// fullscreen viewer (Enter/o).
let max_inline = if ctx.mode == DisplayMode::Truncated {
TRUNCATED_INLINE_LINES
} else {
MAX_INLINE_LINES
};
if let Some(ref output) = self.output {
lines.push(Line::from("").into());
@ -252,12 +254,12 @@ impl BlockContent for WebFetchToolCallBlock {
let total_lines = output.lines().count();
for (i, line) in output.lines().enumerate() {
if i >= MAX_INLINE_LINES {
if i >= max_inline {
lines.push(
BlockLine::from(Line::from(Span::styled(
format!(
"{indent}... ({} more lines, press Enter to view)",
total_lines - MAX_INLINE_LINES
total_lines - max_inline
),
theme.dim(),
)))
@ -347,3 +349,58 @@ impl BlockContent for WebFetchToolCallBlock {
Some(Text::from(vec![self.header_line(&theme, false, None)]))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::scrollback::types::BlockContext;
fn ctx(mode: DisplayMode) -> BlockContext {
BlockContext {
width: 80,
mode,
is_running: false,
raw: false,
max_lines: None,
appearance: Default::default(),
is_selected: false,
cwd: None,
}
}
fn rendered_text(block: &WebFetchToolCallBlock, mode: DisplayMode) -> String {
block
.output(&ctx(mode))
.lines
.iter()
.map(|l| {
l.content
.spans
.iter()
.map(|s| s.content.as_ref())
.collect::<String>()
})
.collect::<Vec<_>>()
.join("\n")
}
#[test]
fn truncated_caps_inline_content_tighter_than_expanded() {
let content: Vec<String> = (1..=12).map(|i| format!("l{i:02} body")).collect();
let block =
WebFetchToolCallBlock::new("https://example.com").with_output(content.join("\n"));
let truncated = rendered_text(&block, DisplayMode::Truncated);
assert!(truncated.contains("l03"), "truncated:\n{truncated}");
assert!(!truncated.contains("l04"), "truncated:\n{truncated}");
assert!(
truncated.contains("(9 more lines"),
"truncated:\n{truncated}"
);
let expanded = rendered_text(&block, DisplayMode::Expanded);
assert!(expanded.contains("l10"), "expanded:\n{expanded}");
assert!(!expanded.contains("l11"), "expanded:\n{expanded}");
assert!(expanded.contains("(2 more lines"), "expanded:\n{expanded}");
}
}

View file

@ -13,8 +13,8 @@ use crate::scrollback::types::{
};
use crate::theme::Theme;
/// Max lines of content shown inline before truncation.
const MAX_INLINE_LINES: usize = 10;
const TRUNCATED_INLINE_LINES: usize = 3;
/// Max number of domain names shown in the sources summary line.
const MAX_INLINE_SOURCES: usize = 3;
@ -258,9 +258,11 @@ impl BlockContent for WebSearchToolCallBlock {
})
.collect();
// Content preview with bg_dark background, capped at
// MAX_INLINE_LINES. Full content is available via the
// fullscreen viewer (Enter/o).
let max_inline = if ctx.mode == DisplayMode::Truncated {
TRUNCATED_INLINE_LINES
} else {
MAX_INLINE_LINES
};
if let Some(ref content) = self.content {
lines.push(BlockLine::separator(Line::from("")));
@ -272,8 +274,8 @@ impl BlockContent for WebSearchToolCallBlock {
let content_lines: Vec<&str> = content.lines().collect();
for (i, line) in content_lines.iter().enumerate() {
if i >= MAX_INLINE_LINES {
let remaining = content_lines.len() - MAX_INLINE_LINES;
if i >= max_inline {
let remaining = content_lines.len() - max_inline;
lines.push(
BlockLine::from(Line::from(Span::styled(
format!(
@ -379,3 +381,58 @@ impl BlockContent for WebSearchToolCallBlock {
Some(Text::from(vec![self.header_line(&theme, false, None)]))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::scrollback::types::BlockContext;
fn ctx(mode: DisplayMode) -> BlockContext {
BlockContext {
width: 80,
mode,
is_running: false,
raw: false,
max_lines: None,
appearance: Default::default(),
is_selected: false,
cwd: None,
}
}
fn rendered_text(block: &WebSearchToolCallBlock, mode: DisplayMode) -> String {
block
.output(&ctx(mode))
.lines
.iter()
.map(|l| {
l.content
.spans
.iter()
.map(|s| s.content.as_ref())
.collect::<String>()
})
.collect::<Vec<_>>()
.join("\n")
}
#[test]
fn truncated_caps_inline_content_tighter_than_expanded() {
let mut block = WebSearchToolCallBlock::new("rust async traits");
let content: Vec<String> = (1..=12).map(|i| format!("l{i:02} result")).collect();
block.content = Some(content.join("\n"));
let truncated = rendered_text(&block, DisplayMode::Truncated);
assert!(truncated.contains("l03"), "truncated:\n{truncated}");
assert!(!truncated.contains("l04"), "truncated:\n{truncated}");
assert!(
truncated.contains("(9 more lines"),
"truncated:\n{truncated}"
);
let expanded = rendered_text(&block, DisplayMode::Expanded);
assert!(expanded.contains("l10"), "expanded:\n{expanded}");
assert!(!expanded.contains("l11"), "expanded:\n{expanded}");
assert!(expanded.contains("(2 more lines"), "expanded:\n{expanded}");
}
}

View file

@ -79,7 +79,6 @@ pub fn builtin_commands() -> Vec<Arc<dyn SlashCommand>> {
Arc::new(docs::DocsCommand),
Arc::new(home::HomeCommand),
Arc::new(new::NewCommand),
Arc::new(fork::ForkCommand),
Arc::new(compact::CompactCommand),
Arc::new(copy::CopyCommand),
@ -117,7 +116,6 @@ pub fn builtin_commands() -> Vec<Arc<dyn SlashCommand>> {
Arc::new(mcps::McpsCommand),
Arc::new(btw::BtwCommand),
Arc::new(recap::RecapCommand),
Arc::new(terminal_setup::TerminalSetupCommand),
Arc::new(voice::VoiceCommand),
Arc::new(loop_cmd::LoopCommand),

View file

@ -6040,9 +6040,10 @@ mod tests {
);
assert!(
lines.iter().any(|l| matches!(
l,
DashboardLine::Header { state, count } if *state == RowState::Working && *count == 2
)),
l,
DashboardLine::Header { state, count }
if *state == RowState::Working && *count == 2
)),
"collapsed Working header must still render with its true count",
);
let working_rows = lines
@ -6080,7 +6081,8 @@ mod tests {
assert!(
lines
.iter()
.any(|l| matches!(l, DashboardLine::PinnedHeader { count } if *count == 1)),
.any(|l| matches!(l, DashboardLine::PinnedHeader { count }
if *count == 1)),
"collapsed Pinned header must still render",
);
// The pinned row is hidden; the (non-pinned) Working row remains.
@ -6142,9 +6144,10 @@ mod tests {
// Header still shows the TRUE total, not the visible count.
assert!(
lines.iter().any(|l| matches!(
l,
DashboardLine::Header { state, count } if *state == RowState::Idle && *count == total as usize
)),
l,
DashboardLine::Header { state, count }
if *state == RowState::Idle && *count == total as usize
)),
"Idle header keeps the true total count",
);
}

View file

@ -595,7 +595,8 @@ fn render_file_list(buf: &mut Buffer, area: Rect, state: &mut MemoryModalState,
);
if is_selected
&& matches!(state.mode, MemoryModalMode::ConfirmingDelete { idx } if idx == filt_idx)
&& matches!(state.mode, MemoryModalMode::ConfirmingDelete { idx }
if idx == filt_idx)
{
let hint = " [x to confirm]";
let hint_w = hint.len() as u16;

View file

@ -275,21 +275,20 @@ impl SettingsModalState {
self.invalidate_filter();
if let Some(key) = subpane_key {
let still_visible = self
.rows
.iter()
.any(|r| matches!(r, RowEntry::Setting { key: k, .. } if *k == key));
let still_visible = self.rows.iter().any(|r| {
matches!(r, RowEntry::Setting { key: k, .. }
if *k == key)
});
if !still_visible {
self.transition_to_browse();
}
}
if let Some(key) = prev_key {
if let Some(idx) = self
.rows
.iter()
.position(|r| matches!(r, RowEntry::Setting { key: k, .. } if *k == key))
{
if let Some(idx) = self.rows.iter().position(|r| {
matches!(r, RowEntry::Setting { key: k, .. }
if *k == key)
}) {
self.selected = idx;
} else {
self.selected = self

View file

@ -36,13 +36,17 @@ fn contextual_hints_group_sub_sheet_flow() {
let group_idx = s
.rows
.iter()
.position(|r| matches!(r, RowEntry::Setting { key, .. } if *key == "contextual_hints"))
.position(|r| {
matches!(r, RowEntry::Setting { key, .. }
if *key == "contextual_hints")
})
.expect("group row present");
assert!(
!s.rows.iter().any(|r| matches!(
r,
RowEntry::Setting { key, .. } if key.starts_with("contextual_hints.")
)),
r,
RowEntry::Setting { key, .. }
if key.starts_with("contextual_hints.")
)),
"child rows must be hidden from the top-level list",
);
@ -4118,7 +4122,10 @@ fn advance_next_recovers_when_selection_is_hidden() {
let compact_idx = s
.rows
.iter()
.position(|r| matches!(r, RowEntry::Setting { key, .. } if *key == "compact_mode"))
.position(|r| {
matches!(r, RowEntry::Setting { key, .. }
if *key == "compact_mode")
})
.unwrap();
s.selected = compact_idx;
// Advance: lands on the first visible setting (show_timestamps).
@ -4127,7 +4134,10 @@ fn advance_next_recovers_when_selection_is_hidden() {
let show_ts_idx = s
.rows
.iter()
.position(|r| matches!(r, RowEntry::Setting { key, .. } if *key == "show_timestamps"))
.position(|r| {
matches!(r, RowEntry::Setting { key, .. }
if *key == "show_timestamps")
})
.unwrap();
assert_eq!(s.selected, show_ts_idx);
}
@ -4150,7 +4160,10 @@ fn advance_prev_recovers_when_selection_is_hidden() {
let compact_idx = s
.rows
.iter()
.position(|r| matches!(r, RowEntry::Setting { key, .. } if *key == "compact_mode"))
.position(|r| {
matches!(r, RowEntry::Setting { key, .. }
if *key == "compact_mode")
})
.unwrap();
s.selected = compact_idx;
let moved = s.advance_prev();
@ -4158,7 +4171,10 @@ fn advance_prev_recovers_when_selection_is_hidden() {
let simple_idx = s
.rows
.iter()
.position(|r| matches!(r, RowEntry::Setting { key, .. } if *key == "simple_mode"))
.position(|r| {
matches!(r, RowEntry::Setting { key, .. }
if *key == "simple_mode")
})
.unwrap();
assert_eq!(s.selected, simple_idx);
}
@ -4258,10 +4274,10 @@ fn section_headers_have_blank_line_above_except_first() {
for cat in SettingCategory::ALL {
// Skip categories the default registry doesn't populate
// (e.g. Session — no settings registered).
let has_setting = s
.rows
.iter()
.any(|r| matches!(r, RowEntry::Header { category } if category == cat));
let has_setting = s.rows.iter().any(|r| {
matches!(r, RowEntry::Header { category }
if category == cat)
});
if !has_setting {
continue;
}
@ -4607,7 +4623,10 @@ fn two_line_row_hit_rect_spans_both_lines() {
let row_idx = s
.rows
.iter()
.position(|r| matches!(r, RowEntry::Setting { key, .. } if *key == "coding_data_sharing"))
.position(|r| {
matches!(r, RowEntry::Setting { key, .. }
if *key == "coding_data_sharing")
})
.expect("coding_data_sharing must be registered");
// Render at a narrow width so coding_data_sharing forces a
// two-line layout.
@ -4672,7 +4691,10 @@ fn two_line_row_with_expansion_renders_three_segments() {
let row_idx = s
.rows
.iter()
.position(|r| matches!(r, RowEntry::Setting { key, .. } if *key == "coding_data_sharing"))
.position(|r| {
matches!(r, RowEntry::Setting { key, .. }
if *key == "coding_data_sharing")
})
.expect("coding_data_sharing must be registered");
s.selected = row_idx;
s.expanded_keys.insert("coding_data_sharing");
@ -4730,7 +4752,10 @@ fn group_row_renders_expanded_description() {
let row_idx = s
.rows
.iter()
.position(|r| matches!(r, RowEntry::Setting { key, .. } if *key == "contextual_hints"))
.position(|r| {
matches!(r, RowEntry::Setting { key, .. }
if *key == "contextual_hints")
})
.expect("contextual_hints group must be registered");
s.selected = row_idx;
s.expanded_keys.insert("contextual_hints");
@ -5769,7 +5794,10 @@ fn enter_picker_for(key: &'static str) -> SettingsModalState {
let row_idx = s
.rows
.iter()
.position(|r| matches!(r, RowEntry::Setting { key: k, .. } if *k == key))
.position(|r| {
matches!(r, RowEntry::Setting { key: k, .. }
if *k == key)
})
.unwrap_or_else(|| panic!("no row for key `{key}` in default registry"));
assert!(s.select_at(row_idx), "select_at({row_idx})");
assert!(

View file

@ -1681,9 +1681,10 @@ mod tests {
let entries = build_entries(&all_contexts(), &registry, true);
let has_row = entries.iter().any(|e| {
matches!(
e,
ShortcutsHelpEntry::Hint { item, .. } if item.label == "mouse reporting"
)
e,
ShortcutsHelpEntry::Hint { item, .. }
if item.label == "mouse reporting"
)
});
assert!(
!has_row,
@ -1776,21 +1777,24 @@ mod tests {
let has_todos = entries.iter().any(|e| {
matches!(
e,
ShortcutsHelpEntry::Hint { item, .. } if item.label == "todos"
)
e,
ShortcutsHelpEntry::Hint { item, .. }
if item.label == "todos"
)
});
let has_sessions = entries.iter().any(|e| {
matches!(
e,
ShortcutsHelpEntry::Hint { item, .. } if item.label == "sessions"
)
e,
ShortcutsHelpEntry::Hint { item, .. }
if item.label == "sessions"
)
});
let has_queue = entries.iter().any(|e| {
matches!(
e,
ShortcutsHelpEntry::Hint { item, .. } if item.label == "queue"
)
e,
ShortcutsHelpEntry::Hint { item, .. }
if item.label == "queue"
)
});
assert!(has_todos, "should include toggle todos");
assert!(has_sessions, "should include open sessions");
@ -1835,13 +1839,14 @@ mod tests {
.iter()
.find(|e| {
matches!(
e,
ShortcutsHelpEntry::Hint {
item,
action_id: None,
..
} if item.label == "paste"
)
e,
ShortcutsHelpEntry::Hint {
item,
action_id: None,
..
}
if item.label == "paste"
)
})
.expect("cheatsheet should list paste");
let ShortcutsHelpEntry::Hint {
@ -1922,9 +1927,10 @@ mod tests {
let nav_dimmed = entries.iter().any(|e| {
matches!(
e,
ShortcutsHelpEntry::Hint { item, dimmed: true, .. } if item.label == "nav"
)
e,
ShortcutsHelpEntry::Hint { item, dimmed: true, .. }
if item.label == "nav"
)
});
assert!(
nav_dimmed,
@ -1933,17 +1939,19 @@ mod tests {
let quit_bright = entries.iter().any(|e| {
matches!(
e,
ShortcutsHelpEntry::Hint { item, dimmed: false, .. } if item.label == "quit"
)
e,
ShortcutsHelpEntry::Hint { item, dimmed: false, .. }
if item.label == "quit"
)
});
assert!(quit_bright, "quit should not be dimmed (When::Always)");
let cancel_bright = entries.iter().any(|e| {
matches!(
e,
ShortcutsHelpEntry::Hint { item, dimmed: false, .. } if item.label == "cancel"
)
e,
ShortcutsHelpEntry::Hint { item, dimmed: false, .. }
if item.label == "cancel"
)
});
assert!(
cancel_bright,
@ -1959,9 +1967,10 @@ mod tests {
let send_dimmed = entries.iter().any(|e| {
matches!(
e,
ShortcutsHelpEntry::Hint { item, dimmed: true, .. } if item.label == "send"
)
e,
ShortcutsHelpEntry::Hint { item, dimmed: true, .. }
if item.label == "send"
)
});
assert!(
send_dimmed,
@ -1970,9 +1979,10 @@ mod tests {
let nav_dimmed = entries.iter().any(|e| {
matches!(
e,
ShortcutsHelpEntry::Hint { item, dimmed: true, .. } if item.label == "nav"
)
e,
ShortcutsHelpEntry::Hint { item, dimmed: true, .. }
if item.label == "nav"
)
});
assert!(
nav_dimmed,
@ -2638,14 +2648,15 @@ mod tests {
.iter()
.position(|e| {
matches!(
e,
ShortcutsHelpEntry::Hint {
item,
action_id: None,
long_help: Some(_),
..
} if item.label == "paste"
)
e,
ShortcutsHelpEntry::Hint {
item,
action_id: None,
long_help: Some(_),
..
}
if item.label == "paste"
)
})
.expect("paste pseudo-row with long_help");
assert_eq!(
@ -3024,9 +3035,10 @@ mod tests {
for label in ["top", "btm", "copy", "copy cmd"] {
let present = entries.iter().any(|e| {
matches!(
e,
ShortcutsHelpEntry::Hint { item, .. } if item.label == label
)
e,
ShortcutsHelpEntry::Hint { item, .. }
if item.label == label
)
});
assert!(
!present,
@ -3315,14 +3327,15 @@ mod tests {
.iter()
.position(|e| {
matches!(
e,
ShortcutsHelpEntry::Hint {
item,
action_id: None,
long_help: Some(_),
..
} if item.label == "paste"
)
e,
ShortcutsHelpEntry::Hint {
item,
action_id: None,
long_help: Some(_),
..
}
if item.label == "paste"
)
})
.expect("paste pseudo-row with long_help");
let key_id = ExpandKey::Pseudo("paste");

View file

@ -992,11 +992,10 @@ impl TasksPane {
};
if changed {
self.rebuild_entries();
if let Some(header) = self
.entries
.iter()
.find(|e| matches!(e, TaskEntry::Header { group: g, .. } if *g == group))
{
if let Some(header) = self.entries.iter().find(|e| {
matches!(e, TaskEntry::Header { group: g, .. }
if *g == group)
}) {
let id = header.stable_id();
self.list_state.select_by_id(id);
}

View file

@ -2949,10 +2949,12 @@ mod tests {
// Verify headers
assert!(
matches!(&result[0], crate::views::picker::PickerEntry::Header { label } if label == &"fw-1")
matches!(&result[0], crate::views::picker::PickerEntry::Header { label }
if label == &"fw-1")
);
assert!(
matches!(&result[2], crate::views::picker::PickerEntry::Header { label } if label == &"xai")
matches!(&result[2], crate::views::picker::PickerEntry::Header { label }
if label == &"xai")
);
}
@ -2981,11 +2983,13 @@ mod tests {
Some("zzz"),
);
assert!(
matches!(&result[0], crate::views::picker::PickerEntry::Header { label } if label == &"zzz"),
matches!(&result[0], crate::views::picker::PickerEntry::Header { label }
if label == &"zzz"),
"current repo group pinned first"
);
assert!(
matches!(&result[2], crate::views::picker::PickerEntry::Header { label } if label == &"aaa"),
matches!(&result[2], crate::views::picker::PickerEntry::Header { label }
if label == &"aaa"),
"remaining group follows alphabetically"
);
}