Show fixed Jupyter workspace status
This commit is contained in:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user