fix(quick-pane): refresh models and window lifecycle

This commit is contained in:
2026-08-26 15:58:01 +02:00
parent d34099771c
commit 4ec951bc05
7 changed files with 869 additions and 130 deletions
+208 -28
View File
@@ -2,14 +2,45 @@ mod bridge;
mod ui_state;
use serde_json::{json, Value};
use std::{env, path::Path, sync::{Arc, Mutex}, time::Duration};
use std::{
env,
path::Path,
sync::{Arc, Mutex},
time::Duration,
};
use tauri::{
async_runtime::JoinHandle, AppHandle, Emitter, Manager, State, WebviewWindow,
WebviewWindowBuilder, WindowEvent,
};
struct LegacySubscription(Mutex<Option<JoinHandle<()>>>);
struct QuickPaneVisibility(Mutex<bool>);
#[derive(Debug, PartialEq, Eq)]
struct ManagedWindowVisibility {
main: bool,
quick_pane: bool,
}
impl ManagedWindowVisibility {
fn main_shown(&mut self) {
self.main = true;
self.quick_pane = false;
}
fn quick_pane_shown(&mut self) {
self.main = false;
self.quick_pane = true;
}
fn hidden(&mut self, label: &str) {
if label == "main" {
self.main = false;
} else if label == "quick-pane" {
self.quick_pane = false;
}
}
}
// One mutex serializes cross-window transitions, avoiding nested visibility-lock ordering.
struct WindowVisibility(Mutex<ManagedWindowVisibility>);
struct WorkspaceSubscription {
generation: Arc<bridge::SubscriptionGeneration>,
task: Mutex<Option<JoinHandle<()>>>,
@@ -62,7 +93,67 @@ fn quick_pane_action(is_visible: bool) -> WindowAction {
}
fn should_hide_close_request(label: &str) -> bool {
label == "quick-pane"
label == "main" || label == "quick-pane"
}
fn get_or_create_main(app: &AppHandle) -> Result<(WebviewWindow, bool), String> {
if let Some(window) = app.get_webview_window("main") {
return Ok((window, false));
}
let config = app
.config()
.app
.windows
.iter()
.find(|window| window.label == "main")
.ok_or_else(|| "Main window configuration is missing".to_owned())?;
let window = WebviewWindowBuilder::from_config(app, config)
.map_err(|error| error.to_string())?
.build()
.map_err(|error| error.to_string())?;
Ok((window, true))
}
fn apply_main_window_action(app: &AppHandle, args: &[String]) {
let (window, created) = match get_or_create_main(app) {
Ok(result) => result,
Err(error) => {
eprintln!("Could not create main window: {error}");
return;
}
};
let Some(state) = app.try_state::<WindowVisibility>() else {
eprintln!("Window visibility state is unavailable");
return;
};
let mut visibility = state
.0
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
if created {
visibility.main = false;
}
match window_action(args, visibility.main) {
WindowAction::Hide => {
if window.hide().is_ok() {
visibility.hidden("main");
}
}
WindowAction::ShowAndFocus => {
if let Some(quick_pane) = app.get_webview_window("quick-pane") {
if let Err(error) = quick_pane.hide() {
eprintln!("Could not hide Quick Pane before showing main window: {error}");
return;
}
}
visibility.hidden("quick-pane");
if window.show().is_ok() {
visibility.main_shown();
let _ = window.unminimize();
let _ = window.set_focus();
}
}
}
}
fn get_or_create_quick_pane(app: &AppHandle) -> Result<(WebviewWindow, bool), String> {
@@ -91,29 +182,33 @@ fn toggle_quick_pane(app: &AppHandle) {
return;
}
};
let Some(state) = app.try_state::<QuickPaneVisibility>() else {
eprintln!("Quick Pane visibility state is unavailable");
let Some(state) = app.try_state::<WindowVisibility>() else {
eprintln!("Window visibility state is unavailable");
return;
};
let mut current = state
let mut visibility = state
.0
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
if created {
*current = false;
visibility.quick_pane = false;
}
match quick_pane_action(*current) {
match quick_pane_action(visibility.quick_pane) {
WindowAction::Hide => {
if window.hide().is_ok() {
*current = false;
visibility.hidden("quick-pane");
}
}
WindowAction::ShowAndFocus => {
if let Some(main) = app.get_webview_window("main") {
let _ = main.hide();
if let Err(error) = main.hide() {
eprintln!("Could not hide main window before showing Quick Pane: {error}");
return;
}
}
visibility.hidden("main");
if window.show().is_ok() {
*current = true;
visibility.quick_pane_shown();
let _ = window.unminimize();
let _ = window.set_focus();
}
@@ -188,6 +283,23 @@ async fn get_model_catalog() -> Result<Value, String> {
bridge::get_model_catalog(&socket_path()?).await
}
#[tauri::command]
fn dismiss_quick_pane(
app: AppHandle,
visibility: State<'_, WindowVisibility>,
) -> Result<(), String> {
let quick_pane = app
.get_webview_window("quick-pane")
.ok_or_else(|| "Quick Pane window is unavailable".to_owned())?;
let mut visibility = visibility
.0
.lock()
.map_err(|_| "Could not update window visibility".to_owned())?;
quick_pane.hide().map_err(|error| error.to_string())?;
visibility.hidden("quick-pane");
Ok(())
}
#[tauri::command]
async fn create_session_runtime(worktree_path: String) -> Result<bridge::RuntimeResult, String> {
bridge::create_session_runtime(&socket_path()?, &worktree_path).await
@@ -443,7 +555,10 @@ pub fn run() {
let builder = tauri::Builder::default()
.plugin(tauri_plugin_dialog::init())
.manage(LegacySubscription(Mutex::new(None)))
.manage(QuickPaneVisibility(Mutex::new(false)))
.manage(WindowVisibility(Mutex::new(ManagedWindowVisibility {
main: true,
quick_pane: false,
})))
.manage(WorkspaceSubscription {
generation: Arc::new(bridge::SubscriptionGeneration::default()),
task: Mutex::new(None),
@@ -454,29 +569,19 @@ pub fn run() {
return;
}
launch_new_session(app.clone(), &args);
if let Some(window) = app.get_webview_window("main") {
match window_action(&args, window.is_visible().unwrap_or(false)) {
WindowAction::Hide => {
let _ = window.hide();
}
WindowAction::ShowAndFocus => {
let _ = window.show();
let _ = window.set_focus();
}
}
}
apply_main_window_action(app, &args);
}))
.on_window_event(|window, event| {
if should_hide_close_request(window.label()) {
if let WindowEvent::CloseRequested { api, .. } = event {
api.prevent_close();
if let Some(state) = window.app_handle().try_state::<QuickPaneVisibility>() {
let mut current = state
if let Some(state) = window.app_handle().try_state::<WindowVisibility>() {
let mut visibility = state
.0
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
if window.hide().is_ok() {
*current = false;
visibility.hidden(window.label());
}
} else {
let _ = window.hide();
@@ -496,6 +601,7 @@ pub fn run() {
get_workspace,
get_workspace_summary,
get_model_catalog,
dismiss_quick_pane,
create_session_runtime,
create_quick_runtime,
open_session_runtime,
@@ -570,9 +676,83 @@ mod tests {
}
#[test]
fn only_quick_pane_native_close_requests_are_hidden() {
fn native_close_requests_hide_managed_windows() {
assert!(should_hide_close_request("main"));
assert!(should_hide_close_request("quick-pane"));
assert!(!should_hide_close_request("main"));
assert!(!should_hide_close_request("other"));
}
#[test]
fn showing_main_clears_quick_pane_visibility() {
let mut visibility = ManagedWindowVisibility {
main: false,
quick_pane: true,
};
visibility.main_shown();
assert_eq!(
visibility,
ManagedWindowVisibility {
main: true,
quick_pane: false,
}
);
}
#[test]
fn showing_quick_pane_clears_main_visibility() {
let mut visibility = ManagedWindowVisibility {
main: true,
quick_pane: false,
};
visibility.quick_pane_shown();
assert_eq!(
visibility,
ManagedWindowVisibility {
main: false,
quick_pane: true,
}
);
}
#[test]
fn dismissed_quick_pane_reopens_with_one_toggle() {
let mut visibility = ManagedWindowVisibility {
main: false,
quick_pane: true,
};
visibility.hidden("quick-pane");
assert_eq!(
quick_pane_action(visibility.quick_pane),
WindowAction::ShowAndFocus
);
visibility.quick_pane_shown();
assert!(visibility.quick_pane);
assert!(!visibility.main);
}
#[test]
fn main_recreation_uses_the_complete_configured_window() {
let context: tauri::Context<tauri::Wry> = tauri::generate_context!();
let main = context
.config()
.app
.windows
.iter()
.find(|window| window.label == "main")
.unwrap();
assert_eq!(main.width, 1100.0);
assert_eq!(main.height, 840.0);
assert_eq!(main.url.to_string(), "index.html");
assert!(main.visible);
assert!(main.transparent);
assert!(main.always_on_top);
assert!(main.skip_taskbar);
}
#[test]