fix: batch server switch to avoid cleanNode crash

The status popover and select-server dialog used to call navigate('/') then
defer server.setActive to the next microtask. With multiple sidecars in v2,
that split triggered two separate disposal cascades - one for the route
change and a second for the ServerKey Show re-key - and the sidebar project
bucket also swaps (local -> wsl:Debian), tearing down every solid-dnd
sortable in the middle. Wrapping both calls in batch() lands them in a
single Solid update so disposal runs once.

Also raise Error.stackTraceLimit to 200 so future disposal crashes capture
the originating frame instead of truncating at the tenth cleanNode.
This commit is contained in:
LukeParkerDev 2026-04-17 11:46:12 +10:00
parent 3360480a2a
commit cfcc6f1353
3 changed files with 19 additions and 6 deletions

View file

@ -8,7 +8,7 @@ import { List } from "@opencode-ai/ui/list"
import { TextField } from "@opencode-ai/ui/text-field"
import { useMutation } from "@tanstack/solid-query"
import { showToast } from "@opencode-ai/ui/toast"
import { createEffect, createMemo, createResource, onCleanup, Show } from "solid-js"
import { batch, createEffect, createMemo, createResource, onCleanup, Show } from "solid-js"
import { createStore, reconcile } from "solid-js/store"
import { DialogWslServer } from "@/components/dialog-wsl-server"
import { ServerHealthIndicator, ServerRow } from "@/components/server/server-row"
@ -364,8 +364,10 @@ export function DialogSelectServer(props: DialogSelectServerProps = {}) {
props.onNavigateHome?.()
return
}
props.onNavigateHome?.()
queueMicrotask(() => server.setActive(ServerConnection.key(conn)))
batch(() => {
props.onNavigateHome?.()
server.setActive(ServerConnection.key(conn))
})
}
const handleAddChange = (value: string) => {

View file

@ -6,7 +6,7 @@ import { Tabs } from "@opencode-ai/ui/tabs"
import { useMutation } from "@tanstack/solid-query"
import { showToast } from "@opencode-ai/ui/toast"
import { useNavigate } from "@solidjs/router"
import { type Accessor, createEffect, createMemo, For, type JSXElement, onCleanup, Show } from "solid-js"
import { type Accessor, batch, createEffect, createMemo, For, type JSXElement, onCleanup, Show } from "solid-js"
import { createStore, reconcile } from "solid-js/store"
import { ServerHealthIndicator, ServerRow } from "@/components/server/server-row"
import { useLanguage } from "@/context/language"
@ -292,8 +292,13 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
aria-disabled={blocked()}
onClick={() => {
if (blocked()) return
navigate("/")
queueMicrotask(() => server.setActive(key))
// Run navigate + setActive in the same tick so Solid
// disposes the old subtree once instead of cascading
// the route change disposal into the ServerKey remount.
batch(() => {
navigate("/")
server.setActive(key)
})
}}
>
<ServerHealthIndicator health={health[key]} />

View file

@ -1,5 +1,11 @@
// @refresh reload
// V8's default Error.stackTraceLimit truncates at 10 frames, which is exactly
// the depth of the recursive cleanNode crash — the real trigger (our code
// calling dispose, or a store update racing disposal) is beyond that. Raise
// it so stacks contain the origin frame.
Error.stackTraceLimit = 200
// Install global error listeners before any other module runs so that
// uncaught errors and rejected promises reach the main process with their
// full stacks intact. Electron's `console-message` event only forwards the