picoclaw/web/frontend/src/components/chat/session-history-menu.tsx
wenjie dea06c391c
feat(web): add agent management UI and improve launcher integration (#1358)
* Improve the web launcher and gateway integration across backend and frontend.

- add runtime model availability checks for local and OAuth-backed models
- support launcher-driven gateway host overrides and websocket URL resolution
- add gateway log clearing and keep incremental log sync consistent after resets
- migrate session history APIs to JSONL metadata-backed storage with legacy fallback
- expose session titles and improve chat history loading and error handling
- move shared backend runtime helpers into the web utils package
- avoid blocking web startup when automatic onboard initialization fails
- add backend tests covering gateway readiness, host resolution, models, logs, and sessions

* feat(agent): add skills and tools management APIs and UI

- add backend APIs to list, view, import, and delete skills
- add tool status and toggle endpoints with dependency-aware config updates
- add agent skills/tools pages, routes, sidebar entries, and i18n strings
- add backend tests for the new skills and tools flows

* chore(frontend): upgrade shadcn to 4.0.5 and refresh lockfile

* chore(web): keep backend dist placeholder tracked
2026-03-11 18:37:00 +08:00

109 lines
3.7 KiB
TypeScript

import { IconHistory, IconTrash } from "@tabler/icons-react"
import dayjs from "dayjs"
import type { RefObject } from "react"
import { useTranslation } from "react-i18next"
import type { SessionSummary } from "@/api/sessions"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { ScrollArea } from "@/components/ui/scroll-area"
interface SessionHistoryMenuProps {
sessions: SessionSummary[]
activeSessionId: string
hasMore: boolean
loadError: boolean
loadErrorMessage: string
observerRef: RefObject<HTMLDivElement | null>
onOpenChange: (open: boolean) => void
onSwitchSession: (sessionId: string) => void
onDeleteSession: (sessionId: string) => void
}
export function SessionHistoryMenu({
sessions,
activeSessionId,
hasMore,
loadError,
loadErrorMessage,
observerRef,
onOpenChange,
onSwitchSession,
onDeleteSession,
}: SessionHistoryMenuProps) {
const { t } = useTranslation()
return (
<DropdownMenu onOpenChange={onOpenChange}>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" className="h-9 gap-2">
<IconHistory className="size-4" />
<span className="hidden sm:inline">{t("chat.history")}</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-72">
<ScrollArea className="max-h-[300px]">
{loadError && (
<DropdownMenuItem disabled>
<span className="text-destructive text-xs">
{loadErrorMessage}
</span>
</DropdownMenuItem>
)}
{sessions.length === 0 && !loadError ? (
<DropdownMenuItem disabled>
<span className="text-muted-foreground text-xs">
{t("chat.noHistory")}
</span>
</DropdownMenuItem>
) : (
sessions.map((session) => (
<DropdownMenuItem
key={session.id}
className={`group relative my-0.5 flex flex-col items-start gap-0.5 pr-8 ${
session.id === activeSessionId ? "bg-accent" : ""
}`}
onClick={() => onSwitchSession(session.id)}
>
<span className="line-clamp-1 text-sm font-medium">
{session.title || session.preview}
</span>
<span className="text-muted-foreground text-xs">
{t("chat.messagesCount", {
count: session.message_count,
})}{" "}
· {dayjs(session.updated).fromNow()}
</span>
<Button
variant="ghost"
size="icon"
aria-label={t("chat.deleteSession")}
className="text-muted-foreground hover:bg-destructive/10 hover:text-destructive absolute top-1/2 right-2 h-6 w-6 -translate-y-1/2 opacity-0 transition-opacity group-hover:opacity-100"
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
onDeleteSession(session.id)
}}
>
<IconTrash className="h-4 w-4" />
</Button>
</DropdownMenuItem>
))
)}
{hasMore && sessions.length > 0 && (
<div ref={observerRef} className="py-2 text-center">
<span className="text-muted-foreground animate-pulse text-xs">
{t("chat.loadingMore")}
</span>
</div>
)}
</ScrollArea>
</DropdownMenuContent>
</DropdownMenu>
)
}