@@ -25,7 +25,7 @@ import {
2525} from "@/components/site/control-primitives" ;
2626import { useApiResource } from "@/lib/api/use-api-resource" ;
2727import { useSession } from "@/lib/api/session" ;
28- import { EmptyState , StateBoundary } from "@/components/site/state-views" ;
28+ import { EmptyState , StateActionButton , StateBoundary } from "@/components/site/state-views" ;
2929import { RefreshMeta } from "@/components/site/refresh-meta" ;
3030import { Skeleton } from "@/components/ui/skeleton" ;
3131import { useLocalStorage } from "@/lib/use-local-storage" ;
@@ -112,6 +112,77 @@ export const Route = createFileRoute("/app/runs")({
112112 component : AgentRuns ,
113113} ) ;
114114
115+ /**
116+ * The Agent Runs filter bar: Status/Kind chips, the search box, and the persistent "Reset filters" control
117+ * (#6818). Before that control existed, the ONLY way to clear filters was the button inside the zero-results
118+ * empty state — unreachable while the active filters still matched at least one run. The reset is always
119+ * rendered here (mirroring `AuditFilters`' always-present Reset in components/site/audit-feed.tsx) and enabled
120+ * only once some filter is away from its default.
121+ *
122+ * Exported and fully prop-driven — like `DrawerSurface` — so the filter/reset behavior is unit-testable without
123+ * mounting the routed page (and its router/session/API context).
124+ */
125+ export function RunsFilterBar ( {
126+ status,
127+ kind,
128+ q,
129+ hasActiveFilters,
130+ onStatusChange,
131+ onKindChange,
132+ onQChange,
133+ onReset,
134+ } : {
135+ status : StatusFilter ;
136+ kind : KindFilter ;
137+ q : string ;
138+ hasActiveFilters : boolean ;
139+ onStatusChange : ( value : StatusFilter ) => void ;
140+ onKindChange : ( value : KindFilter ) => void ;
141+ onQChange : ( value : string ) => void ;
142+ onReset : ( ) => void ;
143+ } ) {
144+ return (
145+ < div className = "rounded-token border border-border bg-transparent p-3" >
146+ < div className = "flex flex-wrap items-center gap-2" >
147+ < div className = "inline-flex items-center gap-1.5 font-mono text-token-2xs uppercase tracking-wider text-muted-foreground" >
148+ < Filter className = "size-3.5" />
149+ Status
150+ </ div >
151+ < div className = "flex flex-wrap gap-1" >
152+ { STATUS_FILTERS . map ( ( s ) => (
153+ < Chip key = { s } active = { status === s } onClick = { ( ) => onStatusChange ( s ) } >
154+ { s }
155+ </ Chip >
156+ ) ) }
157+ </ div >
158+ < span aria-hidden className = "ml-2 hidden accent-divider-v-tall sm:inline-block" />
159+ < div className = "inline-flex items-center gap-1.5 font-mono text-token-2xs uppercase tracking-wider text-muted-foreground" >
160+ Kind
161+ </ div >
162+ < div className = "flex flex-wrap gap-1" >
163+ { KIND_FILTERS . map ( ( k ) => (
164+ < Chip key = { k } active = { kind === k } onClick = { ( ) => onKindChange ( k ) } >
165+ { k }
166+ </ Chip >
167+ ) ) }
168+ </ div >
169+ < div className = "ml-auto inline-flex items-center gap-2 rounded-token border border-border bg-background/40 px-2" >
170+ < Search className = "size-3.5 text-muted-foreground" />
171+ < input
172+ value = { q }
173+ onChange = { ( e ) => onQChange ( e . target . value ) }
174+ placeholder = "Search runs…"
175+ className = "w-40 border-0 bg-transparent py-1 text-token-sm outline-none placeholder:text-muted-foreground"
176+ />
177+ </ div >
178+ < StateActionButton onClick = { onReset } disabled = { ! hasActiveFilters } >
179+ Reset filters
180+ </ StateActionButton >
181+ </ div >
182+ </ div >
183+ ) ;
184+ }
185+
115186function AgentRuns ( ) {
116187 const search = Route . useSearch ( ) ;
117188 const navigate = useNavigate ( { from : Route . fullPath } ) ;
@@ -174,6 +245,27 @@ function AgentRuns() {
174245 replace : true ,
175246 } ) ;
176247
248+ /** Any filter away from its default ("all"/"all"/empty search) — drives the filter bar's persistent Reset. */
249+ const hasActiveFilters = status !== "all" || kind !== "all" || q !== "" ;
250+
251+ /** Clear every filter in a single navigation (#6818). Shared by the filter bar's persistent Reset control and
252+ * the zero-results empty state's "Clear filters" button, so the two can never drift apart — mirroring
253+ * AuditFilters' `resetFilters` in components/site/audit-feed.tsx. */
254+ const resetFilters = ( ) => {
255+ navigate ( {
256+ search : ( p : z . infer < typeof searchSchema > ) => ( {
257+ ...p ,
258+ status : undefined ,
259+ kind : undefined ,
260+ q : undefined ,
261+ } ) ,
262+ replace : true ,
263+ } ) ;
264+ toast ( "Filters cleared" , {
265+ description : "Showing all available agent runs again." ,
266+ } ) ;
267+ } ;
268+
177269 const filtered = useMemo ( ( ) => {
178270 const term = q . trim ( ) . toLowerCase ( ) ;
179271 return runs . filter ( ( r ) => {
@@ -253,41 +345,16 @@ function AgentRuns() {
253345 loadingSkeleton = { < RunsListSkeleton /> }
254346 >
255347 < div className = "space-y-5" >
256- < div className = "rounded-token border border-border bg-transparent p-3" >
257- < div className = "flex flex-wrap items-center gap-2" >
258- < div className = "inline-flex items-center gap-1.5 font-mono text-token-2xs uppercase tracking-wider text-muted-foreground" >
259- < Filter className = "size-3.5" />
260- Status
261- </ div >
262- < div className = "flex flex-wrap gap-1" >
263- { STATUS_FILTERS . map ( ( s ) => (
264- < Chip key = { s } active = { status === s } onClick = { ( ) => setStatus ( s ) } >
265- { s }
266- </ Chip >
267- ) ) }
268- </ div >
269- < span aria-hidden className = "ml-2 hidden accent-divider-v-tall sm:inline-block" />
270- < div className = "inline-flex items-center gap-1.5 font-mono text-token-2xs uppercase tracking-wider text-muted-foreground" >
271- Kind
272- </ div >
273- < div className = "flex flex-wrap gap-1" >
274- { KIND_FILTERS . map ( ( k ) => (
275- < Chip key = { k } active = { kind === k } onClick = { ( ) => setKind ( k ) } >
276- { k }
277- </ Chip >
278- ) ) }
279- </ div >
280- < div className = "ml-auto inline-flex items-center gap-2 rounded-token border border-border bg-background/40 px-2" >
281- < Search className = "size-3.5 text-muted-foreground" />
282- < input
283- value = { q }
284- onChange = { ( e ) => setQ ( e . target . value ) }
285- placeholder = "Search runs…"
286- className = "w-40 border-0 bg-transparent py-1 text-token-sm outline-none placeholder:text-muted-foreground"
287- />
288- </ div >
289- </ div >
290- </ div >
348+ < RunsFilterBar
349+ status = { status }
350+ kind = { kind }
351+ q = { q }
352+ hasActiveFilters = { hasActiveFilters }
353+ onStatusChange = { setStatus }
354+ onKindChange = { setKind }
355+ onQChange = { setQ }
356+ onReset = { resetFilters }
357+ />
291358
292359 < SavedViews
293360 current = { { status, kind, q } }
@@ -316,14 +383,7 @@ function AgentRuns() {
316383 action = {
317384 < button
318385 type = "button"
319- onClick = { ( ) => {
320- setStatus ( "all" ) ;
321- setKind ( "all" ) ;
322- setQ ( "" ) ;
323- toast ( "Filters cleared" , {
324- description : "Showing all available agent runs again." ,
325- } ) ;
326- } }
386+ onClick = { resetFilters }
327387 className = "inline-flex min-w-0 items-center justify-center rounded-token border border-border bg-transparent px-3 py-1.5 text-center text-token-xs font-medium text-foreground transition-all duration-150 hover:bg-accent focus-ring motion-reduce:transition-none motion-reduce:active:scale-100 active:scale-[0.98]"
328388 >
329389 Clear filters
0 commit comments