From 7246bf8f6af2c3844518381b3f9400ff6b423828 Mon Sep 17 00:00:00 2001 From: wuyang6 Date: Wed, 13 May 2026 16:16:02 +0800 Subject: [PATCH] Show fixed Jupyter workspace status --- .../app/components/assistant-ui/thread.tsx | 175 +++++++++++++----- 1 file changed, 130 insertions(+), 45 deletions(-) diff --git a/frontend/app/components/assistant-ui/thread.tsx b/frontend/app/components/assistant-ui/thread.tsx index 10ca8a0..47511c2 100644 --- a/frontend/app/components/assistant-ui/thread.tsx +++ b/frontend/app/components/assistant-ui/thread.tsx @@ -298,12 +298,9 @@ async function cancelLatestRun(sessionId: string, runId?: string | null) { const ChatTopActions: FC = () => { const { openFiles } = useActivityPanel(); const [workspaceOpen, setWorkspaceOpen] = useState(false); - const latestSessionId = useAuiState((s) => - findLatestMessageMetadataValue(s.thread.messages, "sessionId"), - ); - const normalizedSessionId = normalizeSessionId( - typeof latestSessionId === "string" ? latestSessionId : null, - ); + const currentRun = useReplayedRunState(); + const normalizedSessionId = currentRun.sessionId; + const workspaceStatus = useJupyterWorkspaceStatus(normalizedSessionId); return (
@@ -330,9 +327,7 @@ const ChatTopActions: FC = () => { type="button" className="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-sm transition-colors hover:bg-muted" onClick={() => { - openFiles( - typeof latestSessionId === "string" ? latestSessionId : null, - ); + openFiles(normalizedSessionId); }} > @@ -344,7 +339,7 @@ const ChatTopActions: FC = () => { onClick={() => setWorkspaceOpen(true)} > - 切换工作区 + {workspaceStatus?.connected ? "Jupyter 工作区" : "切换工作区"} @@ -381,6 +376,52 @@ const WORKSPACE_SWITCH_STEPS = [ "链接工作区", ]; +function useJupyterWorkspaceStatus(sessionId: string | null) { + const [status, setStatus] = useState(null); + + useEffect(() => { + if (!sessionId) { + setStatus(null); + return; + } + let cancelled = false; + + async function refresh() { + try { + const response = await fetch( + `/api/claw/jupyter?session_id=${encodeURIComponent(sessionId)}`, + { cache: "no-store" }, + ); + const payload = (await response.json().catch(() => ({}))) as + | JupyterWorkspaceStatus + | Record; + if (cancelled) return; + if (!response.ok) { + setStatus(null); + return; + } + setStatus(payload as JupyterWorkspaceStatus); + } catch { + if (!cancelled) setStatus(null); + } + } + + void refresh(); + const onRefresh = () => void refresh(); + window.addEventListener("focus", onRefresh); + window.addEventListener("claw-sessions-changed", onRefresh); + const interval = window.setInterval(refresh, 8000); + return () => { + cancelled = true; + window.removeEventListener("focus", onRefresh); + window.removeEventListener("claw-sessions-changed", onRefresh); + window.clearInterval(interval); + }; + }, [sessionId]); + + return status; +} + function WorkspaceSwitchDialog({ open, onOpenChange, @@ -475,9 +516,13 @@ function WorkspaceSwitchDialog({ - 切换工作区 + + {status?.connected ? "Jupyter 工作区" : "切换工作区"} + - 把当前 session 的工具执行切换到远端 Jupyter 开发机。 + {status?.connected + ? "当前 session 已固定到远端 Jupyter 工作区。" + : "把当前 session 的工具执行切换到远端 Jupyter 开发机。"}
@@ -499,36 +544,40 @@ function WorkspaceSwitchDialog({ ) : null}
) : null} - - - + {!status?.connected ? ( + <> + + + + + ) : null} {progress ? (
@@ -559,9 +608,11 @@ function WorkspaceSwitchDialog({ > 关闭 - + {!status?.connected ? ( + + ) : null}
@@ -682,6 +733,7 @@ const Composer: FC = () => { className="flex w-full flex-col gap-2 rounded-(--composer-radius) border bg-background p-(--composer-padding) transition-shadow focus-within:border-ring/75 focus-within:ring-2 focus-within:ring-ring/20 data-[dragging=true]:border-ring data-[dragging=true]:border-dashed data-[dragging=true]:bg-accent/50" > + { ); }; +const ComposerWorkspaceStatus: FC<{ sessionId: string | null }> = ({ + sessionId, +}) => { + const status = useJupyterWorkspaceStatus(sessionId); + if (!status?.connected) return null; + + return ( +
+
+ + + Jupyter 工作区 + + {status.workspace_cwd ? ( + + {status.workspace_cwd} + + ) : null} + {status.jupyter_tree_url ? ( + + 打开 + + ) : null} +
+
+ ); +}; + const ComposerAction: FC = () => { const runtimeRunning = useAuiState((s) => s.thread.isRunning); const replayedRun = useReplayedRunState();