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 <noreply@anthropic.com>
This commit is contained in:
@@ -105,7 +105,7 @@ export function ActivityRow({ event, agentMap, entityNameMap, className }: Activ
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"px-4 py-2 flex items-center justify-between gap-2 text-sm",
|
||||
"px-4 py-2 flex flex-wrap items-center justify-between gap-x-2 gap-y-0.5 text-sm",
|
||||
link && "cursor-pointer hover:bg-accent/50 transition-colors",
|
||||
className,
|
||||
)}
|
||||
|
||||
@@ -1,12 +1,37 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { useSidebar } from "../context/SidebarContext";
|
||||
|
||||
export interface PageTabItem {
|
||||
value: string;
|
||||
label: ReactNode;
|
||||
}
|
||||
|
||||
export function PageTabBar({ items }: { items: PageTabItem[] }) {
|
||||
interface PageTabBarProps {
|
||||
items: PageTabItem[];
|
||||
value?: string;
|
||||
onValueChange?: (value: string) => void;
|
||||
}
|
||||
|
||||
export function PageTabBar({ items, value, onValueChange }: PageTabBarProps) {
|
||||
const { isMobile } = useSidebar();
|
||||
|
||||
if (isMobile && value !== undefined && onValueChange) {
|
||||
return (
|
||||
<select
|
||||
value={value}
|
||||
onChange={(e) => onValueChange(e.target.value)}
|
||||
className="h-8 rounded-md border border-border bg-background px-2 py-1 text-sm focus:outline-none focus:ring-1 focus:ring-ring"
|
||||
>
|
||||
{items.map((item) => (
|
||||
<option key={item.value} value={item.value}>
|
||||
{typeof item.label === "string" ? item.label : item.value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TabsList variant="line">
|
||||
{items.map((item) => (
|
||||
|
||||
@@ -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,34 +811,10 @@ function ConfigurationTab({
|
||||
|
||||
/* ---- Runs Tab ---- */
|
||||
|
||||
function RunsTab({ runs, companyId, agentId, selectedRunId, adapterType }: { runs: HeartbeatRun[]; companyId: string; agentId: string; selectedRunId: string | null; adapterType: string }) {
|
||||
function RunListItem({ run, isSelected, agentId }: { run: HeartbeatRun; isSelected: boolean; agentId: string }) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
if (runs.length === 0) {
|
||||
return <p className="text-sm text-muted-foreground">No runs yet.</p>;
|
||||
}
|
||||
|
||||
// Sort by created descending
|
||||
const sorted = [...runs].sort(
|
||||
(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;
|
||||
const selectedRun = sorted.find((r) => r.id === effectiveRunId) ?? null;
|
||||
|
||||
return (
|
||||
<div className="flex gap-0">
|
||||
{/* Left: run list — border stretches full height, content sticks */}
|
||||
<div className={cn(
|
||||
"shrink-0 border border-border rounded-lg",
|
||||
selectedRun ? "w-72" : "w-full",
|
||||
)}>
|
||||
<div className="sticky top-4 overflow-y-auto" style={{ maxHeight: "calc(100vh - 2rem)" }}>
|
||||
{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<string, unknown>).summary ?? (run.resultJson as Record<string, unknown>).result ?? "")
|
||||
@@ -844,7 +822,6 @@ function RunsTab({ runs, companyId, agentId, selectedRunId, adapterType }: { run
|
||||
|
||||
return (
|
||||
<button
|
||||
key={run.id}
|
||||
className={cn(
|
||||
"flex flex-col gap-1 w-full px-3 py-2.5 text-left border-b border-border last:border-b-0 transition-colors",
|
||||
isSelected ? "bg-accent/40" : "hover:bg-accent/20",
|
||||
@@ -882,7 +859,62 @@ function RunsTab({ runs, companyId, agentId, selectedRunId, adapterType }: { run
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
}
|
||||
|
||||
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 <p className="text-sm text-muted-foreground">No runs yet.</p>;
|
||||
}
|
||||
|
||||
// Sort by created descending
|
||||
const sorted = [...runs].sort(
|
||||
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
||||
);
|
||||
|
||||
// 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 (
|
||||
<div className="space-y-3">
|
||||
<button
|
||||
className="flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
onClick={() => navigate(`/agents/${agentId}/runs`)}
|
||||
>
|
||||
<ArrowLeft className="h-3.5 w-3.5" />
|
||||
Back to runs
|
||||
</button>
|
||||
<RunDetail key={selectedRun.id} run={selectedRun} adapterType={adapterType} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="border border-border rounded-lg">
|
||||
{sorted.map((run) => (
|
||||
<RunListItem key={run.id} run={run} isSelected={false} agentId={agentId} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Desktop: side-by-side layout
|
||||
return (
|
||||
<div className="flex gap-0">
|
||||
{/* Left: run list — border stretches full height, content sticks */}
|
||||
<div className={cn(
|
||||
"shrink-0 border border-border rounded-lg",
|
||||
selectedRun ? "w-72" : "w-full",
|
||||
)}>
|
||||
<div className="sticky top-4 overflow-y-auto" style={{ maxHeight: "calc(100vh - 2rem)" }}>
|
||||
{sorted.map((run) => (
|
||||
<RunListItem key={run.id} run={run} isSelected={run.id === effectiveRunId} agentId={agentId} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1753,7 +1785,7 @@ function KeysTab({ agentId }: { agentId: string }) {
|
||||
const revokedKeys = (keys ?? []).filter((k: AgentKey) => k.revokedAt);
|
||||
|
||||
return (
|
||||
<div className="space-y-6 max-w-2xl">
|
||||
<div className="space-y-6">
|
||||
{/* New token banner */}
|
||||
{newToken && (
|
||||
<div className="border border-yellow-600/40 bg-yellow-500/5 rounded-lg p-4 space-y-2">
|
||||
|
||||
@@ -108,9 +108,9 @@ export function Issues() {
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<Tabs value={tab} onValueChange={(v) => setTab(v as TabFilter)}>
|
||||
<PageTabBar items={[...issueTabItems]} />
|
||||
<PageTabBar items={[...issueTabItems]} value={tab} onValueChange={(v) => setTab(v as TabFilter)} />
|
||||
</Tabs>
|
||||
<Button size="sm" onClick={() => openNewIssue()}>
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
|
||||
Reference in New Issue
Block a user