@@ -51,7 +51,6 @@ import {
5151} from '@/lib/api/featureSets' ;
5252import { useSpaces } from '@/stores' ;
5353import type { Space } from '@/lib/api/spaces' ;
54- import { resolveRootBinding } from './prefixMatch' ;
5554
5655/**
5756 * Workspaces page.
@@ -69,19 +68,12 @@ import { resolveRootBinding } from './prefixMatch';
6968 * • OFFLINE + mapped → neutral
7069 */
7170
72- type EntryKind = 'unmapped-live' | 'mapped-live' | 'inherited-live' | ' mapped-offline';
71+ type EntryKind = 'unmapped-live' | 'mapped-live' | 'mapped-offline' ;
7372interface Entry {
7473 id : string ;
7574 kind : EntryKind ;
7675 root : string ;
77- /** This folder's OWN (exact) binding, if any. Drives edit/delete. */
7876 binding : WorkspaceBinding | null ;
79- /**
80- * An ancestor binding this folder resolves through when it has no exact
81- * binding of its own (longest-prefix inheritance — mirrors the gateway
82- * resolver). Drives the "inherits from …" display.
83- */
84- inherited : WorkspaceBinding | null ;
8577 isLive : boolean ;
8678}
8779type Selected = { mode : 'new' } | { mode : 'entry' ; id : string } ;
@@ -149,6 +141,11 @@ export function WorkspacesPage() {
149141 }
150142 } ;
151143
144+ const bindingsByRoot = useMemo ( ( ) => {
145+ const m = new Map < string , WorkspaceBinding > ( ) ;
146+ for ( const b of bindings ) m . set ( b . workspace_root . toLowerCase ( ) , b ) ;
147+ return m ;
148+ } , [ bindings ] ) ;
152149 const fsById = useMemo ( ( ) => {
153150 const m = new Map < string , FeatureSet > ( ) ;
154151 for ( const f of featureSets ) m . set ( f . id , f ) ;
@@ -171,21 +168,12 @@ export function WorkspacesPage() {
171168 const key = root . toLowerCase ( ) ;
172169 if ( seen . has ( key ) ) continue ;
173170 seen . add ( key ) ;
174- // Match the gateway resolver: longest-prefix, so a folder with no
175- // binding of its own still resolves through an ancestor's (inherited).
176- const { exact, effective } = resolveRootBinding ( root , bindings ) ;
177- const inherited = exact ? null : effective ;
178- const kind : EntryKind = exact
179- ? 'mapped-live'
180- : inherited
181- ? 'inherited-live'
182- : 'unmapped-live' ;
171+ const binding = bindingsByRoot . get ( key ) ?? null ;
183172 list . push ( {
184- id : exact ?. id ?? `live:${ root } ` ,
185- kind,
173+ id : binding ?. id ?? `live:${ root } ` ,
174+ kind : binding ? 'mapped-live' : 'unmapped-live' ,
186175 root,
187- binding : exact ,
188- inherited,
176+ binding,
189177 isLive : true ,
190178 } ) ;
191179 }
@@ -198,33 +186,31 @@ export function WorkspacesPage() {
198186 kind : 'mapped-offline' ,
199187 root : b . workspace_root ,
200188 binding : b ,
201- inherited : null ,
202189 isLive : false ,
203190 } ) ;
204191 }
205192 const rank : Record < EntryKind , number > = {
206193 'unmapped-live' : 0 ,
207194 'mapped-live' : 1 ,
208- 'inherited-live' : 2 ,
209- 'mapped-offline' : 3 ,
195+ 'mapped-offline' : 2 ,
210196 } ;
211197 return list . sort ( ( a , b ) => {
212198 const o = rank [ a . kind ] - rank [ b . kind ] ;
213199 return o !== 0 ? o : a . root . localeCompare ( b . root ) ;
214200 } ) ;
215- } , [ bindings , reportedRoots ] ) ;
201+ } , [ bindings , bindingsByRoot , reportedRoots ] ) ;
216202
217203 const filtered = useMemo ( ( ) => {
218204 const q = searchQuery . trim ( ) . toLowerCase ( ) ;
219205 return entries . filter ( ( e ) => {
220206 if ( filter === 'live' && ! e . isLive ) return false ;
221207 if ( filter === 'unmapped' && e . kind !== 'unmapped-live' ) return false ;
222208 if ( ! q ) return true ;
223- // Resolve display names from the effective binding (own or inherited).
224- const eff = e . binding ?? e . inherited ;
225- const spaceName = eff ? spaceById . get ( eff . space_id ) ?. name ?? '' : '' ;
226- const fsNames = eff
227- ? eff . feature_set_ids . map ( ( id ) => fsById . get ( id ) ?. name ?? '' ) . join ( ' ' )
209+ const spaceName = e . binding ? spaceById . get ( e . binding . space_id ) ?. name ?? '' : '' ;
210+ const fsNames = e . binding
211+ ? e . binding . feature_set_ids
212+ . map ( ( id ) => fsById . get ( id ) ?. name ?? '' )
213+ . join ( ' ' )
228214 : '' ;
229215 return (
230216 e . root . toLowerCase ( ) . includes ( q ) ||
@@ -377,16 +363,17 @@ export function WorkspacesPage() {
377363 { filtered . map ( ( entry ) => {
378364 const isSelected =
379365 selected ?. mode === 'entry' && selected . id === entry . id ;
380- // Show the EFFECTIVE binding's Space + FeatureSet names — the
381- // folder's own binding, or the ancestor it inherits from.
382- // Truly-unmapped entries (no own + no inherited) show no
383- // preview and read "Not mapped".
384- const eff = entry . binding ?? entry . inherited ;
385- const resolvedSpaceName = eff
386- ? spaceById . get ( eff . space_id ) ?. name
366+ // Mapped entries show their bound Space + FeatureSet names.
367+ // Unmapped entries deliberately show no preview — the card
368+ // reads "Not mapped" because the folder genuinely gets no
369+ // tools until the user maps it.
370+ const resolvedSpaceName = entry . binding
371+ ? spaceById . get ( entry . binding . space_id ) ?. name
387372 : undefined ;
388- const fsNames = eff
389- ? eff . feature_set_ids . map ( ( id ) => fsById . get ( id ) ?. name ?? id )
373+ const fsNames = entry . binding
374+ ? entry . binding . feature_set_ids . map (
375+ ( id ) => fsById . get ( id ) ?. name ?? id
376+ )
390377 : [ ] ;
391378 return (
392379 < EntryCard
@@ -553,10 +540,6 @@ const CARD_TONES = {
553540 strip : 'bg-emerald-500' ,
554541 box : 'bg-emerald-50 text-emerald-600 ring-emerald-200/70 dark:bg-emerald-900/20 dark:text-emerald-400 dark:ring-emerald-800/50' ,
555542 } ,
556- sky : {
557- strip : 'bg-sky-500' ,
558- box : 'bg-sky-50 text-sky-600 ring-sky-200/70 dark:bg-sky-900/20 dark:text-sky-400 dark:ring-sky-800/50' ,
559- } ,
560543 amber : {
561544 strip : 'bg-amber-500' ,
562545 box : 'bg-amber-50 text-amber-600 ring-amber-200/70 dark:bg-amber-900/20 dark:text-amber-400 dark:ring-amber-800/50' ,
@@ -586,13 +569,9 @@ function EntryCard({
586569 ? 'amber'
587570 : entry . kind === 'mapped-live'
588571 ? 'emerald'
589- : entry . kind === 'inherited-live'
590- ? 'sky'
591- : 'neutral' ;
572+ : 'neutral' ;
592573 const t = CARD_TONES [ tone ] ;
593574 const name = folderName ( entry . root ) ;
594- // The ancestor folder this entry inherits its mapping from (if any).
595- const inheritedFrom = entry . inherited ? folderName ( entry . inherited . workspace_root ) : null ;
596575
597576 return (
598577 < Card
@@ -628,7 +607,6 @@ function EntryCard({
628607 { entry . kind === 'unmapped-live' && < Pill tone = "amber" > Unmapped</ Pill > }
629608 { entry . kind === 'mapped-offline' && < Pill tone = "neutral" > Offline</ Pill > }
630609 { entry . kind === 'mapped-live' && < Pill tone = "emerald" > Live</ Pill > }
631- { entry . kind === 'inherited-live' && < Pill tone = "sky" > Inherited</ Pill > }
632610 </ div >
633611 < h3 className = "truncate text-base font-semibold" title = { entry . root } >
634612 { name }
@@ -643,42 +621,29 @@ function EntryCard({
643621 </ div >
644622
645623 < div className = "border-t border-[rgb(var(--border-subtle))] pt-4 text-xs" >
646- { entry . binding || entry . inherited ? (
647- < div className = "space-y-1" >
648- < div className = "flex items-center justify-between gap-3" >
649- < span className = "inline-flex min-w-0 items-center gap-1.5" >
650- < Layers className = "h-3.5 w-3.5 flex-shrink-0 text-primary-500" />
651- < span className = "flex-shrink-0 text-[rgb(var(--muted))]" >
652- { entry . binding ? 'Serves' : 'Inherits' }
653- </ span >
624+ { entry . binding ? (
625+ < div className = "flex items-center justify-between gap-3" >
626+ < span className = "inline-flex min-w-0 items-center gap-1.5" >
627+ < Layers className = "h-3.5 w-3.5 flex-shrink-0 text-primary-500" />
628+ < span
629+ className = "truncate font-medium text-[rgb(var(--foreground))]"
630+ title = { fsNames . join ( ', ' ) }
631+ >
632+ { summarizeFeatureSets ( fsNames ) }
633+ </ span >
634+ { fsNames . length > 1 && (
654635 < span
655- className = "truncate font-medium text-[rgb(var(--foreground))] "
656- title = { fsNames . join ( ', ' ) }
636+ className = "flex-shrink-0 rounded-full bg-primary-500/10 px-1.5 text-[10px] font-bold tabular-nums text-primary-600 dark:text-primary-300 "
637+ title = { ` ${ fsNames . length } feature sets` }
657638 >
658- { summarizeFeatureSets ( fsNames ) }
639+ { fsNames . length }
659640 </ span >
660- { fsNames . length > 1 && (
661- < span
662- className = "flex-shrink-0 rounded-full bg-primary-500/10 px-1.5 text-[10px] font-bold tabular-nums text-primary-600 dark:text-primary-300"
663- title = { `${ fsNames . length } feature sets` }
664- >
665- { fsNames . length }
666- </ span >
667- ) }
668- </ span >
669- < span className = "inline-flex flex-shrink-0 items-center gap-1.5 text-[rgb(var(--muted))]" >
670- < span > in</ span >
671- < Chip tone = "neutral" > { spaceName ?? '—' } </ Chip >
672- </ span >
673- </ div >
674- { entry . inherited && (
675- < div
676- className = "truncate text-[11px] text-sky-600 dark:text-sky-400"
677- title = { entry . inherited . workspace_root }
678- >
679- via parent mapping { inheritedFrom } — map this folder to override
680- </ div >
681- ) }
641+ ) }
642+ </ span >
643+ < span className = "inline-flex flex-shrink-0 items-center gap-1.5 text-[rgb(var(--muted))]" >
644+ < span > in</ span >
645+ < Chip tone = "neutral" > { spaceName ?? '—' } </ Chip >
646+ </ span >
682647 </ div >
683648 ) : (
684649 < span className = "inline-flex items-center gap-1.5 font-medium text-amber-600 dark:text-amber-400" >
@@ -697,16 +662,14 @@ function Pill({
697662 tone,
698663} : {
699664 children : React . ReactNode ;
700- tone : 'amber' | 'emerald' | 'sky' | ' neutral';
665+ tone : 'amber' | 'emerald' | 'neutral' ;
701666} ) {
702667 const cls =
703668 tone === 'amber'
704669 ? 'bg-amber-50 dark:bg-amber-900/20 text-amber-700 dark:text-amber-400 border-amber-200/80 dark:border-amber-800/60'
705670 : tone === 'emerald'
706671 ? 'bg-emerald-50 dark:bg-emerald-900/20 text-emerald-700 dark:text-emerald-400 border-emerald-200/80 dark:border-emerald-800/60'
707- : tone === 'sky'
708- ? 'bg-sky-50 dark:bg-sky-900/20 text-sky-700 dark:text-sky-400 border-sky-200/80 dark:border-sky-800/60'
709- : 'bg-[rgb(var(--surface))] text-[rgb(var(--muted))] border-[rgb(var(--border-subtle))]' ;
672+ : 'bg-[rgb(var(--surface))] text-[rgb(var(--muted))] border-[rgb(var(--border-subtle))]' ;
710673 return (
711674 < span
712675 className = { `inline-flex items-center px-1.5 py-0.5 rounded-md border text-[10px] font-semibold uppercase tracking-wider ${ cls } ` }
@@ -942,10 +905,7 @@ function InspectorPanel({
942905 < div className = "flex-1 min-w-0" >
943906 < div className = "flex items-center gap-2 mb-0.5 flex-wrap" >
944907 { ! isNew && entry ?. isLive && < Pill tone = "emerald" > Live</ Pill > }
945- { ! isNew && entry ?. inherited && < Pill tone = "sky" > Inherited</ Pill > }
946- { ! isNew && entry && ! isMapped && ! entry . inherited && (
947- < Pill tone = "amber" > Unmapped</ Pill >
948- ) }
908+ { ! isNew && entry && ! isMapped && < Pill tone = "amber" > Unmapped</ Pill > }
949909 { ! isNew && entry && isMapped && ! entry . isLive && < Pill tone = "neutral" > Offline</ Pill > }
950910 </ div >
951911 < h2 className = "text-lg font-bold truncate" > { title } </ h2 >
@@ -976,9 +936,7 @@ function InspectorPanel({
976936 mode === 'create'
977937 ? 'Choose the folder and the tools it should get.'
978938 : mode === 'create-from-live'
979- ? entry ?. inherited
980- ? 'Inherits a parent mapping — save here to override it for this folder.'
981- : 'This folder is open in an app but has no tools yet — map it.'
939+ ? 'This folder is open in an app but has no tools yet — map it.'
982940 : isMapped && entry ?. binding
983941 ? `Gives ${
984942 formatFsList (
@@ -995,23 +953,6 @@ function InspectorPanel({
995953 headerExtra = { < SaveStatusPill status = { saveStatus } /> }
996954 testId = "workspace-mapping-section"
997955 >
998- { entry ?. inherited && ! isMapped && (
999- < div
1000- className = "mb-4 rounded-lg border border-sky-200 bg-sky-50 px-3.5 py-3 text-xs leading-relaxed text-sky-800 dark:border-sky-800/60 dark:bg-sky-900/20 dark:text-sky-300"
1001- data-testid = "workspace-inherited-note"
1002- >
1003- < span className = "font-semibold" > Inherited mapping.</ span > This folder
1004- has no mapping of its own — it currently resolves through its parent{ ' ' }
1005- < code className = "font-mono" > { entry . inherited . workspace_root } </ code > (
1006- { formatFsList (
1007- entry . inherited . feature_set_ids . map (
1008- ( id ) => featureSets . find ( ( f ) => f . id === id ) ?. name ?? id
1009- )
1010- ) || 'no tools' }
1011- ). Saving below creates a mapping for < em > this</ em > folder that
1012- overrides the inherited one.
1013- </ div >
1014- ) }
1015956 < BindingForm
1016957 mode = { mode }
1017958 spaces = { spaces }
0 commit comments