fix(native): meet DSH 0.1.6-alpha.2 guide-id and turn-tail contracts

DSH 0.1.6-alpha.2 made two plugin-facing contracts stricter, and both broke
this plugin silently:

- `SidebarRightGuideEntry.id` is now required and unique per provider. The
  native tab registrations sent guide entries without one, so the second kind
  collided on `undefined` and `SidebarRightTabRegistry.register` threw
  'duplicate guide entry id'. The throw happens inside `ctx.inject`'s callback
  body, which cordis swallows, so the whole native surface stayed empty with no
  symptom beyond a missing guide row.
- `conversation.chat.turnTail` changed from a `chain` to an additive `list`.
  The chain's `select` made one entry render and let this plugin REPLACE the
  host's produced-files row; a list cannot express that, and the registration
  now throws 'list slot ... requires options.id'. Upstream chose the list
  deliberately ('An additive list lets both plugins contribute without either
  knowing the other's data or rendering'), so the plugin's row can only ever
  duplicate the host's deliverables row -- which itself opens files through the
  host's openFile, i.e. into this plugin's editor. The contribution is removed
  and the row is the host's alone.

The file-opening half survives as `sidebar-file.ts` (the changes tab and the
editor host still use it), and `resolveSidebarPath` moves to the `paths.ts`
path-helper module beside its siblings. `registerNativeSurface` grows an
optional `reportFailure` so a future contract break reaches the visible
diagnostic strip instead of a silently empty guide, and one bad descriptor can
no longer take the remaining registrations down with it.
This commit is contained in:
Menghuan1918
2026-09-21 20:18:16 +08:00
parent 1fcf43ccbe
commit 58ba17d462
17 changed files with 135 additions and 611 deletions
+36 -4
View File
@@ -39,11 +39,12 @@ interface NativeTabRegistry {
register(definition: {
id: string
kind: string
multiple?: boolean
patterns?: readonly string[]
priority?: 'extension' | 'builtin' | 'fallback'
canOpen?: (address: string) => boolean
title: (address: string) => string
guide?: readonly { order: number; title: () => string; description?: () => string; icon?: unknown }[]
guide?: readonly { id: string; order: number; title: () => string; description?: () => string; icon?: unknown }[]
}): () => void
}
@@ -115,6 +116,18 @@ export interface NativeSurfaceDeps {
readonly service: BetterSidebarService
/** The shared native tab record registry (the surface writes it too). */
readonly records: NativeTabRecords
/**
* Report a tab-type registration failure (phase label + cause).
*
* `ctx.inject`'s callback body and the registry's own subscriber callbacks
* run OUTSIDE this module's control flow, so a throw from
* `sidebarRightTabs.register` is swallowed by cordis and the whole native
* surface stays empty with no symptom beyond a missing guide. Reporting
* through the client's diagnostic strip (index.tsx `fail`) turns a silent
* contract break into a visible one — the failure mode observed when DSH
* 0.1.6-alpha.2 made `SidebarRightGuideEntry.id` required.
*/
readonly reportFailure?: (phase: string, error: unknown) => void
}
/**
@@ -124,7 +137,7 @@ export interface NativeSurfaceDeps {
* @returns a disposer unregistering everything.
*/
export function registerNativeSurface(deps: NativeSurfaceDeps): () => void {
const { ctx, store, service, records } = deps
const { ctx, store, service, records, reportFailure } = deps
// 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
@@ -194,6 +207,11 @@ export function registerNativeSurface(deps: NativeSurfaceDeps): () => void {
? {}
: {
guide: [{
// DSH 0.1.6-alpha.2 made `id` REQUIRED and unique per provider
// (a duplicate throws `sidebarRight: duplicate guide entry id`).
// The descriptor id is already unique per implementation, which
// is exactly the uniqueness the registry asks for.
id: descriptor.id,
order: descriptor.order ?? 100,
title: () => titleOf(descriptor),
...guideDescriptionOf(descriptor),
@@ -221,6 +239,10 @@ export function registerNativeSurface(deps: NativeSurfaceDeps): () => void {
priority: 'extension',
title: () => t('files'),
guide: [{
// Required and unique per provider since DSH 0.1.6-alpha.2. This
// takeover is its own implementation id, so its guide row takes
// that same id.
id: 'files',
order: 10,
title: () => t('files'),
// The takeover IS the editor descriptor's page, so it carries the
@@ -251,14 +273,24 @@ export function registerNativeSurface(deps: NativeSurfaceDeps): () => void {
}
for (const [descriptorId, create] of wanted) {
if (live.has(descriptorId)) continue
live.set(descriptorId, { dispose: create() })
// One descriptor must not take the rest of the surface down with it:
// report and continue so the remaining types still register.
try {
live.set(descriptorId, { dispose: create() })
} catch (error) {
reportFailure?.(`register ${descriptorId}`, error)
}
}
// The built-in files kind follows the editor type's switch: with the
// editor disabled the plugin has no explorer to put there.
const wantsFiles = service.isTabEnabled(EDITOR_KIND)
const hasFiles = live.has(FILES_KIND)
if (wantsFiles && !hasFiles) {
live.set(FILES_KIND, { dispose: registerFilesKind(service.getTab(EDITOR_KIND)) })
try {
live.set(FILES_KIND, { dispose: registerFilesKind(service.getTab(EDITOR_KIND)) })
} catch (error) {
reportFailure?.(`register ${FILES_KIND}`, error)
}
}
if (!wantsFiles && hasFiles) {
live.get(FILES_KIND)?.dispose()