Show fixed Jupyter workspace status

This commit is contained in:
wuyang6
2026-05-13 16:16:02 +08:00
parent 01d148ec93
commit 7246bf8f6a
+97 -12
View File
@@ -298,12 +298,9 @@ async function cancelLatestRun(sessionId: string, runId?: string | null) {
const ChatTopActions: FC = () => { const ChatTopActions: FC = () => {
const { openFiles } = useActivityPanel(); const { openFiles } = useActivityPanel();
const [workspaceOpen, setWorkspaceOpen] = useState(false); const [workspaceOpen, setWorkspaceOpen] = useState(false);
const latestSessionId = useAuiState((s) => const currentRun = useReplayedRunState();
findLatestMessageMetadataValue(s.thread.messages, "sessionId"), const normalizedSessionId = currentRun.sessionId;
); const workspaceStatus = useJupyterWorkspaceStatus(normalizedSessionId);
const normalizedSessionId = normalizeSessionId(
typeof latestSessionId === "string" ? latestSessionId : null,
);
return ( return (
<div className="pointer-events-none sticky top-3 z-20 mr-4 flex justify-end"> <div className="pointer-events-none sticky top-3 z-20 mr-4 flex justify-end">
@@ -330,9 +327,7 @@ const ChatTopActions: FC = () => {
type="button" 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" className="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-sm transition-colors hover:bg-muted"
onClick={() => { onClick={() => {
openFiles( openFiles(normalizedSessionId);
typeof latestSessionId === "string" ? latestSessionId : null,
);
}} }}
> >
<FileTextIcon className="size-4 text-muted-foreground" /> <FileTextIcon className="size-4 text-muted-foreground" />
@@ -344,7 +339,7 @@ const ChatTopActions: FC = () => {
onClick={() => setWorkspaceOpen(true)} onClick={() => setWorkspaceOpen(true)}
> >
<WrenchIcon className="size-4 text-muted-foreground" /> <WrenchIcon className="size-4 text-muted-foreground" />
{workspaceStatus?.connected ? "Jupyter 工作区" : "切换工作区"}
</button> </button>
</PopoverPrimitive.Content> </PopoverPrimitive.Content>
</PopoverPrimitive.Portal> </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({ function WorkspaceSwitchDialog({
open, open,
onOpenChange, onOpenChange,
@@ -475,9 +516,13 @@ function WorkspaceSwitchDialog({
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="pointer-events-auto sm:max-w-xl"> <DialogContent className="pointer-events-auto sm:max-w-xl">
<DialogHeader> <DialogHeader>
<DialogTitle></DialogTitle> <DialogTitle>
{status?.connected ? "Jupyter 工作区" : "切换工作区"}
</DialogTitle>
<DialogDescription> <DialogDescription>
session Jupyter {status?.connected
? "当前 session 已固定到远端 Jupyter 工作区。"
: "把当前 session 的工具执行切换到远端 Jupyter 开发机。"}
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="grid gap-4 text-sm"> <div className="grid gap-4 text-sm">
@@ -499,6 +544,8 @@ function WorkspaceSwitchDialog({
) : null} ) : null}
</div> </div>
) : null} ) : null}
{!status?.connected ? (
<>
<label className="grid gap-1.5" htmlFor={`${fieldId}-url`}> <label className="grid gap-1.5" htmlFor={`${fieldId}-url`}>
<span className="text-muted-foreground">Jupyter </span> <span className="text-muted-foreground">Jupyter </span>
<Input <Input
@@ -529,6 +576,8 @@ function WorkspaceSwitchDialog({
disabled={submitting} disabled={submitting}
/> />
</label> </label>
</>
) : null}
{progress ? ( {progress ? (
<div className="rounded-md border bg-muted/30 px-3 py-2"> <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"> <div className="mb-2 flex items-center justify-between gap-3 text-xs">
@@ -559,9 +608,11 @@ function WorkspaceSwitchDialog({
> >
</Button> </Button>
{!status?.connected ? (
<Button type="button" disabled={submitting} onClick={submit}> <Button type="button" disabled={submitting} onClick={submit}>
{submitting ? "连接中..." : "切换"} {submitting ? "连接中..." : "切换"}
</Button> </Button>
) : null}
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </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" 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 /> <ComposerAttachments />
<ComposerWorkspaceStatus sessionId={replayedRun.sessionId} />
<ImeComposerInput <ImeComposerInput
placeholder="Send a message..." 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" 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 ComposerAction: FC = () => {
const runtimeRunning = useAuiState((s) => s.thread.isRunning); const runtimeRunning = useAuiState((s) => s.thread.isRunning);
const replayedRun = useReplayedRunState(); const replayedRun = useReplayedRunState();