From c318e28317013fd1866d194bba23873ecffde33a Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Sat, 22 Aug 2026 02:33:15 +0000 Subject: [PATCH 1/2] fix(app): recover invalid session routes --- packages/app/src/shell/routes/routes.tsx | 14 ++++++++------ packages/app/src/shell/routes/session.test.ts | 4 +++- packages/app/src/shell/routes/session.ts | 8 +++++++- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/packages/app/src/shell/routes/routes.tsx b/packages/app/src/shell/routes/routes.tsx index 42851b622346..7b94b828158f 100644 --- a/packages/app/src/shell/routes/routes.tsx +++ b/packages/app/src/shell/routes/routes.tsx @@ -1,4 +1,4 @@ -import { Route, useParams } from "@solidjs/router" +import { Navigate, Route, useParams } from "@solidjs/router" import { createMemo, lazy, Show, type ParentProps } from "solid-js" import { Home } from "@/home/route" import { ServerProvider } from "@/runtime/server/current" @@ -6,7 +6,7 @@ import { useGlobal } from "@/runtime/server/runtime" import { ServerConnection } from "@/runtime/server/registry" import { LayoutProvider } from "@/shell/state/layout" import Shell from "@/shell/shell" -import { requireServerKey } from "./session" +import { parseServerKey } from "./session" export const File = lazy(() => import("@opencode-ai/session-ui/file").then((module) => ({ default: module.File }))) const loadDraftRoute = () => Promise.all([import("@/new-session/route"), File.preload()]).then(([module]) => module) @@ -44,12 +44,14 @@ export function AppRoutes() { function TargetServerRoute(props: ParentProps) { const params = useParams<{ serverKey: string }>() const global = useGlobal() - const connection = createMemo(() => - global.servers.list().find((item) => ServerConnection.key(item) === requireServerKey(params.serverKey)), - ) + const connection = createMemo(() => { + const key = parseServerKey(params.serverKey) + if (!key) return + return global.servers.list().find((item) => ServerConnection.key(item) === key) + }) return ( - + }> {(connection) => {props.children}} ) diff --git a/packages/app/src/shell/routes/session.test.ts b/packages/app/src/shell/routes/session.test.ts index de3101866539..3b79fe390c3a 100644 --- a/packages/app/src/shell/routes/session.test.ts +++ b/packages/app/src/shell/routes/session.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test" import { ServerConnection } from "@/runtime/server/registry" -import { requireServerKey, rootSession, sessionHref } from "./session" +import { parseServerKey, requireServerKey, rootSession, sessionHref } from "./session" describe("session routes", () => { test("builds and decodes a server-keyed session route", () => { @@ -12,6 +12,8 @@ describe("session routes", () => { }) test("rejects malformed server keys", () => { + expect(parseServerKey(undefined)).toBeUndefined() + expect(parseServerKey("not-base64")).toBeUndefined() expect(() => requireServerKey("not-base64")).toThrow("Invalid server route") }) diff --git a/packages/app/src/shell/routes/session.ts b/packages/app/src/shell/routes/session.ts index 01c8b9e47ae5..8d3cab37e7e8 100644 --- a/packages/app/src/shell/routes/session.ts +++ b/packages/app/src/shell/routes/session.ts @@ -7,8 +7,14 @@ export function sessionHref(server: ServerConnection.Key, sessionID: string) { } export function requireServerKey(segment: string | undefined) { + const key = parseServerKey(segment) + if (!key) throw new Error("Invalid server route") + return key +} + +export function parseServerKey(segment: string | undefined) { const key = decode64(segment) - if (!key || base64Encode(key) !== segment) throw new Error("Invalid server route") + if (!key || base64Encode(key) !== segment) return return ServerConnection.Key.make(key) } From eb8b006f9fc9d22dc6e2ce4722a92405902be872 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Sat, 22 Aug 2026 02:41:51 +0000 Subject: [PATCH 2/2] fix(app): await draft promotion transition --- .../app/src/new-session/composer-adapter.ts | 32 ++++++++----------- packages/app/src/shell/tabs/tabs.tsx | 4 +-- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/packages/app/src/new-session/composer-adapter.ts b/packages/app/src/new-session/composer-adapter.ts index 548669aa9fc8..7e9ca3dba4dc 100644 --- a/packages/app/src/new-session/composer-adapter.ts +++ b/packages/app/src/new-session/composer-adapter.ts @@ -1,6 +1,5 @@ import { base64Encode } from "@opencode-ai/util/encode" import { getDirectory } from "@opencode-ai/util/path" -import { startTransition } from "solid-js" import type { NewSessionComposerAdapter } from "@/composer/adapter" import { useComposerState } from "@/composer/persistence" import { createComposerControls, createComposerModelSelection } from "@/composer/selection" @@ -72,24 +71,21 @@ export function createNewSessionComposerAdapter(props: { if (!created) return data.session.remember(created) - await startTransition(() => { - tabs.updateDraft(props.draftID, { worktree: undefined }) - if (permission.isAutoAcceptingDirectory(projectDirectory)) { - permission.enableAutoAccept(created.id, sessionDirectory) - } - local.session.promote(sessionDirectory, created.id, { - agent: selection.agent, - model: selection.model, - variant: selection.variant ?? null, - }) - tabs.promoteDraft(props.draftID, { server: server.key, sessionId: created.id }) - submission.retarget( - prompt.capture( - { dir: base64Encode(sessionDirectory), id: created.id }, - { server: server.key, scope: serverSDK.scope }, - ), - ) + if (permission.isAutoAcceptingDirectory(projectDirectory)) { + permission.enableAutoAccept(created.id, sessionDirectory) + } + local.session.promote(sessionDirectory, created.id, { + agent: selection.agent, + model: selection.model, + variant: selection.variant ?? null, }) + await tabs.promoteDraft(props.draftID, { server: server.key, sessionId: created.id }) + submission.retarget( + prompt.capture( + { dir: base64Encode(sessionDirectory), id: created.id }, + { server: server.key, scope: serverSDK.scope }, + ), + ) return { id: created.id, diff --git a/packages/app/src/shell/tabs/tabs.tsx b/packages/app/src/shell/tabs/tabs.tsx index 7767ce17efdf..7e3da89a3f06 100644 --- a/packages/app/src/shell/tabs/tabs.tsx +++ b/packages/app/src/shell/tabs/tabs.tsx @@ -229,12 +229,12 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({ ) }) }, - promoteDraft(draftID: string, session: Omit) { + async promoteDraft(draftID: string, session: Omit) { // Keep the replacement and navigation atomic so /new-session never renders // after its backing draft tab has been removed from the store. const active = location.pathname === "/new-session" && location.query.draftId === draftID const next = { type: "session" as const, ...session } - void startTransition(() => { + await startTransition(() => { setStore( produce((tabs) => { const index = tabs.findIndex((tab) => tab.type === "draft" && tab.draftID === draftID)