chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:32:57 +08:00
commit cd420f9332
4811 changed files with 884702 additions and 0 deletions
@@ -0,0 +1,69 @@
import { DialogClose } from "@radix-ui/react-dialog";
import { Form, useNavigation } from "@remix-run/react";
import { Button } from "~/components/primitives/Buttons";
import { DialogContent, DialogHeader } from "~/components/primitives/Dialog";
import { FormButtons } from "~/components/primitives/FormButtons";
import { Input } from "~/components/primitives/Input";
import { Label } from "~/components/primitives/Label";
import { Paragraph } from "~/components/primitives/Paragraph";
type CloseSessionDialogProps = {
sessionParam: string;
environmentId: string;
redirectPath: string;
};
export function CloseSessionDialog({
sessionParam,
environmentId,
redirectPath,
}: CloseSessionDialogProps) {
const navigation = useNavigation();
const formAction = `/resources/sessions/${encodeURIComponent(sessionParam)}/close`;
const isLoading = navigation.formAction === formAction;
return (
<DialogContent key="close-session">
<DialogHeader>Close this session?</DialogHeader>
<div className="flex flex-col gap-3 pt-3">
<Paragraph>
Closing a session is permanent. The session will no longer accept new input or trigger new
runs. Any in-flight run continues until it finishes on its own.
</Paragraph>
<Form action={formAction} method="post" className="flex flex-col gap-3">
<input type="hidden" name="redirectUrl" value={redirectPath} />
<input type="hidden" name="environmentId" value={environmentId} />
<div className="flex flex-col gap-1">
<Label htmlFor="close-session-reason">Reason (optional)</Label>
<Input
id="close-session-reason"
name="reason"
placeholder="e.g. user signed out, ticket resolved"
variant="medium"
spellCheck={false}
autoFocus
/>
</div>
<FormButtons
confirmButton={
<Button
type="submit"
variant="danger/medium"
disabled={isLoading}
shortcut={{ modifiers: ["mod"], key: "enter" }}
>
{isLoading ? "Closing…" : "Close session"}
</Button>
}
cancelButton={
<DialogClose asChild>
<Button variant={"secondary/medium"}>Cancel</Button>
</DialogClose>
}
/>
</Form>
</div>
</DialogContent>
);
}
@@ -0,0 +1,861 @@
import * as Ariakit from "@ariakit/react";
import {
CpuChipIcon,
FingerPrintIcon,
PlusIcon,
TagIcon,
XMarkIcon,
} from "@heroicons/react/20/solid";
import { Form } from "@remix-run/react";
import { type ReactNode, useCallback, useMemo, useRef, useState } from "react";
import { z } from "zod";
import { StatusIcon } from "~/assets/icons/StatusIcon";
import { CubeSparkleIcon } from "~/assets/icons/CubeSparkleIcon";
import { AppliedFilter } from "~/components/primitives/AppliedFilter";
import { Input } from "~/components/primitives/Input";
import { Label } from "~/components/primitives/Label";
import { Paragraph } from "~/components/primitives/Paragraph";
import { MiddleTruncate } from "~/components/primitives/MiddleTruncate";
import {
ComboBox,
SelectButtonItem,
SelectGroup,
SelectGroupLabel,
SelectItem,
SelectList,
SelectPopover,
SelectProvider,
SelectTrigger,
shortcutFromIndex,
} from "~/components/primitives/Select";
import { ShortcutKey } from "~/components/primitives/ShortcutKey";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "~/components/primitives/Tooltip";
import { useOptimisticLocation } from "~/hooks/useOptimisticLocation";
import { useSearchParams } from "~/hooks/useSearchParam";
import { useShortcutKeys } from "~/hooks/useShortcutKeys";
import { Button } from "../../primitives/Buttons";
import { appliedSummary, FilterMenuProvider, TimeFilter } from "../../runs/v3/SharedFilters";
import {
allSessionStatuses,
descriptionForSessionStatus,
SessionStatusCombo,
sessionStatusTitle,
} from "./SessionStatus";
const StringOrStringArray = z.preprocess(
(value) => (typeof value === "string" ? [value] : value),
z.array(z.string()).optional()
);
export const SessionStatus = z.enum(allSessionStatuses);
export const SessionListSearchFilters = z.object({
cursor: z.string().optional(),
direction: z.enum(["forward", "backward"]).optional(),
statuses: z.preprocess(
(value) => (typeof value === "string" ? [value] : value),
SessionStatus.array().optional()
),
types: StringOrStringArray,
taskIdentifiers: StringOrStringArray,
externalId: z.string().optional(),
tags: StringOrStringArray,
period: z.preprocess((value) => (value === "all" ? undefined : value), z.string().optional()),
from: z.coerce.number().optional(),
to: z.coerce.number().optional(),
});
export type SessionListSearchFilters = z.infer<typeof SessionListSearchFilters>;
export type SessionListSearchFilterKey = keyof SessionListSearchFilters;
export function getSessionFiltersFromSearchParams(
searchParams: URLSearchParams
): SessionListSearchFilters {
function listOrUndefined(key: string) {
const values = searchParams.getAll(key).filter((v) => v.length > 0);
return values.length > 0 ? values : undefined;
}
const params = {
cursor: searchParams.get("cursor") ?? undefined,
direction: searchParams.get("direction") ?? undefined,
statuses: listOrUndefined("statuses"),
types: listOrUndefined("types"),
taskIdentifiers: listOrUndefined("taskIdentifiers"),
externalId: searchParams.get("externalId") ?? undefined,
tags: listOrUndefined("tags"),
period: searchParams.get("period") ?? undefined,
from: searchParams.get("from") ?? undefined,
to: searchParams.get("to") ?? undefined,
};
const parsed = SessionListSearchFilters.safeParse(params);
if (!parsed.success) {
return {};
}
return parsed.data;
}
type PossibleTask = { slug: string; isInLatestDeployment: boolean };
type SessionFiltersProps = {
hasFilters: boolean;
possibleTypes?: string[];
possibleTasks: PossibleTask[];
};
export function SessionFilters(props: SessionFiltersProps) {
const location = useOptimisticLocation();
const searchParams = new URLSearchParams(location.search);
const hasFilters =
searchParams.has("statuses") ||
searchParams.has("types") ||
searchParams.has("taskIdentifiers") ||
searchParams.has("externalId") ||
searchParams.has("tags");
return (
<div className="flex flex-row flex-wrap items-center gap-1.5">
<PermanentStatusFilter />
<PermanentTaskIdentifierFilter possibleTasks={props.possibleTasks} />
<TimeFilter shortcut={{ key: "d" }} />
<AppliedFilters />
<FilterMenu {...props} />
{hasFilters && (
<Form className="-ml-1 h-6">
<Button
variant="minimal/small"
LeadingIcon={XMarkIcon}
tooltip="Clear all filters"
className="group-hover/button:bg-transparent"
leadingIconClassName="group-hover/button:text-text-bright"
/>
</Form>
)}
</div>
);
}
const filterTypes = [
{ name: "types", title: "Type", icon: <CpuChipIcon className="size-4" /> },
{
name: "externalId",
title: "External ID",
icon: <FingerPrintIcon className="size-4" />,
},
{ name: "tags", title: "Tags", icon: <TagIcon className="size-4" /> },
] as const;
type FilterType = (typeof filterTypes)[number]["name"];
const shortcut = { key: "f" };
function FilterMenu(props: SessionFiltersProps) {
const [filterType, setFilterType] = useState<FilterType | undefined>();
const filterTrigger = (
<SelectTrigger
icon={
<div className="flex size-4 items-center justify-center">
<PlusIcon className="size-3.5" />
</div>
}
variant={"secondary/small"}
shortcut={shortcut}
tooltipTitle={"More filters"}
className="pl-1 pr-2"
>
More filters
</SelectTrigger>
);
return (
<FilterMenuProvider onClose={() => setFilterType(undefined)}>
{(search, setSearch) => (
<Menu
searchValue={search}
clearSearchValue={() => setSearch("")}
trigger={filterTrigger}
filterType={filterType}
setFilterType={setFilterType}
{...props}
/>
)}
</FilterMenuProvider>
);
}
function AppliedFilters() {
return (
<>
<AppliedTypeFilter />
<AppliedExternalIdFilter />
<AppliedTagsFilter />
</>
);
}
type MenuProps = {
searchValue: string;
clearSearchValue: () => void;
trigger: React.ReactNode;
filterType: FilterType | undefined;
setFilterType: (filterType: FilterType | undefined) => void;
} & SessionFiltersProps;
function Menu(props: MenuProps) {
switch (props.filterType) {
case undefined:
return <MainMenu {...props} />;
case "types":
return <TypeDropdown onClose={() => props.setFilterType(undefined)} {...props} />;
case "externalId":
return <ExternalIdDropdown onClose={() => props.setFilterType(undefined)} {...props} />;
case "tags":
return <TagsDropdown onClose={() => props.setFilterType(undefined)} {...props} />;
}
}
function MainMenu({ searchValue, trigger, clearSearchValue, setFilterType }: MenuProps) {
const filtered = useMemo(() => {
return filterTypes.filter((item) =>
item.title.toLowerCase().includes(searchValue.toLowerCase())
);
}, [searchValue]);
return (
<SelectProvider virtualFocus={true}>
{trigger}
<SelectPopover>
<ComboBox placeholder={"Filter by..."} shortcut={shortcut} value={searchValue} />
<SelectList>
{filtered.map((type, index) => (
<SelectButtonItem
key={type.name}
onClick={() => {
clearSearchValue();
setFilterType(type.name);
}}
icon={type.icon}
shortcut={shortcutFromIndex(index, { shortcutsEnabled: true })}
>
<span className="text-text-bright">{type.title}</span>
</SelectButtonItem>
))}
</SelectList>
</SelectPopover>
</SelectProvider>
);
}
const statusItems = allSessionStatuses.map((status) => ({
title: sessionStatusTitle(status),
value: status,
}));
function StatusDropdown({
trigger,
clearSearchValue,
searchValue,
onClose,
}: {
trigger: ReactNode;
clearSearchValue: () => void;
searchValue: string;
onClose?: () => void;
}) {
const { values, replace } = useSearchParams();
const handleChange = (next: string[]) => {
clearSearchValue();
replace({ statuses: next, cursor: undefined, direction: undefined });
};
const filtered = useMemo(() => {
return statusItems.filter((item) =>
item.title.toLowerCase().includes(searchValue.toLowerCase())
);
}, [searchValue]);
return (
<SelectProvider value={values("statuses")} setValue={handleChange} virtualFocus={true}>
{trigger}
<SelectPopover
className="min-w-0 max-w-[min(240px,var(--popover-available-width))]"
hideOnEscape={() => {
if (onClose) {
onClose();
return false;
}
return true;
}}
>
<ComboBox placeholder={"Filter by status..."} value={searchValue} />
<SelectList>
{filtered.map((item, index) => (
<SelectItem
key={item.value}
value={item.value}
shortcut={shortcutFromIndex(index, { shortcutsEnabled: true })}
>
<TooltipProvider>
<Tooltip>
<TooltipTrigger className="group flex w-full flex-col py-0">
<SessionStatusCombo status={item.value} />
</TooltipTrigger>
<TooltipContent side="right" sideOffset={9}>
<Paragraph variant="extra-small">
{descriptionForSessionStatus(item.value)}
</Paragraph>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</SelectItem>
))}
</SelectList>
</SelectPopover>
</SelectProvider>
);
}
const statusShortcut = { key: "s" };
function PermanentStatusFilter() {
const { values, del } = useSearchParams();
const statuses = values("statuses");
const hasStatuses = statuses.length > 0 && !statuses.every((v) => v === "");
const triggerRef = useRef<HTMLButtonElement>(null);
useShortcutKeys({
shortcut: statusShortcut,
action: (e) => {
e.preventDefault();
e.stopPropagation();
triggerRef.current?.click();
},
});
return (
<FilterMenuProvider>
{(search, setSearch) => (
<StatusDropdown
trigger={
<Ariakit.TooltipProvider timeout={200}>
<Ariakit.TooltipAnchor
render={
// eslint-disable-next-line @typescript-eslint/no-explicit-any
<Ariakit.Select
ref={triggerRef as any}
render={<div className="group cursor-pointer focus-custom" />}
/>
}
>
{hasStatuses ? (
<AppliedFilter
label="Status"
icon={<StatusIcon className="size-4 border-text-bright" />}
value={appliedSummary(
statuses.map((v) =>
sessionStatusTitle(v as (typeof allSessionStatuses)[number])
)
)}
onRemove={() => del(["statuses", "cursor", "direction"])}
variant="secondary/small"
className="pl-1"
/>
) : (
<div className="flex h-6 items-center gap-1 rounded border border-border-bright bg-secondary pl-1 pr-2 text-xs text-text-bright transition group-hover:border-border-brighter group-hover:bg-surface-control">
<div className="grid size-4 place-items-center">
<div className="size-[75%] rounded-full border-2 border-text-bright" />
</div>
<span>Status</span>
</div>
)}
</Ariakit.TooltipAnchor>
<Ariakit.Tooltip className="z-40 cursor-default rounded border border-grid-bright bg-background-bright px-2 py-1.5 text-xs">
<div className="flex items-center gap-2">
<span>Filter by status</span>
<ShortcutKey
className="size-4 flex-none"
shortcut={statusShortcut}
variant="small"
/>
</div>
</Ariakit.Tooltip>
</Ariakit.TooltipProvider>
}
searchValue={search}
clearSearchValue={() => setSearch("")}
/>
)}
</FilterMenuProvider>
);
}
function TypeDropdown({
trigger,
searchValue,
clearSearchValue,
possibleTypes,
onClose,
}: {
trigger: ReactNode;
searchValue: string;
clearSearchValue: () => void;
possibleTypes?: string[];
onClose?: () => void;
}) {
const { values, replace } = useSearchParams();
const handleChange = (next: string[]) => {
clearSearchValue();
replace({ types: next, cursor: undefined, direction: undefined });
};
const items = useMemo(() => {
const all = possibleTypes && possibleTypes.length > 0 ? possibleTypes : ["chat"];
const seen = new Set(all);
for (const v of values("types")) {
if (!seen.has(v)) {
all.push(v);
seen.add(v);
}
}
return all.filter((t) => t.toLowerCase().includes(searchValue.toLowerCase()));
}, [possibleTypes, searchValue, values]);
return (
<SelectProvider value={values("types")} setValue={handleChange} virtualFocus={true}>
{trigger}
<SelectPopover
className="min-w-0 max-w-[min(240px,var(--popover-available-width))]"
hideOnEscape={() => {
if (onClose) {
onClose();
return false;
}
return true;
}}
>
<ComboBox placeholder={"Filter by type..."} value={searchValue} />
<SelectList>
{items.map((value, index) => (
<SelectItem
key={value}
value={value}
shortcut={shortcutFromIndex(index, { shortcutsEnabled: true })}
>
<span className="font-mono text-xs">{value}</span>
</SelectItem>
))}
</SelectList>
</SelectPopover>
</SelectProvider>
);
}
function AppliedTypeFilter() {
const { values, del } = useSearchParams();
const types = values("types");
if (types.length === 0) return null;
return (
<FilterMenuProvider>
{(search, setSearch) => (
<TypeDropdown
trigger={
<Ariakit.Select render={<div className="group cursor-pointer focus-custom" />}>
<AppliedFilter
label="Type"
icon={<CpuChipIcon className="size-4" />}
value={appliedSummary(types)}
onRemove={() => del(["types", "cursor", "direction"])}
variant="secondary/small"
/>
</Ariakit.Select>
}
searchValue={search}
clearSearchValue={() => setSearch("")}
/>
)}
</FilterMenuProvider>
);
}
function TaskIdentifierDropdown({
trigger,
searchValue,
clearSearchValue,
onClose,
possibleTasks,
}: {
trigger: ReactNode;
searchValue: string;
clearSearchValue: () => void;
onClose?: () => void;
possibleTasks: PossibleTask[];
}) {
const { values, replace } = useSearchParams();
const handleChange = (newValues: string[]) => {
clearSearchValue();
replace({
taskIdentifiers: newValues.length > 0 ? newValues : undefined,
cursor: undefined,
direction: undefined,
});
};
const selected = values("taskIdentifiers");
const filtered = useMemo(() => {
// Surface any selected identifiers that aren't in the registry (deleted
// agents) so the dropdown can still show + un-check them.
const seen = new Set(possibleTasks.map((t) => t.slug));
const extras: PossibleTask[] = selected
.filter((slug) => slug && !seen.has(slug))
.map((slug) => ({ slug, isInLatestDeployment: false }));
return [...possibleTasks, ...extras].filter((task) =>
task.slug.toLowerCase().includes(searchValue.toLowerCase())
);
}, [possibleTasks, searchValue, selected]);
return (
<SelectProvider value={selected} setValue={handleChange} virtualFocus={true}>
{trigger}
<SelectPopover
className="min-w-0 max-w-[min(360px,var(--popover-available-width))]"
hideOnEscape={() => {
if (onClose) {
onClose();
return false;
}
return true;
}}
>
<ComboBox placeholder={"Filter by Agent ID..."} value={searchValue} />
<SelectList>
{filtered
.filter((item) => item.isInLatestDeployment)
.map((item) => (
<SelectItem
key={item.slug}
value={item.slug}
icon={<CubeSparkleIcon className="size-4 flex-none text-agents" />}
className="text-text-bright"
>
<MiddleTruncate text={item.slug} />
</SelectItem>
))}
{filtered.some((item) => !item.isInLatestDeployment) && (
<SelectGroup>
<SelectGroupLabel>Archived</SelectGroupLabel>
{filtered
.filter((item) => !item.isInLatestDeployment)
.map((item) => (
<SelectItem
key={item.slug}
value={item.slug}
icon={
<span className="opacity-50">
<CubeSparkleIcon className="size-4 flex-none text-agents" />
</span>
}
className="text-text-bright"
>
<MiddleTruncate text={item.slug} />
</SelectItem>
))}
</SelectGroup>
)}
{filtered.length === 0 && <SelectItem disabled>No agents found</SelectItem>}
</SelectList>
</SelectPopover>
</SelectProvider>
);
}
const taskShortcut = { key: "t" };
function PermanentTaskIdentifierFilter({ possibleTasks }: { possibleTasks: PossibleTask[] }) {
const { values, del } = useSearchParams();
const taskIdentifiers = values("taskIdentifiers");
const hasTasks = taskIdentifiers.length > 0 && !taskIdentifiers.every((v) => v === "");
const triggerRef = useRef<HTMLButtonElement>(null);
useShortcutKeys({
shortcut: taskShortcut,
action: (e) => {
e.preventDefault();
e.stopPropagation();
triggerRef.current?.click();
},
});
return (
<FilterMenuProvider>
{(search, setSearch) => (
<TaskIdentifierDropdown
trigger={
<Ariakit.TooltipProvider timeout={200}>
<Ariakit.TooltipAnchor
render={
// eslint-disable-next-line @typescript-eslint/no-explicit-any
<Ariakit.Select
ref={triggerRef as any}
render={<div className="group cursor-pointer focus-custom" />}
/>
}
>
{hasTasks ? (
<AppliedFilter
label="Agent ID"
icon={<CubeSparkleIcon className="size-4 text-text-bright" />}
value={appliedSummary(taskIdentifiers)}
onRemove={() => del(["taskIdentifiers", "cursor", "direction"])}
variant="secondary/small"
className="pl-1"
/>
) : (
<div className="flex h-6 items-center gap-1.5 rounded border border-border-bright bg-secondary pl-1 pr-2 text-xs text-text-bright transition group-hover:border-border-brighter group-hover:bg-surface-control">
<CubeSparkleIcon className="size-4 text-text-bright" />
<span>Agent ID</span>
</div>
)}
</Ariakit.TooltipAnchor>
<Ariakit.Tooltip className="z-40 cursor-default rounded border border-grid-bright bg-background-bright px-2 py-1.5 text-xs">
<div className="flex items-center gap-2">
<span>Filter by Agent ID</span>
<ShortcutKey
className="size-4 flex-none"
shortcut={taskShortcut}
variant="small"
/>
</div>
</Ariakit.Tooltip>
</Ariakit.TooltipProvider>
}
searchValue={search}
clearSearchValue={() => setSearch("")}
possibleTasks={possibleTasks}
/>
)}
</FilterMenuProvider>
);
}
function ExternalIdDropdown({
trigger,
searchValue,
clearSearchValue,
onClose,
}: {
trigger: ReactNode;
searchValue: string;
clearSearchValue: () => void;
onClose?: () => void;
}) {
const [open, setOpen] = useState<boolean | undefined>();
const { value, replace } = useSearchParams();
const current = value("externalId");
const [draft, setDraft] = useState(current ?? "");
const apply = useCallback(() => {
clearSearchValue();
replace({
externalId: draft.trim() === "" ? undefined : draft.trim(),
cursor: undefined,
direction: undefined,
});
setOpen(false);
}, [clearSearchValue, draft, replace]);
return (
<SelectProvider virtualFocus={true} open={open} setOpen={setOpen}>
{trigger}
<SelectPopover
hideOnEnter={false}
hideOnEscape={() => {
if (onClose) {
onClose();
return false;
}
return true;
}}
className="max-w-[min(36ch,var(--popover-available-width))]"
>
<div className="flex flex-col gap-4 p-3">
<div className="flex flex-col gap-1">
<Label>External ID</Label>
<Input
placeholder="user-supplied id"
value={draft}
onChange={(e) => setDraft(e.target.value)}
variant="small"
className="w-[33ch] font-mono"
spellCheck={false}
/>
</div>
<div className="flex justify-between gap-1 border-t border-grid-dimmed pt-3">
<Button variant="tertiary/small" onClick={() => setOpen(false)}>
Cancel
</Button>
<Button
variant="secondary/small"
shortcut={{
modifiers: ["mod"],
key: "Enter",
enabledOnInputElements: true,
}}
onClick={apply}
>
Apply
</Button>
</div>
</div>
</SelectPopover>
</SelectProvider>
);
}
function AppliedExternalIdFilter() {
const { value, del } = useSearchParams();
const externalId = value("externalId");
if (!externalId) return null;
return (
<FilterMenuProvider>
{(search, setSearch) => (
<ExternalIdDropdown
trigger={
<Ariakit.Select render={<div className="group cursor-pointer focus-custom" />}>
<AppliedFilter
label="External ID"
icon={<FingerPrintIcon className="size-4" />}
value={externalId}
onRemove={() => del(["externalId", "cursor", "direction"])}
variant="secondary/small"
/>
</Ariakit.Select>
}
searchValue={search}
clearSearchValue={() => setSearch("")}
/>
)}
</FilterMenuProvider>
);
}
function TagsDropdown({
trigger,
searchValue,
clearSearchValue,
onClose,
}: {
trigger: ReactNode;
searchValue: string;
clearSearchValue: () => void;
onClose?: () => void;
}) {
const [open, setOpen] = useState<boolean | undefined>();
const { values, replace } = useSearchParams();
const current = values("tags");
const [draft, setDraft] = useState(current.join(", "));
const apply = useCallback(() => {
clearSearchValue();
const next = draft
.split(/[,\n]/)
.map((t) => t.trim())
.filter((t) => t.length > 0);
replace({
tags: next.length === 0 ? undefined : next,
cursor: undefined,
direction: undefined,
});
setOpen(false);
}, [clearSearchValue, draft, replace]);
return (
<SelectProvider virtualFocus={true} open={open} setOpen={setOpen}>
{trigger}
<SelectPopover
hideOnEnter={false}
hideOnEscape={() => {
if (onClose) {
onClose();
return false;
}
return true;
}}
className="max-w-[min(40ch,var(--popover-available-width))]"
>
<div className="flex flex-col gap-4 p-3">
<div className="flex flex-col gap-1">
<Label>Tags</Label>
<Input
placeholder="tag1, tag2"
value={draft}
onChange={(e) => setDraft(e.target.value)}
variant="small"
className="w-[37ch] font-mono"
spellCheck={false}
/>
<Paragraph variant="extra-small/dimmed">
Comma-separated. Matches sessions with any of these tags.
</Paragraph>
</div>
<div className="flex justify-between gap-1 border-t border-grid-dimmed pt-3">
<Button variant="secondary/small" onClick={() => setOpen(false)}>
Cancel
</Button>
<Button
variant="secondary/small"
shortcut={{
modifiers: ["mod"],
key: "Enter",
enabledOnInputElements: true,
}}
onClick={apply}
>
Apply
</Button>
</div>
</div>
</SelectPopover>
</SelectProvider>
);
}
function AppliedTagsFilter() {
const { values, del } = useSearchParams();
const tags = values("tags");
if (tags.length === 0) return null;
return (
<FilterMenuProvider>
{(search, setSearch) => (
<TagsDropdown
trigger={
<Ariakit.Select render={<div className="group cursor-pointer focus-custom" />}>
<AppliedFilter
label="Tags"
icon={<TagIcon className="size-4" />}
value={appliedSummary(tags)}
onRemove={() => del(["tags", "cursor", "direction"])}
variant="secondary/small"
/>
</Ariakit.Select>
}
searchValue={search}
clearSearchValue={() => setSearch("")}
/>
)}
</FilterMenuProvider>
);
}
@@ -0,0 +1,97 @@
import { CheckCircleIcon, ClockIcon } from "@heroicons/react/20/solid";
import assertNever from "assert-never";
import { type SessionStatus } from "~/services/sessionsRepository/sessionsRepository.server";
import { cn } from "~/utils/cn";
export const allSessionStatuses = ["ACTIVE", "CLOSED", "EXPIRED"] as const satisfies Readonly<
Array<SessionStatus>
>;
const descriptions: Record<SessionStatus, string> = {
ACTIVE: "The session is open and can receive input or schedule new runs.",
CLOSED: "The session was closed; no further input or runs can be triggered against it.",
EXPIRED: "The session passed its expiry time without being closed explicitly.",
};
export function descriptionForSessionStatus(status: SessionStatus): string {
return descriptions[status];
}
export function sessionStatusTitle(status: SessionStatus): string {
switch (status) {
case "ACTIVE":
return "Active";
case "CLOSED":
return "Closed";
case "EXPIRED":
return "Expired";
default:
assertNever(status);
}
}
export function sessionStatusColor(status: SessionStatus): string {
switch (status) {
case "ACTIVE":
return "text-pending";
case "CLOSED":
return "text-success";
case "EXPIRED":
return "text-text-dimmed";
default:
assertNever(status);
}
}
export function SessionStatusIcon({
status,
className,
pulse = true,
}: {
status: SessionStatus;
className: string;
pulse?: boolean;
}) {
switch (status) {
case "ACTIVE":
return (
<span className={cn("inline-flex items-center justify-center", className)}>
<span className="relative flex size-2">
{pulse && (
<span className="absolute h-full w-full animate-ping rounded-full border border-pending opacity-100 duration-1000" />
)}
<span className="size-2 rounded-full bg-pending" />
</span>
</span>
);
case "CLOSED":
return <CheckCircleIcon className={cn(sessionStatusColor(status), className)} />;
case "EXPIRED":
return <ClockIcon className={cn(sessionStatusColor(status), className)} />;
default:
assertNever(status);
}
}
export function SessionStatusLabel({ status }: { status: SessionStatus }) {
return <span className={sessionStatusColor(status)}>{sessionStatusTitle(status)}</span>;
}
export function SessionStatusCombo({
status,
className,
iconClassName,
pulse = true,
}: {
status: SessionStatus;
className?: string;
iconClassName?: string;
pulse?: boolean;
}) {
return (
<span className={cn("flex items-center gap-1", className)}>
<SessionStatusIcon status={status} className={cn("size-4", iconClassName)} pulse={pulse} />
<SessionStatusLabel status={status} />
</span>
);
}
@@ -0,0 +1,241 @@
import { ArrowRightIcon } from "@heroicons/react/20/solid";
import { CheckIcon } from "@heroicons/react/24/solid";
import { useLocation, useNavigation } from "@remix-run/react";
import { formatDuration } from "@trigger.dev/core/v3/utils/durations";
import { RunsIconExtraSmall } from "~/assets/icons/RunsIcon";
import { MiddleTruncate } from "~/components/primitives/MiddleTruncate";
import { DateTime } from "~/components/primitives/DateTime";
import { Paragraph } from "~/components/primitives/Paragraph";
import { PopoverMenuItem } from "~/components/primitives/Popover";
import { Spinner } from "~/components/primitives/Spinner";
import {
Table,
TableBlankRow,
TableBody,
TableCell,
TableCellMenu,
TableHeader,
TableHeaderCell,
TableRow,
} from "~/components/primitives/Table";
import { SimpleTooltip } from "~/components/primitives/Tooltip";
import { LiveTimer } from "~/components/runs/v3/LiveTimer";
import { RunTag } from "~/components/runs/v3/RunTag";
import { useEnvironment } from "~/hooks/useEnvironment";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import {
type SessionListItem,
type SessionList,
} from "~/presenters/v3/SessionListPresenter.server";
import { v3RunPath, v3RunsPath, v3SessionPath } from "~/utils/pathBuilder";
import {
descriptionForSessionStatus,
SessionStatusCombo,
allSessionStatuses,
} from "./SessionStatus";
type SessionsTableProps = Pick<SessionList, "sessions" | "filters" | "hasFilters"> & {
showTopBorder?: boolean;
stickyHeader?: boolean;
};
export function SessionsTable({
sessions,
hasFilters,
showTopBorder = true,
stickyHeader = false,
}: SessionsTableProps) {
const navigation = useNavigation();
const location = useLocation();
const isLoading =
navigation.state !== "idle" && navigation.location?.pathname === location.pathname;
const organization = useOrganization();
const project = useProject();
const environment = useEnvironment();
return (
<Table
className="max-h-full overflow-y-auto"
showTopBorder={showTopBorder}
stickyHeader={stickyHeader}
>
<TableHeader>
<TableRow>
<TableHeaderCell>ID</TableHeaderCell>
<TableHeaderCell
tooltip={
<div className="flex flex-col divide-y divide-grid-dimmed">
{allSessionStatuses.map((status) => (
<div
key={status}
className="grid grid-cols-[4rem_1fr] gap-x-3 py-2 first:pt-1 last:pb-1"
>
<div className="mb-0.5 flex items-center gap-1.5 whitespace-nowrap">
<SessionStatusCombo status={status} />
</div>
<Paragraph variant="extra-small" className="text-wrap! text-text-dimmed">
{descriptionForSessionStatus(status)}
</Paragraph>
</div>
))}
</div>
}
disableTooltipHoverableContent
>
Status
</TableHeaderCell>
<TableHeaderCell>Type</TableHeaderCell>
<TableHeaderCell>Agent ID</TableHeaderCell>
<TableHeaderCell>Test</TableHeaderCell>
<TableHeaderCell>Tags</TableHeaderCell>
<TableHeaderCell>Created</TableHeaderCell>
<TableHeaderCell>Duration</TableHeaderCell>
<TableHeaderCell>
<span className="sr-only">Actions</span>
</TableHeaderCell>
</TableRow>
</TableHeader>
<TableBody>
{sessions.length === 0 ? (
<TableBlankRow colSpan={9}>
<div className="flex items-center justify-center">
<Paragraph className="w-auto">
{hasFilters
? "No sessions match these filters"
: "No sessions in this environment yet"}
</Paragraph>
</div>
</TableBlankRow>
) : (
sessions.map((session) => {
const runPath = session.currentRunFriendlyId
? v3RunPath(organization, project, environment, {
friendlyId: session.currentRunFriendlyId,
})
: undefined;
const displayId = session.externalId ?? session.friendlyId;
const sessionPath = v3SessionPath(organization, project, environment, {
friendlyId: session.friendlyId,
});
const allRunsPath = v3RunsPath(organization, project, environment, {
tags: [`chat:${displayId}`],
});
return (
<TableRow key={session.id}>
<TableCell to={sessionPath} isTabbableCell>
<div className="w-[28ch]">
<MiddleTruncate text={displayId} className="font-mono text-xs" />
</div>
</TableCell>
<TableCell to={sessionPath}>
<SimpleTooltip
content={descriptionForSessionStatus(session.status)}
disableHoverableContent
button={<SessionStatusCombo status={session.status} />}
/>
</TableCell>
<TableCell to={sessionPath}>
<span className="font-mono text-xs">{session.type}</span>
</TableCell>
<TableCell to={sessionPath}>
<div className="w-[24ch]">
<MiddleTruncate text={session.taskIdentifier} className="font-mono text-xs" />
</div>
</TableCell>
<TableCell to={sessionPath}>
<span className="sr-only">{session.isTest ? "Yes" : "No"}</span>
{session.isTest ? (
<CheckIcon
aria-hidden
className="size-4 text-text-dimmed group-hover/table-row:text-text-bright"
/>
) : (
<span aria-hidden className="text-text-dimmed">
</span>
)}
</TableCell>
<TableCell to={sessionPath}>
{session.tags.length > 0 ? (
<div className="flex flex-wrap gap-1">
{session.tags.map((tag) => (
<RunTag key={tag} tag={tag} />
))}
</div>
) : (
<span className="text-text-dimmed"></span>
)}
</TableCell>
<TableCell to={sessionPath}>
<DateTime date={session.createdAt} />
</TableCell>
<TableCell to={sessionPath} className="w-[1%]" actionClassName="pr-0 tabular-nums">
<SessionDuration session={session} />
</TableCell>
<SessionActionsCell runPath={runPath} allRunsPath={allRunsPath} />
</TableRow>
);
})
)}
{isLoading && (
<TableBlankRow
colSpan={9}
className="absolute left-0 top-0 flex h-full w-full items-center justify-center gap-2 bg-background-deep/90"
>
<Spinner /> <span className="text-text-dimmed">Loading</span>
</TableBlankRow>
)}
</TableBody>
</Table>
);
}
function SessionDuration({ session }: { session: SessionListItem }) {
// Active sessions tick live; closed/expired sessions freeze at the
// moment they ended (closedAt for explicit closes, expiresAt when the
// TTL ran out without a close call).
const endedAt =
session.status === "CLOSED"
? session.closedAt
: session.status === "EXPIRED"
? session.expiresAt
: undefined;
if (endedAt) {
return (
<>{formatDuration(new Date(session.createdAt), new Date(endedAt), { style: "short" })}</>
);
}
return <LiveTimer startTime={new Date(session.createdAt)} />;
}
function SessionActionsCell({ runPath, allRunsPath }: { runPath?: string; allRunsPath: string }) {
return (
<TableCellMenu
isSticky
popoverContent={
<>
{runPath && (
<PopoverMenuItem
to={runPath}
icon={ArrowRightIcon}
leadingIconClassName="text-runs"
title="View current run"
/>
)}
<PopoverMenuItem
to={allRunsPath}
icon={RunsIconExtraSmall}
leadingIconClassName="text-runs"
title="View all runs"
/>
</>
}
/>
);
}