优化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(());
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_pnpm_lock = false;
let mut has_yarn_lock = false;

View File

@@ -117,6 +117,45 @@ pub async fn reorder_folder_commands(
Ok(Json(()))
}
// TODO: bootstrap_folder_commands_from_package_json — requires access to
// `load_package_scripts_as_commands` which is private in commands/folder_commands.rs.
// Make it pub(crate) first, then add the web handler here.
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
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))
}
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(
Extension(app): Extension<tauri::AppHandle>,
Json(params): Json<FolderIdParams>,
@@ -55,6 +65,17 @@ pub async fn open_folder_window(
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 ---
#[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))
// ─── Folders ───
.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("/open_folder_window", post(handlers::folders::open_folder_window))
.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("/delete_folder_command", post(handlers::folder_commands::delete_folder_command))
.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 ───
.route("/mcp_scan_local", post(handlers::mcp::mcp_scan_local))
.route("/mcp_list_marketplaces", post(handlers::mcp::mcp_list_marketplaces))

View File

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

View File

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

View File

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

View File

@@ -11,7 +11,8 @@ import {
type ReactNode,
} from "react"
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 {
AgentType,
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) => {
setSelectedConversation({ id, agentType })
setNewConversation(null)

View File

@@ -839,16 +839,23 @@ export async function gitAddFiles(
// 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()) {
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 }>(
"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> {
@@ -900,7 +907,25 @@ export async function listOpenFolders(): Promise<FolderHistoryEntry[]> {
}
export async function focusFolderWindow(folderId: number): Promise<void> {
return getTransport().call("focus_folder_window", { folderId })
if (getTransport().isDesktop()) {
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