Show fixed Jupyter workspace status

This commit is contained in:
wuyang6
2026-05-13 16:16:02 +08:00
parent 01d148ec93
commit 7246bf8f6a
+130 -45
View File
@@ -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 (
<div className="pointer-events-none sticky top-3 z-20 mr-4 flex justify-end">
@@ -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);
}}
>
<FileTextIcon className="size-4 text-muted-foreground" />
@@ -344,7 +339,7 @@ const ChatTopActions: FC = () => {
onClick={() => setWorkspaceOpen(true)}
>
<WrenchIcon className="size-4 text-muted-foreground" />
{workspaceStatus?.connected ? "Jupyter 工作区" : "切换工作区"}
</button>
</PopoverPrimitive.Content>
</PopoverPrimitive.Portal>
@@ -381,6 +376,52 @@ const WORKSPACE_SWITCH_STEPS = [
"链接工作区",
];
function useJupyterWorkspaceStatus(sessionId: string | null) {
const [status, setStatus] = useState<JupyterWorkspaceStatus | null>(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<string, never>;
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({
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="pointer-events-auto sm:max-w-xl">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogTitle>
{status?.connected ? "Jupyter 工作区" : "切换工作区"}
</DialogTitle>
<DialogDescription>
session Jupyter
{status?.connected
? "当前 session 已固定到远端 Jupyter 工作区。"
: "把当前 session 的工具执行切换到远端 Jupyter 开发机。"}
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 text-sm">
@@ -499,36 +544,40 @@ function WorkspaceSwitchDialog({
) : null}
</div>
) : null}
<label className="grid gap-1.5" htmlFor={`${fieldId}-url`}>
<span className="text-muted-foreground">Jupyter </span>
<Input
id={`${fieldId}-url`}
value={jupyterUrl}
onChange={(event) => setJupyterUrl(event.target.value)}
placeholder="https://...-jupyter.../lab"
disabled={submitting}
/>
</label>
<label className="grid gap-1.5" htmlFor={`${fieldId}-password`}>
<span className="text-muted-foreground"></span>
<Input
id={`${fieldId}-password`}
type="password"
value={password}
onChange={(event) => setPassword(event.target.value)}
placeholder="Jupyter 登录密码"
disabled={submitting}
/>
</label>
<label className="grid gap-1.5" htmlFor={`${fieldId}-root`}>
<span className="text-muted-foreground"></span>
<Input
id={`${fieldId}-root`}
value={workspaceRoot}
onChange={(event) => setWorkspaceRoot(event.target.value)}
disabled={submitting}
/>
</label>
{!status?.connected ? (
<>
<label className="grid gap-1.5" htmlFor={`${fieldId}-url`}>
<span className="text-muted-foreground">Jupyter </span>
<Input
id={`${fieldId}-url`}
value={jupyterUrl}
onChange={(event) => setJupyterUrl(event.target.value)}
placeholder="https://...-jupyter.../lab"
disabled={submitting}
/>
</label>
<label className="grid gap-1.5" htmlFor={`${fieldId}-password`}>
<span className="text-muted-foreground"></span>
<Input
id={`${fieldId}-password`}
type="password"
value={password}
onChange={(event) => setPassword(event.target.value)}
placeholder="Jupyter 登录密码"
disabled={submitting}
/>
</label>
<label className="grid gap-1.5" htmlFor={`${fieldId}-root`}>
<span className="text-muted-foreground"></span>
<Input
id={`${fieldId}-root`}
value={workspaceRoot}
onChange={(event) => setWorkspaceRoot(event.target.value)}
disabled={submitting}
/>
</label>
</>
) : null}
{progress ? (
<div className="rounded-md border bg-muted/30 px-3 py-2">
<div className="mb-2 flex items-center justify-between gap-3 text-xs">
@@ -559,9 +608,11 @@ function WorkspaceSwitchDialog({
>
</Button>
<Button type="button" disabled={submitting} onClick={submit}>
{submitting ? "连接中..." : "切换"}
</Button>
{!status?.connected ? (
<Button type="button" disabled={submitting} onClick={submit}>
{submitting ? "连接中..." : "切换"}
</Button>
) : null}
</DialogFooter>
</DialogContent>
</Dialog>
@@ -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"
>
<ComposerAttachments />
<ComposerWorkspaceStatus sessionId={replayedRun.sessionId} />
<ImeComposerInput
placeholder="Send a message..."
className="aui-composer-input max-h-32 min-h-10 w-full resize-none bg-transparent px-1.75 py-1 text-sm outline-none placeholder:text-muted-foreground/80"
@@ -697,6 +749,39 @@ const Composer: FC = () => {
);
};
const ComposerWorkspaceStatus: FC<{ sessionId: string | null }> = ({
sessionId,
}) => {
const status = useJupyterWorkspaceStatus(sessionId);
if (!status?.connected) return null;
return (
<div className="flex items-center justify-center px-1 pt-0.5">
<div className="flex min-w-0 items-center gap-2 rounded-full border bg-muted/40 px-3 py-1 text-xs text-muted-foreground">
<span className="size-2 shrink-0 rounded-full bg-emerald-500" />
<span className="shrink-0 font-medium text-foreground">
Jupyter
</span>
{status.workspace_cwd ? (
<span className="hidden max-w-56 truncate sm:inline">
{status.workspace_cwd}
</span>
) : null}
{status.jupyter_tree_url ? (
<a
className="shrink-0 text-primary underline-offset-4 hover:underline"
href={status.jupyter_tree_url}
target="_blank"
rel="noreferrer"
>
</a>
) : null}
</div>
</div>
);
};
const ComposerAction: FC = () => {
const runtimeRunning = useAuiState((s) => s.thread.isRunning);
const replayedRun = useReplayedRunState();