diff --git a/src/web/src/App.tsx b/src/web/src/App.tsx index a3c52b7..dd033db 100644 --- a/src/web/src/App.tsx +++ b/src/web/src/App.tsx @@ -1,6 +1,6 @@ // 根组件:auth + 路由 + 全局 WS 订阅 -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { Routes, Route, useNavigate, useLocation } from "react-router"; import { useViewportHeight } from "./hooks/useViewportHeight"; import { useAuth } from "./hooks/useAuth"; @@ -15,6 +15,7 @@ import { NewInstanceModal } from "./components/ui/NewInstanceModal"; import { LoginPage } from "./components/LoginPage"; import { ToastContainer, showToast } from "./components/ui/Toast"; import type { InstanceId } from "../../protocol/types"; +import { isBusy } from "./utils/status"; import { useTranslation } from "react-i18next"; /** 从 URL pathname 派生当前选中的实例 ID */ @@ -73,14 +74,23 @@ export default function App() { ); // ── Shutdown ── + // 追踪主动 shutdown 的实例,区分主动退出与意外断连 + const shuttingDownRef = useRef>(new Set()); + const handleShutdown = useCallback( (id: InstanceId) => { - send({ type: "shutdown", payload: { instanceId: id } }); - if (id === selectedInstanceId) { - navigate("/"); + // busy 时拒绝退出 + const instance = useInstances + .getState() + .instances.find((i) => i.id === id); + if (instance && isBusy(instance.status)) { + showToast(t("sidebar.shutdownBusy")); + return; } + shuttingDownRef.current.add(id); + send({ type: "shutdown", payload: { instanceId: id } }); }, - [send, selectedInstanceId, navigate], + [send, t], ); // 新建实例弹窗 @@ -98,7 +108,12 @@ export default function App() { if (!instanceListReady || !selectedInstanceId) return; const exists = instances.some((i) => i.id === selectedInstanceId); if (!exists) { - showToast(t("eventStream.instanceNotFound")); + // 主动 shutdown 引起的消失不弹 toast + if (shuttingDownRef.current.has(selectedInstanceId)) { + shuttingDownRef.current.delete(selectedInstanceId); + } else { + showToast(t("eventStream.instanceNotFound")); + } navigate("/"); } }, [instanceListReady, selectedInstanceId, instances, navigate, t]); diff --git a/src/web/src/components/ui/ContextMenu.tsx b/src/web/src/components/ui/ContextMenu.tsx index 6a592db..f4805f6 100644 --- a/src/web/src/components/ui/ContextMenu.tsx +++ b/src/web/src/components/ui/ContextMenu.tsx @@ -67,19 +67,13 @@ export function ContextMenu({ position, onClose, children }: ContextMenuProps) { stableClose(); } } - function handleScroll() { - stableClose(); - } - document.addEventListener("mousedown", handleMouseDown); document.addEventListener("contextmenu", handleContextMenu); document.addEventListener("keydown", handleKeyDown); - window.addEventListener("scroll", handleScroll, true); return () => { document.removeEventListener("mousedown", handleMouseDown); document.removeEventListener("contextmenu", handleContextMenu); document.removeEventListener("keydown", handleKeyDown); - window.removeEventListener("scroll", handleScroll, true); }; }, [stableClose]); diff --git a/src/web/src/i18n/en.ts b/src/web/src/i18n/en.ts index cda8065..5262ff1 100644 --- a/src/web/src/i18n/en.ts +++ b/src/web/src/i18n/en.ts @@ -32,6 +32,7 @@ const en: LocaleResource = { noInstances: "No pi instances connected", settings: "Settings", shutdown: "Shutdown instance", + shutdownBusy: "Instance is running, please wait before shutting down", statusStreaming: "Streaming", statusCompacting: "Compacting", statusIdle: "Idle", diff --git a/src/web/src/i18n/zh-CN.ts b/src/web/src/i18n/zh-CN.ts index 6cd13c7..e6f6657 100644 --- a/src/web/src/i18n/zh-CN.ts +++ b/src/web/src/i18n/zh-CN.ts @@ -30,6 +30,7 @@ const zhCN = { noInstances: "暂无 pi 实例连接", settings: "设置", shutdown: "退出实例", + shutdownBusy: "实例正在执行中,请等待完成后再退出", statusStreaming: "生成中", statusCompacting: "压缩中", statusIdle: "空闲",