Run chats independently
This commit is contained in:
361
src/app.rs
361
src/app.rs
@@ -66,6 +66,8 @@ pub(crate) struct App {
|
||||
/// Unsaved sessions, keyed by project. A draft only becomes a `sessions` row
|
||||
/// when its first chat turn is stored, so empty ones vanish on restart.
|
||||
drafts: HashMap<i32, String>,
|
||||
#[cfg(target_os = "macos")]
|
||||
background_chats: HashMap<i32, ChatSnapshot>,
|
||||
/// Session whose quick-actions menu is open.
|
||||
session_menu: Option<i32>,
|
||||
/// Session waiting for explicit confirmation before deletion.
|
||||
@@ -146,6 +148,58 @@ pub(crate) struct App {
|
||||
manual_compaction_queued: bool,
|
||||
#[cfg(target_os = "macos")]
|
||||
skip_compaction_once: bool,
|
||||
quit_confirmation: bool,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
struct ChatSnapshot {
|
||||
selected_project: Option<i32>,
|
||||
selected_session: Option<i32>,
|
||||
composer: String,
|
||||
queued_inputs: VecDeque<String>,
|
||||
conversation: Vec<ChatMessage>,
|
||||
chat_follow_tail: bool,
|
||||
active_turn: Option<generation::TurnSummary>,
|
||||
a2ui: crate::a2ui::Store,
|
||||
a2ui_history: Vec<crate::a2ui::Store>,
|
||||
a2ui_history_index: Option<usize>,
|
||||
a2ui_tabs: HashMap<(String, String), usize>,
|
||||
a2ui_modals: HashSet<(String, String)>,
|
||||
a2ui_editors: HashMap<(String, String, String), text_editor::Content>,
|
||||
a2ui_markdown: HashMap<(String, String, String), markdown::Content>,
|
||||
a2ui_choice_filters: HashMap<(String, String, String), String>,
|
||||
a2ui_images: HashMap<String, iced::widget::image::Handle>,
|
||||
a2ui_image_requests: HashSet<String>,
|
||||
a2ui_image_loading: bool,
|
||||
pending_a2ui_dismissal: Option<String>,
|
||||
a2ui_auto_switch_pending: bool,
|
||||
generating: bool,
|
||||
context_used: u32,
|
||||
context_limit: u32,
|
||||
tokens_per_second: Option<f32>,
|
||||
detail_tab: DetailTab,
|
||||
active_generation: Option<ActiveGeneration>,
|
||||
active_compaction: Option<generation::CompactionRequest>,
|
||||
active_tool_check: Option<generation::ToolResultCheck>,
|
||||
agent_tools: Option<(i32, Arc<Mutex<crate::agent::Tools>>)>,
|
||||
active_tools: Option<crate::agent::ActiveTools>,
|
||||
tool_cards: Vec<crate::agent::ToolCard>,
|
||||
pending_tool_approval: Option<(crate::agent::ApprovalPrompt, std::sync::mpsc::Sender<bool>)>,
|
||||
active_titling: Option<generation::TitleRequest>,
|
||||
error: Option<String>,
|
||||
activity: Option<String>,
|
||||
context_notice: Option<String>,
|
||||
stop_requested: bool,
|
||||
system_prompt_seen_at: u32,
|
||||
manual_compaction_queued: bool,
|
||||
skip_compaction_once: bool,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
impl ChatSnapshot {
|
||||
fn needs_poll(&self) -> bool {
|
||||
chat_needs_poll(self.generating, self.active_titling.is_some())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
@@ -229,10 +283,14 @@ pub(crate) enum Message {
|
||||
HelpOpened(window::Id),
|
||||
NewChat,
|
||||
ExportChat,
|
||||
ExportChatPicked(Option<PathBuf>),
|
||||
ExportChatPicked(Option<PathBuf>, String),
|
||||
ModelManagerOpened(window::Id),
|
||||
WindowOpened(window::Id),
|
||||
WindowCloseRequested(window::Id),
|
||||
WindowClosed(window::Id),
|
||||
RequestQuit,
|
||||
ConfirmQuit,
|
||||
CancelQuit,
|
||||
Escape(window::Id),
|
||||
DismissPanel,
|
||||
/// Tab and shift-tab: iced leaves the key to the application, so the fields
|
||||
@@ -286,7 +344,7 @@ pub(crate) enum Message {
|
||||
A2uiAction(String, String, Option<String>),
|
||||
A2uiSelectTab(String, String, usize),
|
||||
A2uiToggleModal(String, String),
|
||||
A2uiImageLoaded(String, Result<Vec<u8>, String>),
|
||||
A2uiImageLoaded(Option<i32>, String, Result<Vec<u8>, String>),
|
||||
A2uiPlayMedia(String, String, bool),
|
||||
RequestA2uiDismiss(String),
|
||||
ConfirmA2uiDismiss,
|
||||
@@ -398,6 +456,8 @@ impl App {
|
||||
selected_project: last_project,
|
||||
selected_session: None,
|
||||
drafts,
|
||||
#[cfg(target_os = "macos")]
|
||||
background_chats: HashMap::new(),
|
||||
session_menu: None,
|
||||
pending_session_delete: None,
|
||||
session_rename: None,
|
||||
@@ -480,6 +540,7 @@ impl App {
|
||||
manual_compaction_queued: false,
|
||||
#[cfg(target_os = "macos")]
|
||||
skip_compaction_once: false,
|
||||
quit_confirmation: false,
|
||||
}
|
||||
}
|
||||
Err(error) => Self::failed(
|
||||
@@ -527,6 +588,8 @@ impl App {
|
||||
selected_project: None,
|
||||
selected_session: None,
|
||||
drafts: HashMap::new(),
|
||||
#[cfg(target_os = "macos")]
|
||||
background_chats: HashMap::new(),
|
||||
session_menu: None,
|
||||
pending_session_delete: None,
|
||||
session_rename: None,
|
||||
@@ -601,9 +664,180 @@ impl App {
|
||||
manual_compaction_queued: false,
|
||||
#[cfg(target_os = "macos")]
|
||||
skip_compaction_once: false,
|
||||
quit_confirmation: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn take_chat_snapshot(&mut self) -> ChatSnapshot {
|
||||
ChatSnapshot {
|
||||
selected_project: self.selected_project.take(),
|
||||
selected_session: self.selected_session.take(),
|
||||
composer: std::mem::take(&mut self.composer),
|
||||
queued_inputs: std::mem::take(&mut self.queued_inputs),
|
||||
conversation: std::mem::take(&mut self.conversation),
|
||||
chat_follow_tail: std::mem::replace(&mut self.chat_follow_tail, true),
|
||||
active_turn: self.active_turn.take(),
|
||||
a2ui: std::mem::take(&mut self.a2ui),
|
||||
a2ui_history: std::mem::take(&mut self.a2ui_history),
|
||||
a2ui_history_index: self.a2ui_history_index.take(),
|
||||
a2ui_tabs: std::mem::take(&mut self.a2ui_tabs),
|
||||
a2ui_modals: std::mem::take(&mut self.a2ui_modals),
|
||||
a2ui_editors: std::mem::take(&mut self.a2ui_editors),
|
||||
a2ui_markdown: std::mem::take(&mut self.a2ui_markdown),
|
||||
a2ui_choice_filters: std::mem::take(&mut self.a2ui_choice_filters),
|
||||
a2ui_images: std::mem::take(&mut self.a2ui_images),
|
||||
a2ui_image_requests: std::mem::take(&mut self.a2ui_image_requests),
|
||||
a2ui_image_loading: std::mem::take(&mut self.a2ui_image_loading),
|
||||
pending_a2ui_dismissal: self.pending_a2ui_dismissal.take(),
|
||||
a2ui_auto_switch_pending: std::mem::take(&mut self.a2ui_auto_switch_pending),
|
||||
generating: std::mem::take(&mut self.generating),
|
||||
context_used: std::mem::take(&mut self.context_used),
|
||||
context_limit: std::mem::replace(
|
||||
&mut self.context_limit,
|
||||
self.config.generation.context_tokens.max(0) as u32,
|
||||
),
|
||||
tokens_per_second: self.tokens_per_second.take(),
|
||||
detail_tab: std::mem::take(&mut self.detail_tab),
|
||||
active_generation: self.active_generation.take(),
|
||||
active_compaction: self.active_compaction.take(),
|
||||
active_tool_check: self.active_tool_check.take(),
|
||||
agent_tools: self.agent_tools.take(),
|
||||
active_tools: self.active_tools.take(),
|
||||
tool_cards: std::mem::take(&mut self.tool_cards),
|
||||
pending_tool_approval: self.pending_tool_approval.take(),
|
||||
active_titling: self.active_titling.take(),
|
||||
error: self.error.take(),
|
||||
activity: self.activity.take(),
|
||||
context_notice: self.context_notice.take(),
|
||||
stop_requested: std::mem::take(&mut self.stop_requested),
|
||||
system_prompt_seen_at: std::mem::take(&mut self.system_prompt_seen_at),
|
||||
manual_compaction_queued: std::mem::take(&mut self.manual_compaction_queued),
|
||||
skip_compaction_once: std::mem::take(&mut self.skip_compaction_once),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn restore_chat_snapshot(&mut self, snapshot: ChatSnapshot) {
|
||||
self.selected_project = snapshot.selected_project;
|
||||
self.selected_session = snapshot.selected_session;
|
||||
self.composer = snapshot.composer;
|
||||
self.queued_inputs = snapshot.queued_inputs;
|
||||
self.conversation = snapshot.conversation;
|
||||
self.chat_follow_tail = snapshot.chat_follow_tail;
|
||||
self.active_turn = snapshot.active_turn;
|
||||
self.a2ui = snapshot.a2ui;
|
||||
self.a2ui_history = snapshot.a2ui_history;
|
||||
self.a2ui_history_index = snapshot.a2ui_history_index;
|
||||
self.a2ui_tabs = snapshot.a2ui_tabs;
|
||||
self.a2ui_modals = snapshot.a2ui_modals;
|
||||
self.a2ui_editors = snapshot.a2ui_editors;
|
||||
self.a2ui_markdown = snapshot.a2ui_markdown;
|
||||
self.a2ui_choice_filters = snapshot.a2ui_choice_filters;
|
||||
self.a2ui_images = snapshot.a2ui_images;
|
||||
self.a2ui_image_requests = snapshot.a2ui_image_requests;
|
||||
self.a2ui_image_loading = snapshot.a2ui_image_loading;
|
||||
self.pending_a2ui_dismissal = snapshot.pending_a2ui_dismissal;
|
||||
self.a2ui_auto_switch_pending = snapshot.a2ui_auto_switch_pending;
|
||||
self.generating = snapshot.generating;
|
||||
self.context_used = snapshot.context_used;
|
||||
self.context_limit = snapshot.context_limit;
|
||||
self.tokens_per_second = snapshot.tokens_per_second;
|
||||
self.detail_tab = snapshot.detail_tab;
|
||||
self.active_generation = snapshot.active_generation;
|
||||
self.active_compaction = snapshot.active_compaction;
|
||||
self.active_tool_check = snapshot.active_tool_check;
|
||||
self.agent_tools = snapshot.agent_tools;
|
||||
self.active_tools = snapshot.active_tools;
|
||||
self.tool_cards = snapshot.tool_cards;
|
||||
self.pending_tool_approval = snapshot.pending_tool_approval;
|
||||
self.active_titling = snapshot.active_titling;
|
||||
self.error = snapshot.error;
|
||||
self.activity = snapshot.activity;
|
||||
self.context_notice = snapshot.context_notice;
|
||||
self.stop_requested = snapshot.stop_requested;
|
||||
self.system_prompt_seen_at = snapshot.system_prompt_seen_at;
|
||||
self.manual_compaction_queued = snapshot.manual_compaction_queued;
|
||||
self.skip_compaction_once = snapshot.skip_compaction_once;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub(super) fn leave_current_chat(&mut self) {
|
||||
let snapshot = self.take_chat_snapshot();
|
||||
if let Some(session_id) = snapshot.selected_session
|
||||
&& (snapshot.needs_poll() || snapshot.a2ui_image_loading)
|
||||
{
|
||||
self.background_chats.insert(session_id, snapshot);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn restore_background_chat(&mut self, session_id: i32) -> bool {
|
||||
let Some(mut snapshot) = self.background_chats.remove(&session_id) else {
|
||||
return false;
|
||||
};
|
||||
snapshot.chat_follow_tail = true;
|
||||
self.restore_chat_snapshot(snapshot);
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn poll_background_chats(&mut self) {
|
||||
let session_ids = self
|
||||
.background_chats
|
||||
.iter()
|
||||
.filter_map(|(session_id, chat)| chat.needs_poll().then_some(*session_id))
|
||||
.collect::<Vec<_>>();
|
||||
for session_id in session_ids {
|
||||
let foreground = self.take_chat_snapshot();
|
||||
let Some(background) = self.background_chats.remove(&session_id) else {
|
||||
self.restore_chat_snapshot(foreground);
|
||||
continue;
|
||||
};
|
||||
self.restore_chat_snapshot(background);
|
||||
self.poll_titling();
|
||||
self.poll_generation();
|
||||
let background = self.take_chat_snapshot();
|
||||
self.background_chats.insert(session_id, background);
|
||||
self.restore_chat_snapshot(foreground);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub(super) fn session_is_active(&self, session_id: i32) -> bool {
|
||||
(self.selected_session == Some(session_id) && self.generating)
|
||||
|| self
|
||||
.background_chats
|
||||
.get(&session_id)
|
||||
.is_some_and(|chat| chat.generating)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub(super) fn session_is_active(&self, _session_id: i32) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn active_chat_count(&self) -> usize {
|
||||
#[cfg(target_os = "macos")]
|
||||
return active_chat_total(
|
||||
self.generating,
|
||||
self.background_chats.values().map(|chat| chat.generating),
|
||||
);
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
active_chat_total(self.generating, std::iter::empty())
|
||||
}
|
||||
|
||||
fn project_has_active_chat(&self, project_id: i32) -> bool {
|
||||
self.projects
|
||||
.iter()
|
||||
.find(|item| item.project.id == project_id)
|
||||
.is_some_and(|item| {
|
||||
item.sessions
|
||||
.iter()
|
||||
.any(|session| self.session_is_active(session.id))
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn update(&mut self, message: Message) -> Task<Message> {
|
||||
match message {
|
||||
Message::Noop => {
|
||||
@@ -645,6 +879,7 @@ impl App {
|
||||
return Task::none();
|
||||
};
|
||||
let file_name = format!("{}.md", export_file_stem(&title));
|
||||
let content = export_markdown(&title, &self.conversation);
|
||||
return Task::perform(
|
||||
async move {
|
||||
AsyncFileDialog::new()
|
||||
@@ -655,13 +890,12 @@ impl App {
|
||||
.await
|
||||
.map(|file| file.path().to_path_buf())
|
||||
},
|
||||
Message::ExportChatPicked,
|
||||
move |path| Message::ExportChatPicked(path, content),
|
||||
);
|
||||
}
|
||||
Message::ExportChatPicked(path) => {
|
||||
Message::ExportChatPicked(path, content) => {
|
||||
if let Some(path) = path
|
||||
&& let Some(title) = self.active_chat_title()
|
||||
&& let Err(error) = fs::write(&path, export_markdown(title, &self.conversation))
|
||||
&& let Err(error) = fs::write(&path, content)
|
||||
{
|
||||
self.error = Some(format!("Could not export the chat: {error}"));
|
||||
}
|
||||
@@ -688,10 +922,13 @@ impl App {
|
||||
return focus_composer();
|
||||
}
|
||||
}
|
||||
Message::WindowClosed(id) => {
|
||||
Message::WindowCloseRequested(id) => {
|
||||
if id == self.main_window {
|
||||
return iced::exit();
|
||||
return self.update(Message::RequestQuit);
|
||||
}
|
||||
return window::close(id);
|
||||
}
|
||||
Message::WindowClosed(id) => {
|
||||
if self.model_manager_window == Some(id) {
|
||||
self.model_manager_window = None;
|
||||
self.pending_model_delete = None;
|
||||
@@ -704,6 +941,14 @@ impl App {
|
||||
self.help_window = None;
|
||||
}
|
||||
}
|
||||
Message::RequestQuit => {
|
||||
if self.active_chat_count() == 0 {
|
||||
return iced::exit();
|
||||
}
|
||||
self.quit_confirmation = true;
|
||||
}
|
||||
Message::ConfirmQuit => return iced::exit(),
|
||||
Message::CancelQuit => self.quit_confirmation = false,
|
||||
Message::Escape(id) => {
|
||||
if self.preferences_window == Some(id) {
|
||||
return self.update(Message::ClosePreferences);
|
||||
@@ -713,7 +958,9 @@ impl App {
|
||||
}
|
||||
}
|
||||
Message::DismissPanel => {
|
||||
if self.pending_session_delete.is_some() {
|
||||
if self.quit_confirmation {
|
||||
self.quit_confirmation = false;
|
||||
} else if self.pending_session_delete.is_some() {
|
||||
self.pending_session_delete = None;
|
||||
} else if self.pending_a2ui_dismissal.is_some() {
|
||||
self.pending_a2ui_dismissal = None;
|
||||
@@ -1091,7 +1338,29 @@ impl App {
|
||||
self.a2ui_modals.insert(key);
|
||||
}
|
||||
}
|
||||
Message::A2uiImageLoaded(url, result) => {
|
||||
Message::A2uiImageLoaded(session_id, url, result) => {
|
||||
if session_id != self.selected_session {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let foreground = self.take_chat_snapshot();
|
||||
if let Some(background) = session_id
|
||||
.and_then(|session_id| self.background_chats.remove(&session_id))
|
||||
{
|
||||
self.restore_chat_snapshot(background);
|
||||
let task =
|
||||
self.update(Message::A2uiImageLoaded(session_id, url, result));
|
||||
let background = self.take_chat_snapshot();
|
||||
self.background_chats.insert(
|
||||
session_id.expect("background chat has a session"),
|
||||
background,
|
||||
);
|
||||
self.restore_chat_snapshot(foreground);
|
||||
return task;
|
||||
}
|
||||
self.restore_chat_snapshot(foreground);
|
||||
}
|
||||
return Task::none();
|
||||
}
|
||||
self.a2ui_image_loading = false;
|
||||
match result {
|
||||
Ok(bytes) => {
|
||||
@@ -1245,6 +1514,8 @@ impl App {
|
||||
#[cfg(target_os = "macos")]
|
||||
self.poll_titling();
|
||||
self.poll_generation();
|
||||
#[cfg(target_os = "macos")]
|
||||
self.poll_background_chats();
|
||||
let images = self.load_next_a2ui_image();
|
||||
return images;
|
||||
}
|
||||
@@ -1301,7 +1572,7 @@ impl App {
|
||||
}
|
||||
}
|
||||
Message::DeleteProject(project_id) => {
|
||||
if self.generating && self.selected_project == Some(project_id) {
|
||||
if self.project_has_active_chat(project_id) {
|
||||
self.error =
|
||||
Some("Stop the active generation before deleting its project.".into());
|
||||
return Task::none();
|
||||
@@ -1322,6 +1593,8 @@ impl App {
|
||||
Ok(()) => {
|
||||
for session_id in checkpoint_ids {
|
||||
let _ = fs::remove_file(session_checkpoint_path(session_id));
|
||||
#[cfg(target_os = "macos")]
|
||||
self.background_chats.remove(&session_id);
|
||||
}
|
||||
self.drafts.remove(&project_id);
|
||||
if self.config.interface.last_project_id == Some(project_id) {
|
||||
@@ -1425,7 +1698,7 @@ impl App {
|
||||
}
|
||||
Message::RebuildSessionContext(session_id) => {
|
||||
self.session_menu = None;
|
||||
if self.generating && self.selected_session == Some(session_id) {
|
||||
if self.session_is_active(session_id) {
|
||||
self.error =
|
||||
Some("Stop the active generation before rebuilding context.".into());
|
||||
} else {
|
||||
@@ -1461,18 +1734,24 @@ impl App {
|
||||
}
|
||||
}
|
||||
Message::SelectSession(project_id, session_id) => {
|
||||
if self.generating && self.selected_session != Some(session_id) {
|
||||
self.error =
|
||||
Some("Stop the active generation before changing sessions.".into());
|
||||
return Task::none();
|
||||
}
|
||||
if self.selected_session == Some(session_id) {
|
||||
return Task::none();
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
self.stop_agent_jobs();
|
||||
#[cfg(target_os = "macos")]
|
||||
self.tool_cards.clear();
|
||||
{
|
||||
self.leave_current_chat();
|
||||
if self.restore_background_chat(session_id) {
|
||||
self.chat_follow_tail = true;
|
||||
self.remember_project(project_id);
|
||||
if let Some(database) = &mut self.database
|
||||
&& let Err(error) = database.touch_session(session_id)
|
||||
{
|
||||
self.error = Some(format!("Could not update the session: {error}"));
|
||||
}
|
||||
self.reload_projects();
|
||||
return Task::batch([scroll_chat_to_end(), self.load_next_a2ui_image()]);
|
||||
}
|
||||
}
|
||||
let saved_context = self
|
||||
.projects
|
||||
.iter()
|
||||
@@ -1542,7 +1821,7 @@ impl App {
|
||||
}
|
||||
}
|
||||
Message::RequestDeleteSession(session_id) => {
|
||||
if self.generating && self.selected_session == Some(session_id) {
|
||||
if self.session_is_active(session_id) {
|
||||
self.error =
|
||||
Some("Stop the active generation before deleting its session.".into());
|
||||
return Task::none();
|
||||
@@ -1563,7 +1842,7 @@ impl App {
|
||||
let Some(session_id) = self.pending_session_delete.take() else {
|
||||
return Task::none();
|
||||
};
|
||||
if self.generating && self.selected_session == Some(session_id) {
|
||||
if self.session_is_active(session_id) {
|
||||
self.error =
|
||||
Some("Stop the active generation before deleting its session.".into());
|
||||
return Task::none();
|
||||
@@ -1572,6 +1851,8 @@ impl App {
|
||||
match database.delete_session(session_id) {
|
||||
Ok(()) => {
|
||||
let _ = fs::remove_file(session_checkpoint_path(session_id));
|
||||
#[cfg(target_os = "macos")]
|
||||
self.background_chats.remove(&session_id);
|
||||
self.finish_cache_change();
|
||||
if self.session_menu == Some(session_id) {
|
||||
self.session_menu = None;
|
||||
@@ -1622,7 +1903,7 @@ impl App {
|
||||
)
|
||||
.then_some(Message::Escape(id))
|
||||
}),
|
||||
window::close_requests().map(Message::WindowClosed),
|
||||
window::close_requests().map(Message::WindowCloseRequested),
|
||||
window::close_events().map(Message::WindowClosed),
|
||||
];
|
||||
subscriptions
|
||||
@@ -1638,6 +1919,7 @@ impl App {
|
||||
Some(crate::native_menu::NativeMenuEvent::ExportChat) => Message::ExportChat,
|
||||
Some(crate::native_menu::NativeMenuEvent::ToggleSidebar) => Message::ToggleSidebar,
|
||||
Some(crate::native_menu::NativeMenuEvent::Help) => Message::OpenHelp,
|
||||
Some(crate::native_menu::NativeMenuEvent::Quit) => Message::RequestQuit,
|
||||
Some(crate::native_menu::NativeMenuEvent::Edit(command)) => {
|
||||
Message::NativeEdit(command)
|
||||
}
|
||||
@@ -1653,7 +1935,11 @@ impl App {
|
||||
let titling = self.active_titling.is_some();
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let titling = false;
|
||||
if self.generating || titling {
|
||||
#[cfg(target_os = "macos")]
|
||||
let background_polling = self.background_chats.values().any(ChatSnapshot::needs_poll);
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let background_polling = false;
|
||||
if self.generating || titling || background_polling {
|
||||
subscriptions.push(
|
||||
iced::time::every(Duration::from_millis(50)).map(|_| Message::GenerationTick),
|
||||
);
|
||||
@@ -1824,7 +2110,7 @@ impl App {
|
||||
.lock()
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
||||
menu.update(
|
||||
self.selected_project.is_some() && !self.generating,
|
||||
self.selected_project.is_some(),
|
||||
self.active_chat_title().is_some(),
|
||||
!self.config.interface.sidebar_collapsed,
|
||||
edit,
|
||||
@@ -1893,6 +2179,7 @@ fn shortcut(key: keyboard::Key, modifiers: keyboard::Modifiers) -> Option<Messag
|
||||
}
|
||||
keyboard::Key::Character("b") if modifiers.command() => Some(Message::ToggleSidebar),
|
||||
keyboard::Key::Character("n") if modifiers.command() => Some(Message::NewChat),
|
||||
keyboard::Key::Character("q") if modifiers.command() => Some(Message::RequestQuit),
|
||||
keyboard::Key::Named(keyboard::key::Named::Tab) => Some(if modifiers.shift() {
|
||||
Message::FocusPrevious
|
||||
} else {
|
||||
@@ -1902,6 +2189,14 @@ fn shortcut(key: keyboard::Key, modifiers: keyboard::Modifiers) -> Option<Messag
|
||||
}
|
||||
}
|
||||
|
||||
fn chat_needs_poll(generating: bool, titling: bool) -> bool {
|
||||
generating || titling
|
||||
}
|
||||
|
||||
fn active_chat_total(foreground: bool, background: impl IntoIterator<Item = bool>) -> usize {
|
||||
usize::from(foreground) + background.into_iter().filter(|active| *active).count()
|
||||
}
|
||||
|
||||
fn export_file_stem(title: &str) -> String {
|
||||
let stem = title
|
||||
.chars()
|
||||
@@ -2242,6 +2537,13 @@ mod tests {
|
||||
keyboard::Modifiers::COMMAND | keyboard::Modifiers::SHIFT,
|
||||
);
|
||||
assert!(matches!(message, Some(Message::OpenModelManager)));
|
||||
assert!(matches!(
|
||||
shortcut(
|
||||
keyboard::Key::Character("q".into()),
|
||||
keyboard::Modifiers::COMMAND
|
||||
),
|
||||
Some(Message::RequestQuit)
|
||||
));
|
||||
let tab = keyboard::Key::Named(keyboard::key::Named::Tab);
|
||||
assert!(matches!(
|
||||
shortcut(tab.clone(), keyboard::Modifiers::empty()),
|
||||
@@ -2256,6 +2558,15 @@ mod tests {
|
||||
assert!(!ModelChoice::Glm52.supports_dspark());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn background_activity_drives_polling_indicators_and_quit_confirmation() {
|
||||
assert!(chat_needs_poll(true, false));
|
||||
assert!(chat_needs_poll(false, true));
|
||||
assert!(!chat_needs_poll(false, false));
|
||||
assert_eq!(active_chat_total(false, [true, false, true]), 2);
|
||||
assert_eq!(active_chat_total(true, [true, false, true]), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tab_scrolls_a_field_back_into_the_preferences_viewport() {
|
||||
let viewport = iced::Rectangle {
|
||||
|
||||
Reference in New Issue
Block a user