From 3ad421965cfe4b5bc658938ae5d2d5532705ad37 Mon Sep 17 00:00:00 2001 From: Forgotten Date: Fri, 20 Feb 2026 10:33:18 -0600 Subject: [PATCH] fix(ui): responsive tab bar, activity row wrapping, and layout tweaks Make PageTabBar render a native select on mobile, allow ActivityRow text to wrap on narrow viewports, and minor layout adjustments in AgentDetail and Issues pages. Co-Authored-By: Claude Opus 4.6 --- ui/src/components/ActivityRow.tsx | 2 +- ui/src/components/PageTabBar.tsx | 27 +++++- ui/src/pages/AgentDetail.tsx | 138 ++++++++++++++++++------------ ui/src/pages/Issues.tsx | 4 +- 4 files changed, 114 insertions(+), 57 deletions(-) diff --git a/ui/src/components/ActivityRow.tsx b/ui/src/components/ActivityRow.tsx index bdd314a8..26fc5db2 100644 --- a/ui/src/components/ActivityRow.tsx +++ b/ui/src/components/ActivityRow.tsx @@ -105,7 +105,7 @@ export function ActivityRow({ event, agentMap, entityNameMap, className }: Activ return (
void; +} + +export function PageTabBar({ items, value, onValueChange }: PageTabBarProps) { + const { isMobile } = useSidebar(); + + if (isMobile && value !== undefined && onValueChange) { + return ( + + ); + } + return ( {items.map((item) => ( diff --git a/ui/src/pages/AgentDetail.tsx b/ui/src/pages/AgentDetail.tsx index 090f43e1..a881f857 100644 --- a/ui/src/pages/AgentDetail.tsx +++ b/ui/src/pages/AgentDetail.tsx @@ -6,6 +6,7 @@ import { heartbeatsApi } from "../api/heartbeats"; import { activityApi } from "../api/activity"; import { issuesApi } from "../api/issues"; import { usePanel } from "../context/PanelContext"; +import { useSidebar } from "../context/SidebarContext"; import { useCompany } from "../context/CompanyContext"; import { useDialog } from "../context/DialogContext"; import { useBreadcrumbs } from "../context/BreadcrumbContext"; @@ -47,6 +48,7 @@ import { EyeOff, Copy, ChevronRight, + ArrowLeft, } from "lucide-react"; import { Input } from "@/components/ui/input"; import type { Agent, HeartbeatRun, HeartbeatRunEvent, AgentRuntimeState } from "@paperclip/shared"; @@ -809,8 +811,59 @@ function ConfigurationTab({ /* ---- Runs Tab ---- */ +function RunListItem({ run, isSelected, agentId }: { run: HeartbeatRun; isSelected: boolean; agentId: string }) { + const navigate = useNavigate(); + const statusInfo = runStatusIcons[run.status] ?? { icon: Clock, color: "text-neutral-400" }; + const StatusIcon = statusInfo.icon; + const metrics = runMetrics(run); + const summary = run.resultJson + ? String((run.resultJson as Record).summary ?? (run.resultJson as Record).result ?? "") + : run.error ?? ""; + + return ( + + ); +} + function RunsTab({ runs, companyId, agentId, selectedRunId, adapterType }: { runs: HeartbeatRun[]; companyId: string; agentId: string; selectedRunId: string | null; adapterType: string }) { const navigate = useNavigate(); + const { isMobile } = useSidebar(); if (runs.length === 0) { return

No runs yet.

; @@ -821,10 +874,36 @@ function RunsTab({ runs, companyId, agentId, selectedRunId, adapterType }: { run (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() ); - // Auto-select latest run when no run is selected - const effectiveRunId = selectedRunId ?? sorted[0]?.id ?? null; + // On mobile, don't auto-select so the list shows first; on desktop, auto-select latest + const effectiveRunId = isMobile ? selectedRunId : (selectedRunId ?? sorted[0]?.id ?? null); const selectedRun = sorted.find((r) => r.id === effectiveRunId) ?? null; + // Mobile: show either run list OR run detail with back button + if (isMobile) { + if (selectedRun) { + return ( +
+ + +
+ ); + } + return ( +
+ {sorted.map((run) => ( + + ))} +
+ ); + } + + // Desktop: side-by-side layout return (
{/* Left: run list — border stretches full height, content sticks */} @@ -833,56 +912,9 @@ function RunsTab({ runs, companyId, agentId, selectedRunId, adapterType }: { run selectedRun ? "w-72" : "w-full", )}>
- {sorted.map((run) => { - const statusInfo = runStatusIcons[run.status] ?? { icon: Clock, color: "text-neutral-400" }; - const StatusIcon = statusInfo.icon; - const isSelected = run.id === effectiveRunId; - const metrics = runMetrics(run); - const summary = run.resultJson - ? String((run.resultJson as Record).summary ?? (run.resultJson as Record).result ?? "") - : run.error ?? ""; - - return ( - - ); - })} + {sorted.map((run) => ( + + ))}
@@ -1753,7 +1785,7 @@ function KeysTab({ agentId }: { agentId: string }) { const revokedKeys = (keys ?? []).filter((k: AgentKey) => k.revokedAt); return ( -
+
{/* New token banner */} {newToken && (
diff --git a/ui/src/pages/Issues.tsx b/ui/src/pages/Issues.tsx index eaa12008..cb29bae0 100644 --- a/ui/src/pages/Issues.tsx +++ b/ui/src/pages/Issues.tsx @@ -108,9 +108,9 @@ export function Issues() { return (
-
+
setTab(v as TabFilter)}> - + setTab(v as TabFilter)} />