优化folder打开逻辑
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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(¶ms.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))
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)]
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user