@@ -35,15 +35,8 @@ import {
3535 collapsibleHandleClassName ,
3636} from "~/components/primitives/Resizable" ;
3737import { SearchInput } from "~/components/primitives/SearchInput" ;
38- import {
39- ComboboxProvider ,
40- SelectItem ,
41- SelectList ,
42- SelectPopover ,
43- SelectProvider ,
44- SelectTrigger ,
45- shortcutFromIndex ,
46- } from "~/components/primitives/Select" ;
38+ import SegmentedControl from "~/components/primitives/SegmentedControl" ;
39+ import { ShortcutKey } from "~/components/primitives/ShortcutKey" ;
4740import { Spinner } from "~/components/primitives/Spinner" ;
4841import {
4942 Table ,
@@ -69,6 +62,7 @@ import { useFuzzyFilter } from "~/hooks/useFuzzyFilter";
6962import { useOrganization } from "~/hooks/useOrganizations" ;
7063import { useProject } from "~/hooks/useProject" ;
7164import { useSearchParams } from "~/hooks/useSearchParam" ;
65+ import { useShortcutKeys } from "~/hooks/useShortcutKeys" ;
7266import { findProjectBySlug } from "~/models/project.server" ;
7367import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server" ;
7468import {
@@ -169,6 +163,25 @@ function parseTypesParam(values: string[]): UnifiedTaskKind[] {
169163 return values . filter ( ( v ) : v is UnifiedTaskKind => VALID_KINDS . has ( v as UnifiedTaskKind ) ) ;
170164}
171165
166+ const ALL_TASK_TYPES = "ALL" ;
167+
168+ type TaskTypeSegment = typeof ALL_TASK_TYPES | UnifiedTaskKind ;
169+
170+ /** Segmented control options. "All" shows as a word; the task kinds show as
171+ * icon-only segments. Every segment has a tooltip (label + shortcut).
172+ * Order = shortcut keys 0–3. */
173+ const TASK_TYPE_SEGMENTS : {
174+ value : TaskTypeSegment ;
175+ tooltip : string ;
176+ text ?: string ;
177+ source ?: UnifiedTaskKind ;
178+ } [ ] = [
179+ { value : ALL_TASK_TYPES , tooltip : "All tasks" , text : "All" } ,
180+ { value : "AGENT" , tooltip : "Agent tasks" , source : "AGENT" } ,
181+ { value : "STANDARD" , tooltip : "Standard tasks" , source : "STANDARD" } ,
182+ { value : "SCHEDULED" , tooltip : "Scheduled tasks" , source : "SCHEDULED" } ,
183+ ] ;
184+
172185const PAGE_SIZE = 25 ;
173186
174187export default function Page ( ) {
@@ -217,7 +230,8 @@ export default function Page() {
217230
218231 const selectedTypes = useMemo ( ( ) => {
219232 const raw = parseTypesParam ( values ( "types" ) ) ;
220- return raw . length > 0 ? new Set ( raw ) : null ; // null = all
233+ // Single-select: one kind filters to it; none or legacy multi → all.
234+ return raw . length === 1 ? new Set ( raw ) : null ; // null = all
221235 } , [ values ] ) ;
222236
223237 const { filteredItems } = useFuzzyFilter < UnifiedTaskListItem > ( {
@@ -265,7 +279,7 @@ export default function Page() {
265279 < div className = "flex h-full flex-col overflow-hidden" >
266280 < div className = "flex shrink-0 items-center justify-between gap-1.5 p-2" >
267281 < div className = "flex flex-1 items-center gap-1.5" >
268- < SearchInput placeholder = "Search tasks…" autoFocus resetParams = { [ "page" ] } />
282+ < SearchInput placeholder = "Search tasks…" resetParams = { [ "page" ] } />
269283 < TaskTypeFilter />
270284 </ div >
271285 < div className = "flex items-center gap-1.5" >
@@ -496,48 +510,80 @@ function RunningCell({ state }: { state: UnifiedRunningState | undefined }) {
496510function TaskTypeFilter ( ) {
497511 const { values, replace } = useSearchParams ( ) ;
498512 const raw = parseTypesParam ( values ( "types" ) ) ;
499- const isAll = raw . length === 0 || raw . length === KIND_OPTIONS . length ;
500- // No filter → preselect everything so users can uncheck from "all".
501- const popoverValue = isAll ? KIND_OPTIONS . map ( ( k ) => k . value ) : raw ;
502-
503- const handleChange = ( next : string [ ] ) => {
504- // Empty or fully-selected → drop the param so the default (all) applies. Always reset page.
505- if ( next . length === 0 || next . length === KIND_OPTIONS . length ) {
506- replace ( { types : undefined , page : undefined } ) ;
507- } else {
508- replace ( { types : next , page : undefined } ) ;
509- }
513+ // Single-select: exactly one kind selects it, anything else falls back to All.
514+ const current : TaskTypeSegment = raw . length === 1 ? raw [ 0 ] : ALL_TASK_TYPES ;
515+
516+ const select = ( value : string ) => {
517+ // "All" drops the param; a kind filters to just that kind. Always reset page.
518+ replace ( { types : value === ALL_TASK_TYPES ? undefined : [ value ] , page : undefined } ) ;
510519 } ;
511520
512- const label = isAll
513- ? "All"
514- : raw . map ( ( v ) => KIND_OPTIONS . find ( ( k ) => k . value === v ) ?. label ?? v ) . join ( ", " ) ;
521+ return (
522+ < >
523+ { TASK_TYPE_SEGMENTS . map ( ( option , index ) => (
524+ < TaskTypeShortcut
525+ key = { option . value }
526+ shortcut = { String ( index ) }
527+ onSelect = { ( ) => select ( option . value ) }
528+ />
529+ ) ) }
530+ < SegmentedControl
531+ name = "task-type"
532+ value = { current }
533+ variant = "secondary/small"
534+ onChange = { select }
535+ options = { TASK_TYPE_SEGMENTS . map ( ( option , index ) => ( {
536+ value : option . value ,
537+ label : < TaskTypeSegmentLabel option = { option } shortcut = { String ( index ) } /> ,
538+ } ) ) }
539+ />
540+ </ >
541+ ) ;
542+ }
543+
544+ // Registers a number-key shortcut that selects one segment.
545+ function TaskTypeShortcut ( { shortcut, onSelect } : { shortcut : string ; onSelect : ( ) => void } ) {
546+ useShortcutKeys ( {
547+ shortcut : { key : shortcut } ,
548+ action : ( event ) => {
549+ event . preventDefault ( ) ;
550+ onSelect ( ) ;
551+ } ,
552+ } ) ;
553+ return null ;
554+ }
515555
556+ function TaskTypeSegmentLabel ( {
557+ option,
558+ shortcut,
559+ } : {
560+ option : ( typeof TASK_TYPE_SEGMENTS ) [ number ] ;
561+ shortcut : string ;
562+ } ) {
516563 return (
517- < ComboboxProvider >
518- < SelectProvider value = { popoverValue } setValue = { handleChange } virtualFocus >
519- < SelectTrigger variant = "secondary/small" dropdownIcon >
520- < span className = "text-text-bright" > Task type: </ span >
521- < span className = "max-w-[180px] truncate text-text-dimmed" > { label } </ span >
522- </ SelectTrigger >
523- < SelectPopover className = "min-w-fit" >
524- < SelectList >
525- { KIND_OPTIONS . map ( ( opt , index ) => (
526- < SelectItem
527- key = { opt . value }
528- value = { opt . value }
529- shortcut = { shortcutFromIndex ( index , { shortcutsEnabled : true } ) }
530- >
531- < span className = "flex items-center gap-2" >
532- < TaskTriggerSourceIcon source = { opt . value } />
533- < span className = "text-text-bright" > { opt . label } </ span >
534- </ span >
535- </ SelectItem >
536- ) ) }
537- </ SelectList >
538- </ SelectPopover >
539- </ SelectProvider >
540- </ ComboboxProvider >
564+ < SimpleTooltip
565+ asChild
566+ button = {
567+ option . source ? (
568+ // -mx-0.5 tightens the icon segment toward a square button.
569+ < span className = "-mx-0.5 flex items-center justify-center" >
570+ < TaskTriggerSourceIcon source = { option . source } />
571+ < span className = "sr-only" > { option . tooltip } </ span >
572+ </ span >
573+ ) : (
574+ < span className = "flex items-center justify-center" > { option . text } </ span >
575+ )
576+ }
577+ content = {
578+ < div className = "flex items-center gap-1" >
579+ < span className = "text-text-bright" > { option . tooltip } </ span >
580+ < ShortcutKey shortcut = { { key : shortcut } } variant = "small" />
581+ </ div >
582+ }
583+ className = "px-2 py-1.5 text-xs"
584+ sideOffset = { 6 }
585+ disableHoverableContent
586+ />
541587 ) ;
542588}
543589
0 commit comments