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 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,36 +544,40 @@ function WorkspaceSwitchDialog({
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<label className="grid gap-1.5" htmlFor={`${fieldId}-url`}>
|
{!status?.connected ? (
|
||||||
<span className="text-muted-foreground">Jupyter 地址</span>
|
<>
|
||||||
<Input
|
<label className="grid gap-1.5" htmlFor={`${fieldId}-url`}>
|
||||||
id={`${fieldId}-url`}
|
<span className="text-muted-foreground">Jupyter 地址</span>
|
||||||
value={jupyterUrl}
|
<Input
|
||||||
onChange={(event) => setJupyterUrl(event.target.value)}
|
id={`${fieldId}-url`}
|
||||||
placeholder="https://...-jupyter.../lab"
|
value={jupyterUrl}
|
||||||
disabled={submitting}
|
onChange={(event) => setJupyterUrl(event.target.value)}
|
||||||
/>
|
placeholder="https://...-jupyter.../lab"
|
||||||
</label>
|
disabled={submitting}
|
||||||
<label className="grid gap-1.5" htmlFor={`${fieldId}-password`}>
|
/>
|
||||||
<span className="text-muted-foreground">密码</span>
|
</label>
|
||||||
<Input
|
<label className="grid gap-1.5" htmlFor={`${fieldId}-password`}>
|
||||||
id={`${fieldId}-password`}
|
<span className="text-muted-foreground">密码</span>
|
||||||
type="password"
|
<Input
|
||||||
value={password}
|
id={`${fieldId}-password`}
|
||||||
onChange={(event) => setPassword(event.target.value)}
|
type="password"
|
||||||
placeholder="Jupyter 登录密码"
|
value={password}
|
||||||
disabled={submitting}
|
onChange={(event) => setPassword(event.target.value)}
|
||||||
/>
|
placeholder="Jupyter 登录密码"
|
||||||
</label>
|
disabled={submitting}
|
||||||
<label className="grid gap-1.5" htmlFor={`${fieldId}-root`}>
|
/>
|
||||||
<span className="text-muted-foreground">远端工作区根目录</span>
|
</label>
|
||||||
<Input
|
<label className="grid gap-1.5" htmlFor={`${fieldId}-root`}>
|
||||||
id={`${fieldId}-root`}
|
<span className="text-muted-foreground">远端工作区根目录</span>
|
||||||
value={workspaceRoot}
|
<Input
|
||||||
onChange={(event) => setWorkspaceRoot(event.target.value)}
|
id={`${fieldId}-root`}
|
||||||
disabled={submitting}
|
value={workspaceRoot}
|
||||||
/>
|
onChange={(event) => setWorkspaceRoot(event.target.value)}
|
||||||
</label>
|
disabled={submitting}
|
||||||
|
/>
|
||||||
|
</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>
|
||||||
<Button type="button" disabled={submitting} onClick={submit}>
|
{!status?.connected ? (
|
||||||
{submitting ? "连接中..." : "切换"}
|
<Button type="button" disabled={submitting} onClick={submit}>
|
||||||
</Button>
|
{submitting ? "连接中..." : "切换"}
|
||||||
|
</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();
|
||||||
|
|||||||
Reference in New Issue
Block a user