From 93a0eb74d7db520ae0a80612778301c02e8f5077 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=AD=A6=E9=98=B3?= Date: Wed, 6 May 2026 17:46:49 +0800 Subject: [PATCH] Improve workspace sidebar and file panel --- frontend/app/app/api/claw/files/route.ts | 55 ++- frontend/app/app/assistant.tsx | 23 +- .../assistant-ui/activity-panel.tsx | 223 ++++++++- .../components/assistant-ui/thread-list.tsx | 2 +- .../app/components/assistant-ui/thread.tsx | 52 ++- .../assistant-ui/threadlist-sidebar.tsx | 430 +++++++++++++++--- 6 files changed, 702 insertions(+), 83 deletions(-) diff --git a/frontend/app/app/api/claw/files/route.ts b/frontend/app/app/api/claw/files/route.ts index 57b6265..06cc1fd 100644 --- a/frontend/app/app/api/claw/files/route.ts +++ b/frontend/app/app/api/claw/files/route.ts @@ -1,6 +1,11 @@ -import { readFile, stat } from "node:fs/promises"; +import { readdir, readFile, stat } from "node:fs/promises"; import path from "node:path"; -import { accountBaseRoot, getCurrentAccount } from "@/lib/claw-auth"; +import { + accountBaseRoot, + accountSessionInputRoot, + accountSessionOutputRoot, + getCurrentAccount, +} from "@/lib/claw-auth"; export const runtime = "nodejs"; @@ -11,8 +16,15 @@ export async function GET(req: Request) { const url = new URL(req.url); const requestedPath = url.searchParams.get("path"); + const sessionId = url.searchParams.get("session_id"); + if (!requestedPath && sessionId) { + return Response.json({ + input: await listSessionFiles(account.id, sessionId, "input"), + output: await listSessionFiles(account.id, sessionId, "output"), + }); + } if (!requestedPath) { - return Response.json({ error: "缺少文件路径" }, { status: 400 }); + return Response.json({ error: "缺少文件路径或会话 ID" }, { status: 400 }); } const accountRoot = path.resolve(accountBaseRoot(account.id)); @@ -38,6 +50,43 @@ export async function GET(req: Request) { } } +async function listSessionFiles( + accountId: string, + sessionId: string, + kind: "input" | "output", +) { + const accountRoot = path.resolve(accountBaseRoot(accountId)); + const dir = + kind === "input" + ? accountSessionInputRoot(accountId, sessionId) + : accountSessionOutputRoot(accountId, sessionId); + const resolvedDir = path.resolve(dir); + if (!resolvedDir.startsWith(`${accountRoot}${path.sep}`)) return []; + + try { + const entries = await readdir(resolvedDir, { withFileTypes: true }); + const files = await Promise.all( + entries + .filter((entry) => entry.isFile()) + .map(async (entry) => { + const filePath = path.join(resolvedDir, entry.name); + const fileStat = await stat(filePath); + return { + name: entry.name, + path: filePath, + kind, + size: fileStat.size, + modified_at: fileStat.mtime.toISOString(), + download_url: `/api/claw/files?path=${encodeURIComponent(filePath)}`, + }; + }), + ); + return files.sort((a, b) => b.modified_at.localeCompare(a.modified_at)); + } catch { + return []; + } +} + function contentTypeFor(filePath: string) { const ext = path.extname(filePath).toLowerCase(); if (ext === ".json") return "application/json; charset=utf-8"; diff --git a/frontend/app/app/assistant.tsx b/frontend/app/app/assistant.tsx index 2c891f2..f03613e 100644 --- a/frontend/app/app/assistant.tsx +++ b/frontend/app/app/assistant.tsx @@ -14,16 +14,9 @@ import { } from "@/components/assistant-ui/activity-panel"; import { Thread } from "@/components/assistant-ui/thread"; import { ThreadListSidebar } from "@/components/assistant-ui/threadlist-sidebar"; -import { Separator } from "@/components/ui/separator"; -import { - SidebarInset, - SidebarProvider, - SidebarTrigger, -} from "@/components/ui/sidebar"; +import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"; import { ClawSessionReplayProvider } from "@/lib/claw-session-replay"; import { ClawAccountGate, useClawAccount } from "./claw-account-gate"; -import { ClawLlmSettings } from "./claw-llm-settings"; -import { ClawThemeSwitcher } from "./claw-theme-switcher"; export const Assistant = () => { const { account, isLoading, setAccount } = useClawAccount(); @@ -83,24 +76,12 @@ export const Assistant = () => { -
+
setAccount(null)} /> -
- - -
-
新会话
-
- ZK Data Agent -
-
- - -
diff --git a/frontend/app/components/assistant-ui/activity-panel.tsx b/frontend/app/components/assistant-ui/activity-panel.tsx index 8bbe224..1c3f820 100644 --- a/frontend/app/components/assistant-ui/activity-panel.tsx +++ b/frontend/app/components/assistant-ui/activity-panel.tsx @@ -12,7 +12,9 @@ import { CheckCircle2Icon, ChevronDownIcon, ClockIcon, + DownloadIcon, FileTextIcon, + MessageSquareTextIcon, PanelRightCloseIcon, WrenchIcon, XCircleIcon, @@ -37,8 +39,11 @@ import { cn } from "@/lib/utils"; type ActivityContextValue = { open: boolean; + mode: "activity" | "files"; selectedId: string | null; + sessionId: string | null; openItem: (id: string) => void; + openFiles: (sessionId?: string | null) => void; close: () => void; }; @@ -46,20 +51,36 @@ const ActivityContext = createContext(null); export function ActivityProvider({ children }: { children: ReactNode }) { const [open, setOpen] = useState(false); + const [mode, setMode] = useState<"activity" | "files">("activity"); const [selectedId, setSelectedId] = useState(null); + const [sessionId, setSessionId] = useState(null); const openItem = useCallback((id: string) => { + setMode("activity"); setSelectedId(id); setOpen(true); }, []); + const openFiles = useCallback((nextSessionId?: string | null) => { + setMode("files"); + setSessionId( + nextSessionId ?? + (typeof window !== "undefined" + ? window.localStorage.getItem("claw.activeSessionId") + : null), + ); + setOpen(true); + }, []); const close = useCallback(() => setOpen(false), []); const value = useMemo( () => ({ open, + mode, selectedId, + sessionId, openItem, + openFiles, close, }), - [open, selectedId, openItem, close], + [open, mode, selectedId, sessionId, openItem, openFiles, close], ); return ( @@ -91,7 +112,8 @@ type ActivityItem = { }; export function ActivityPanel() { - const { open, selectedId, openItem, close } = useActivityPanel(); + const { open, mode, selectedId, sessionId, openItem, close } = + useActivityPanel(); const messages = useAuiState((s) => s.thread.messages); const items = useMemo(() => collectActivityItems(messages), [messages]); const selectedMessageId = selectedId ? activityMessageId(selectedId) : null; @@ -101,14 +123,18 @@ export function ActivityPanel() { const prevCountRef = useRef(0); useEffect(() => { - if (items.length > prevCountRef.current) { + if (mode === "activity" && items.length > prevCountRef.current) { openItem(items.at(-1)?.id ?? items[0]?.id ?? ""); } prevCountRef.current = items.length; - }, [items, openItem]); + }, [items, mode, openItem]); if (!open) return null; + if (mode === "files") { + return ; + } + return (