fix(native): keep component tabs' path seeds on the page open (#632)

The native openTab branch rerouted EVERY path seed into openResource (a
dsh-resource://file/... open) without looking at seed.type, so any
component tab opened with a path landed in the editor (the only
dsh-resource://file/** claimant) and its registered component never
mounted. This also contradicted the guide, whose TabDescriptor/openTab
contract and WebDocsView example document path-seed component tabs.

Route by type instead: only the editor kind (the only registration with
dsh-resource://file/** patterns) turns its path seeds into resource
opens; every other kind keeps the page open and carries the path in the
navigation params, which NativeTabRecords.ensure already merges back
onto the synthetic record's tab.path for the component. The bottom
workbench path is untouched, editor opens behave exactly as before, and
onOpen still receives the synthetic tab with the path seed.
This commit is contained in:
HuanLinOTO
2026-09-11 21:12:15 +08:00
parent 238ba52ab9
commit d6a816a374
3 changed files with 86 additions and 16 deletions
+48 -1
View File
@@ -11,7 +11,7 @@ import { act } from 'react-dom/test-utils'
import { createNativeTabRecords, NativeTabBody, NativeTabTitle } from '../src/client/native/tab-adapter.tsx'
import { registerNativeSurface } from '../src/client/native/index.ts'
import { createBetterSidebarService, type SidebarSurface } from '../src/client/service.ts'
import { createSidebarStore } from '../src/client/state.ts'
import { createSidebarStore, type SidebarTab } from '../src/client/state.ts'
const scope = { sessionId: 's1', cwd: '/work' }
@@ -119,6 +119,53 @@ describe('service routing into the native surface', () => {
expect(calls).toEqual([{ op: 'openTab', sessionId: 's1', kind: 'files', params: {}, revealIfOpened: true }])
})
it('keeps a component type path seed on the page open (no resource reroute)', () => {
// Regression #632: a path seed on a component type was rerouted into
// openResource, so the editor (the dsh-resource://file/** claimant)
// received the open and the registered component never mounted.
const { service, calls } = mount()
service.registerTab({ id: 'my-plugin:doc', title: 'Doc', component: () => null })
service.openTab({ type: 'my-plugin:doc', path: '/work/spec.md', title: 'Spec' }, scope)
expect(calls).toEqual([{
op: 'openTab',
sessionId: 's1',
kind: 'my-plugin:doc',
params: { title: 'Spec', path: '/work/spec.md' },
revealIfOpened: true,
}])
})
it('carries the path seed and meta on a multi-instance component open', () => {
const { service, calls } = mount()
service.registerTab({
id: 'my-plugin:console',
title: 'Console',
createTab: (state) => ({
tab: { id: `console:${state.nextTerminal}`, type: 'my-plugin:console', title: 'Console' },
patch: { nextTerminal: state.nextTerminal + 1 },
}),
component: () => null,
})
service.openTab({ type: 'my-plugin:console', path: '/work/x.md', meta: { k: 1 } }, scope)
expect(calls).toEqual([{
op: 'openTab',
sessionId: 's1',
kind: 'my-plugin:console',
params: { title: 'Console', path: '/work/x.md', meta: { k: 1 } },
// Multi-instance kinds mint a fresh tab per open: no forced reveal.
revealIfOpened: false,
}])
})
it('reports a component path seed to onOpen on the synthetic tab', () => {
const { service } = mount()
const seen: Array<SidebarTab | undefined> = []
service.registerTab({ id: 'my-plugin:doc', title: 'Doc', onOpen: (tab) => { seen.push(tab) }, component: () => null })
service.openTab({ type: 'my-plugin:doc', path: '/work/spec.md' }, scope)
expect(seen).toHaveLength(1)
expect(seen[0]).toMatchObject({ type: 'my-plugin:doc', path: '/work/spec.md' })
})
it('keeps a bottom-targeted open in the plugin layout', () => {
const { service, calls } = mount()
service.openTab({ type: 'terminal', target: 'bottom' }, scope)