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;
+ }