diff --git a/app-prefixable/src/app.tsx b/app-prefixable/src/app.tsx index f82d1c07..9cf9504e 100644 --- a/app-prefixable/src/app.tsx +++ b/app-prefixable/src/app.tsx @@ -1,5 +1,5 @@ import { Router, Route, Navigate, useParams } from "@solidjs/router" -import { createSignal, onMount, onCleanup, For } from "solid-js" +import { createSignal, onMount, onCleanup, For, type Accessor } from "solid-js" import { BasePathProvider, useBasePath } from "./context/base-path" import { ServerProvider, useServer } from "./context/server" import { BrandingProvider } from "./context/branding" @@ -33,7 +33,7 @@ function getLastSessionHref(encodedDir: string): string { function DirectoryIndex() { const params = useParams<{ dir: string }>() - return + return } function SessionIndex() { @@ -41,7 +41,7 @@ function SessionIndex() { const href = getLastSessionHref(params.dir) if (href === "session") return const id = href.replace(/^session\//, "") - return + return } function AppRoutes() { @@ -71,16 +71,21 @@ function AppRoutes() { * Reads the active directory from window.location (outside Router context). * Re-evaluates on popstate and on history.pushState/history.replaceState navigation. */ -function useActiveDirectory() { +function useRouteState() { const [dir, setDir] = createSignal( typeof window === "undefined" ? undefined : deriveDirectoryFromPathname(), ) + const [pathname, setPathname] = createSignal( + typeof window === "undefined" ? "" : window.location.pathname, + ) onMount(() => { - // Ensure correct value once mounted (covers SSR hydration) - setDir(deriveDirectoryFromPathname()) + function update() { + setDir(deriveDirectoryFromPathname()) + setPathname(window.location.pathname) + } - function update() { setDir(deriveDirectoryFromPathname()) } + update() // Patch pushState/replaceState to detect SolidJS Router navigations // instead of polling with setInterval @@ -97,7 +102,10 @@ function useActiveDirectory() { }) }) - return dir + return { + activeDirectory: dir, + pathname, + } } function useProjectsList() { @@ -129,7 +137,11 @@ function useProjectsList() { return projects } -function AppWithServer(props: { projects: () => Project[]; activeDirectory: () => string | undefined }) { +function AppWithServer(props: { + projects: () => Project[] + activeDirectory: Accessor + pathname: Accessor +}) { const { serverUrl, activeServerKey } = useServer() // Key by server config to force full remount when switching or editing servers @@ -141,7 +153,11 @@ function AppWithServer(props: { projects: () => Project[]; activeDirectory: () = - + @@ -158,11 +174,15 @@ function AppWithServer(props: { projects: () => Project[]; activeDirectory: () = export function App() { const projects = useProjectsList() - const activeDirectory = useActiveDirectory() + const route = useRouteState() return ( - + ) } diff --git a/app-prefixable/src/context/global-events.tsx b/app-prefixable/src/context/global-events.tsx index 8565e38f..7a23e643 100644 --- a/app-prefixable/src/context/global-events.tsx +++ b/app-prefixable/src/context/global-events.tsx @@ -44,6 +44,7 @@ const GlobalEventsContext = createContext() export function GlobalEventsProvider(props: ParentProps & { projects: () => { worktree: string }[] activeDirectory: () => string | undefined + pathname: () => string }) { const { authHeaders, serverUrl, activeServer } = useServer() @@ -458,11 +459,18 @@ export function GlobalEventsProvider(props: ParentProps & { // Active project is excluded — it has its own EventProvider. // Remote servers are skipped — local projects don't exist on remote servers. createEffect(on( - () => ({ dirs: props.projects().map((p) => p.worktree), active: props.activeDirectory(), isRemote: !activeServer().isDefault }), + () => ({ + dirs: props.projects().map((p) => p.worktree), + active: props.activeDirectory(), + isRemote: !activeServer().isDefault, + isSettings: props.pathname().endsWith("/settings"), + }), (current) => { // When a remote server is active, disconnect all global event connections // since local projects don't exist on the remote server. - if (current.isRemote) { + // Also suspend these background connections on settings routes to avoid + // opening one SSE stream per saved project while the user edits settings. + if (current.isRemote || current.isSettings) { for (const dir of [...connections.keys()]) { disconnectDirectory(dir) } diff --git a/app-prefixable/src/context/server.tsx b/app-prefixable/src/context/server.tsx index 278748a1..06ed3662 100644 --- a/app-prefixable/src/context/server.tsx +++ b/app-prefixable/src/context/server.tsx @@ -75,7 +75,7 @@ function loadServers(): ServerConfig[] { const parsed: ServerConfig[] = raw .filter(isValidServerEntry) .map((s) => { - const obj = s as Record + const obj = s as unknown as Record // Prefer credentials from sessionStorage; fall back to authMethod stub if (creds[obj.id as string]) { return { ...s, auth: normalizeAuth({ auth: creds[obj.id as string] } as unknown as Record) } diff --git a/app-prefixable/src/context/theme.tsx b/app-prefixable/src/context/theme.tsx index 88e216aa..9b640022 100644 --- a/app-prefixable/src/context/theme.tsx +++ b/app-prefixable/src/context/theme.tsx @@ -40,6 +40,10 @@ export function ThemeProvider(props: ParentProps) { const query = typeof window !== "undefined" && typeof window.matchMedia === "function" ? window.matchMedia("(prefers-color-scheme: dark)") : undefined + const legacyQuery = query as (MediaQueryList & { + addListener?: (listener: (event: MediaQueryListEvent) => void) => void + removeListener?: (listener: (event: MediaQueryListEvent) => void) => void + }) | undefined const [systemDark, setSystemDark] = createSignal(query?.matches ?? false) @@ -48,9 +52,9 @@ export function ThemeProvider(props: ParentProps) { if ("addEventListener" in query) { query.addEventListener("change", handler) onCleanup(() => query.removeEventListener("change", handler)) - } else if ("addListener" in query) { - query.addListener(handler) - onCleanup(() => query.removeListener(handler)) + } else if (legacyQuery?.addListener && legacyQuery.removeListener) { + legacyQuery.addListener(handler) + onCleanup(() => legacyQuery.removeListener?.(handler)) } } diff --git a/app-prefixable/src/entry.tsx b/app-prefixable/src/entry.tsx index b8a89b45..cfbad27a 100644 --- a/app-prefixable/src/entry.tsx +++ b/app-prefixable/src/entry.tsx @@ -11,8 +11,10 @@ if (!root) { throw new Error("Root element not found") } +const mount = root + // Clear the loading text first -root.innerHTML = "" +mount.innerHTML = "" async function start() { const cleanup = await preventLegacyServiceWorkerCaching().catch((e) => { @@ -27,17 +29,17 @@ async function start() { } console.log("[OpenCode] Rendering...") - render(() => , root) + render(() => , mount) console.log("[OpenCode] Rendered successfully") - console.log("[OpenCode] Root innerHTML:", root.innerHTML.slice(0, 200)) + console.log("[OpenCode] Root innerHTML:", mount.innerHTML.slice(0, 200)) } start().catch((e) => { console.error("[OpenCode] Render error:", e) - root.innerHTML = "" + mount.innerHTML = "" const message = document.createElement("div") message.style.color = "red" message.style.padding = "20px" message.textContent = `Error: ${e instanceof Error && e.message.trim() ? e.message : String(e)}` - root.appendChild(message) + mount.appendChild(message) }) diff --git a/app-prefixable/src/pages/layout.tsx b/app-prefixable/src/pages/layout.tsx index d7eea7a7..a7f13142 100644 --- a/app-prefixable/src/pages/layout.tsx +++ b/app-prefixable/src/pages/layout.tsx @@ -878,14 +878,16 @@ export function Layout(props: ParentProps) { const [now, setNow] = createSignal(new Date()); onMount(() => { - const timer = { id: 0 as ReturnType }; + let timer: ReturnType | undefined; const schedule = () => { const next = new Date(); next.setHours(24, 0, 0, 0); - timer.id = setTimeout(() => { setNow(new Date()); schedule(); }, next.getTime() - Date.now()); + timer = setTimeout(() => { setNow(new Date()); schedule(); }, next.getTime() - Date.now()); }; schedule(); - onCleanup(() => clearTimeout(timer.id)); + onCleanup(() => { + if (timer) clearTimeout(timer); + }); }); const pinnedSessions = createMemo(() => { diff --git a/shared/proxy.ts b/shared/proxy.ts index cb0c4558..9c11d7bb 100644 --- a/shared/proxy.ts +++ b/shared/proxy.ts @@ -115,7 +115,11 @@ export async function handleProxyRequest(path: string, req: Request): Promise