diff --git a/web/frontend/src/components/agent/hub/market-skill-card.tsx b/web/frontend/src/components/agent/hub/market-skill-card.tsx index 64493ddf4..f3ee426a1 100644 --- a/web/frontend/src/components/agent/hub/market-skill-card.tsx +++ b/web/frontend/src/components/agent/hub/market-skill-card.tsx @@ -38,7 +38,7 @@ export function MarketSkillCard({ return ( {result.installed && ( @@ -51,16 +51,16 @@ export function MarketSkillCard({ {result.display_name || result.slug} - + {result.registry_name} {result.installed ? ( - + {t("pages.agent.skills.marketplace_installed")} ) : null} -
+
{result.slug} {result.version ? ( @@ -78,7 +78,7 @@ export function MarketSkillCard({ href={result.url} target="_blank" rel="noreferrer" - className="inline-flex text-xs text-primary/80 transition-colors hover:text-primary hover:underline hover:underline-offset-4" + className="text-primary/80 hover:text-primary inline-flex text-xs transition-colors hover:underline hover:underline-offset-4" > {result.url} @@ -109,7 +109,7 @@ export function MarketSkillCard({ variant="outline" size="xs" onClick={onViewInstalled} - className="w-full shadow-sm hover:bg-muted" + className="hover:bg-muted w-full shadow-sm" > {t("pages.agent.skills.marketplace_view_installed")} diff --git a/web/frontend/src/components/agent/hub/tool-support.ts b/web/frontend/src/components/agent/hub/tool-support.ts index 257f9c12a..1553b156a 100644 --- a/web/frontend/src/components/agent/hub/tool-support.ts +++ b/web/frontend/src/components/agent/hub/tool-support.ts @@ -2,7 +2,9 @@ import type { TFunction } from "i18next" import type { ToolSupportItem } from "@/api/tools" -type MarketplaceTool = Pick | undefined +type MarketplaceTool = + | Pick + | undefined export interface UnavailableToolMessage { key: "search" | "install" diff --git a/web/frontend/src/components/agent/hub/use-hub-marketplace.ts b/web/frontend/src/components/agent/hub/use-hub-marketplace.ts index 07e8c36fb..2777aa376 100644 --- a/web/frontend/src/components/agent/hub/use-hub-marketplace.ts +++ b/web/frontend/src/components/agent/hub/use-hub-marketplace.ts @@ -5,17 +5,17 @@ import { useQueryClient, } from "@tanstack/react-query" import { useNavigate } from "@tanstack/react-router" -import { useEffect, useRef, useState, type UIEvent } from "react" +import { type UIEvent, useEffect, useRef, useState } from "react" import { useTranslation } from "react-i18next" import { toast } from "sonner" import { + type SkillRegistrySearchResult, + type SkillSearchResponse, + type SkillSupportItem, getSkills, installSkill, searchSkills, - type SkillSearchResponse, - type SkillRegistrySearchResult, - type SkillSupportItem, } from "@/api/skills" import { getTools } from "@/api/tools" @@ -71,7 +71,7 @@ export function useHubMarketplace() { Number(pageParam) || 0, ), getNextPageParam: (lastPage: SkillSearchResponse) => - lastPage.has_more ? lastPage.next_offset ?? undefined : undefined, + lastPage.has_more ? (lastPage.next_offset ?? undefined) : undefined, enabled: isMarketSearchActive, staleTime: 5 * 60 * 1000, refetchOnMount: false, @@ -112,9 +112,7 @@ export function useHubMarketplace() { !marketSearchData && (isMarketSearchPending || isMarketSearchFetching) const isMarketSearchLoadingMore = - isMarketSearchActive && - Boolean(marketSearchData) && - isFetchingNextPage + isMarketSearchActive && Boolean(marketSearchData) && isFetchingNextPage const installPendingKey = installMutation.isPending && installMutation.variables ? `${installMutation.variables.registry}:${installMutation.variables.slug}` @@ -179,7 +177,9 @@ export function useHubMarketplace() { void fetchNextPage() } - const getInstalledSkill = (installedName?: string): SkillSupportItem | null => { + const getInstalledSkill = ( + installedName?: string, + ): SkillSupportItem | null => { if (!installedName) { return null } diff --git a/web/frontend/src/components/agent/skills/delete-dialog.tsx b/web/frontend/src/components/agent/skills/delete-dialog.tsx index 3dbeed342..1f4eba4c3 100644 --- a/web/frontend/src/components/agent/skills/delete-dialog.tsx +++ b/web/frontend/src/components/agent/skills/delete-dialog.tsx @@ -1,3 +1,6 @@ +import { IconLoader2, IconTrash } from "@tabler/icons-react" +import { useTranslation } from "react-i18next" + import type { SkillSupportItem } from "@/api/skills" import { AlertDialog, @@ -9,8 +12,6 @@ import { AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog" -import { IconLoader2, IconTrash } from "@tabler/icons-react" -import { useTranslation } from "react-i18next" interface DeleteDialogProps { open: boolean diff --git a/web/frontend/src/components/agent/skills/detail-sheet.tsx b/web/frontend/src/components/agent/skills/detail-sheet.tsx index 699366bf5..e6f2c75a6 100644 --- a/web/frontend/src/components/agent/skills/detail-sheet.tsx +++ b/web/frontend/src/components/agent/skills/detail-sheet.tsx @@ -23,10 +23,7 @@ import { Skeleton } from "@/components/ui/skeleton" import { cn } from "@/lib/utils" import { OriginBadge } from "./origin-badge" -import { - getOriginLabel, - getSkillOriginKind, -} from "./origin-utils" +import { getOriginLabel, getSkillOriginKind } from "./origin-utils" import type { SkillDetailView } from "./types" const DETAIL_VIEWS = [ @@ -86,7 +83,8 @@ export function DetailSheet({
- {activeSkillDetail?.name || t("pages.agent.skills.viewer_title")} + {activeSkillDetail?.name || + t("pages.agent.skills.viewer_title")} {activeSkillDetail?.description || diff --git a/web/frontend/src/components/agent/skills/filter-bar.tsx b/web/frontend/src/components/agent/skills/filter-bar.tsx index 303fd6f60..033609ea6 100644 --- a/web/frontend/src/components/agent/skills/filter-bar.tsx +++ b/web/frontend/src/components/agent/skills/filter-bar.tsx @@ -1,8 +1,4 @@ -import { - IconLayoutGrid, - IconLayoutList, - IconSearch, -} from "@tabler/icons-react" +import { IconLayoutGrid, IconLayoutList, IconSearch } from "@tabler/icons-react" import { useTranslation } from "react-i18next" import { Input } from "@/components/ui/input" diff --git a/web/frontend/src/components/agent/skills/use-skills-page.ts b/web/frontend/src/components/agent/skills/use-skills-page.ts index ffe9fc90c..7cf4a01ad 100644 --- a/web/frontend/src/components/agent/skills/use-skills-page.ts +++ b/web/frontend/src/components/agent/skills/use-skills-page.ts @@ -150,7 +150,10 @@ export function useSkillsPage() { }, [allSkills, normalizedSearchQuery, sourceFilter]) const sortedSkills = useMemo( - () => [...filteredSkills].sort((left, right) => compareSkills(left, right, sortOrder)), + () => + [...filteredSkills].sort((left, right) => + compareSkills(left, right, sortOrder), + ), [filteredSkills, sortOrder], ) diff --git a/web/frontend/src/components/logs/logs-panel.tsx b/web/frontend/src/components/logs/logs-panel.tsx index 083fb74d8..35148ad44 100644 --- a/web/frontend/src/components/logs/logs-panel.tsx +++ b/web/frontend/src/components/logs/logs-panel.tsx @@ -4,6 +4,15 @@ import { useTranslation } from "react-i18next" import { AnsiLogLine } from "@/components/logs/ansi-log-line" import { ScrollArea } from "@/components/ui/scroll-area" +const AUTO_SCROLL_THRESHOLD_PX = 24 + +function isNearBottom(viewport: HTMLDivElement) { + const distanceToBottom = + viewport.scrollHeight - viewport.scrollTop - viewport.clientHeight + + return distanceToBottom <= AUTO_SCROLL_THRESHOLD_PX +} + type LogsPanelProps = { logs: string[] wrapColumns: number @@ -18,17 +27,57 @@ export function LogsPanel({ measureRef, }: LogsPanelProps) { const { t } = useTranslation() - const scrollRef = useRef(null) + const scrollAreaRef = useRef(null) + const viewportRef = useRef(null) + const shouldStickToBottomRef = useRef(true) useEffect(() => { - if (scrollRef.current) { - scrollRef.current.scrollIntoView({ behavior: "smooth" }) + const scrollArea = scrollAreaRef.current + const viewport = scrollArea?.querySelector( + '[data-slot="scroll-area-viewport"]', + ) + + if (!viewport) { + return + } + + viewportRef.current = viewport + + const updateStickToBottom = () => { + shouldStickToBottomRef.current = isNearBottom(viewport) + } + + updateStickToBottom() + viewport.addEventListener("scroll", updateStickToBottom) + + return () => { + viewport.removeEventListener("scroll", updateStickToBottom) + if (viewportRef.current === viewport) { + viewportRef.current = null + } + } + }, []) + + useEffect(() => { + const viewport = viewportRef.current + if (!viewport) { + return + } + + // Clearing logs or switching runs can replace the buffer with much shorter + // content, so a previously stale "not sticky" state needs to be rechecked. + if (!shouldStickToBottomRef.current) { + shouldStickToBottomRef.current = isNearBottom(viewport) + } + + if (shouldStickToBottomRef.current) { + viewport.scrollTop = viewport.scrollHeight } }, [logs]) return (
- +
)) )} -
diff --git a/web/frontend/src/components/models/edit-model-sheet.tsx b/web/frontend/src/components/models/edit-model-sheet.tsx index 52e2d8d9d..026d2ff97 100644 --- a/web/frontend/src/components/models/edit-model-sheet.tsx +++ b/web/frontend/src/components/models/edit-model-sheet.tsx @@ -161,9 +161,7 @@ export function EditModelSheet({ {!isOAuth && ( diff --git a/web/frontend/src/components/tour/tour-guide.tsx b/web/frontend/src/components/tour/tour-guide.tsx index cc1e6e3a1..bd3761096 100644 --- a/web/frontend/src/components/tour/tour-guide.tsx +++ b/web/frontend/src/components/tour/tour-guide.tsx @@ -7,14 +7,14 @@ import { useAtom } from "jotai" import { useTranslation } from "react-i18next" import { Button } from "@/components/ui/button" +import { cn } from "@/lib/utils" import { + type TourStep, tourAtom, tourCurrentStepAtom, tourIsActiveAtom, - type TourStep, useTourActions, } from "@/store/tour" -import { cn } from "@/lib/utils" interface TourStepConfig { title: string @@ -177,7 +177,7 @@ export function TourGuide() { {targetElement && (
) { +const ScrollArea = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => { return ( ) -} +}) + +ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName function ScrollBar({ className, diff --git a/web/frontend/src/routes/__root.tsx b/web/frontend/src/routes/__root.tsx index d2303a29c..c34558554 100644 --- a/web/frontend/src/routes/__root.tsx +++ b/web/frontend/src/routes/__root.tsx @@ -1,8 +1,4 @@ -import { - Outlet, - createRootRoute, - useRouterState, -} from "@tanstack/react-router" +import { Outlet, createRootRoute, useRouterState } from "@tanstack/react-router" import { TanStackRouterDevtools } from "@tanstack/react-router-devtools" import { useEffect } from "react"