You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
tab type "dsh-better-sidebar:files" already registered on sidebarRightTabs re-prove (+ React #130) #770
On DSH 0.1.7-rc.1 (Desktop Beta 2.0.14-beta.1) + dsh-better-sidebar 0.21.1, I intermittently get both errors together:
[dsh-better-sidebar] native register files error: sidebarRight: tab type id "dsh-better-sidebar:files" is already registered
Minified React error #130 (Element type is invalid: got undefined)
What I found
registerNativeSurface (src/client/native/index.ts:186) uses ctx.inject(['sidebarRightTabs'], body). The body re-runs when sidebarRightTabs re-proves, and each re-run builds a fresh empty live map and calls sync(), which re-registers every tab type — including registerFilesKind (:286) doing tabs.register({ id: 'dsh-better-sidebar:files', ... }).
The "already registered" means the previous run's host tab type is still in the registry when the new run registers — i.e. the previous body's disposer (:355-359) didn't remove it before the new run, whether because cordis didn't run it first or the host dispose didn't take effect. The throw is caught by sync()'s try/catch + reportFailure (:330-345) and surfaced as the red strip.
The React #130 in the same burst appears to come from the slot side: registerSlots registers NativeTabBody/NativeTabTitle under the same key id, and a re-run collides with the previous run's slots, leaving the host rendering an undefined element type. I haven't pinned this down as tightly as the already-registered part.
Trigger
Intermittent. Fires when sidebarRightTabs re-proves — switching sessions, opening a second client window, toggling the global panel, cross-session operations, reload. Not every re-prove collides; it depends on whether the previous disposer finished first. Stable single-session use doesn't trigger it.
#638 is already fixed in 0.21.1 — the lib uses IconSendOutlineRegular and host dsh-client-ui-primitives@0.1.7-rc.1 exports it. The #130 here is the slot fallout above, not a missing icon.
Possible fix
Make the re-run idempotent: keep a module-level WeakMap<tabs, liveMap> and at the top of the body, dispose the previous run's registrations for that tabs instance before sync(). Then the new run never collides regardless of whether cordis disposed the old body first. Alternatively, ensure the inject re-prove path awaits the old disposer before running the new body.
Repro
DSH 0.1.7-rc.1 + dsh-better-sidebar 0.21.1; use the sidebar across session switches / new windows / panel toggles; eventually the red strip + React #130 appear.
Reproduced and root-caused this on DSH 0.1.7-rc.2 + dsh-better-sidebar@0.21.1 (profile web, pnpm patched install). Same red strip, and it fires on every plugin-market update — not only on session switches.
I think there is a second, more direct cause than the inject re-prove race, and it is inside a singlesync() run rather than between two runs.
Observation that pins it down
I instrumented SidebarRightTabRegistry.ids (a Set) in the live page and forced a client-side replacement of the dsh-better-sidebar entry. The recorded order, for the failing run, was:
The stack of that first delete is not the body disposer at the end of the injected callback. It is:
SidebarRightTabRegistry.register (the effect body that runs this.ids.delete(id))
<- Object.dispose (registration.dispose())
<- syncOnce (src/client/native/index.ts)
syncOnce disposing the id it is about to re-register, in the same pass, is the bug.
Root cause
live is a single map that holds two different kinds of entries, but the cleanup loop only understands one of them:
per-descriptor native types, keyed by descriptor id (editor, git, subagent, ...), which come from service.getTabs() and therefore appear in wanted;
the files takeover, stored under FILES_KIND ('files'), which is created by registerFilesKind() and is never in service.getTabs(), so it is never in wanted.
So in stock 0.21.1 (src/client/native/index.ts, cleanup loop at ~line 320):
for(const[descriptorId,registration]oflive){if(wanted.has(descriptorId))continue// 'files' is never in wantedregistration.dispose()// <- disposes the files takeoverlive.delete(descriptorId)}
wanted is then rebuilt, and further down the same function:
constwantsFiles=service.isTabEnabled(EDITOR_KIND)consthasFiles=live.has(FILES_KIND)// false again, we just deleted itif(wantsFiles&&!hasFiles){live.set(FILES_KIND,{dispose: registerFilesKind(...)})// re-registers the same fixed id}
registerFilesKind() registers a fixed id (dsh-better-sidebar:files, registerFilesKind at line 286, tabs.register({ id, kind: FILES_KIND, ... }) at ~288). SidebarRightTabRegistry.register refuses a duplicate id by design, so any sync() that runs while the takeover is live tears it down and immediately trips over itself.
This also explains the intermittency without invoking an inject race: BetterSidebarService.notify() and the store are synchronous (for (const fn of [...listeners]) fn()), and tabs.register / registration.dispose() both publish. A notification landing between the dispose and the re-register observes the window and throws. Whether you hit it depends only on whether such a notification arrives in that window — which is why it looks like "sometimes".
Fix (verified)
Skip the takeover in the descriptor cleanup loop — it is managed entirely by the wantsFiles/hasFiles block below it:
for(const[descriptorId,registration]oflive){// `files` is a native takeover managed below, not a service tab descriptor.if(descriptorId===FILES_KIND||wanted.has(descriptorId))continueregistration.dispose()live.delete(descriptorId)}
I applied this plus two defensive changes, since both underlying hazards are real and worth closing regardless:
a disposal latch, with subscriptions removed before live registrations are disposed (releasing a registration publishes, so a late sync must be a no-op);
re-entrancy coalescing around sync, because notify() is synchronous:
Note on ordering: with only the FILES_KIND skip, the failure disappears. The latch and the coalescer are belt-and-braces for the dispose-publishes window and are not what carries the fix — I mention it only so the intent of each hunk is clear.
Verification
Applied through a pnpm patch on dsh-better-sidebar@0.21.1 (so src and both lib bundles move together), then dsh-restart:
served bundle contains the fix (checked the live /plugins/... response, not just disk);
3 consecutive forced client-entry replacements of dsh-better-sidebar: console errors [], no [dsh-better-sidebar] native register files error strip, and exactly one dsh-better-sidebar:files in the registry at all times;
independent Playwright pass with system Chromium: pageerror, console errors and failed requests all 0, dsh-better-sidebar:files present in the registry;
web.log clean.
Before the fix the same forced replacement reliably produced the red strip plus a burst of already registered; after it, nothing.
About the re-prove theory in this issue
I did not manage to observe the "previous body disposer did not finish" path — in my runs the entry is fully torn down and re-created (one fiber at a time, old gone before the new appears), and the duplicate still fires inside the new instance's own first sync. So I would treat the FILES_KIND bookkeeping as a sufficient cause on its own, independent of the re-prove ordering. If both are real, the WeakMap<tabs, liveMap> idea in this issue is still a reasonable belt for the re-prove half, but it is not what fixes the update-time failure I am seeing.
Environment: DSH 0.1.7-rc.2, dsh-better-sidebar@0.21.1, pnpm 12.3.4, profile web. Happy to open a PR with the three hunks if useful.
Opened a separate issue with the root cause I landed on, in case it is easier to track there: #771.
Short version: the ids.delete I instrumented traces to the cleanup loop inside syncOnce, not to the injected body disposer at the end of the callback. live holds both descriptor-derived types (which appear in wanted) and the files takeover under FILES_KIND (which never does), so the loop disposes the takeover and the wantsFiles / hasFiles block below re-registers the same fixed id in the same invocation. I could not reproduce the re-prove path you describe — the old entry is fully gone before the new one appears in my runs, and the duplicate still fires inside the new instance's first sync().
If it turns out to be the same defect, feel free to close either one. I will not push a PR unless asked.
Activity
Mlte0907 commentedon Sep 26, 2026
Reproduced and root-caused this on DSH
0.1.7-rc.2+dsh-better-sidebar@0.21.1(profileweb, pnpm patched install). Same red strip, and it fires on every plugin-market update — not only on session switches.I think there is a second, more direct cause than the inject re-prove race, and it is inside a single
sync()run rather than between two runs.Observation that pins it down
I instrumented
SidebarRightTabRegistry.ids(aSet) in the live page and forced a client-side replacement of thedsh-better-sidebarentry. The recorded order, for the failing run, was:The stack of that first
deleteis not the body disposer at the end of the injected callback. It is:syncOncedisposing the id it is about to re-register, in the same pass, is the bug.Root cause
liveis a single map that holds two different kinds of entries, but the cleanup loop only understands one of them:editor,git,subagent, ...), which come fromservice.getTabs()and therefore appear inwanted;filestakeover, stored underFILES_KIND('files'), which is created byregisterFilesKind()and is never inservice.getTabs(), so it is never inwanted.So in stock 0.21.1 (
src/client/native/index.ts, cleanup loop at ~line 320):wantedis then rebuilt, and further down the same function:registerFilesKind()registers a fixed id (dsh-better-sidebar:files,registerFilesKindat line 286,tabs.register({ id, kind: FILES_KIND, ... })at ~288).SidebarRightTabRegistry.registerrefuses a duplicate id by design, so anysync()that runs while the takeover is live tears it down and immediately trips over itself.This also explains the intermittency without invoking an inject race:
BetterSidebarService.notify()and the store are synchronous (for (const fn of [...listeners]) fn()), andtabs.register/registration.dispose()both publish. A notification landing between thedisposeand the re-registerobserves the window and throws. Whether you hit it depends only on whether such a notification arrives in that window — which is why it looks like "sometimes".Fix (verified)
Skip the takeover in the descriptor cleanup loop — it is managed entirely by the
wantsFiles/hasFilesblock below it:I applied this plus two defensive changes, since both underlying hazards are real and worth closing regardless:
syncmust be a no-op);sync, becausenotify()is synchronous:Note on ordering: with only the
FILES_KINDskip, the failure disappears. The latch and the coalescer are belt-and-braces for the dispose-publishes window and are not what carries the fix — I mention it only so the intent of each hunk is clear.Verification
Applied through a pnpm patch on
dsh-better-sidebar@0.21.1(sosrcand bothlibbundles move together), thendsh-restart:/plugins/...response, not just disk);dsh-better-sidebar:consoleerrors[], no[dsh-better-sidebar] native register files errorstrip, and exactly onedsh-better-sidebar:filesin the registry at all times;pageerror, console errors and failed requests all0,dsh-better-sidebar:filespresent in the registry;web.logclean.Before the fix the same forced replacement reliably produced the red strip plus a burst of
already registered; after it, nothing.About the re-prove theory in this issue
I did not manage to observe the "previous body disposer did not finish" path — in my runs the entry is fully torn down and re-created (one fiber at a time, old gone before the new appears), and the duplicate still fires inside the new instance's own first
sync. So I would treat theFILES_KINDbookkeeping as a sufficient cause on its own, independent of the re-prove ordering. If both are real, theWeakMap<tabs, liveMap>idea in this issue is still a reasonable belt for the re-prove half, but it is not what fixes the update-time failure I am seeing.Environment: DSH
0.1.7-rc.2,dsh-better-sidebar@0.21.1, pnpm 12.3.4, profileweb. Happy to open a PR with the three hunks if useful.Mlte0907 commentedon Sep 26, 2026
Opened a separate issue with the root cause I landed on, in case it is easier to track there: #771.
Short version: the
ids.deleteI instrumented traces to the cleanup loop insidesyncOnce, not to the injected body disposer at the end of the callback.liveholds both descriptor-derived types (which appear inwanted) and thefilestakeover underFILES_KIND(which never does), so the loop disposes the takeover and thewantsFiles/hasFilesblock below re-registers the same fixed id in the same invocation. I could not reproduce the re-prove path you describe — the old entry is fully gone before the new one appears in my runs, and the duplicate still fires inside the new instance's firstsync().If it turns out to be the same defect, feel free to close either one. I will not push a PR unless asked.
Menghuan1918 commentedon Sep 28, 2026
同一串报错:真机日志确认了两条不同的栈,根因已定位并修复(v0.22.1 待发)
本 issue 归因到「inject re-prove 竞态」,而 #771 归因到「同一次
sync()内自撞」。官方桌面壳日志(desktop.frontdesk.log,2026-09-27 15:51)显示两条栈都真实存在,但它们是同一根因链的两段:cannot create effect on inactive context,栈是registerSlots ← registerFilesKind ← sync ← notify——teardown 期间service.notify()驱动sync(),清理循环把files接管释放后在同一轮重建;tabs.register建在宿主 ctx 上照样成功取走 id,而ctx.slots.inject建在插件 ctx 上抛INACTIVE_EFFECT,于是 disposer 丢失、id 被永久占用。register ← registerFilesKind ← sync ← _reload——新激活(_reload)尝试注册同一个仍被占用的 id。所以「新激活先于旧激活释放」确实是一条真实可达路径,但它只是把已经被泄漏的 id 再撞一次;把泄漏本身修掉(清理循环不再拆建接管 + 注册失败回滚 id)之后,这条路径就退化成一次自愈的日志噪音。
修复:#777(已合并)+ #785(守护断言与部署级回归门)。回归门在 scratch profile 上用 npm 上的 0.22.0 实测 3/3 红(栈与上面两条逐字一致)、修复版 3/3 绿。事故记录:
docs/plans/2026-09-28-native-files-takeover-reload-leak.md。未做:跨激活竞态的「吸收 + 补注册」机制(#766 的方向)——它需要在账本里记下插件并不拥有的注册,旧持有者随后释放时无人补注册,
files接管可能永久缺位,风险大于收益。此取舍已写进设计文档,如果你有反例请直接贴在这里。Menghuan1918 commentedon Sep 28, 2026
v0.22.1 已发布(npm
latest)——本 issue 的缺陷已修复修复随 v0.22.1 上线:
dsh plugin --profile web add dsh-better-sidebar@latest(或@0.22.1)。发布说明:https://github.com/omdsh-dev/DSH-better-sidebar/releases/tag/v0.22.1本次修了什么
tab type id "dsh-better-sidebar:files" is already registered):清理循环不再把files接管当描述符拆建(跳过FILES_KIND),并且任何在宿主取走 id 之后失败的注册都会回滚释放该 type 与已建槽位——id 不再可能被孤儿化。附带把守护断言改写到注册表事件日志、补上部分回滚覆盖,并新增部署级回归门tests/e2e/native-reload.e2e.ts(在 npm 0.22.0 上连续 3 次运行全红、修复版连续 3 次全绿)。[data-dsh-better-sidebar]/[data-dsh-panel-host](> *保持no-drag)与放大视图.mermaidModal用中性值-webkit-app-region: initial !important退出 app-region 计算。验证方式(如果你想复核):升级到 0.22.1 后触发一次客户端条目替换(Plugins 页禁用→启用该插件,或插件市场更新),确认左下的红色诊断条不再出现、文件树仍然可用;macOS 上连续拖拽窗口标题区 5–6 次、并双击标题栏看缩放是否恢复。
如果升级后仍能复现,请把 DSH 版本 + 插件版本 + Console 里的完整栈贴回来,我按新证据重开。