commit 1bda26c904b338bb3fbaeb211553590ec6a41345
parent d944ad79196dfd53ef453ace3b598511fa1728d6
Author: William Casarin <jb55@jb55.com>
Date: Fri, 20 Feb 2026 13:06:06 -0800
Merge remote-tracking branches 'monad/columns', 'monad/dave' and 'monad/dave-dev'
Diffstat:
13 files changed, 150 insertions(+), 220 deletions(-)
diff --git a/crates/notedeck/src/lib.rs b/crates/notedeck/src/lib.rs
@@ -36,7 +36,7 @@ mod style;
pub mod theme;
mod time;
mod timecache;
-mod timed_serializer;
+pub mod timed_serializer;
pub mod ui;
mod unknowns;
mod urls;
diff --git a/crates/notedeck_chrome/src/chrome.rs b/crates/notedeck_chrome/src/chrome.rs
@@ -159,6 +159,7 @@ impl Chrome {
cc.wgpu_render_state.as_ref(),
context.ndb.clone(),
cc.egui_ctx.clone(),
+ context.path,
);
let mut chrome = Chrome::default();
diff --git a/crates/notedeck_columns/src/draft.rs b/crates/notedeck_columns/src/draft.rs
@@ -7,6 +7,7 @@ use crate::{
ui::{note::PostType, search::FocusState},
Error,
};
+use notedeck_ui::ProfileSearchResult;
use std::collections::HashMap;
#[derive(Default)]
@@ -24,6 +25,7 @@ pub struct MentionHint {
pub index: usize,
pub pos: egui::Pos2,
pub text: String,
+ pub results: Vec<ProfileSearchResult>,
}
#[derive(Default)]
diff --git a/crates/notedeck_columns/src/ui/add_column.rs b/crates/notedeck_columns/src/ui/add_column.rs
@@ -27,7 +27,7 @@ use tokenator::{ParseError, TokenParser, TokenSerializable, TokenWriter};
use crate::ui::widgets::styled_button;
use notedeck_ui::{
anim::AnimationHelper, padding, profile_row, search_input_box, search_profiles,
- ContactsListView, ProfilePreview,
+ ContactsListView,
};
pub enum AddColumnResponse {
@@ -241,10 +241,7 @@ impl<'a> AddColumnView<'a> {
}
fn external_notification_ui(&mut self, ui: &mut Ui) -> Option<AddColumnResponse> {
- let id = ui.id().with("external_notif");
- self.external_ui(ui, id, |pubkey| {
- AddColumnOption::Notification(PubkeySource::Explicit(pubkey))
- })
+ self.external_search_ui(ui, "external_notif", notification_column_response)
}
fn algo_last_per_pk_ui(
@@ -308,7 +305,16 @@ impl<'a> AddColumnView<'a> {
}
fn external_individual_ui(&mut self, ui: &mut Ui) -> Option<AddColumnResponse> {
- let id = ui.id().with("external_individual");
+ self.external_search_ui(ui, "external_individual", individual_column_response)
+ }
+
+ fn external_search_ui(
+ &mut self,
+ ui: &mut Ui,
+ id_salt: &str,
+ to_response: fn(Pubkey, &UserAccount) -> AddColumnResponse,
+ ) -> Option<AddColumnResponse> {
+ let id = ui.id().with(id_salt);
ui.add_space(8.0);
let hint = tr!(
@@ -337,6 +343,7 @@ impl<'a> AddColumnView<'a> {
self.jobs,
self.i18n,
self.cur_account,
+ to_response,
)
} else if query.is_empty() {
self.key_state_map.remove(&id);
@@ -348,6 +355,7 @@ impl<'a> AddColumnView<'a> {
self.img_cache,
self.i18n,
self.cur_account,
+ to_response,
)
} else {
self.key_state_map.remove(&id);
@@ -360,79 +368,11 @@ impl<'a> AddColumnView<'a> {
self.jobs,
self.i18n,
self.cur_account,
+ to_response,
)
}
}
- fn external_ui(
- &mut self,
- ui: &mut Ui,
- id: egui::Id,
- to_option: fn(Pubkey) -> AddColumnOption,
- ) -> Option<AddColumnResponse> {
- padding(16.0, ui, |ui| {
- let key_state = self.key_state_map.entry(id).or_default();
-
- let text_edit = key_state.get_acquire_textedit(|text| {
- egui::TextEdit::singleline(text)
- .hint_text(
- RichText::new(tr!(
- self.i18n,
- "Enter the user's key (npub, hex, nip05) here...",
- "Hint text to prompt entering the user's public key."
- ))
- .text_style(NotedeckTextStyle::Body.text_style()),
- )
- .vertical_align(Align::Center)
- .desired_width(f32::INFINITY)
- .min_size(Vec2::new(0.0, 40.0))
- .margin(Margin::same(12))
- });
-
- ui.add(text_edit);
-
- key_state.handle_input_change_after_acquire();
- key_state.loading_and_error_ui(ui, self.i18n);
-
- if key_state.get_login_keypair().is_none()
- && ui.add(find_user_button(self.i18n)).clicked()
- {
- key_state.apply_acquire();
- }
-
- let resp = if let Some(keypair) = key_state.get_login_keypair() {
- {
- let txn = Transaction::new(self.ndb).expect("txn");
- if let Ok(profile) =
- self.ndb.get_profile_by_pubkey(&txn, keypair.pubkey.bytes())
- {
- egui::Frame::window(ui.style())
- .outer_margin(Margin {
- left: 4,
- right: 4,
- top: 12,
- bottom: 32,
- })
- .show(ui, |ui| {
- ProfilePreview::new(&profile, self.img_cache, self.jobs).ui(ui);
- });
- }
- }
-
- ui.add(add_column_button(self.i18n))
- .clicked()
- .then(|| to_option(keypair.pubkey).take_as_response(self.cur_account))
- } else {
- None
- };
- if resp.is_some() {
- self.key_state_map.remove(&id);
- };
- resp
- })
- .inner
- }
-
fn column_option_ui(&mut self, ui: &mut Ui, data: ColumnOptionData) -> egui::Response {
let icon_padding = 8.0;
let min_icon_width = 32.0;
@@ -685,12 +625,6 @@ impl<'a> AddColumnView<'a> {
}
}
-fn find_user_button(i18n: &mut Localization) -> impl Widget {
- let label = tr!(i18n, "Find User", "Label for find user button");
- let color = notedeck_ui::colors::PINK;
- move |ui: &mut egui::Ui| styled_button(label.as_str(), color).ui(ui)
-}
-
fn add_column_button(i18n: &mut Localization) -> impl Widget {
let label = tr!(i18n, "Add", "Label for add column button");
let color = notedeck_ui::colors::PINK;
@@ -701,6 +635,10 @@ fn individual_column_response(pubkey: Pubkey, cur_account: &UserAccount) -> AddC
AddColumnOption::Individual(PubkeySource::Explicit(pubkey)).take_as_response(cur_account)
}
+fn notification_column_response(pubkey: Pubkey, cur_account: &UserAccount) -> AddColumnResponse {
+ AddColumnOption::Notification(PubkeySource::Explicit(pubkey)).take_as_response(cur_account)
+}
+
#[allow(clippy::too_many_arguments)]
fn nip05_profile_ui(
ui: &mut Ui,
@@ -712,6 +650,7 @@ fn nip05_profile_ui(
jobs: &MediaJobSender,
i18n: &mut Localization,
cur_account: &UserAccount,
+ to_response: fn(Pubkey, &UserAccount) -> AddColumnResponse,
) -> Option<AddColumnResponse> {
let key_state = key_state_map.entry(id).or_default();
@@ -730,7 +669,7 @@ fn nip05_profile_ui(
let profile = ndb.get_profile_by_pubkey(&txn, keypair.pubkey.bytes()).ok();
profile_row(ui, profile.as_ref(), false, img_cache, jobs, i18n)
- .then(|| individual_column_response(keypair.pubkey, cur_account))
+ .then(|| to_response(keypair.pubkey, cur_account))
} else {
None
};
@@ -750,6 +689,7 @@ fn contacts_list_column_ui(
img_cache: &mut Images,
i18n: &mut Localization,
cur_account: &UserAccount,
+ to_response: fn(Pubkey, &UserAccount) -> AddColumnResponse,
) -> Option<AddColumnResponse> {
let ContactState::Received {
contacts: contact_set,
@@ -763,9 +703,7 @@ fn contacts_list_column_ui(
let resp = ContactsListView::new(contact_set, jobs, ndb, img_cache, &txn, i18n).ui(ui);
resp.output.map(|a| match a {
- notedeck_ui::ContactsListAction::Select(pubkey) => {
- individual_column_response(pubkey, cur_account)
- }
+ notedeck_ui::ContactsListAction::Select(pubkey) => to_response(pubkey, cur_account),
})
}
@@ -779,6 +717,7 @@ fn profile_search_column_ui(
jobs: &MediaJobSender,
i18n: &mut Localization,
cur_account: &UserAccount,
+ to_response: fn(Pubkey, &UserAccount) -> AddColumnResponse,
) -> Option<AddColumnResponse> {
let txn = Transaction::new(ndb).expect("txn");
let results = search_profiles(ndb, &txn, query, contacts, 128);
@@ -808,10 +747,7 @@ fn profile_search_column_ui(
jobs,
i18n,
) {
- action = Some(individual_column_response(
- Pubkey::new(result.pk),
- cur_account,
- ));
+ action = Some(to_response(Pubkey::new(result.pk), cur_account));
}
}
});
diff --git a/crates/notedeck_columns/src/ui/mentions_picker.rs b/crates/notedeck_columns/src/ui/mentions_picker.rs
@@ -1,14 +1,7 @@
-use egui::{vec2, FontId, Layout, Pos2, Rect, ScrollArea, UiBuilder, Vec2b};
-use nostrdb::{Ndb, ProfileRecord, Transaction};
-use notedeck::{
- fonts::get_font_size, name::get_display_name, profile::get_profile_url, DragResponse, Images,
- MediaJobSender, NotedeckTextStyle,
-};
-use notedeck_ui::{
- anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE},
- widgets::x_button,
- ProfilePic,
-};
+use egui::{vec2, Layout, ScrollArea, UiBuilder, Vec2b};
+use nostrdb::{Ndb, Transaction};
+use notedeck::{DragResponse, Images, Localization, MediaJobSender};
+use notedeck_ui::{profile_row, widgets::x_button, ProfileSearchResult};
use tracing::error;
/// Displays user profiles for the user to pick from.
@@ -17,8 +10,9 @@ pub struct MentionPickerView<'a> {
ndb: &'a Ndb,
txn: &'a Transaction,
img_cache: &'a mut Images,
- results: &'a Vec<&'a [u8; 32]>,
+ results: &'a [ProfileSearchResult],
jobs: &'a MediaJobSender,
+ i18n: &'a mut Localization,
}
pub enum MentionPickerResponse {
@@ -31,8 +25,9 @@ impl<'a> MentionPickerView<'a> {
img_cache: &'a mut Images,
ndb: &'a Ndb,
txn: &'a Transaction,
- results: &'a Vec<&'a [u8; 32]>,
+ results: &'a [ProfileSearchResult],
jobs: &'a MediaJobSender,
+ i18n: &'a mut Localization,
) -> Self {
Self {
ndb,
@@ -40,25 +35,30 @@ impl<'a> MentionPickerView<'a> {
img_cache,
results,
jobs,
+ i18n,
}
}
- fn show(&mut self, ui: &mut egui::Ui, width: f32) -> MentionPickerResponse {
+ fn show(&mut self, ui: &mut egui::Ui) -> MentionPickerResponse {
let mut selection = None;
ui.vertical(|ui| {
for (i, res) in self.results.iter().enumerate() {
- let profile = match self.ndb.get_profile_by_pubkey(self.txn, res) {
+ let profile = match self.ndb.get_profile_by_pubkey(self.txn, &res.pk) {
Ok(rec) => rec,
Err(e) => {
- error!("Error fetching profile for pubkey {:?}: {e}", res);
+ error!("Error fetching profile for pubkey {:?}: {e}", res.pk);
return;
}
};
- if ui
- .add(user_result(&profile, self.img_cache, self.jobs, i, width))
- .clicked()
- {
+ if profile_row(
+ ui,
+ Some(&profile),
+ res.is_contact,
+ self.img_cache,
+ self.jobs,
+ self.i18n,
+ ) {
selection = Some(i)
}
}
@@ -82,11 +82,10 @@ impl<'a> MentionPickerView<'a> {
egui::Frame::NONE
.fill(ui.visuals().panel_fill)
.show(ui, |ui| {
- let width = rect.width() - (2.0 * inner_margin_size);
-
ui.allocate_space(vec2(ui.available_width(), inner_margin_size));
let close_button_resp = {
let close_button_size = 16.0;
+ let width = rect.width() - (2.0 * inner_margin_size);
let (close_section_rect, _) = ui.allocate_exact_size(
vec2(width, close_button_size),
egui::Sense::hover(),
@@ -109,7 +108,7 @@ impl<'a> MentionPickerView<'a> {
let scroll_resp = ScrollArea::vertical()
.max_width(rect.width())
.auto_shrink(Vec2b::FALSE)
- .show(ui, |ui| Some(self.show(ui, width)));
+ .show(ui, |ui| Some(self.show(ui)));
ui.advance_cursor_after_rect(rect);
DragResponse::scroll(scroll_resp).map_output(|o| {
@@ -126,68 +125,3 @@ impl<'a> MentionPickerView<'a> {
area_resp.inner
}
}
-
-fn user_result<'a>(
- profile: &'a ProfileRecord<'_>,
- cache: &'a mut Images,
- jobs: &'a MediaJobSender,
- index: usize,
- width: f32,
-) -> impl egui::Widget + 'a {
- move |ui: &mut egui::Ui| -> egui::Response {
- let min_img_size = 48.0;
- let max_image = min_img_size * ICON_EXPANSION_MULTIPLE;
- let spacing = 8.0;
- let body_font_size = get_font_size(ui.ctx(), &NotedeckTextStyle::Body);
-
- let animation_rect = {
- let max_width = ui.available_width();
- let extra_width = (max_width - width) / 2.0;
- let left = ui.cursor().left();
- let (rect, _) =
- ui.allocate_exact_size(vec2(width + extra_width, max_image), egui::Sense::click());
-
- let (_, right) = rect.split_left_right_at_x(left + extra_width);
- right
- };
-
- let helper = AnimationHelper::new_from_rect(ui, ("user_result", index), animation_rect);
-
- let icon_rect = {
- let r = helper.get_animation_rect();
- let mut center = r.center();
- center.x = r.left() + (max_image / 2.0);
- let size = helper.scale_1d_pos(min_img_size);
- Rect::from_center_size(center, vec2(size, size))
- };
-
- let pfp_resp = ui.put(
- icon_rect,
- &mut ProfilePic::new(cache, jobs, get_profile_url(Some(profile)))
- .size(helper.scale_1d_pos(min_img_size)),
- );
-
- let name_font = FontId::new(
- helper.scale_1d_pos(body_font_size),
- NotedeckTextStyle::Body.font_family(),
- );
- let painter = ui.painter_at(helper.get_animation_rect());
- let name_galley = painter.layout(
- get_display_name(Some(profile)).name().to_owned(),
- name_font,
- ui.visuals().text_color(),
- width,
- );
-
- let galley_pos = {
- let right_top = pfp_resp.rect.right_top();
- let galley_pos_y = pfp_resp.rect.center().y - (name_galley.rect.height() / 2.0);
- Pos2::new(right_top.x + spacing, galley_pos_y)
- };
-
- painter.galley(galley_pos, name_galley, ui.visuals().text_color());
- ui.advance_cursor_after_rect(helper.get_animation_rect());
-
- pfp_resp.union(helper.take_animation_response())
- }
-}
diff --git a/crates/notedeck_columns/src/ui/note/post.rs b/crates/notedeck_columns/src/ui/note/post.rs
@@ -25,7 +25,7 @@ use notedeck_ui::{
app_images,
context_menu::{input_context, PasteBehavior},
note::render_note_preview,
- NoteOptions, ProfilePic,
+ search_profiles, NoteOptions, ProfilePic,
};
use tracing::error;
#[cfg(not(target_os = "android"))]
@@ -278,20 +278,39 @@ impl<'a, 'd> PostView<'a, 'd> {
let mention_str = self.draft.buffer.get_mention_string(&mention);
if !mention_str.is_empty() {
- if let Some(mention_hint) = &mut self.draft.cur_mention_hint {
- if mention_hint.index != mention.index {
- mention_hint.index = mention.index;
- mention_hint.pos =
+ let text_changed;
+ if let Some(hint) = &mut self.draft.cur_mention_hint {
+ text_changed = hint.text != mention_str;
+ if hint.index != mention.index {
+ hint.index = mention.index;
+ hint.pos =
calculate_mention_hints_pos(textedit_output, mention.info.start_index);
}
- mention_hint.text = mention_str.to_owned();
+ if text_changed {
+ hint.text = mention_str.to_owned();
+ }
} else {
+ text_changed = true;
self.draft.cur_mention_hint = Some(MentionHint {
index: mention.index,
text: mention_str.to_owned(),
pos: calculate_mention_hints_pos(textedit_output, mention.info.start_index),
+ results: Vec::new(),
});
}
+
+ if text_changed {
+ let contacts = self
+ .note_context
+ .accounts
+ .get_selected_account()
+ .data
+ .contacts
+ .get_state();
+ let hint = self.draft.cur_mention_hint.as_mut().unwrap();
+ hint.results =
+ search_profiles(self.note_context.ndb, txn, &hint.text, contacts, 128);
+ }
}
let hint_rect = {
@@ -304,18 +323,15 @@ impl<'a, 'd> PostView<'a, 'd> {
hint_rect
};
- let res = self
- .note_context
- .ndb
- .search_profile(txn, mention_str, 10)
- .ok()?;
+ let hint = self.draft.cur_mention_hint.as_ref().unwrap();
let resp = MentionPickerView::new(
self.note_context.img_cache,
self.note_context.ndb,
txn,
- &res,
+ &hint.results,
self.note_context.jobs,
+ self.note_context.i18n,
)
.show_in_rect(hint_rect, ui);
@@ -328,14 +344,15 @@ impl<'a, 'd> PostView<'a, 'd> {
match out {
ui::mentions_picker::MentionPickerResponse::SelectResult(selection) => {
if let Some(hint_index) = selection {
- if let Some(pk) = res.get(hint_index) {
- let record = self.note_context.ndb.get_profile_by_pubkey(txn, pk);
+ let hint = self.draft.cur_mention_hint.as_ref().unwrap();
+ if let Some(result) = hint.results.get(hint_index) {
+ let record = self.note_context.ndb.get_profile_by_pubkey(txn, &result.pk);
if let Some(made_selection) =
self.draft.buffer.select_mention_and_replace_name(
mention.index,
get_display_name(record.ok().as_ref()).name(),
- Pubkey::new(**pk),
+ Pubkey::new(result.pk),
)
{
selection_made = Some(made_selection);
diff --git a/crates/notedeck_columns/src/ui/search/mod.rs b/crates/notedeck_columns/src/ui/search/mod.rs
@@ -164,21 +164,27 @@ impl<'a, 'd> SearchView<'a, 'd> {
return;
};
- 's: {
- let Ok(results) = self
+ if self.query.last_mention_query != mention_name {
+ let contacts = self
.note_context
- .ndb
- .search_profile(self.txn, mention_name, 10)
- else {
- break 's;
- };
+ .accounts
+ .get_selected_account()
+ .data
+ .contacts
+ .get_state();
+ self.query.mention_results =
+ search_profiles(self.note_context.ndb, self.txn, mention_name, contacts, 128);
+ self.query.last_mention_query = mention_name.to_owned();
+ }
+ 's: {
let search_res = MentionPickerView::new(
self.note_context.img_cache,
self.note_context.ndb,
self.txn,
- &results,
+ &self.query.mention_results,
self.note_context.jobs,
+ self.note_context.i18n,
)
.show_in_rect(ui.available_rect_before_wrap(), ui);
@@ -188,20 +194,20 @@ impl<'a, 'd> SearchView<'a, 'd> {
*search_action = match res {
MentionPickerResponse::SelectResult(Some(index)) => {
- let Some(pk_bytes) = results.get(index) else {
+ let Some(result) = self.query.mention_results.get(index) else {
break 's;
};
let username = self
.note_context
.ndb
- .get_profile_by_pubkey(self.txn, pk_bytes)
+ .get_profile_by_pubkey(self.txn, &result.pk)
.ok()
.and_then(|p| p.record().profile().and_then(|p| p.name()))
.unwrap_or(&self.query.string);
Some(SearchAction::NewSearch {
- search_type: SearchType::Profile(Pubkey::new(**pk_bytes)),
+ search_type: SearchType::Profile(Pubkey::new(result.pk)),
new_search_text: format!("@{username}"),
})
}
diff --git a/crates/notedeck_columns/src/ui/search/state.rs b/crates/notedeck_columns/src/ui/search/state.rs
@@ -1,5 +1,6 @@
use crate::timeline::TimelineTab;
use enostr::Pubkey;
+use notedeck_ui::ProfileSearchResult;
use super::SearchType;
@@ -60,6 +61,12 @@ pub struct SearchQueryState {
/// Recent search history (most recent first, max 10)
pub recent_searches: Vec<RecentSearchItem>,
+
+ /// Cached @mention search results
+ pub mention_results: Vec<ProfileSearchResult>,
+
+ /// The query string that produced `mention_results`
+ pub last_mention_query: String,
}
impl Default for SearchQueryState {
@@ -78,6 +85,8 @@ impl SearchQueryState {
selected_index: -1,
user_results: Vec::new(),
recent_searches: Vec::new(),
+ mention_results: Vec::new(),
+ last_mention_query: String::new(),
}
}
diff --git a/crates/notedeck_dave/src/config.rs b/crates/notedeck_dave/src/config.rs
@@ -1,5 +1,6 @@
use crate::backend::BackendType;
use async_openai::config::OpenAIConfig;
+use serde::{Deserialize, Serialize};
/// AI interaction mode - determines UI complexity and feature set
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -11,7 +12,7 @@ pub enum AiMode {
}
/// Available AI providers for Dave
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum AiProvider {
#[default]
OpenAI,
@@ -77,7 +78,7 @@ impl AiProvider {
}
/// User-configurable settings for Dave AI
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DaveSettings {
pub provider: AiProvider,
pub model: String,
diff --git a/crates/notedeck_dave/src/lib.rs b/crates/notedeck_dave/src/lib.rs
@@ -31,7 +31,10 @@ use egui_wgpu::RenderState;
use enostr::KeypairUnowned;
use focus_queue::FocusQueue;
use nostrdb::{Subscription, Transaction};
-use notedeck::{try_process_events_core, ui::is_narrow, AppAction, AppContext, AppResponse};
+use notedeck::{
+ timed_serializer::TimedSerializer, try_process_events_core, ui::is_narrow, AppAction,
+ AppContext, AppResponse, DataPath, DataPathType,
+};
use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
use std::string::ToString;
@@ -149,6 +152,8 @@ pub struct Dave {
hostname: String,
/// PNS relay URL (configurable via DAVE_RELAY env or settings UI).
pns_relay_url: String,
+ /// Persists DaveSettings to dave_settings.json
+ settings_serializer: TimedSerializer<DaveSettings>,
}
use update::PermissionPublish;
@@ -299,9 +304,25 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
))
}
- pub fn new(render_state: Option<&RenderState>, ndb: nostrdb::Ndb, ctx: egui::Context) -> Self {
- let model_config = ModelConfig::default();
- //let model_config = ModelConfig::ollama();
+ pub fn new(
+ render_state: Option<&RenderState>,
+ ndb: nostrdb::Ndb,
+ ctx: egui::Context,
+ path: &DataPath,
+ ) -> Self {
+ let settings_serializer =
+ TimedSerializer::new(path, DataPathType::Setting, "dave_settings.json".to_owned());
+
+ // Load saved settings, falling back to env-var-based defaults
+ let (model_config, settings) = if let Some(saved_settings) = settings_serializer.get_item()
+ {
+ let config = ModelConfig::from_settings(&saved_settings);
+ (config, saved_settings)
+ } else {
+ let config = ModelConfig::default();
+ let settings = DaveSettings::from_model_config(&config);
+ (config, settings)
+ };
// Determine AI mode from backend type
let ai_mode = model_config.ai_mode();
@@ -329,7 +350,6 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
tools.insert(tool.name().to_string(), tool);
}
- let settings = DaveSettings::from_model_config(&model_config);
let pns_relay_url = model_config
.pns_relay
.clone()
@@ -388,6 +408,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
pending_summaries: Vec::new(),
hostname,
pns_relay_url,
+ settings_serializer,
}
}
@@ -396,13 +417,15 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
&self.settings
}
- /// Apply new settings. Note: Provider changes require app restart to take effect.
+ /// Apply new settings and persist to disk.
+ /// Note: Provider changes require app restart to take effect.
pub fn apply_settings(&mut self, settings: DaveSettings) {
self.model_config = ModelConfig::from_settings(&settings);
self.pns_relay_url = settings
.pns_relay
.clone()
.unwrap_or_else(|| DEFAULT_PNS_RELAY.to_string());
+ self.settings_serializer.try_save(settings.clone());
self.settings = settings;
}
@@ -927,7 +950,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
let (dave_response, view_action) = ui::scene_ui(
&mut self.session_manager,
&mut self.scene,
- &self.focus_queue,
+ &mut self.focus_queue,
&self.model_config,
is_interrupt_pending,
self.auto_steal_focus,
@@ -981,6 +1004,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
SessionListAction::NewSession => return DaveResponse::new(DaveAction::NewChat),
SessionListAction::SwitchTo(id) => {
self.session_manager.switch_to(id);
+ self.focus_queue.dequeue(id);
}
SessionListAction::Delete(id) => {
self.delete_session(id);
@@ -1013,6 +1037,7 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
}
SessionListAction::SwitchTo(id) => {
self.session_manager.switch_to(id);
+ self.focus_queue.dequeue(id);
self.show_session_list = false;
}
SessionListAction::Delete(id) => {
diff --git a/crates/notedeck_dave/src/ui/mod.rs b/crates/notedeck_dave/src/ui/mod.rs
@@ -208,7 +208,7 @@ pub enum SceneViewAction {
pub fn scene_ui(
session_manager: &mut SessionManager,
scene: &mut AgentScene,
- focus_queue: &FocusQueue,
+ focus_queue: &mut FocusQueue,
model_config: &ModelConfig,
is_interrupt_pending: bool,
auto_steal_focus: bool,
@@ -294,6 +294,7 @@ pub fn scene_ui(
SceneAction::SelectionChanged(ids) => {
if let Some(id) = ids.first() {
session_manager.switch_to(*id);
+ focus_queue.dequeue(*id);
}
}
SceneAction::SpawnAgent => {
diff --git a/crates/notedeck_dave/src/update.rs b/crates/notedeck_dave/src/update.rs
@@ -574,17 +574,14 @@ pub fn process_auto_steal_focus(
tracing::debug!("Auto-steal: saved home session {:?}", home_session);
}
- // Jump to first Done item and clear it from the queue
+ // Jump to first Done item (keep in queue so blue dot renders;
+ // cleared when user manually focuses the session)
if let Some(idx) = focus_queue.first_done_index() {
focus_queue.set_cursor(idx);
if let Some(entry) = focus_queue.current() {
let sid = entry.session_id;
switch_and_focus_session(session_manager, scene, show_scene, sid);
- focus_queue.dequeue(sid);
- tracing::debug!(
- "Auto-steal: switched to Done session {:?} and cleared indicator",
- sid
- );
+ tracing::debug!("Auto-steal: switched to Done session {:?}", sid);
return true;
}
}
diff --git a/crates/notedeck_ui/src/contacts_list.rs b/crates/notedeck_ui/src/contacts_list.rs
@@ -214,6 +214,7 @@ where
}
/// A profile search result.
+#[derive(Debug)]
pub struct ProfileSearchResult {
/// The public key bytes of the matched profile.
pub pk: [u8; 32],