diff --git a/src/client/native/index.ts b/src/client/native/index.ts index a38a4b5..77014a0 100644 --- a/src/client/native/index.ts +++ b/src/client/native/index.ts @@ -63,6 +63,20 @@ function titleOf(descriptor: TabDescriptor): string { return typeof descriptor.title === 'function' ? descriptor.title() : descriptor.title } +/** + * The chip title of a file address: its own file name. A resource tab is + * identified by its address, so the strip must name the FILE — the + * descriptor's title ("Files") would make every open file look identical. + * @param address - the native tab's content address. + * @returns the file name, or undefined when the address is not a file. + */ +function fileTitleOf(address: string): string | undefined { + const parsed = parseFileAddress(address) + if (parsed === undefined) return undefined + const segments = parsed.path.split('/').filter(segment => segment !== '') + return segments.length === 0 ? undefined : segments[segments.length - 1] +} + /** One descriptor's live native registrations. */ interface Registration { readonly dispose: () => void @@ -143,7 +157,9 @@ export function registerNativeSurface(deps: NativeSurfaceDeps): () => void { // An external implementation outranks the product's own viewers, which // is what lets the plugin's editor take over file addresses. priority: 'extension', - title: () => titleOf(descriptor), + // A resource tab is titled by the file it shows; a page tab keeps the + // descriptor's own title. + title: (address: string) => (isEditor ? fileTitleOf(address) ?? titleOf(descriptor) : titleOf(descriptor)), ...(descriptor.hidden === true ? {} : { diff --git a/tests/native-surface.spec.ts b/tests/native-surface.spec.ts index 4307246..05978b6 100644 --- a/tests/native-surface.spec.ts +++ b/tests/native-surface.spec.ts @@ -166,12 +166,12 @@ describe('registerNativeSurface lifecycle (service-driven registration)', () => service.registerTab({ id: 'editor', title: 'Files', component: () => null }) const records = createNativeTabRecords() - const registered: Array<{ id: string; kind: string }> = [] + const registered: Array<{ id: string; kind: string; title: (address: string) => string }> = [] const slotKeys: string[] = [] // The registry is ABSENT while the slot callback fires and appears later // (that ordering is the regression): a holder keeps the timing honest // without a reassigned binding. - const registry: { current: { register: (definition: { id: string; kind: string }) => () => void } | undefined } = { current: undefined } + const registry: { current: { register: (definition: { id: string; kind: string; title: (address: string) => string }) => () => void } | undefined } = { current: undefined } let runInjected: (() => void) | undefined const ctx = { @@ -200,7 +200,7 @@ describe('registerNativeSurface lifecycle (service-driven registration)', () => // …and everything registers once it appears. registry.current = { register: (definition) => { - registered.push({ id: definition.id, kind: definition.kind }) + registered.push({ id: definition.id, kind: definition.kind, title: definition.title }) return () => {} }, } @@ -210,6 +210,14 @@ describe('registerNativeSurface lifecycle (service-driven registration)', () => expect(slotKeys).toContain('dsh-better-sidebar:terminal') expect(slotKeys).toContain('dsh-better-sidebar:files') + // A resource tab is titled by the FILE it shows (the descriptor's own + // title would make every open file look identical in the strip), while a + // page tab keeps the descriptor's title. + const editorType = registered.find(entry => entry.kind === 'editor') + expect(editorType?.title('dsh-resource://file/session/s1/src/main.ts')).toBe('main.ts') + expect(editorType?.title('dsh-resource://file/absolute/work/pkg/a/b.txt')).toBe('b.txt') + expect(editorType?.title('sidebar://editor')).toBe('Files') + dispose() }) })