修改
This commit is contained in:
@@ -41,6 +41,7 @@ import {
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible";
|
||||
import { Sheet, SheetContent, SheetTitle } from "@/components/ui/sheet";
|
||||
import {
|
||||
ACTIVE_SESSION_CHANGED_EVENT,
|
||||
readActiveSessionId,
|
||||
@@ -140,7 +141,7 @@ type ActivityItem = {
|
||||
|
||||
type LiveRunEvent = NonNullable<ClawRunStatus["events"]>[number];
|
||||
|
||||
export function ActivityPanel() {
|
||||
export function ActivityPanel({ asDrawer = false }: { asDrawer?: boolean } = {}) {
|
||||
const {
|
||||
open,
|
||||
mode,
|
||||
@@ -221,20 +222,12 @@ export function ActivityPanel() {
|
||||
prevLatestIdRef.current = latestId;
|
||||
}, [items, mode, openActivity, selectedId]);
|
||||
|
||||
if (!open) return null;
|
||||
if (!asDrawer && !open) return null;
|
||||
|
||||
if (mode === "files") {
|
||||
return (
|
||||
<SessionFilesPanel
|
||||
key={`${sessionId ?? "active"}:${filesRefreshToken}`}
|
||||
sessionId={sessionId}
|
||||
onClose={close}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const isFilesMode = mode === "files";
|
||||
|
||||
return (
|
||||
<aside className="fixed inset-y-0 right-0 z-30 flex w-[min(23rem,calc(100vw-1rem))] flex-col border-l bg-background shadow-xl lg:static lg:z-auto lg:h-full lg:w-92 lg:shrink-0 lg:shadow-none">
|
||||
const activityBody = (
|
||||
<>
|
||||
<header className="flex h-16 shrink-0 items-center justify-between border-b px-4">
|
||||
<div className="min-w-0">
|
||||
<h2 className="font-semibold text-base">活动</h2>
|
||||
@@ -244,16 +237,18 @@ export function ActivityPanel() {
|
||||
: "暂无链路"}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-8"
|
||||
onClick={close}
|
||||
aria-label="关闭活动面板"
|
||||
>
|
||||
<PanelRightCloseIcon className="size-4" />
|
||||
</Button>
|
||||
{!asDrawer ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-8"
|
||||
onClick={close}
|
||||
aria-label="关闭活动面板"
|
||||
>
|
||||
<PanelRightCloseIcon className="size-4" />
|
||||
</Button>
|
||||
) : null}
|
||||
</header>
|
||||
<div className="min-h-0 flex-1 overflow-y-auto px-4 py-4">
|
||||
{visibleItems.length === 0 ? (
|
||||
@@ -280,6 +275,52 @@ export function ActivityPanel() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
if (asDrawer) {
|
||||
return (
|
||||
<Sheet
|
||||
open={open}
|
||||
onOpenChange={(next) => {
|
||||
if (!next) close();
|
||||
}}
|
||||
>
|
||||
<SheetContent
|
||||
side="right"
|
||||
className="flex w-[min(25rem,calc(100vw-1rem))] flex-col gap-0 p-0 sm:max-w-md"
|
||||
>
|
||||
<SheetTitle className="sr-only">
|
||||
{isFilesMode ? "聊天中的文件" : "活动"}
|
||||
</SheetTitle>
|
||||
{isFilesMode ? (
|
||||
<SessionFilesPanel
|
||||
key={`drawer:${sessionId ?? "active"}:${filesRefreshToken}`}
|
||||
sessionId={sessionId}
|
||||
onClose={close}
|
||||
embedded
|
||||
/>
|
||||
) : (
|
||||
activityBody
|
||||
)}
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
|
||||
if (isFilesMode) {
|
||||
return (
|
||||
<SessionFilesPanel
|
||||
key={`${sessionId ?? "active"}:${filesRefreshToken}`}
|
||||
sessionId={sessionId}
|
||||
onClose={close}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<aside className="fixed inset-y-0 right-0 z-30 flex w-[min(23rem,calc(100vw-1rem))] flex-col border-l bg-background shadow-xl lg:static lg:z-auto lg:h-full lg:w-92 lg:shrink-0 lg:shadow-none">
|
||||
{activityBody}
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
@@ -308,9 +349,11 @@ type SessionFilesPayload = {
|
||||
function SessionFilesPanel({
|
||||
sessionId,
|
||||
onClose,
|
||||
embedded = false,
|
||||
}: {
|
||||
sessionId: string | null;
|
||||
onClose: () => void;
|
||||
embedded?: boolean;
|
||||
}) {
|
||||
const [payload, setPayload] = useState<SessionFilesPayload | null>(null);
|
||||
const [status, setStatus] = useState("正在读取聊天中的文件...");
|
||||
@@ -377,8 +420,8 @@ function SessionFilesPanel({
|
||||
const outputFiles = payload?.output ?? [];
|
||||
const total = inputFiles.length + outputFiles.length;
|
||||
|
||||
return (
|
||||
<aside className="fixed inset-y-0 right-0 z-30 flex w-[min(25rem,calc(100vw-1rem))] flex-col border-l bg-background shadow-xl lg:static lg:z-auto lg:h-full lg:w-96 lg:shrink-0 lg:shadow-none">
|
||||
const body = (
|
||||
<>
|
||||
<header className="flex h-16 shrink-0 items-center justify-between border-b px-4">
|
||||
<div className="min-w-0">
|
||||
<h2 className="font-semibold text-base">聊天中的文件</h2>
|
||||
@@ -386,16 +429,18 @@ function SessionFilesPanel({
|
||||
{status || `${total} 个文件`}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-8"
|
||||
onClick={onClose}
|
||||
aria-label="关闭文件面板"
|
||||
>
|
||||
<PanelRightCloseIcon className="size-4" />
|
||||
</Button>
|
||||
{!embedded ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-8"
|
||||
onClick={onClose}
|
||||
aria-label="关闭文件面板"
|
||||
>
|
||||
<PanelRightCloseIcon className="size-4" />
|
||||
</Button>
|
||||
) : null}
|
||||
</header>
|
||||
<div className="min-h-0 flex-1 overflow-y-auto px-4 py-4">
|
||||
{!status && total === 0 ? (
|
||||
@@ -406,6 +451,16 @@ function SessionFilesPanel({
|
||||
<FileSection title="输入" files={inputFiles} />
|
||||
<FileSection title="输出" files={outputFiles} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
if (embedded) {
|
||||
return <div className="flex min-h-0 flex-1 flex-col">{body}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<aside className="fixed inset-y-0 right-0 z-30 flex w-[min(25rem,calc(100vw-1rem))] flex-col border-l bg-background shadow-xl lg:static lg:z-auto lg:h-full lg:w-96 lg:shrink-0 lg:shadow-none">
|
||||
{body}
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -425,17 +425,16 @@ async function cancelLatestRun(sessionId: string, runId?: string | null) {
|
||||
const ChatTopActions: FC = () => {
|
||||
const { openFiles } = useActivityPanel();
|
||||
const [workspaceOpen, setWorkspaceOpen] = useState(false);
|
||||
const actionSessionId = useCurrentThreadSessionId({
|
||||
includePending: true,
|
||||
// 工作区切换必须绑定当前可见会话;空白新会话不能回退到上一个
|
||||
// activeSessionId,否则会把 Jupyter 工作区切到旧会话上。
|
||||
includeActive: false,
|
||||
});
|
||||
const displaySessionId = useCurrentThreadSessionId({
|
||||
// "切换工作区" dialog 与 indicator 必须用同一个 sessionId,否则会出现
|
||||
// "绑给 A、查的是 B" 的错位(agent 的 bash 路由也是用 active session id,
|
||||
// 错位时表面 indicator 显示已连接,实际 agent 跑在本地)。
|
||||
// 新建空会话场景由 thread-list.tsx 的 clearActiveSessionId() 兜底,
|
||||
// 此处不再需要刻意排除 activeSessionId。
|
||||
const sessionId = useCurrentThreadSessionId({
|
||||
includePending: true,
|
||||
includeActive: true,
|
||||
});
|
||||
const workspaceStatus = useJupyterWorkspaceStatus(displaySessionId);
|
||||
const workspaceStatus = useJupyterWorkspaceStatus(sessionId);
|
||||
|
||||
return (
|
||||
<div className="pointer-events-none sticky top-3 z-20 mr-4 flex justify-end">
|
||||
@@ -462,7 +461,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(displaySessionId);
|
||||
openFiles(sessionId);
|
||||
}}
|
||||
>
|
||||
<FileTextIcon className="size-4 text-muted-foreground" />
|
||||
@@ -482,7 +481,7 @@ const ChatTopActions: FC = () => {
|
||||
<WorkspaceSwitchDialog
|
||||
open={workspaceOpen}
|
||||
onOpenChange={setWorkspaceOpen}
|
||||
sessionId={actionSessionId}
|
||||
sessionId={sessionId}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -72,6 +72,7 @@ type ClawSessionSummary = {
|
||||
preview?: string;
|
||||
model?: string;
|
||||
usage?: UsageSummary;
|
||||
is_training?: boolean;
|
||||
};
|
||||
|
||||
type UsageSummary = {
|
||||
@@ -365,8 +366,15 @@ function SessionResultButton({
|
||||
className="flex w-full flex-col rounded-lg px-2 py-2 text-left transition-colors hover:bg-muted"
|
||||
onClick={onClick}
|
||||
>
|
||||
<span className="truncate font-medium text-sm">
|
||||
{loading ? "回放中..." : session.preview || session.session_id}
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span className="truncate font-medium text-sm">
|
||||
{loading ? "回放中..." : session.preview || session.session_id}
|
||||
</span>
|
||||
{session.is_training ? (
|
||||
<span className="shrink-0 rounded bg-amber-100 px-1.5 text-[10px] text-amber-800">
|
||||
训练
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
<span className="mt-0.5 text-muted-foreground text-xs">
|
||||
{session.turns ?? 0} 轮 · {session.tool_calls ?? 0} 次工具
|
||||
@@ -382,17 +390,23 @@ function useSidebarSessions(open: boolean) {
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
let cancelled = false;
|
||||
fetch("/api/claw/sessions", { cache: "no-store" })
|
||||
.then((res) => (res.ok ? res.json() : []))
|
||||
.then((payload) => {
|
||||
if (cancelled || !Array.isArray(payload)) return;
|
||||
setSessions(dedupeSidebarSessions(payload));
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) setSessions([]);
|
||||
});
|
||||
const reload = () => {
|
||||
fetch("/api/claw/sessions", { cache: "no-store" })
|
||||
.then((res) => (res.ok ? res.json() : []))
|
||||
.then((payload) => {
|
||||
if (cancelled || !Array.isArray(payload)) return;
|
||||
setSessions(dedupeSidebarSessions(payload));
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) setSessions([]);
|
||||
});
|
||||
};
|
||||
reload();
|
||||
const handleUpdate = () => reload();
|
||||
window.addEventListener("claw-session-updated", handleUpdate);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
window.removeEventListener("claw-session-updated", handleUpdate);
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user