feat(tabs): detach SQL and data tabs into separate windows

This commit is contained in:
DengQingNian authored and GitHub committed 2026-09-03 01:45:08 +08:00
1 parent 717dcc1294
commit d38be39e03
17 files changed
+957 -21

No files matched your search

+73 -1
View File
@@ -1,6 +1,7 @@
use std::{
collections::HashSet,
path::{Path, PathBuf},
sync::Arc,
sync::{Arc, Mutex, OnceLock},
};
use dbx_core::storage::{DesktopSettings, McpGlobalPolicy, McpGlobalPolicyState};
@@ -13,6 +14,30 @@ use crate::{
};
const DEVELOPMENT_OPEN_TABS_STATE_KEY: &str = "development_open_tabs";
const DETACHED_TABS_STATE_KEY: &str = "detached_tabs";
static DETACHED_TABS_LOCK: OnceLock<tokio::sync::Mutex<()>> = OnceLock::new();
static APPROVED_DETACHED_CLOSES: OnceLock<Mutex<HashSet<String>>> = OnceLock::new();
fn detached_tabs_lock() -> &'static tokio::sync::Mutex<()> {
DETACHED_TABS_LOCK.get_or_init(|| tokio::sync::Mutex::new(()))
}
fn approved_detached_closes() -> &'static Mutex<HashSet<String>> {
APPROVED_DETACHED_CLOSES.get_or_init(|| Mutex::new(HashSet::new()))
}
pub(crate) fn take_approved_detached_window_close(label: &str) -> bool {
approved_detached_closes().lock().map(|mut labels| labels.remove(label)).unwrap_or(false)
}
async fn load_detached_tabs_map(state: &AppState) -> Result<serde_json::Map<String, serde_json::Value>, String> {
Ok(state
.storage
.load_open_tabs_state_with_key(DETACHED_TABS_STATE_KEY)
.await?
.and_then(|value| value.as_object().cloned())
.unwrap_or_default())
}
fn open_tabs_state_key(debug_build: bool) -> &'static str {
if debug_build {
@@ -161,6 +186,53 @@ pub async fn save_open_tabs_state(state: State<'_, Arc<AppState>>, payload: serd
state.storage.save_open_tabs_state_with_key(open_tabs_state_key(cfg!(debug_assertions)), &payload).await
}
#[tauri::command]
pub async fn save_detached_tab_handoff(
state: State<'_, Arc<AppState>>,
tab_id: String,
handoff: serde_json::Value,
) -> Result<(), String> {
let _guard = detached_tabs_lock().lock().await;
let mut tabs = load_detached_tabs_map(state.inner()).await?;
tabs.insert(tab_id, handoff);
state.storage.save_open_tabs_state_with_key(DETACHED_TABS_STATE_KEY, &serde_json::Value::Object(tabs)).await
}
#[tauri::command]
pub async fn load_detached_tab_handoff(
state: State<'_, Arc<AppState>>,
tab_id: String,
) -> Result<Option<serde_json::Value>, String> {
let _guard = detached_tabs_lock().lock().await;
Ok(load_detached_tabs_map(state.inner()).await?.remove(&tab_id))
}
#[tauri::command]
pub async fn list_detached_tab_handoffs(state: State<'_, Arc<AppState>>) -> Result<Vec<serde_json::Value>, String> {
let _guard = detached_tabs_lock().lock().await;
Ok(load_detached_tabs_map(state.inner()).await?.into_values().collect())
}
#[tauri::command]
pub async fn delete_detached_tab_handoff(state: State<'_, Arc<AppState>>, tab_id: String) -> Result<(), String> {
let _guard = detached_tabs_lock().lock().await;
let mut tabs = load_detached_tabs_map(state.inner()).await?;
tabs.remove(&tab_id);
state.storage.save_open_tabs_state_with_key(DETACHED_TABS_STATE_KEY, &serde_json::Value::Object(tabs)).await
}
#[tauri::command]
pub fn approve_detached_window_close(window: Window) -> Result<(), String> {
if !window.label().starts_with("detached-tab-") {
return Err("current window is not a detached tab window".to_string());
}
approved_detached_closes()
.lock()
.map_err(|_| "detached window close state is unavailable".to_string())?
.insert(window.label().to_string());
Ok(())
}
#[tauri::command]
pub async fn load_saved_sql_editor_positions(
state: State<'_, Arc<AppState>>,
+22
View File
@@ -1697,7 +1697,24 @@ pub fn run() {
Ok(())
})
.on_window_event(|window, event| {
if let tauri::WindowEvent::Destroyed = event {
if let Some(tab_id) = window.label().strip_prefix("detached-tab-") {
let _ = window.emit("dbx:detached-tab-lost", serde_json::json!({ "tabId": tab_id }));
}
return;
}
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
if let Some(tab_id) = window.label().strip_prefix("detached-tab-") {
if commands::app_settings::take_approved_detached_window_close(window.label()) {
return;
}
api.prevent_close();
// Broadcast with the tabId payload: JS listeners registered
// with the default `listen()` target receive events emitted
// to any window, so the frontend must filter by tabId.
let _ = window.emit("dbx:detached-tab-close-requested", serde_json::json!({ "tabId": tab_id }));
return;
}
if !should_hide_window_on_close(std::env::consts::OS) {
return;
}
@@ -1768,6 +1785,11 @@ pub fn run() {
commands::app_settings::save_editor_settings,
commands::app_settings::load_open_tabs_state,
commands::app_settings::save_open_tabs_state,
commands::app_settings::save_detached_tab_handoff,
commands::app_settings::load_detached_tab_handoff,
commands::app_settings::list_detached_tab_handoffs,
commands::app_settings::delete_detached_tab_handoff,
commands::app_settings::approve_detached_window_close,
commands::app_settings::load_saved_sql_editor_positions,
commands::app_settings::save_saved_sql_editor_positions,
commands::app_settings::load_transfer_task_library,