{text}}>
{text}
diff --git a/apps/webapp/app/components/sessions/v1/CloseSessionDialog.tsx b/apps/webapp/app/components/sessions/v1/CloseSessionDialog.tsx
new file mode 100644
index 000000000..7feba8e6d
--- /dev/null
+++ b/apps/webapp/app/components/sessions/v1/CloseSessionDialog.tsx
@@ -0,0 +1,72 @@
+import { XCircleIcon } from "@heroicons/react/24/solid";
+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";
+import { SpinnerWhite } from "~/components/primitives/Spinner";
+
+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 (
+
+ Close this session?
+
+
+ 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.
+
+
+
+
+ );
+}
diff --git a/apps/webapp/app/components/sessions/v1/SessionFilters.tsx b/apps/webapp/app/components/sessions/v1/SessionFilters.tsx
new file mode 100644
index 000000000..9c13b7b4b
--- /dev/null
+++ b/apps/webapp/app/components/sessions/v1/SessionFilters.tsx
@@ -0,0 +1,764 @@
+import * as Ariakit from "@ariakit/react";
+import {
+ CpuChipIcon,
+ FingerPrintIcon,
+ TagIcon,
+ XMarkIcon,
+} from "@heroicons/react/20/solid";
+import { Form } from "@remix-run/react";
+import { ListFilterIcon } from "lucide-react";
+import { type ReactNode, useCallback, useMemo, useState } from "react";
+import { z } from "zod";
+import { StatusIcon } from "~/assets/icons/StatusIcon";
+import { TaskIcon } from "~/assets/icons/TaskIcon";
+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 {
+ ComboBox,
+ SelectButtonItem,
+ SelectItem,
+ SelectList,
+ SelectPopover,
+ SelectProvider,
+ SelectTrigger,
+ shortcutFromIndex,
+} from "~/components/primitives/Select";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from "~/components/primitives/Tooltip";
+import { useOptimisticLocation } from "~/hooks/useOptimisticLocation";
+import { useSearchParams } from "~/hooks/useSearchParam";
+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
;
+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 SessionFiltersProps = {
+ hasFilters: boolean;
+ possibleTypes?: string[];
+};
+
+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 (
+
+
+
+
+ {hasFilters && (
+
+ )}
+
+ );
+}
+
+const filterTypes = [
+ {
+ name: "statuses",
+ title: "Status",
+ icon: ,
+ },
+ { name: "types", title: "Type", icon: },
+ {
+ name: "taskIdentifiers",
+ title: "Task",
+ icon: ,
+ },
+ {
+ name: "externalId",
+ title: "External ID",
+ icon: ,
+ },
+ { name: "tags", title: "Tags", icon: },
+] as const;
+
+type FilterType = (typeof filterTypes)[number]["name"];
+
+const shortcut = { key: "f" };
+
+function FilterMenu(props: SessionFiltersProps) {
+ const [filterType, setFilterType] = useState();
+
+ const filterTrigger = (
+
+
+
+ }
+ variant={"secondary/small"}
+ shortcut={shortcut}
+ tooltipTitle={"Filter sessions"}
+ >
+ Filter
+
+ );
+
+ return (
+
+ >
+ );
+}
+
+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