fix(native): title a file tab by its own name, not the descriptor's

The editor type serves both the files PAGE and every file RESOURCE; its
native `title` callback ignored the address, so every opened file showed
up in the strip as "Files" — indistinguishable tabs. Verified on a real
profile: after opening `AGENTS.md` from the tree and a chat file link,
the strip read ["文件", "文件", "文件"].

`title(address)` now returns the file name for a file address and keeps
the descriptor title for a page address (`sidebar://editor`). Covered by
the registration-lifecycle test in tests/native-surface.spec.ts.
This commit is contained in:
Menghuan1918
2026-09-09 14:34:08 +08:00
parent aa1b9945d9
commit 4da2b43610
2 changed files with 28 additions and 4 deletions
+17 -1
View File
@@ -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
? {}
: {
+11 -3
View File
@@ -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()
})
})