优化folder打开逻辑

This commit is contained in:
xintaofei
2026-03-25 18:24:32 +08:00
parent b330a4f936
commit 388f92637c
9 changed files with 119 additions and 13 deletions

View File

@@ -7,7 +7,7 @@ use tokio::sync::Mutex;
static BOOTSTRAP_FOLDER_COMMANDS_LOCK: Mutex<()> = Mutex::const_new(()); static BOOTSTRAP_FOLDER_COMMANDS_LOCK: Mutex<()> = Mutex::const_new(());
fn load_package_scripts_as_commands(folder_path: &str) -> Vec<(String, String)> { pub(crate) fn load_package_scripts_as_commands(folder_path: &str) -> Vec<(String, String)> {
let mut has_package_json = false; let mut has_package_json = false;
let mut has_pnpm_lock = false; let mut has_pnpm_lock = false;
let mut has_yarn_lock = false; let mut has_yarn_lock = false;

View File

@@ -117,6 +117,45 @@ pub async fn reorder_folder_commands(
Ok(Json(())) Ok(Json(()))
} }
// TODO: bootstrap_folder_commands_from_package_json — requires access to #[derive(Deserialize)]
// `load_package_scripts_as_commands` which is private in commands/folder_commands.rs. #[serde(rename_all = "camelCase")]
// Make it pub(crate) first, then add the web handler here. pub struct BootstrapFolderCommandsParams {
pub folder_id: i32,
pub folder_path: String,
}
pub async fn bootstrap_folder_commands_from_package_json(
Extension(app): Extension<tauri::AppHandle>,
Json(params): Json<BootstrapFolderCommandsParams>,
) -> Result<Json<Vec<FolderCommandInfo>>, AppCommandError> {
let db = app.state::<AppDatabase>();
let existing = folder_command_service::list_by_folder(&db.conn, params.folder_id)
.await
.map_err(AppCommandError::from)?;
if !existing.is_empty() {
return Ok(Json(existing));
}
let commands_to_create = tokio::task::spawn_blocking(move || {
crate::commands::folder_commands::load_package_scripts_as_commands(&params.folder_path)
})
.await
.map_err(|e| AppCommandError::new(
crate::app_error::AppErrorCode::TaskExecutionFailed,
format!("bootstrap task failed: {e}"),
))?;
if commands_to_create.is_empty() {
return Ok(Json(existing));
}
folder_command_service::create_many(&db.conn, params.folder_id, &commands_to_create)
.await
.map_err(AppCommandError::from)?;
let result = folder_command_service::list_by_folder(&db.conn, params.folder_id)
.await
.map_err(AppCommandError::from)?;
Ok(Json(result))
}

View File

@@ -24,6 +24,16 @@ pub async fn load_folder_history(
Ok(Json(result)) Ok(Json(result))
} }
pub async fn list_open_folders(
Extension(app): Extension<tauri::AppHandle>,
) -> Result<Json<Vec<FolderHistoryEntry>>, AppCommandError> {
let db = app.state::<AppDatabase>();
let result = folder_service::list_open_folders(&db.conn)
.await
.map_err(AppCommandError::from)?;
Ok(Json(result))
}
pub async fn get_folder( pub async fn get_folder(
Extension(app): Extension<tauri::AppHandle>, Extension(app): Extension<tauri::AppHandle>,
Json(params): Json<FolderIdParams>, Json(params): Json<FolderIdParams>,
@@ -55,6 +65,17 @@ pub async fn open_folder_window(
Ok(Json(entry)) Ok(Json(entry))
} }
pub async fn close_folder_window(
Extension(app): Extension<tauri::AppHandle>,
Json(params): Json<FolderIdParams>,
) -> Result<Json<()>, AppCommandError> {
let db = app.state::<AppDatabase>();
folder_service::set_folder_open(&db.conn, params.folder_id, false)
.await
.map_err(AppCommandError::from)?;
Ok(Json(()))
}
// --- New handlers below --- // --- New handlers below ---
#[derive(Deserialize)] #[derive(Deserialize)]

View File

@@ -37,6 +37,8 @@ pub fn build_router(app: tauri::AppHandle, token: String, static_dir: std::path:
.route("/update_conversation_external_id", post(handlers::conversations::update_conversation_external_id)) .route("/update_conversation_external_id", post(handlers::conversations::update_conversation_external_id))
// ─── Folders ─── // ─── Folders ───
.route("/load_folder_history", post(handlers::folders::load_folder_history)) .route("/load_folder_history", post(handlers::folders::load_folder_history))
.route("/list_open_folders", post(handlers::folders::list_open_folders))
.route("/close_folder_window", post(handlers::folders::close_folder_window))
.route("/get_folder", post(handlers::folders::get_folder)) .route("/get_folder", post(handlers::folders::get_folder))
.route("/open_folder_window", post(handlers::folders::open_folder_window)) .route("/open_folder_window", post(handlers::folders::open_folder_window))
.route("/add_folder_to_history", post(handlers::folders::add_folder_to_history)) .route("/add_folder_to_history", post(handlers::folders::add_folder_to_history))
@@ -115,6 +117,7 @@ pub fn build_router(app: tauri::AppHandle, token: String, static_dir: std::path:
.route("/update_folder_command", post(handlers::folder_commands::update_folder_command)) .route("/update_folder_command", post(handlers::folder_commands::update_folder_command))
.route("/delete_folder_command", post(handlers::folder_commands::delete_folder_command)) .route("/delete_folder_command", post(handlers::folder_commands::delete_folder_command))
.route("/reorder_folder_commands", post(handlers::folder_commands::reorder_folder_commands)) .route("/reorder_folder_commands", post(handlers::folder_commands::reorder_folder_commands))
.route("/bootstrap_folder_commands_from_package_json", post(handlers::folder_commands::bootstrap_folder_commands_from_package_json))
// ─── MCP ─── // ─── MCP ───
.route("/mcp_scan_local", post(handlers::mcp::mcp_scan_local)) .route("/mcp_scan_local", post(handlers::mcp::mcp_scan_local))
.route("/mcp_list_marketplaces", post(handlers::mcp::mcp_list_marketplaces)) .route("/mcp_list_marketplaces", post(handlers::mcp::mcp_list_marketplaces))

View File

@@ -345,7 +345,7 @@ export function BranchDropdown({
setWorktreeOpen(false) setWorktreeOpen(false)
await runGitTask(t("tasks.newWorktree", { name }), async () => { await runGitTask(t("tasks.newWorktree", { name }), async () => {
await gitWorktreeAdd(folderPath, name, wtPath) await gitWorktreeAdd(folderPath, name, wtPath)
await openFolderWindow(wtPath) await openFolderWindow(wtPath, { newWindow: true })
await setFolderParentBranch(wtPath, branch) await setFolderParentBranch(wtPath, branch)
}) })
} }

View File

@@ -54,13 +54,14 @@ export function FolderNameDropdown() {
if (selected) { if (selected) {
await openFolderWindow( await openFolderWindow(
Array.isArray(selected) ? selected[0] : selected, Array.isArray(selected) ? selected[0] : selected,
{ newWindow: true },
) )
} }
} }
async function handleSelect(path: string) { async function handleSelect(path: string) {
try { try {
await openFolderWindow(path) await openFolderWindow(path, { newWindow: true })
} catch { } catch {
// ignore // ignore
} }

View File

@@ -82,7 +82,7 @@ export function FolderTitleBar() {
const result = await openFileDialog({ directory: true, multiple: false }) const result = await openFileDialog({ directory: true, multiple: false })
if (!result) return if (!result) return
const selected = Array.isArray(result) ? result[0] : result const selected = Array.isArray(result) ? result[0] : result
await openFolderWindow(selected) await openFolderWindow(selected, { newWindow: true })
} catch (err) { } catch (err) {
console.error("[FolderTitleBar] failed to open folder:", err) console.error("[FolderTitleBar] failed to open folder:", err)
} }

View File

@@ -11,7 +11,8 @@ import {
type ReactNode, type ReactNode,
} from "react" } from "react"
import { toErrorMessage } from "@/lib/app-error" import { toErrorMessage } from "@/lib/app-error"
import { getFolder, listFolderConversations } from "@/lib/api" import { getFolder, listFolderConversations, closeFolderWindow } from "@/lib/api"
import { isDesktop } from "@/lib/transport"
import type { import type {
AgentType, AgentType,
AgentStats, AgentStats,
@@ -195,6 +196,22 @@ export function FolderProvider({
} }
}, []) }, [])
// Web mode: register this tab's name so that window.open(url, "folder-{id}")
// from other pages can find and reuse it instead of opening duplicates.
// Also notify backend when the folder tab closes.
useEffect(() => {
if (isDesktop() || !folderId) return
window.name = `folder-${folderId}`
const onUnload = () => closeFolderWindow(folderId)
window.addEventListener("pagehide", onUnload)
return () => {
window.removeEventListener("pagehide", onUnload)
}
}, [folderId])
const selectConversation = useCallback((id: number, agentType: AgentType) => { const selectConversation = useCallback((id: number, agentType: AgentType) => {
setSelectedConversation({ id, agentType }) setSelectedConversation({ id, agentType })
setNewConversation(null) setNewConversation(null)

View File

@@ -839,16 +839,23 @@ export async function gitAddFiles(
// Window management commands // Window management commands
export async function openFolderWindow(path: string): Promise<void> { export async function openFolderWindow(
path: string,
options?: { newWindow?: boolean },
): Promise<void> {
if (getTransport().isDesktop()) { if (getTransport().isDesktop()) {
return getTransport().call("open_folder_window", { path }) return getTransport().call("open_folder_window", { path })
} }
// Web mode: add folder to DB and navigate to folder page
const entry = await getTransport().call<{ id: number }>( const entry = await getTransport().call<{ id: number }>(
"open_folder_window", "open_folder_window",
{ path } { path },
) )
window.location.href = `/folder?id=${entry.id}` const url = `/folder?id=${entry.id}`
if (options?.newWindow) {
window.open(url, `folder-${entry.id}`)
} else {
window.location.href = url
}
} }
export async function openCommitWindow(folderId: number): Promise<void> { export async function openCommitWindow(folderId: number): Promise<void> {
@@ -900,8 +907,26 @@ export async function listOpenFolders(): Promise<FolderHistoryEntry[]> {
} }
export async function focusFolderWindow(folderId: number): Promise<void> { export async function focusFolderWindow(folderId: number): Promise<void> {
if (getTransport().isDesktop()) {
return getTransport().call("focus_folder_window", { folderId }) return getTransport().call("focus_folder_window", { folderId })
} }
// Web mode: use named window — reuses existing tab if still open,
// otherwise opens a new one.
window.open(`/folder?id=${folderId}`, `folder-${folderId}`)
}
/**
* Notify the backend that a folder tab has been closed.
* Uses sendBeacon for reliability during page unload.
*/
export function closeFolderWindow(folderId: number): void {
if (getTransport().isDesktop()) return
const token = localStorage.getItem("codeg_token") ?? ""
navigator.sendBeacon(
`/api/close_folder_window?token=${encodeURIComponent(token)}`,
JSON.stringify({ folderId }),
)
}
// Conversation CRUD commands // Conversation CRUD commands