summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src/pages
diff options
context:
space:
mode:
authorLuke Parker <[email protected]>2026-03-20 14:12:06 +1000
committerGitHub <[email protected]>2026-03-20 00:12:06 -0400
commitd460614cd7ad9e047a2792139ea67e16caa82ea7 (patch)
treeff415e8719c7b6edd73bc824da379308e4e62589 /packages/app/src/pages
parent7866dbcfcc36a60d22ad466eddf54c54b21fabe3 (diff)
downloadopencode-d460614cd7ad9e047a2792139ea67e16caa82ea7.tar.gz
opencode-d460614cd7ad9e047a2792139ea67e16caa82ea7.zip
fix: lots of desktop stability, better e2e error logging (#18300)
Diffstat (limited to 'packages/app/src/pages')
-rw-r--r--packages/app/src/pages/error.tsx10
-rw-r--r--packages/app/src/pages/layout.tsx47
-rw-r--r--packages/app/src/pages/layout/helpers.ts4
-rw-r--r--packages/app/src/pages/layout/sidebar-project.tsx7
-rw-r--r--packages/app/src/pages/layout/sidebar-workspace.tsx4
5 files changed, 42 insertions, 30 deletions
diff --git a/packages/app/src/pages/error.tsx b/packages/app/src/pages/error.tsx
index 11284b3d2..1cdc06116 100644
--- a/packages/app/src/pages/error.tsx
+++ b/packages/app/src/pages/error.tsx
@@ -1,11 +1,12 @@
import { TextField } from "@opencode-ai/ui/text-field"
import { Logo } from "@opencode-ai/ui/logo"
import { Button } from "@opencode-ai/ui/button"
-import { Component, Show } from "solid-js"
+import { Component, Show, onMount } from "solid-js"
import { createStore } from "solid-js/store"
import { usePlatform } from "@/context/platform"
import { useLanguage } from "@/context/language"
import { Icon } from "@opencode-ai/ui/icon"
+import type { E2EWindow } from "@/testing/terminal"
export type InitError = {
name: string
@@ -226,6 +227,13 @@ export const ErrorPage: Component<ErrorPageProps> = (props) => {
actionError: undefined as string | undefined,
})
+ onMount(() => {
+ const win = window as E2EWindow
+ if (!win.__opencode_e2e) return
+ const detail = formatError(props.error, language.t)
+ console.error(`[e2e:error-boundary] ${window.location.pathname}\n${detail}`)
+ })
+
async function checkForUpdates() {
if (!platform.checkUpdate) return
setStore("checking", true)
diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx
index 52ac7c5f3..8e2248469 100644
--- a/packages/app/src/pages/layout.tsx
+++ b/packages/app/src/pages/layout.tsx
@@ -129,6 +129,16 @@ export default function Layout(props: ParentProps) {
const theme = useTheme()
const language = useLanguage()
const initialDirectory = decode64(params.dir)
+ const route = createMemo(() => {
+ const slug = params.dir
+ if (!slug) return { slug, dir: "" }
+ const dir = decode64(slug)
+ if (!dir) return { slug, dir: "" }
+ return {
+ slug,
+ dir: globalSync.peek(dir, { bootstrap: false })[0].path.directory || dir,
+ }
+ })
const availableThemeEntries = createMemo(() => Object.entries(theme.themes()))
const colorSchemeOrder: ColorScheme[] = ["system", "light", "dark"]
const colorSchemeKey: Record<ColorScheme, "theme.scheme.system" | "theme.scheme.light" | "theme.scheme.dark"> = {
@@ -137,7 +147,7 @@ export default function Layout(props: ParentProps) {
dark: "theme.scheme.dark",
}
const colorSchemeLabel = (scheme: ColorScheme) => language.t(colorSchemeKey[scheme])
- const currentDir = createMemo(() => decode64(params.dir) ?? "")
+ const currentDir = createMemo(() => route().dir)
const [state, setState] = createStore({
autoselect: !initialDirectory,
@@ -484,8 +494,8 @@ export default function Layout(props: ParentProps) {
}
const currentSession = params.id
- if (directory === currentDir() && props.sessionID === currentSession) return
- if (directory === currentDir() && session?.parentID === currentSession) return
+ if (workspaceKey(directory) === workspaceKey(currentDir()) && props.sessionID === currentSession) return
+ if (workspaceKey(directory) === workspaceKey(currentDir()) && session?.parentID === currentSession) return
dismissSessionAlert(sessionKey)
@@ -620,7 +630,7 @@ export default function Layout(props: ParentProps) {
const activeDir = currentDir()
return workspaceIds(project).filter((directory) => {
const expanded = store.workspaceExpanded[directory] ?? directory === project.worktree
- const active = directory === activeDir
+ const active = workspaceKey(directory) === workspaceKey(activeDir)
return expanded || active
})
})
@@ -687,7 +697,7 @@ export default function Layout(props: ParentProps) {
seen: lru,
keep: sessionID,
limit: PREFETCH_MAX_SESSIONS_PER_DIR,
- preserve: directory === params.dir && params.id ? [params.id] : undefined,
+ preserve: params.id && workspaceKey(directory) === workspaceKey(currentDir()) ? [params.id] : undefined,
})
}
@@ -700,7 +710,7 @@ export default function Layout(props: ParentProps) {
})
createEffect(() => {
- params.dir
+ route()
globalSDK.url
prefetchToken.value += 1
@@ -1692,13 +1702,10 @@ export default function Layout(props: ParentProps) {
createEffect(
on(
() => {
- const dir = params.dir
- const directory = dir ? decode64(dir) : undefined
- const resolved = directory ? globalSync.child(directory, { bootstrap: false })[0].path.directory : ""
- return [pageReady(), dir, params.id, currentProject()?.worktree, directory, resolved] as const
+ return [pageReady(), route().slug, params.id, currentProject()?.worktree, currentDir()] as const
},
- ([ready, dir, id, root, directory, resolved]) => {
- if (!ready || !dir || !directory) {
+ ([ready, slug, id, root, dir]) => {
+ if (!ready || !slug || !dir) {
activeRoute.session = ""
activeRoute.sessionProject = ""
activeRoute.directory = ""
@@ -1712,29 +1719,28 @@ export default function Layout(props: ParentProps) {
return
}
- const next = resolved || directory
- const session = `${dir}/${id}`
+ const session = `${slug}/${id}`
if (!root) {
activeRoute.session = session
- activeRoute.directory = next
+ activeRoute.directory = dir
activeRoute.sessionProject = ""
return
}
if (server.projects.last() !== root) server.projects.touch(root)
- const changed = session !== activeRoute.session || next !== activeRoute.directory
+ const changed = session !== activeRoute.session || dir !== activeRoute.directory
if (changed) {
activeRoute.session = session
- activeRoute.directory = next
- activeRoute.sessionProject = syncSessionRoute(next, id, root)
+ activeRoute.directory = dir
+ activeRoute.sessionProject = syncSessionRoute(dir, id, root)
return
}
if (root === activeRoute.sessionProject) return
- activeRoute.directory = next
- activeRoute.sessionProject = rememberSessionRoute(next, id, root)
+ activeRoute.directory = dir
+ activeRoute.sessionProject = rememberSessionRoute(dir, id, root)
},
),
)
@@ -1927,6 +1933,7 @@ export default function Layout(props: ParentProps) {
const projectSidebarCtx: ProjectSidebarContext = {
currentDir,
+ currentProject,
sidebarOpened: () => layout.sidebar.opened(),
sidebarHovering,
hoverProject: () => state.hoverProject,
diff --git a/packages/app/src/pages/layout/helpers.ts b/packages/app/src/pages/layout/helpers.ts
index 209cff8a7..226098c1c 100644
--- a/packages/app/src/pages/layout/helpers.ts
+++ b/packages/app/src/pages/layout/helpers.ts
@@ -40,10 +40,10 @@ export const latestRootSession = (stores: SessionStore[], now: number) =>
stores.flatMap(roots).sort(sortSessions(now))[0]
export function hasProjectPermissions<T>(
- request: Record<string, T[] | undefined>,
+ request: Record<string, T[] | undefined> | undefined,
include: (item: T) => boolean = () => true,
) {
- return Object.values(request).some((list) => list?.some(include))
+ return Object.values(request ?? {}).some((list) => list?.some(include))
}
export const childMapByParent = (sessions: Session[] | undefined) => {
diff --git a/packages/app/src/pages/layout/sidebar-project.tsx b/packages/app/src/pages/layout/sidebar-project.tsx
index a26bc1831..252826456 100644
--- a/packages/app/src/pages/layout/sidebar-project.tsx
+++ b/packages/app/src/pages/layout/sidebar-project.tsx
@@ -15,6 +15,7 @@ import { childMapByParent, displayName, sortedRootSessions } from "./helpers"
export type ProjectSidebarContext = {
currentDir: Accessor<string>
+ currentProject: Accessor<LocalProject | undefined>
sidebarOpened: Accessor<boolean>
sidebarHovering: Accessor<boolean>
hoverProject: Accessor<string | undefined>
@@ -278,11 +279,7 @@ export const SortableProject = (props: {
const globalSync = useGlobalSync()
const language = useLanguage()
const sortable = createSortable(props.project.worktree)
- const selected = createMemo(
- () =>
- props.project.worktree === props.ctx.currentDir() ||
- props.project.sandboxes?.includes(props.ctx.currentDir()) === true,
- )
+ const selected = createMemo(() => props.ctx.currentProject()?.worktree === props.project.worktree)
const workspaces = createMemo(() => props.ctx.workspaceIds(props.project).slice(0, 2))
const workspaceEnabled = createMemo(() => props.ctx.workspacesEnabled(props.project))
const dirs = createMemo(() => props.ctx.workspaceIds(props.project))
diff --git a/packages/app/src/pages/layout/sidebar-workspace.tsx b/packages/app/src/pages/layout/sidebar-workspace.tsx
index 127626feb..3bf00ea42 100644
--- a/packages/app/src/pages/layout/sidebar-workspace.tsx
+++ b/packages/app/src/pages/layout/sidebar-workspace.tsx
@@ -17,7 +17,7 @@ import { type LocalProject } from "@/context/layout"
import { useGlobalSync } from "@/context/global-sync"
import { useLanguage } from "@/context/language"
import { NewSessionItem, SessionItem, SessionSkeleton } from "./sidebar-items"
-import { childMapByParent, sortedRootSessions } from "./helpers"
+import { childMapByParent, sortedRootSessions, workspaceKey } from "./helpers"
type InlineEditorComponent = (props: {
id: string
@@ -323,7 +323,7 @@ export const SortableWorkspace = (props: {
const sessions = createMemo(() => sortedRootSessions(workspaceStore, props.sortNow()))
const children = createMemo(() => childMapByParent(workspaceStore.session))
const local = createMemo(() => props.directory === props.project.worktree)
- const active = createMemo(() => props.ctx.currentDir() === props.directory)
+ const active = createMemo(() => workspaceKey(props.ctx.currentDir()) === workspaceKey(props.directory))
const workspaceValue = createMemo(() => {
const branch = workspaceStore.vcs?.branch
const name = branch ?? getFilename(props.directory)