diff --git a/UI/src/components/AgentModals.tsx b/UI/src/components/AgentModals.tsx
index d7acc26..e5632fb 100644
--- a/UI/src/components/AgentModals.tsx
+++ b/UI/src/components/AgentModals.tsx
@@ -5,6 +5,7 @@ import type { Agent } from "../api/types";
import { Modal } from "./Modal";
import { Button, Input, Textarea } from "./ui";
import { CodeBlock } from "./CodeBlock";
+import { ScrollRow } from "./ScrollRow";
// ── Create / edit form ───────────────────────────────────────────────────────────
@@ -229,7 +230,7 @@ curl -X POST ${origin}/v1/change-requests/{request_id}/consume -H "x-api-key: ${
)}
-
+
{tabs.map((t) => (
))}
-
+
{tab === "openclaw" && (
diff --git a/UI/src/components/ScrollRow.tsx b/UI/src/components/ScrollRow.tsx
new file mode 100644
index 0000000..7bbeb49
--- /dev/null
+++ b/UI/src/components/ScrollRow.tsx
@@ -0,0 +1,63 @@
+import { ReactNode, useCallback, useEffect, useRef, useState } from "react";
+
+const FADE = 28; // px of taper at each overflowing edge
+
+/**
+ * Horizontally scrollable row of chips/tabs. Fades whichever edge still has
+ * content behind it, so a clipped item reads as "scroll for more" rather than
+ * as a layout bug — and contains its overscroll so a sideways swipe never
+ * triggers the browser's back gesture.
+ */
+export function ScrollRow({
+ children,
+ className = "",
+ wrapperClassName = "",
+ bleed = true,
+}: {
+ children: ReactNode;
+ /** Applied to the scrolling element (layout of the items). */
+ className?: string;
+ /** Applied to the positioned wrapper (borders, margins). */
+ wrapperClassName?: string;
+ /** Extend to the screen edges on phones so the row reads as scrollable. */
+ bleed?: boolean;
+}) {
+ const ref = useRef
(null);
+ const [edges, setEdges] = useState({ start: false, end: false });
+
+ const update = useCallback(() => {
+ const el = ref.current;
+ if (!el) return;
+ const max = el.scrollWidth - el.clientWidth;
+ setEdges({ start: el.scrollLeft > 4, end: max > 4 && el.scrollLeft < max - 4 });
+ }, []);
+
+ useEffect(() => {
+ update();
+ const el = ref.current;
+ if (!el || typeof ResizeObserver === "undefined") return;
+ const ro = new ResizeObserver(update);
+ ro.observe(el);
+ for (const child of Array.from(el.children)) ro.observe(child);
+ return () => ro.disconnect();
+ }, [update]);
+
+ const mask = `linear-gradient(to right, ${
+ edges.start ? `transparent 0, black ${FADE}px` : "black 0"
+ }, ${edges.end ? `black calc(100% - ${FADE}px), transparent 100%` : "black 100%"})`;
+
+ return (
+
+ );
+}
diff --git a/UI/src/pages/Admin.tsx b/UI/src/pages/Admin.tsx
index 0e66198..ebedd91 100644
--- a/UI/src/pages/Admin.tsx
+++ b/UI/src/pages/Admin.tsx
@@ -5,6 +5,7 @@ import type { AdminUser, AuditLog, Agent, GlobalSettings } from "../api/types";
import { useAuth } from "../context/AuthContext";
import { Button, Card, PageHeader, Spinner, Toggle } from "../components/ui";
import { ConfirmSheet } from "../components/ConfirmSheet";
+import { ScrollRow } from "../components/ScrollRow";
import { relativeTime } from "../utils";
type Tab = "users" | "agents" | "settings" | "audit";
@@ -21,7 +22,7 @@ export function AdminPage() {
return (
-
+
{tabs.map((t) => (
))}
-
+
{tab === "users" &&
}
{tab === "agents" &&
}
{tab === "settings" &&
}
diff --git a/UI/src/pages/Requests.tsx b/UI/src/pages/Requests.tsx
index ebb75ec..18c3f38 100644
--- a/UI/src/pages/Requests.tsx
+++ b/UI/src/pages/Requests.tsx
@@ -7,6 +7,7 @@ import { Card, EmptyState, PageHeader, Spinner, Button } from "../components/ui"
import { StateBadge } from "../components/StateBadge";
import { AgentAvatar } from "../components/AgentAvatar";
import { ChevronRightIcon } from "../components/icons";
+import { ScrollRow } from "../components/ScrollRow";
import { relativeTime } from "../utils";
const STATES: RequestState[] = [
@@ -61,7 +62,7 @@ export function RequestsPage() {
const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
const chip = (active: boolean) =>
- `tap-sm min-h-9 shrink-0 snap-start rounded-full border px-3.5 text-sm font-medium transition ${
+ `tap-sm min-h-9 shrink-0 rounded-full border px-3.5 text-sm font-medium transition ${
active
? "border-primary/40 bg-primary/15 text-primary"
: "border-border-strong bg-surface text-muted hover:text-text"
@@ -71,10 +72,11 @@ export function RequestsPage() {
-
- {/* Swipeable filter pills — bleeds to the screen edges on phones so the
- row reads as scrollable. */}
-
+
+ {/* No scroll-snap: it clamps the resting scrollLeft to the container's
+ padding, which eats the left gutter, and flick-snapping through
+ short chips feels wrong anyway. */}
+
{
setStateFilter("");
@@ -96,7 +98,7 @@ export function RequestsPage() {
{s.replace("_", " ").toLowerCase()}
))}
-
+