commit dcf5423e42cdf38bcdfa0eb29704319cce3b7389
parent 2e8ebb5352c4e466d15e153fb348b5c20bbd6635
Author: William Casarin <jb55@jb55.com>
Date: Tue, 17 Feb 2026 13:01:54 -0800
refactor: extract shared helpers to reduce duplication in dave crate
- switch_and_focus_session(): consolidates switch_to/select/focus_on/focus_requested pattern (8 call sites)
- cycle_agent(): generic direction-based agent cycling, used by next/prev
- secret_key_bytes(): replaces 5 copies of keypair→secret_key→as_secret_bytes→try_into
- init_note_builder(): replaces duplicated NoteBuilder::new().kind().content().options() chains (6 call sites)
- finalize_built_event(): replaces duplicated sign/build/id/json pattern (6 call sites)
- now_secs(): replaces 4 copies of SystemTime::now timestamp boilerplate
Net: 128 insertions, 316 deletions across 3 files.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat:
4 files changed, 193 insertions(+), 427 deletions(-)
diff --git a/crates/notedeck_dave/src/lib.rs b/crates/notedeck_dave/src/lib.rs
@@ -62,6 +62,15 @@ pub use vec3::Vec3;
/// TODO: make this configurable in the UI
const PNS_RELAY_URL: &str = "ws://relay.jb55.com";
+/// Extract a 32-byte secret key from a keypair.
+fn secret_key_bytes(keypair: KeypairUnowned<'_>) -> Option<[u8; 32]> {
+ keypair.secret_key.map(|sk| {
+ sk.as_secret_bytes()
+ .try_into()
+ .expect("secret key is 32 bytes")
+ })
+}
+
/// Represents which full-screen overlay (if any) is currently active
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum DaveOverlay {
@@ -364,16 +373,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
let active_id = self.session_manager.active_id();
// Extract secret key once for live event generation
- let secret_key: Option<[u8; 32]> = app_ctx
- .accounts
- .get_selected_account()
- .keypair()
- .secret_key
- .map(|sk| {
- sk.as_secret_bytes()
- .try_into()
- .expect("secret key is 32 bytes")
- });
+ let secret_key = secret_key_bytes(app_ctx.accounts.get_selected_account().keypair());
// Get all session IDs to process
let session_ids = self.session_manager.session_ids();
@@ -1066,18 +1066,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
/// Publish kind-31988 state events for sessions whose status changed.
fn publish_dirty_session_states(&mut self, ctx: &mut AppContext<'_>) {
- let secret_key: Option<[u8; 32]> = ctx
- .accounts
- .get_selected_account()
- .keypair()
- .secret_key
- .map(|sk| {
- sk.as_secret_bytes()
- .try_into()
- .expect("secret key is 32 bytes")
- });
-
- let Some(sk) = secret_key else {
+ let Some(sk) = secret_key_bytes(ctx.accounts.get_selected_account().keypair()) else {
return;
};
@@ -1130,18 +1119,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
return;
}
- let secret_key: Option<[u8; 32]> = ctx
- .accounts
- .get_selected_account()
- .keypair()
- .secret_key
- .map(|sk| {
- sk.as_secret_bytes()
- .try_into()
- .expect("secret key is 32 bytes")
- });
-
- let Some(sk) = secret_key else {
+ let Some(sk) = secret_key_bytes(ctx.accounts.get_selected_account().keypair()) else {
return;
};
@@ -1175,18 +1153,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
return;
}
- let secret_key: Option<[u8; 32]> = ctx
- .accounts
- .get_selected_account()
- .keypair()
- .secret_key
- .map(|sk| {
- sk.as_secret_bytes()
- .try_into()
- .expect("secret key is 32 bytes")
- });
-
- let Some(sk) = secret_key else {
+ let Some(sk) = secret_key_bytes(ctx.accounts.get_selected_account().keypair()) else {
tracing::warn!("no secret key for publishing permission responses");
self.pending_perm_responses.clear();
return;
@@ -1838,15 +1805,8 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
session.input.clear();
// Generate live event for user message
- if let Some(sk) = app_ctx
- .accounts
- .get_selected_account()
- .keypair()
- .secret_key
- {
- let sb = sk.as_secret_bytes();
- let secret_bytes: [u8; 32] = sb.try_into().expect("secret key is 32 bytes");
- if let Some(evt) = ingest_live_event(session, app_ctx.ndb, &secret_bytes, &user_text, "user", None) {
+ if let Some(sk) = secret_key_bytes(app_ctx.accounts.get_selected_account().keypair()) {
+ if let Some(evt) = ingest_live_event(session, app_ctx.ndb, &sk, &user_text, "user", None) {
self.pending_relay_events.push(evt);
}
}
@@ -2027,8 +1987,7 @@ impl notedeck::App for Dave {
}
}
} else {
- let keypair = ctx.accounts.get_selected_account().keypair();
- if let Some(sk) = keypair.secret_key {
+ if let Some(secret_bytes) = secret_key_bytes(ctx.accounts.get_selected_account().keypair()) {
// Subscribe for 1988 events BEFORE ingesting so we catch them
let sub_filter = nostrdb::Filter::new()
.kinds([session_events::AI_CONVERSATION_KIND as u64])
@@ -2037,9 +1996,6 @@ impl notedeck::App for Dave {
match ctx.ndb.subscribe(&[sub_filter]) {
Ok(sub) => {
- let sb = sk.as_secret_bytes();
- let secret_bytes: [u8; 32] =
- sb.try_into().expect("secret key is 32 bytes");
match session_converter::convert_session_to_events(
&file_path,
ctx.ndb,
diff --git a/crates/notedeck_dave/src/session_events.rs b/crates/notedeck_dave/src/session_events.rs
@@ -67,24 +67,10 @@ pub fn wrap_pns(
let ciphertext = enostr::pns::encrypt(&pns_keys.conversation_key, inner_json)
.map_err(|e| EventBuildError::Serialize(format!("PNS encrypt: {e}")))?;
- let now = std::time::SystemTime::now()
- .duration_since(std::time::UNIX_EPOCH)
- .unwrap_or_default()
- .as_secs();
-
let pns_secret = pns_keys.keypair.secret_key.secret_bytes();
-
- let note = NoteBuilder::new()
- .kind(enostr::pns::PNS_KIND)
- .content(&ciphertext)
- .options(NoteBuildOptions::default())
- .created_at(now)
- .sign(&pns_secret)
- .build()
- .ok_or_else(|| EventBuildError::Build("PNS NoteBuilder::build returned None".into()))?;
-
- note.json()
- .map_err(|e| EventBuildError::Serialize(format!("PNS json: {e:?}")))
+ let builder = init_note_builder(enostr::pns::PNS_KIND, &ciphertext, Some(now_secs()));
+ let event = finalize_built_event(builder, &pns_secret, enostr::pns::PNS_KIND)?;
+ Ok(event.note_json)
}
/// Maintains threading state across a session's events.
@@ -175,6 +161,52 @@ impl ThreadingState {
}
}
+/// Get the current Unix timestamp in seconds.
+fn now_secs() -> u64 {
+ std::time::SystemTime::now()
+ .duration_since(std::time::UNIX_EPOCH)
+ .unwrap_or_default()
+ .as_secs()
+}
+
+/// Initialize a NoteBuilder with kind, content, and optional timestamp.
+fn init_note_builder(kind: u32, content: &str, timestamp: Option<u64>) -> NoteBuilder<'_> {
+ let mut builder = NoteBuilder::new()
+ .kind(kind)
+ .content(content)
+ .options(NoteBuildOptions::default());
+
+ if let Some(ts) = timestamp {
+ builder = builder.created_at(ts);
+ }
+
+ builder
+}
+
+/// Sign, build, and serialize a NoteBuilder into a BuiltEvent.
+fn finalize_built_event(
+ builder: NoteBuilder,
+ secret_key: &[u8; 32],
+ kind: u32,
+) -> Result<BuiltEvent, EventBuildError> {
+ let note = builder
+ .sign(secret_key)
+ .build()
+ .ok_or_else(|| EventBuildError::Build("NoteBuilder::build returned None".to_string()))?;
+
+ let note_id: [u8; 32] = *note.id();
+
+ let note_json = note
+ .json()
+ .map_err(|e| EventBuildError::Serialize(format!("{:?}", e)))?;
+
+ Ok(BuiltEvent {
+ note_json,
+ note_id,
+ kind,
+ })
+}
+
/// Whether a role represents a conversation message (not metadata).
pub fn is_conversation_role(role: &str) -> bool {
matches!(role, "user" | "assistant" | "tool_call" | "tool_result")
@@ -325,14 +357,7 @@ fn build_source_data_event(
let raw_json = line.to_json();
let seq_str = seq.to_string();
- let mut builder = NoteBuilder::new()
- .kind(AI_SOURCE_DATA_KIND)
- .content("")
- .options(NoteBuildOptions::default());
-
- if let Some(ts) = timestamp {
- builder = builder.created_at(ts);
- }
+ let mut builder = init_note_builder(AI_SOURCE_DATA_KIND, "", timestamp);
// Link to the corresponding 1988 event
builder = builder
@@ -353,22 +378,7 @@ fn build_source_data_event(
.tag_str("source-data")
.tag_str(&raw_json);
- let note = builder
- .sign(secret_key)
- .build()
- .ok_or_else(|| EventBuildError::Build("NoteBuilder::build returned None".to_string()))?;
-
- let note_id: [u8; 32] = *note.id();
-
- let note_json = note
- .json()
- .map_err(|e| EventBuildError::Serialize(format!("{:?}", e)))?;
-
- Ok(BuiltEvent {
- note_json,
- note_id,
- kind: AI_SOURCE_DATA_KIND,
- })
+ finalize_built_event(builder, secret_key, AI_SOURCE_DATA_KIND)
}
/// Build a single kind-1988 nostr event.
@@ -394,14 +404,7 @@ fn build_single_event(
threading: &ThreadingState,
secret_key: &[u8; 32],
) -> Result<BuiltEvent, EventBuildError> {
- let mut builder = NoteBuilder::new()
- .kind(AI_CONVERSATION_KIND)
- .content(content)
- .options(NoteBuildOptions::default());
-
- if let Some(ts) = timestamp {
- builder = builder.created_at(ts);
- }
+ let mut builder = init_note_builder(AI_CONVERSATION_KIND, content, timestamp);
// -- Session identity tags --
if let Some(session_id) = session_id {
@@ -474,23 +477,7 @@ fn build_single_event(
// -- Discoverability --
builder = builder.start_tag().tag_str("t").tag_str("ai-conversation");
- // Sign and build
- let note = builder
- .sign(secret_key)
- .build()
- .ok_or_else(|| EventBuildError::Build("NoteBuilder::build returned None".to_string()))?;
-
- let note_id: [u8; 32] = *note.id();
-
- let note_json = note
- .json()
- .map_err(|e| EventBuildError::Serialize(format!("{:?}", e)))?;
-
- Ok(BuiltEvent {
- note_json,
- note_id,
- kind: AI_CONVERSATION_KIND,
- })
+ finalize_built_event(builder, secret_key, AI_CONVERSATION_KIND)
}
/// Build a kind-1988 event for a live conversation message.
@@ -508,11 +495,6 @@ pub fn build_live_event(
threading: &mut ThreadingState,
secret_key: &[u8; 32],
) -> Result<BuiltEvent, EventBuildError> {
- let now = std::time::SystemTime::now()
- .duration_since(std::time::UNIX_EPOCH)
- .unwrap_or_default()
- .as_secs();
-
let event = build_single_event(
None,
content,
@@ -522,7 +504,7 @@ pub fn build_live_event(
tool_id,
Some(session_id),
cwd,
- Some(now),
+ Some(now_secs()),
threading,
secret_key,
)?;
@@ -545,11 +527,6 @@ pub fn build_permission_request_event(
session_id: &str,
secret_key: &[u8; 32],
) -> Result<BuiltEvent, EventBuildError> {
- let now = std::time::SystemTime::now()
- .duration_since(std::time::UNIX_EPOCH)
- .unwrap_or_default()
- .as_secs();
-
// Content is a JSON summary for display on remote clients
let content = serde_json::json!({
"tool_name": tool_name,
@@ -559,11 +536,7 @@ pub fn build_permission_request_event(
let perm_id_str = perm_id.to_string();
- let mut builder = NoteBuilder::new()
- .kind(AI_CONVERSATION_KIND)
- .content(&content)
- .options(NoteBuildOptions::default())
- .created_at(now);
+ let mut builder = init_note_builder(AI_CONVERSATION_KIND, &content, Some(now_secs()));
// Session identity
builder = builder.start_tag().tag_str("d").tag_str(session_id);
@@ -596,21 +569,7 @@ pub fn build_permission_request_event(
.tag_str("t")
.tag_str("ai-permission");
- let note = builder
- .sign(secret_key)
- .build()
- .ok_or_else(|| EventBuildError::Build("NoteBuilder::build returned None".to_string()))?;
-
- let note_id: [u8; 32] = *note.id();
- let note_json = note
- .json()
- .map_err(|e| EventBuildError::Serialize(format!("{:?}", e)))?;
-
- Ok(BuiltEvent {
- note_json,
- note_id,
- kind: AI_CONVERSATION_KIND,
- })
+ finalize_built_event(builder, secret_key, AI_CONVERSATION_KIND)
}
/// Build a kind-1988 permission response event.
@@ -629,11 +588,6 @@ pub fn build_permission_response_event(
session_id: &str,
secret_key: &[u8; 32],
) -> Result<BuiltEvent, EventBuildError> {
- let now = std::time::SystemTime::now()
- .duration_since(std::time::UNIX_EPOCH)
- .unwrap_or_default()
- .as_secs();
-
let content = serde_json::json!({
"decision": if allowed { "allow" } else { "deny" },
"message": message.unwrap_or(""),
@@ -642,11 +596,7 @@ pub fn build_permission_response_event(
let perm_id_str = perm_id.to_string();
- let mut builder = NoteBuilder::new()
- .kind(AI_CONVERSATION_KIND)
- .content(&content)
- .options(NoteBuildOptions::default())
- .created_at(now);
+ let mut builder = init_note_builder(AI_CONVERSATION_KIND, &content, Some(now_secs()));
// Session identity
builder = builder.start_tag().tag_str("d").tag_str(session_id);
@@ -681,21 +631,7 @@ pub fn build_permission_response_event(
.tag_str("t")
.tag_str("ai-permission");
- let note = builder
- .sign(secret_key)
- .build()
- .ok_or_else(|| EventBuildError::Build("NoteBuilder::build returned None".to_string()))?;
-
- let note_id: [u8; 32] = *note.id();
- let note_json = note
- .json()
- .map_err(|e| EventBuildError::Serialize(format!("{:?}", e)))?;
-
- Ok(BuiltEvent {
- note_json,
- note_id,
- kind: AI_CONVERSATION_KIND,
- })
+ finalize_built_event(builder, secret_key, AI_CONVERSATION_KIND)
}
/// Build a kind-31988 session state event (parameterized replaceable).
@@ -710,16 +646,7 @@ pub fn build_session_state_event(
status: &str,
secret_key: &[u8; 32],
) -> Result<BuiltEvent, EventBuildError> {
- let now = std::time::SystemTime::now()
- .duration_since(std::time::UNIX_EPOCH)
- .unwrap_or_default()
- .as_secs();
-
- let mut builder = NoteBuilder::new()
- .kind(AI_SESSION_STATE_KIND)
- .content("")
- .options(NoteBuildOptions::default())
- .created_at(now);
+ let mut builder = init_note_builder(AI_SESSION_STATE_KIND, "", Some(now_secs()));
// Session identity (makes this a parameterized replaceable event)
builder = builder.start_tag().tag_str("d").tag_str(claude_session_id);
@@ -743,21 +670,7 @@ pub fn build_session_state_event(
.tag_str("source")
.tag_str("notedeck-dave");
- let note = builder
- .sign(secret_key)
- .build()
- .ok_or_else(|| EventBuildError::Build("NoteBuilder::build returned None".to_string()))?;
-
- let note_id: [u8; 32] = *note.id();
- let note_json = note
- .json()
- .map_err(|e| EventBuildError::Serialize(format!("{:?}", e)))?;
-
- Ok(BuiltEvent {
- note_json,
- note_id,
- kind: AI_SESSION_STATE_KIND,
- })
+ finalize_built_event(builder, secret_key, AI_SESSION_STATE_KIND)
}
#[cfg(test)]
diff --git a/crates/notedeck_dave/src/ui/mod.rs b/crates/notedeck_dave/src/ui/mod.rs
@@ -34,11 +34,59 @@ use crate::agent_status::AgentStatus;
use crate::config::{AiMode, DaveSettings, ModelConfig};
use crate::focus_queue::FocusQueue;
use crate::messages::PermissionResponse;
-use crate::session::{PermissionMessageState, SessionId, SessionManager};
+use crate::session::{ChatSession, PermissionMessageState, SessionId, SessionManager};
use crate::session_discovery::discover_sessions;
use crate::update;
use crate::DaveOverlay;
+/// Build a DaveUi from a session, wiring up all the common builder fields.
+fn build_dave_ui<'a>(
+ session: &'a mut ChatSession,
+ model_config: &ModelConfig,
+ is_interrupt_pending: bool,
+ auto_steal_focus: bool,
+) -> DaveUi<'a> {
+ let is_working = session.status() == AgentStatus::Working;
+ let has_pending_permission = session.has_pending_permissions();
+ let plan_mode_active = session.is_plan_mode();
+ let is_remote = session.is_remote();
+
+ let mut ui_builder = DaveUi::new(
+ model_config.trial,
+ &session.chat,
+ &mut session.input,
+ &mut session.focus_requested,
+ session.ai_mode,
+ )
+ .is_working(is_working)
+ .interrupt_pending(is_interrupt_pending)
+ .has_pending_permission(has_pending_permission)
+ .plan_mode_active(plan_mode_active)
+ .auto_steal_focus(auto_steal_focus)
+ .is_remote(is_remote);
+
+ if let Some(agentic) = &mut session.agentic {
+ ui_builder = ui_builder
+ .permission_message_state(agentic.permission_message_state)
+ .question_answers(&mut agentic.question_answers)
+ .question_index(&mut agentic.question_index)
+ .is_compacting(agentic.is_compacting)
+ .git_status(&mut agentic.git_status);
+ }
+
+ ui_builder
+}
+
+/// Set tentative permission state on the active session's agentic data.
+fn set_tentative_state(session_manager: &mut SessionManager, state: PermissionMessageState) {
+ if let Some(session) = session_manager.get_active_mut() {
+ if let Some(agentic) = &mut session.agentic {
+ agentic.permission_message_state = state;
+ }
+ session.focus_requested = true;
+ }
+}
+
/// UI result from overlay rendering
pub enum OverlayResult {
/// No action taken
@@ -213,36 +261,14 @@ pub fn scene_ui(
ui.heading(&session.title);
ui.separator();
- let is_working = session.status() == AgentStatus::Working;
- let has_pending_permission = session.has_pending_permissions();
- let plan_mode_active = session.is_plan_mode();
- let is_remote = session.is_remote();
-
- let mut ui_builder = DaveUi::new(
- model_config.trial,
- &session.chat,
- &mut session.input,
- &mut session.focus_requested,
- session.ai_mode,
+ let response = build_dave_ui(
+ session,
+ model_config,
+ is_interrupt_pending,
+ auto_steal_focus,
)
.compact(true)
- .is_working(is_working)
- .interrupt_pending(is_interrupt_pending)
- .has_pending_permission(has_pending_permission)
- .plan_mode_active(plan_mode_active)
- .auto_steal_focus(auto_steal_focus)
- .is_remote(is_remote);
-
- if let Some(agentic) = &mut session.agentic {
- ui_builder = ui_builder
- .permission_message_state(agentic.permission_message_state)
- .question_answers(&mut agentic.question_answers)
- .question_index(&mut agentic.question_index)
- .is_compacting(agentic.is_compacting)
- .git_status(&mut agentic.git_status);
- }
-
- let response = ui_builder.ui(app_ctx, ui);
+ .ui(app_ctx, ui);
if response.action.is_some() {
dave_response = response;
}
@@ -339,35 +365,8 @@ pub fn desktop_ui(
let chat_response = ui
.allocate_new_ui(egui::UiBuilder::new().max_rect(chat_rect), |ui| {
if let Some(session) = session_manager.get_active_mut() {
- let is_working = session.status() == AgentStatus::Working;
- let has_pending_permission = session.has_pending_permissions();
- let plan_mode_active = session.is_plan_mode();
- let is_remote = session.is_remote();
-
- let mut ui_builder = DaveUi::new(
- model_config.trial,
- &session.chat,
- &mut session.input,
- &mut session.focus_requested,
- session.ai_mode,
- )
- .is_working(is_working)
- .interrupt_pending(is_interrupt_pending)
- .has_pending_permission(has_pending_permission)
- .plan_mode_active(plan_mode_active)
- .auto_steal_focus(auto_steal_focus)
- .is_remote(is_remote);
-
- if let Some(agentic) = &mut session.agentic {
- ui_builder = ui_builder
- .permission_message_state(agentic.permission_message_state)
- .question_answers(&mut agentic.question_answers)
- .question_index(&mut agentic.question_index)
- .is_compacting(agentic.is_compacting)
- .git_status(&mut agentic.git_status);
- }
-
- ui_builder.ui(app_ctx, ui)
+ build_dave_ui(session, model_config, is_interrupt_pending, auto_steal_focus)
+ .ui(app_ctx, ui)
} else {
DaveResponse::default()
}
@@ -401,35 +400,10 @@ pub fn narrow_ui(
.inner;
(DaveResponse::default(), session_action)
} else if let Some(session) = session_manager.get_active_mut() {
- let is_working = session.status() == AgentStatus::Working;
- let has_pending_permission = session.has_pending_permissions();
- let plan_mode_active = session.is_plan_mode();
- let is_remote = session.is_remote();
-
- let mut ui_builder = DaveUi::new(
- model_config.trial,
- &session.chat,
- &mut session.input,
- &mut session.focus_requested,
- session.ai_mode,
- )
- .is_working(is_working)
- .interrupt_pending(is_interrupt_pending)
- .has_pending_permission(has_pending_permission)
- .plan_mode_active(plan_mode_active)
- .auto_steal_focus(auto_steal_focus)
- .is_remote(is_remote);
-
- if let Some(agentic) = &mut session.agentic {
- ui_builder = ui_builder
- .permission_message_state(agentic.permission_message_state)
- .question_answers(&mut agentic.question_answers)
- .question_index(&mut agentic.question_index)
- .is_compacting(agentic.is_compacting)
- .git_status(&mut agentic.git_status);
- }
-
- (ui_builder.ui(app_ctx, ui), None)
+ let response =
+ build_dave_ui(session, model_config, is_interrupt_pending, auto_steal_focus)
+ .ui(app_ctx, ui);
+ (response, None)
} else {
(DaveResponse::default(), None)
}
@@ -519,21 +493,11 @@ pub fn handle_key_action(
KeyActionResult::None
}
KeyAction::TentativeAccept => {
- if let Some(session) = session_manager.get_active_mut() {
- if let Some(agentic) = &mut session.agentic {
- agentic.permission_message_state = PermissionMessageState::TentativeAccept;
- }
- session.focus_requested = true;
- }
+ set_tentative_state(session_manager, PermissionMessageState::TentativeAccept);
KeyActionResult::None
}
KeyAction::TentativeDeny => {
- if let Some(session) = session_manager.get_active_mut() {
- if let Some(agentic) = &mut session.agentic {
- agentic.permission_message_state = PermissionMessageState::TentativeDeny;
- }
- session.focus_requested = true;
- }
+ set_tentative_state(session_manager, PermissionMessageState::TentativeDeny);
KeyActionResult::None
}
KeyAction::CancelTentative => {
@@ -769,21 +733,11 @@ pub fn handle_ui_action(
UiActionResult::Handled
}
DaveAction::TentativeAccept => {
- if let Some(session) = session_manager.get_active_mut() {
- if let Some(agentic) = &mut session.agentic {
- agentic.permission_message_state = PermissionMessageState::TentativeAccept;
- }
- session.focus_requested = true;
- }
+ set_tentative_state(session_manager, PermissionMessageState::TentativeAccept);
UiActionResult::Handled
}
DaveAction::TentativeDeny => {
- if let Some(session) = session_manager.get_active_mut() {
- if let Some(agentic) = &mut session.agentic {
- agentic.permission_message_state = PermissionMessageState::TentativeDeny;
- }
- session.focus_requested = true;
- }
+ set_tentative_state(session_manager, PermissionMessageState::TentativeDeny);
UiActionResult::Handled
}
DaveAction::QuestionResponse {
diff --git a/crates/notedeck_dave/src/update.rs b/crates/notedeck_dave/src/update.rs
@@ -404,6 +404,32 @@ pub fn handle_question_response(
// Agent Navigation
// =============================================================================
+/// Switch to a session and optionally focus it in the scene.
+///
+/// Handles the common pattern of: switch_to → scene.select → scene.focus_on → focus_requested.
+/// Used by navigation, focus queue, and auto-steal-focus operations.
+pub fn switch_and_focus_session(
+ session_manager: &mut SessionManager,
+ scene: &mut AgentScene,
+ show_scene: bool,
+ id: SessionId,
+) {
+ session_manager.switch_to(id);
+ if show_scene {
+ scene.select(id);
+ if let Some(session) = session_manager.get(id) {
+ if let Some(agentic) = &session.agentic {
+ scene.focus_on(agentic.scene_position);
+ }
+ }
+ }
+ if let Some(session) = session_manager.get_mut(id) {
+ if !session.has_pending_permissions() {
+ session.focus_requested = true;
+ }
+ }
+}
+
/// Switch to agent by index in the ordered list (0-indexed).
pub fn switch_to_agent_by_index(
session_manager: &mut SessionManager,
@@ -413,23 +439,16 @@ pub fn switch_to_agent_by_index(
) {
let ids = session_manager.session_ids();
if let Some(&id) = ids.get(index) {
- session_manager.switch_to(id);
- if show_scene {
- scene.select(id);
- }
- if let Some(session) = session_manager.get_mut(id) {
- if !session.has_pending_permissions() {
- session.focus_requested = true;
- }
- }
+ switch_and_focus_session(session_manager, scene, show_scene, id);
}
}
-/// Cycle to the next agent.
-pub fn cycle_next_agent(
+/// Cycle agents using a direction function that computes the next index.
+fn cycle_agent(
session_manager: &mut SessionManager,
scene: &mut AgentScene,
show_scene: bool,
+ index_fn: impl FnOnce(usize, usize) -> usize,
) {
let ids = session_manager.session_ids();
if ids.is_empty() {
@@ -439,50 +458,32 @@ pub fn cycle_next_agent(
.active_id()
.and_then(|active| ids.iter().position(|&id| id == active))
.unwrap_or(0);
- let next_idx = (current_idx + 1) % ids.len();
+ let next_idx = index_fn(current_idx, ids.len());
if let Some(&id) = ids.get(next_idx) {
- session_manager.switch_to(id);
- if show_scene {
- scene.select(id);
- }
- if let Some(session) = session_manager.get_mut(id) {
- if !session.has_pending_permissions() {
- session.focus_requested = true;
- }
- }
+ switch_and_focus_session(session_manager, scene, show_scene, id);
}
}
+/// Cycle to the next agent.
+pub fn cycle_next_agent(
+ session_manager: &mut SessionManager,
+ scene: &mut AgentScene,
+ show_scene: bool,
+) {
+ cycle_agent(session_manager, scene, show_scene, |idx, len| {
+ (idx + 1) % len
+ });
+}
+
/// Cycle to the previous agent.
pub fn cycle_prev_agent(
session_manager: &mut SessionManager,
scene: &mut AgentScene,
show_scene: bool,
) {
- let ids = session_manager.session_ids();
- if ids.is_empty() {
- return;
- }
- let current_idx = session_manager
- .active_id()
- .and_then(|active| ids.iter().position(|&id| id == active))
- .unwrap_or(0);
- let prev_idx = if current_idx == 0 {
- ids.len() - 1
- } else {
- current_idx - 1
- };
- if let Some(&id) = ids.get(prev_idx) {
- session_manager.switch_to(id);
- if show_scene {
- scene.select(id);
- }
- if let Some(session) = session_manager.get_mut(id) {
- if !session.has_pending_permissions() {
- session.focus_requested = true;
- }
- }
- }
+ cycle_agent(session_manager, scene, show_scene, |idx, len| {
+ if idx == 0 { len - 1 } else { idx - 1 }
+ });
}
// =============================================================================
@@ -497,20 +498,7 @@ pub fn focus_queue_next(
show_scene: bool,
) {
if let Some(session_id) = focus_queue.next() {
- session_manager.switch_to(session_id);
- if show_scene {
- scene.select(session_id);
- if let Some(session) = session_manager.get(session_id) {
- if let Some(agentic) = &session.agentic {
- scene.focus_on(agentic.scene_position);
- }
- }
- }
- if let Some(session) = session_manager.get_mut(session_id) {
- if !session.has_pending_permissions() {
- session.focus_requested = true;
- }
- }
+ switch_and_focus_session(session_manager, scene, show_scene, session_id);
}
}
@@ -522,20 +510,7 @@ pub fn focus_queue_prev(
show_scene: bool,
) {
if let Some(session_id) = focus_queue.prev() {
- session_manager.switch_to(session_id);
- if show_scene {
- scene.select(session_id);
- if let Some(session) = session_manager.get(session_id) {
- if let Some(agentic) = &session.agentic {
- scene.focus_on(agentic.scene_position);
- }
- }
- }
- if let Some(session) = session_manager.get_mut(session_id) {
- if !session.has_pending_permissions() {
- session.focus_requested = true;
- }
- }
+ switch_and_focus_session(session_manager, scene, show_scene, session_id);
}
}
@@ -566,15 +541,7 @@ pub fn toggle_auto_steal(
} else {
// Disabling: switch back to home session if set
if let Some(home_id) = home_session.take() {
- session_manager.switch_to(home_id);
- if show_scene {
- scene.select(home_id);
- if let Some(session) = session_manager.get(home_id) {
- if let Some(agentic) = &session.agentic {
- scene.focus_on(agentic.scene_position);
- }
- }
- }
+ switch_and_focus_session(session_manager, scene, show_scene, home_id);
tracing::debug!("Auto-steal focus disabled, returned to home session");
}
}
@@ -622,15 +589,7 @@ pub fn process_auto_steal_focus(
if let Some(idx) = focus_queue.first_needs_input_index() {
focus_queue.set_cursor(idx);
if let Some(entry) = focus_queue.current() {
- session_manager.switch_to(entry.session_id);
- if show_scene {
- scene.select(entry.session_id);
- if let Some(session) = session_manager.get(entry.session_id) {
- if let Some(agentic) = &session.agentic {
- scene.focus_on(agentic.scene_position);
- }
- }
- }
+ switch_and_focus_session(session_manager, scene, show_scene, entry.session_id);
tracing::debug!("Auto-steal: switched to session {:?}", entry.session_id);
return true;
}
@@ -653,15 +612,7 @@ pub fn process_auto_steal_focus(
if let Some(idx) = focus_queue.first_done_index() {
focus_queue.set_cursor(idx);
if let Some(entry) = focus_queue.current() {
- session_manager.switch_to(entry.session_id);
- if show_scene {
- scene.select(entry.session_id);
- if let Some(session) = session_manager.get(entry.session_id) {
- if let Some(agentic) = &session.agentic {
- scene.focus_on(agentic.scene_position);
- }
- }
- }
+ switch_and_focus_session(session_manager, scene, show_scene, entry.session_id);
tracing::debug!("Auto-steal: switched to Done session {:?}", entry.session_id);
return true;
}
@@ -669,15 +620,7 @@ pub fn process_auto_steal_focus(
}
} else if let Some(home_id) = home_session.take() {
// No more NeedsInput or Done items - return to saved session
- session_manager.switch_to(home_id);
- if show_scene {
- scene.select(home_id);
- if let Some(session) = session_manager.get(home_id) {
- if let Some(agentic) = &session.agentic {
- scene.focus_on(agentic.scene_position);
- }
- }
- }
+ switch_and_focus_session(session_manager, scene, show_scene, home_id);
tracing::debug!("Auto-steal: returned to home session {:?}", home_id);
}