@@ -121,32 +121,38 @@ export function stripFavoriteSearchParam(search: string): string {
121121 return result . length > 0 ? `?${ result } ` : "" ;
122122}
123123
124- /** A favorite is active when its marker param is in the URL and the pathname still matches. */
124+ /**
125+ * A favorite is active only while the URL is exactly the view it saved: its marker param is
126+ * present AND the rest of the URL still matches. Changing any filter on the page diverges the
127+ * URL from the favorite, so it deactivates (and the regular menu item takes over).
128+ */
125129export function isFavoriteActive (
126130 favorite : FavoritePage ,
127131 pathname : string ,
128132 search : string
129133) : boolean {
130- const favoritePath = favorite . url . split ( "?" ) [ 0 ] ;
131134 return (
132- pathname === favoritePath &&
133- new URLSearchParams ( search ) . get ( FAVORITE_SEARCH_PARAM ) === favorite . id
135+ new URLSearchParams ( search ) . get ( FAVORITE_SEARCH_PARAM ) === favorite . id &&
136+ favorite . url === pathname + stripFavoriteSearchParam ( search )
134137 ) ;
135138}
136139
137140/**
138141 * The id of the favorite driving the current view: the URL's marker param, but only when it
139- * belongs to one of the current user's favorites. A marker arriving via someone else's shared
140- * link (or a removed favorite's stale link) resolves to undefined, so the page loads with
141- * regular menu highlighting instead of suppressing it.
142+ * belongs to one of the current user's favorites AND the URL still matches that favorite's
143+ * saved view. A marker from someone else's shared link, a removed favorite's stale link, or a
144+ * view whose filters have since been changed resolves to undefined, so regular menu
145+ * highlighting applies.
142146 */
143147export function useActiveFavoriteId ( ) : string | undefined {
144148 const location = useLocation ( ) ;
145149 const favorites = useFavorites ( ) ;
146150
147151 const marker = new URLSearchParams ( location . search ) . get ( FAVORITE_SEARCH_PARAM ) ;
148152 if ( ! marker ) return undefined ;
149- return favorites . some ( ( favorite ) => favorite . id === marker ) ? marker : undefined ;
153+ const favorite = favorites . find ( ( f ) => f . id === marker ) ;
154+ if ( ! favorite ) return undefined ;
155+ return isFavoriteActive ( favorite , location . pathname , location . search ) ? marker : undefined ;
150156}
151157
152158type PageMeta = {
@@ -253,6 +259,10 @@ export function resolvePageMeta(pathname: string): PageMeta {
253259
254260const MAX_LABEL_LENGTH = 50 ;
255261
262+ function truncateLabel ( label : string ) : string {
263+ return label . length > MAX_LABEL_LENGTH ? `${ label . slice ( 0 , MAX_LABEL_LENGTH - 1 ) } …` : label ;
264+ }
265+
256266/**
257267 * Short id for a detail page whose last URL segment is a friendly id ("run_cmryyza…05hrqq9n").
258268 * Uses the same 8-character tail the dashboard tables display, so the label matches what the
@@ -267,31 +277,115 @@ function detailIdFromPath(pathname: string): string | undefined {
267277 return undefined ;
268278}
269279
280+ /** Task type filter on the Tasks page (?types=…) becomes the whole favorite name. */
281+ const TASK_TYPE_LABELS : Record < string , string > = {
282+ AGENT : "Agent tasks" ,
283+ STANDARD : "Standard tasks" ,
284+ SCHEDULED : "Scheduled tasks" ,
285+ } ;
286+
287+ /** "COMPLETED_SUCCESSFULLY" -> "Completed successfully", "history" -> "History". */
288+ function humanizeValue ( value : string ) : string {
289+ const lowered = value . toLowerCase ( ) . replaceAll ( "_" , " " ) ;
290+ return lowered . charAt ( 0 ) . toUpperCase ( ) + lowered . slice ( 1 ) ;
291+ }
292+
293+ /** Pagination/UI-state params that never describe what the user filtered. */
294+ const NON_FILTER_PARAMS = [ FAVORITE_SEARCH_PARAM , "cursor" , "direction" , "page" , "span" ] ;
295+
296+ /**
297+ * Summarize a filtered view's search params into a short, selective descriptor for the favorite
298+ * label ("Completed successfully, last 7d +2"). The best-known filters are named (at most two);
299+ * everything else only counts toward a "+N" so heavily filtered views stay readable.
300+ */
301+ function describeFilters ( search : string ) : string | undefined {
302+ const params = new URLSearchParams ( search ) ;
303+ for ( const param of NON_FILTER_PARAMS ) {
304+ params . delete ( param ) ;
305+ }
306+
307+ const parts : string [ ] = [ ] ;
308+ const consumed = new Set < string > ( ) ;
309+
310+ const take = ( key : string , describe : ( values : string [ ] ) => string | undefined ) => {
311+ const values = params . getAll ( key ) . filter ( ( value ) => value . length > 0 ) ;
312+ if ( values . length === 0 ) return ;
313+ consumed . add ( key ) ;
314+ const described = describe ( values ) ;
315+ if ( described ) parts . push ( described ) ;
316+ } ;
317+
318+ // Priority order: the filters most likely to identify the view come first
319+ take ( "statuses" , ( v ) => ( v . length === 1 ? humanizeValue ( v [ 0 ] ) : `${ v . length } statuses` ) ) ;
320+ take ( "levels" , ( v ) => ( v . length === 1 ? humanizeValue ( v [ 0 ] ) : `${ v . length } levels` ) ) ;
321+ take ( "tasks" , ( v ) => ( v . length === 1 ? v [ 0 ] : `${ v . length } tasks` ) ) ;
322+ take ( "queues" , ( v ) => ( v . length === 1 ? v [ 0 ] . replace ( / ^ t a s k \/ / , "" ) : `${ v . length } queues` ) ) ;
323+ take ( "tags" , ( v ) => ( v . length === 1 ? v [ 0 ] : `${ v . length } tags` ) ) ;
324+ take ( "period" , ( v ) => `last ${ v [ 0 ] } ` ) ;
325+ if ( params . has ( "from" ) || params . has ( "to" ) ) {
326+ consumed . add ( "from" ) ;
327+ consumed . add ( "to" ) ;
328+ parts . push ( "custom range" ) ;
329+ }
330+ take ( "versions" , ( v ) => ( v . length === 1 ? v [ 0 ] : `${ v . length } versions` ) ) ;
331+ take ( "machines" , ( v ) => ( v . length === 1 ? v [ 0 ] : `${ v . length } machines` ) ) ;
332+ take ( "tab" , ( v ) => humanizeValue ( v [ 0 ] ) ) ;
333+ // The runs list appends rootOnly=false by default; only the non-default value is a filter
334+ take ( "rootOnly" , ( v ) => ( v [ 0 ] === "true" ? "root only" : undefined ) ) ;
335+
336+ const remaining = new Set ( [ ...params . keys ( ) ] . filter ( ( key ) => ! consumed . has ( key ) ) ) . size ;
337+
338+ const MAX_NAMED_PARTS = 2 ;
339+ const shown = parts . slice ( 0 , MAX_NAMED_PARTS ) ;
340+ const extra = parts . length - shown . length + remaining ;
341+
342+ if ( shown . length === 0 ) {
343+ return extra > 0 ? `${ extra } filter${ extra === 1 ? "" : "s" } ` : undefined ;
344+ }
345+ return shown . join ( ", " ) + ( extra > 0 ? ` +${ extra } ` : "" ) ;
346+ }
347+
270348/**
271- * Compose the default side menu label for a favorited page. List pages keep their nav name
272- * ("Queues"); detail pages get an identifying prefix ("Queue: email-queue", or the short id for
273- * friendly-id pages: "Run: 05hrqq9n"). Users can rename.
349+ * Compose the default side menu label for a favorited page. Plain list pages keep their nav
350+ * name ("Queues"); detail pages get an identifying prefix ("Queue: email-queue", or the short
351+ * id for friendly-id pages: "Run: 05hrqq9n"); filtered views summarize their filters ("Runs:
352+ * Completed successfully, last 7d"). Users can always rename.
274353 */
275- export function buildFavoriteLabel ( pathname : string , pageTitle : string | undefined ) : string {
354+ export function buildFavoriteLabel (
355+ pathname : string ,
356+ search : string ,
357+ pageTitle : string | undefined
358+ ) : string {
276359 const meta = resolvePageMeta ( pathname ) ;
277360 const title = pageTitle ?. trim ( ) ;
278361 const prefix = meta . singular ?? meta . name ;
279362
280- // Generic titles ("Runs", "Run") identify nothing on a detail page ; prefer the short id
363+ // Generic titles ("Runs", "Run") identify nothing on their own ; prefer ids/filters from the URL
281364 const isGenericTitle =
282365 ! title ||
283366 title . toLowerCase ( ) === meta . name . toLowerCase ( ) ||
284367 title . toLowerCase ( ) === prefix . toLowerCase ( ) ;
285368
286369 if ( isGenericTitle ) {
370+ // The Tasks page filtered to a single task type takes that type as the whole name
371+ if ( meta . icon === "tasks" ) {
372+ const types = new URLSearchParams ( search ) . getAll ( "types" ) ;
373+ if ( types . length === 1 && TASK_TYPE_LABELS [ types [ 0 ] ] ) {
374+ return TASK_TYPE_LABELS [ types [ 0 ] ] ;
375+ }
376+ }
377+
287378 const detailId = detailIdFromPath ( pathname ) ;
288- return detailId ? `${ prefix } : ${ detailId } ` : meta . name ;
379+ if ( detailId ) return `${ prefix } : ${ detailId } ` ;
380+
381+ const filters = describeFilters ( search ) ;
382+ return truncateLabel ( filters ? `${ meta . name } : ${ filters } ` : meta . name ) ;
289383 }
290384
291385 const label = title . toLowerCase ( ) . startsWith ( prefix . toLowerCase ( ) )
292386 ? title
293387 : `${ prefix } : ${ title } ` ;
294- return label . length > MAX_LABEL_LENGTH ? ` ${ label . slice ( 0 , MAX_LABEL_LENGTH - 1 ) } …` : label ;
388+ return truncateLabel ( label ) ;
295389}
296390
297391/**
0 commit comments