diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index b4496ba7de1f..bc623f3406ce 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -66,7 +66,7 @@ import { useCheckServerHealth } from "./utils/server-health" import { legacySessionHref, legacySessionServer, requireServerKey, sessionHref } from "./utils/session-route" import { createSessionLineage } from "@/pages/session/session-lineage" -import { SessionPage, SessionRouteErrorBoundary, TargetSessionRouteContent } from "@/pages/session" +import { SessionRouteErrorBoundary, TargetSessionRouteContent, WorkspaceSessionPage } from "@/pages/session" import { NewHome, LegacyHome } from "@/pages/home" const NewSession = lazy(() => import("@/pages/new-session")) @@ -102,7 +102,7 @@ const SessionRoute = () => { return ( - + ) } diff --git a/packages/app/src/components/prompt-input/editor-dom.test.ts b/packages/app/src/components/prompt-input/editor-dom.test.ts index 3088522a59f6..52e5d98796ca 100644 --- a/packages/app/src/components/prompt-input/editor-dom.test.ts +++ b/packages/app/src/components/prompt-input/editor-dom.test.ts @@ -96,4 +96,14 @@ describe("prompt-input editor dom", () => { container.remove() }) + + test("setCursorPosition ignores editors detached during session remount", () => { + const container = document.createElement("div") + container.textContent = "old session" + window.getSelection()?.removeAllRanges() + + setCursorPosition(container, 3) + + expect(window.getSelection()?.rangeCount).toBe(0) + }) }) diff --git a/packages/app/src/components/prompt-input/editor-dom.ts b/packages/app/src/components/prompt-input/editor-dom.ts index 8575140d7d54..e771f10f428b 100644 --- a/packages/app/src/components/prompt-input/editor-dom.ts +++ b/packages/app/src/components/prompt-input/editor-dom.ts @@ -54,6 +54,9 @@ export function getCursorPosition(parent: HTMLElement): number { } export function setCursorPosition(parent: HTMLElement, position: number) { + // Focus restoration can outlive a session panel remount. Do not install a + // selection range backed by the detached editor from the previous session. + if (!parent.isConnected) return let remaining = position let node = parent.firstChild while (node) { diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 3ce28416760f..b66545e69b73 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -99,6 +99,7 @@ import { Persist, persisted } from "@/utils/persist" import { extractPromptFromParts } from "@/utils/prompt" import { formatServerError, isLocalSessionNotFoundError, isSessionNotFoundError } from "@/utils/server-errors" import { legacySessionHref, requireServerKey, sessionHref } from "@/utils/session-route" +import { ScopedKey } from "@/utils/server-scope" import { useUsageExceededDialogs } from "./session/usage-exceeded-dialogs" import { createSessionOwnership } from "./session/session-ownership" import { createSessionLineage } from "./session/session-lineage" @@ -152,9 +153,10 @@ export function SessionPage() { } // Rendered under app.tsx's TargetSessionRoute, which owns the per-server keyed -// remount around the server-scoped providers. Nothing here may key on the -// session ID: session tabs on the same server share this route instance, and -// workspace-scoped state (terminal, directory providers) lives below. +// remount around the server-scoped providers. Session changes are handled +// reactively inside Page: nothing here keys on the session ID, so session +// tabs on the same server share this route instance and workspace-scoped +// state (terminal, directory providers) stays alive across switches. export function TargetSessionRouteContent() { const params = useParams<{ serverKey: string; id: string }>() const serverSync = useServerSync() @@ -271,7 +273,7 @@ function ResolvedTargetSessionRoute() { - + @@ -281,11 +283,11 @@ function ResolvedTargetSessionRoute() { // Owns the workspace-identity remount. Must not include the session ID in the // key: SessionPage handles session changes reactively, and remounting here // destroys workspace-scoped state (terminal PTYs, file/prompt providers). -function TargetSessionPage() { +export function WorkspaceSessionPage() { const sdk = useSDK() const serverSDK = useServerSDK() return ( - + ) @@ -405,7 +407,6 @@ export default function Page() { }) const workspaceTabs = createMemo(() => layout.tabs(workspaceKey)) - const sessionPanelKey = createMemo(() => (params.id ? `${serverSDK().scope}\0${params.id}` : undefined)) createEffect( on( @@ -2114,7 +2115,7 @@ export default function Page() { - + {(_id) => ( {settings.general.newLayoutDesigns() ? ( - - {(_) => ( - - {sessionPanelContent()} - - )} - + + {sessionPanelContent()} + ) : ( - {sessionPanelContent()} + {sessionPanelContent()} )} diff --git a/patches/solid-js@1.9.10.patch b/patches/solid-js@1.9.10.patch index e4e38c2e6840..b1ef007025a7 100644 --- a/patches/solid-js@1.9.10.patch +++ b/patches/solid-js@1.9.10.patch @@ -1,11 +1,5 @@ -diff --git a/Users/brendonovich/github.com/anomalyco/opencode/node_modules/solid-js/.bun-tag-6fcb6b48d6947d2c b/.bun-tag-6fcb6b48d6947d2c -new file mode 100644 -index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 -diff --git a/Users/brendonovich/github.com/anomalyco/opencode/node_modules/solid-js/.bun-tag-b272f631c12927b0 b/.bun-tag-b272f631c12927b0 -new file mode 100644 -index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/dist/dev.cjs b/dist/dev.cjs -index 7104749486e4361e8c4ee7836a8046582cec7aa1..0501eb1ec5d13b81ecb13a5ac1a82db42502b976 100644 +index 7104749..dc3eac9 100644 --- a/dist/dev.cjs +++ b/dist/dev.cjs @@ -764,6 +764,8 @@ function runComputation(node, value, time) { @@ -17,8 +11,33 @@ index 7104749486e4361e8c4ee7836a8046582cec7aa1..0501eb1ec5d13b81ecb13a5ac1a82db4 Transition.sources.add(node); node.tValue = nextValue; } else node.value = nextValue; +@@ -987,18 +989,21 @@ function cleanNode(node) { + } + } + if (node.tOwned) { +- for (i = node.tOwned.length - 1; i >= 0; i--) cleanNode(node.tOwned[i]); ++ const tOwned = node.tOwned; + delete node.tOwned; ++ for (i = tOwned.length - 1; i >= 0; i--) cleanNode(tOwned[i]); + } + if (Transition && Transition.running && node.pure) { + reset(node, true); + } else if (node.owned) { +- for (i = node.owned.length - 1; i >= 0; i--) cleanNode(node.owned[i]); ++ const owned = node.owned; + node.owned = null; ++ for (i = owned.length - 1; i >= 0; i--) cleanNode(owned[i]); + } + if (node.cleanups) { +- for (i = node.cleanups.length - 1; i >= 0; i--) node.cleanups[i](); ++ const cleanups = node.cleanups; + node.cleanups = null; ++ for (i = cleanups.length - 1; i >= 0; i--) cleanups[i](); + } + if (Transition && Transition.running) node.tState = 0;else node.state = 0; + delete node.sourceMap; diff --git a/dist/dev.js b/dist/dev.js -index ea5e4bc2fd4f0b3922a73d9134439529dc81339f..4b3ec07e624d20fdd23d6941a4fdde6d3a78cca3 100644 +index ea5e4bc..a2e2d59 100644 --- a/dist/dev.js +++ b/dist/dev.js @@ -762,6 +762,8 @@ function runComputation(node, value, time) { @@ -30,8 +49,33 @@ index ea5e4bc2fd4f0b3922a73d9134439529dc81339f..4b3ec07e624d20fdd23d6941a4fdde6d Transition.sources.add(node); node.tValue = nextValue; } else node.value = nextValue; +@@ -985,18 +987,21 @@ function cleanNode(node) { + } + } + if (node.tOwned) { +- for (i = node.tOwned.length - 1; i >= 0; i--) cleanNode(node.tOwned[i]); ++ const tOwned = node.tOwned; + delete node.tOwned; ++ for (i = tOwned.length - 1; i >= 0; i--) cleanNode(tOwned[i]); + } + if (Transition && Transition.running && node.pure) { + reset(node, true); + } else if (node.owned) { +- for (i = node.owned.length - 1; i >= 0; i--) cleanNode(node.owned[i]); ++ const owned = node.owned; + node.owned = null; ++ for (i = owned.length - 1; i >= 0; i--) cleanNode(owned[i]); + } + if (node.cleanups) { +- for (i = node.cleanups.length - 1; i >= 0; i--) node.cleanups[i](); ++ const cleanups = node.cleanups; + node.cleanups = null; ++ for (i = cleanups.length - 1; i >= 0; i--) cleanups[i](); + } + if (Transition && Transition.running) node.tState = 0;else node.state = 0; + delete node.sourceMap; diff --git a/dist/solid.cjs b/dist/solid.cjs -index 7c133a2b254678a84fd61d719fbeffad766e1331..2f68c99f2698210cc0bac62f074cc8cd3beb2881 100644 +index 7c133a2..5ef1501 100644 --- a/dist/solid.cjs +++ b/dist/solid.cjs @@ -717,6 +717,8 @@ function runComputation(node, value, time) { @@ -43,8 +87,33 @@ index 7c133a2b254678a84fd61d719fbeffad766e1331..2f68c99f2698210cc0bac62f074cc8cd Transition.sources.add(node); node.tValue = nextValue; } else node.value = nextValue; +@@ -938,18 +940,21 @@ function cleanNode(node) { + } + } + if (node.tOwned) { +- for (i = node.tOwned.length - 1; i >= 0; i--) cleanNode(node.tOwned[i]); ++ const tOwned = node.tOwned; + delete node.tOwned; ++ for (i = tOwned.length - 1; i >= 0; i--) cleanNode(tOwned[i]); + } + if (Transition && Transition.running && node.pure) { + reset(node, true); + } else if (node.owned) { +- for (i = node.owned.length - 1; i >= 0; i--) cleanNode(node.owned[i]); ++ const owned = node.owned; + node.owned = null; ++ for (i = owned.length - 1; i >= 0; i--) cleanNode(owned[i]); + } + if (node.cleanups) { +- for (i = node.cleanups.length - 1; i >= 0; i--) node.cleanups[i](); ++ const cleanups = node.cleanups; + node.cleanups = null; ++ for (i = cleanups.length - 1; i >= 0; i--) cleanups[i](); + } + if (Transition && Transition.running) node.tState = 0;else node.state = 0; + } diff --git a/dist/solid.js b/dist/solid.js -index 656fd26e7e5c794aa22df19c2377ff5c0591fc29..f08e9f5a7157c3506e5b6922fe2ef991335a80be 100644 +index 656fd26..6e0038c 100644 --- a/dist/solid.js +++ b/dist/solid.js @@ -715,6 +715,8 @@ function runComputation(node, value, time) { @@ -56,3 +125,28 @@ index 656fd26e7e5c794aa22df19c2377ff5c0591fc29..f08e9f5a7157c3506e5b6922fe2ef991 Transition.sources.add(node); node.tValue = nextValue; } else node.value = nextValue; +@@ -936,18 +938,21 @@ function cleanNode(node) { + } + } + if (node.tOwned) { +- for (i = node.tOwned.length - 1; i >= 0; i--) cleanNode(node.tOwned[i]); ++ const tOwned = node.tOwned; + delete node.tOwned; ++ for (i = tOwned.length - 1; i >= 0; i--) cleanNode(tOwned[i]); + } + if (Transition && Transition.running && node.pure) { + reset(node, true); + } else if (node.owned) { +- for (i = node.owned.length - 1; i >= 0; i--) cleanNode(node.owned[i]); ++ const owned = node.owned; + node.owned = null; ++ for (i = owned.length - 1; i >= 0; i--) cleanNode(owned[i]); + } + if (node.cleanups) { +- for (i = node.cleanups.length - 1; i >= 0; i--) node.cleanups[i](); ++ const cleanups = node.cleanups; + node.cleanups = null; ++ for (i = cleanups.length - 1; i >= 0; i--) cleanups[i](); + } + if (Transition && Transition.running) node.tState = 0;else node.state = 0; + }