fix(native): register the plugin's tab types when the registry service arrives

The native seat declares `sidebar.right.pane.tab` BEFORE it provides
`sidebarRightTabs`, so driving the registration off the slot declaration
read the service as missing and registered nothing — permanently, since a
declaration never collapses. Observed on a real DSH profile (0.1.5-alpha.1,
`dsh web` on the web profile): the plugin host and its other slot
registrations mounted, the native guide page stayed empty, and a probe
printed `sidebarRightTabs= undefined` at declaration time with the registry
present three seconds later. The scratch mount lane never saw it because
there the plugin activates after the native package finished providing.

`registerNativeSurface` now waits on the SERVICE (`ctx.inject`), which
re-runs whenever the registry appears/reappears, and keeps the per-body
slot registrations on `slots.inject` (they only need the declaration).

Regression test: `tests/native-surface.spec.ts` models the real order
(slot callback fires first, registry arrives later) and fails on the old
implementation. Verified on the real web profile: all guide entries
(files / editor / git / subagent / sidechat / terminal / browser) appear
and the terminal tab renders.
This commit is contained in:
Menghuan1918
2026-09-09 13:07:53 +08:00
parent d7e3e2b986
commit aa1b9945d9
2 changed files with 79 additions and 12 deletions
+15 -12
View File
@@ -85,15 +85,18 @@ export interface NativeSurfaceDeps {
*/
export function registerNativeSurface(deps: NativeSurfaceDeps): () => void {
const { ctx, store, service, records } = deps
const disposers: Array<() => void> = []
// The native seat declares `sidebar.right.pane.tab` when its client half
// activates, which may be after this plugin; the slot registry's `inject`
// re-runs on every declaration lifetime, and the tab-type registry
// (`ctx.sidebarRightTabs`) is provided by that same package, so one wait
// covers both.
const disposeSeat = ctx.slots.inject('sidebar.right.pane.tab', () => {
const tabs = ctx.get('sidebarRightTabs') as unknown as NativeTabRegistry | undefined
if (tabs === undefined) return () => {}
// Wait for the tab-type REGISTRY (a service), not for the slot declaration:
// the native seat declares `sidebar.right.pane.tab` BEFORE it provides
// `sidebarRightTabs`, so a declaration-triggered registration reads the
// service as missing and — because the declaration never collapses —
// registers nothing, permanently. Observed on a real profile: the
// declaration callback fired with both `sidebarRight` and
// `sidebarRightTabs` undefined, and the registry appeared a moment later.
// `ctx.inject` re-runs this body whenever the service appears/reappears,
// which is the lifecycle the registrations need.
const seat = ctx.inject(['sidebarRightTabs'], (injected) => {
const tabs = injected.get('sidebarRightTabs') as unknown as NativeTabRegistry | undefined
if (tabs === undefined) return
const live = new Map<string, Registration>()
const fileParamsOf = (info: NativeTabInfo): NativeTabParams | undefined => {
@@ -213,16 +216,16 @@ export function registerNativeSurface(deps: NativeSurfaceDeps): () => void {
}
}
disposers.push(service.subscribe(sync), store.subscribe(sync))
const disposeSubscriptions = [service.subscribe(sync), store.subscribe(sync)]
sync()
return () => {
for (const registration of live.values()) registration.dispose()
live.clear()
for (const dispose of disposeSubscriptions.reverse()) dispose()
}
})
return () => {
disposeSeat()
for (const dispose of disposers.reverse()) dispose()
void seat.dispose()
}
}