Phase 2 of the 0.1.5 adaptation: the plugin stops drawing its own right
panel and contributes tab TYPES to DSH's native right Sidebar, which owns
the right column. The bottom workbench (terminal dock) stays plugin-owned.
Registrations (src/client/native/):
- every TabDescriptor becomes a native tab type (kind = descriptor.id, the
default `extension` band) plus a keyed body in `sidebar.right.pane.tab`
and a live chip title in `sidebar.right.pane.tab.title`
- the `editor` type also claims `dsh-resource://file/**`, so it outranks
the built-in `text` preview (fallback) and every file the product opens
lands in the plugin's editor
- the built-in `files` page kind is TAKEN OVER by an extension
registration of the same kind: `openTab('files')` draws the plugin's
explorer, and the built-in resumes when the plugin unregisters
- registrations follow the plugin registry + the side-card settings: a
type registered later (external plugin) gets its native type too, a
disabled type is unregistered (absent from the guide, refuses opens)
Adapter (src/client/native/tab-adapter.tsx):
- one synthetic plugin `SidebarTab` per native tab id, minted from the
native record + navigation params (or the descriptor's own createTab,
which a native guide open bypasses), kept live across navigations and
updateTab writes
- the explorer's per-tab expansion/reveal sets and the per-kind instance
counter, which the plugin's own layout used to hold
Service routing (src/client/service.ts):
- `setSurface` installs the native write face; openTab/openFile land
natively (a path becomes a resource address, a path-less editor open
becomes the `files` page, the rest carry the seed as navigation params),
closeTab/updateTab/activateTab recognise native tab ids
- new optional `OpenTabSeed.target` ('right' default, 'bottom' keeps the
bottom workbench); the bottom panel's own + menu and auto-terminal pass
'bottom'
- src/client/native/surface.ts probes the controller's per-session writes
(`openTabIn`/`openResourceIn`/`closeIn`, not in `ISidebarRight`) and
queues opens for a session whose surface is not mounted yet
Right panel retired behaviourally (Phase 3 deletes the code): Sidebar
renders its bottom workbench only, keeps the panel collapsed, and hides
the right-panel toggle. The panel-specific e2e specs are removed; the
mount lane now drives the native guide (tab sweep), the plugin explorer
inside the native pane (editor/mermaid/README chunks) and the sidechat
host routes.
Verified: pnpm typecheck / lint / build / test (126 files, 1349 tests) /
check:consumer-types, plus pnpm test:mount against a real
@deepseek-ai/dsh@0.1.5-alpha.1 host (7/7).
96 lines
3.6 KiB
TypeScript
96 lines
3.6 KiB
TypeScript
/**
|
|
* Unit tests for the file-address grammar (src/client/resource-address.ts):
|
|
* the plugin parses DSH's `dsh-resource://file/…` addresses itself (the
|
|
* client bundle's purity gate forbids value-importing the upstream util), so
|
|
* these tests pin the upstream shapes it must agree with.
|
|
*/
|
|
import { describe, expect, it } from 'vitest'
|
|
import {
|
|
absoluteFileAddress,
|
|
fileAddressFor,
|
|
parseFileAddress,
|
|
sessionFileAddress,
|
|
} from '../src/client/resource-address.ts'
|
|
|
|
describe('parseFileAddress', () => {
|
|
it('reads a session-scoped address', () => {
|
|
expect(parseFileAddress('dsh-resource://file/session/s1/src/a.ts')).toEqual({
|
|
scope: 'session', sessionId: 's1', path: 'src/a.ts',
|
|
})
|
|
})
|
|
|
|
it('reads an absolute address (POSIX, drive, UNC)', () => {
|
|
expect(parseFileAddress('dsh-resource://file/absolute/home/me/notes.txt')).toEqual({
|
|
scope: 'absolute', path: '/home/me/notes.txt',
|
|
})
|
|
expect(parseFileAddress('dsh-resource://file/absolute/C:/x/y.txt')).toEqual({
|
|
scope: 'absolute', path: 'C:/x/y.txt',
|
|
})
|
|
expect(parseFileAddress('dsh-resource://file/absolute//server/share/x.txt')).toEqual({
|
|
scope: 'absolute', path: '//server/share/x.txt',
|
|
})
|
|
})
|
|
|
|
it('decodes per segment so #, ? and spaces survive', () => {
|
|
expect(parseFileAddress('dsh-resource://file/session/s1/a%20b/c%23d.txt')).toEqual({
|
|
scope: 'session', sessionId: 's1', path: 'a b/c#d.txt',
|
|
})
|
|
})
|
|
|
|
it('refuses anything that is not a file address', () => {
|
|
for (const address of [
|
|
'sidebar://guide',
|
|
'dsh-resource://attachment/session/s1/a.png',
|
|
'dsh-resource://file/other/s1/a.ts',
|
|
'dsh-resource://file/session/s1',
|
|
'dsh-resource://file/absolute/',
|
|
'not a url',
|
|
]) {
|
|
expect(parseFileAddress(address), address).toBeUndefined()
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('address builders', () => {
|
|
it('builds a session address and round-trips it', () => {
|
|
const address = sessionFileAddress('s1', './src\\a b.ts')
|
|
expect(address).toBe('dsh-resource://file/session/s1/src/a%20b.ts')
|
|
expect(parseFileAddress(address)).toEqual({ scope: 'session', sessionId: 's1', path: 'src/a b.ts' })
|
|
})
|
|
|
|
it('builds an absolute address and round-trips it', () => {
|
|
for (const [path, address] of [
|
|
['/home/me/x.txt', 'dsh-resource://file/absolute/home/me/x.txt'],
|
|
['C:\\x\\y.txt', 'dsh-resource://file/absolute/C:/x/y.txt'],
|
|
['\\\\server\\share\\x.txt', 'dsh-resource://file/absolute//server/share/x.txt'],
|
|
] as const) {
|
|
expect(absoluteFileAddress(path)).toBe(address)
|
|
expect(parseFileAddress(address)?.path.replace(/\\/g, '/')).toBe(path.replace(/\\/g, '/'))
|
|
}
|
|
})
|
|
|
|
it('keeps a drive colon literal', () => {
|
|
expect(absoluteFileAddress('C:/x/y.txt')).toContain('C:')
|
|
expect(absoluteFileAddress('C:/x/y.txt')).not.toContain('%3A')
|
|
})
|
|
})
|
|
|
|
describe('fileAddressFor', () => {
|
|
it('scopes a relative path to the session', () => {
|
|
expect(fileAddressFor('s1', '/work', 'src/a.ts')).toBe('dsh-resource://file/session/s1/src/a.ts')
|
|
})
|
|
|
|
it('scopes an absolute path inside the workspace to the session', () => {
|
|
expect(fileAddressFor('s1', '/work', '/work/src/a.ts')).toBe('dsh-resource://file/session/s1/src/a.ts')
|
|
})
|
|
|
|
it('scopes the workspace root itself to the session', () => {
|
|
expect(fileAddressFor('s1', '/work', '/work')).toBe('dsh-resource://file/session/s1/')
|
|
})
|
|
|
|
it('falls back to an absolute address outside the workspace (or with no cwd)', () => {
|
|
expect(fileAddressFor('s1', '/work', '/other/a.ts')).toBe('dsh-resource://file/absolute/other/a.ts')
|
|
expect(fileAddressFor('s1', undefined, '/other/a.ts')).toBe('dsh-resource://file/absolute/other/a.ts')
|
|
})
|
|
})
|