From 095328faf439fbdc62506f3653875fbfec5c60ab Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Tue, 27 Jan 2026 06:27:27 -0600 Subject: fix(app): non-fatal error handling --- packages/app/src/pages/directory-layout.tsx | 22 ++++++++++++++++++---- packages/app/src/pages/layout.tsx | 24 +++++++++++++----------- packages/app/src/pages/session.tsx | 27 ++++++++++++++++++++++++--- 3 files changed, 55 insertions(+), 18 deletions(-) (limited to 'packages/app/src/pages') diff --git a/packages/app/src/pages/directory-layout.tsx b/packages/app/src/pages/directory-layout.tsx index caad6c996..037b08c72 100644 --- a/packages/app/src/pages/directory-layout.tsx +++ b/packages/app/src/pages/directory-layout.tsx @@ -1,22 +1,36 @@ -import { createMemo, Show, type ParentProps } from "solid-js" +import { createEffect, createMemo, Show, type ParentProps } from "solid-js" import { useNavigate, useParams } from "@solidjs/router" import { SDKProvider, useSDK } from "@/context/sdk" import { SyncProvider, useSync } from "@/context/sync" import { LocalProvider } from "@/context/local" -import { base64Decode } from "@opencode-ai/util/encode" import { DataProvider } from "@opencode-ai/ui/context" import { iife } from "@opencode-ai/util/iife" import type { QuestionAnswer } from "@opencode-ai/sdk/v2" +import { decode64 } from "@/utils/base64" +import { showToast } from "@opencode-ai/ui/toast" +import { useLanguage } from "@/context/language" export default function Layout(props: ParentProps) { const params = useParams() const navigate = useNavigate() + const language = useLanguage() const directory = createMemo(() => { - return base64Decode(params.dir!) + return decode64(params.dir) ?? "" + }) + + createEffect(() => { + if (!params.dir) return + if (directory()) return + showToast({ + variant: "error", + title: language.t("common.requestFailed"), + description: "Invalid directory in URL.", + }) + navigate("/") }) return ( - + {iife(() => { diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 283144872..ae86dbb7e 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -19,7 +19,8 @@ import { A, useNavigate, useParams } from "@solidjs/router" import { useLayout, getAvatarColors, LocalProject } from "@/context/layout" import { useGlobalSync } from "@/context/global-sync" import { Persist, persisted } from "@/utils/persist" -import { base64Decode, base64Encode } from "@opencode-ai/util/encode" +import { base64Encode } from "@opencode-ai/util/encode" +import { decode64 } from "@/utils/base64" import { Avatar } from "@opencode-ai/ui/avatar" import { ResizeHandle } from "@opencode-ai/ui/resize-handle" import { Button } from "@opencode-ai/ui/button" @@ -420,7 +421,7 @@ export default function Layout(props: ParentProps) { } } - const currentDir = params.dir ? base64Decode(params.dir) : undefined + const currentDir = decode64(params.dir) const currentSession = params.id if (directory === currentDir && props.sessionID === currentSession) return if (directory === currentDir && session?.parentID === currentSession) return @@ -449,7 +450,7 @@ export default function Layout(props: ParentProps) { onCleanup(unsub) createEffect(() => { - const currentDir = params.dir ? base64Decode(params.dir) : undefined + const currentDir = decode64(params.dir) const currentSession = params.id if (!currentDir || !currentSession) return const sessionKey = `${currentDir}:${currentSession}` @@ -503,7 +504,7 @@ export default function Layout(props: ParentProps) { } const currentProject = createMemo(() => { - const directory = params.dir ? base64Decode(params.dir) : undefined + const directory = decode64(params.dir) if (!directory) return const projects = layout.projects.list() @@ -638,7 +639,7 @@ export default function Layout(props: ParentProps) { const compare = sortSessions(Date.now()) if (workspaceSetting()) { const dirs = workspaceIds(project) - const activeDir = params.dir ? base64Decode(params.dir) : "" + const activeDir = decode64(params.dir) ?? "" const result: Session[] = [] for (const dir of dirs) { const expanded = store.workspaceExpanded[dir] ?? dir === project.worktree @@ -1188,7 +1189,7 @@ export default function Layout(props: ParentProps) { layout.projects.close(directory) layout.projects.open(root) - if (params.dir && base64Decode(params.dir) === directory) { + if (params.dir && decode64(params.dir) === directory) { navigateToProject(root) } } @@ -1431,7 +1432,8 @@ export default function Layout(props: ParentProps) { const dir = value.dir const id = value.id if (!dir || !id) return - const directory = base64Decode(dir) + const directory = decode64(dir) + if (!directory) return setStore("lastSession", directory, id) notification.session.markViewed(id) const expanded = untrack(() => store.workspaceExpanded[directory]) @@ -1454,7 +1456,7 @@ export default function Layout(props: ParentProps) { if (!project) return if (workspaceSetting()) { - const activeDir = params.dir ? base64Decode(params.dir) : "" + const activeDir = decode64(params.dir) ?? "" const dirs = [project.worktree, ...(project.sandboxes ?? [])] for (const directory of dirs) { const expanded = store.workspaceExpanded[directory] ?? directory === project.worktree @@ -1504,7 +1506,7 @@ export default function Layout(props: ParentProps) { const local = project.worktree const dirs = [local, ...(project.sandboxes ?? [])] const active = currentProject() - const directory = active?.worktree === project.worktree && params.dir ? base64Decode(params.dir) : undefined + const directory = active?.worktree === project.worktree ? decode64(params.dir) : undefined const extra = directory && directory !== local && !dirs.includes(directory) ? directory : undefined const pending = extra ? WorktreeState.get(extra)?.status === "pending" : false @@ -1930,7 +1932,7 @@ export default function Layout(props: ParentProps) { }) const local = createMemo(() => props.directory === props.project.worktree) const active = createMemo(() => { - const current = params.dir ? base64Decode(params.dir) : "" + const current = decode64(params.dir) ?? "" return current === props.directory }) const workspaceValue = createMemo(() => { @@ -2131,7 +2133,7 @@ export default function Layout(props: ParentProps) { const SortableProject = (props: { project: LocalProject; mobile?: boolean }): JSX.Element => { const sortable = createSortable(props.project.worktree) const selected = createMemo(() => { - const current = params.dir ? base64Decode(params.dir) : "" + const current = decode64(params.dir) ?? "" return props.project.worktree === current || props.project.sandboxes?.includes(current) }) diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 39f5b057e..d316efef7 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -28,7 +28,7 @@ import { useSync } from "@/context/sync" import { useTerminal, type LocalPTY } from "@/context/terminal" import { useLayout } from "@/context/layout" import { Terminal } from "@/components/terminal" -import { checksum, base64Encode, base64Decode } from "@opencode-ai/util/encode" +import { checksum, base64Encode } from "@opencode-ai/util/encode" import { findLast } from "@opencode-ai/util/array" import { useDialog } from "@opencode-ai/ui/context/dialog" import { DialogSelectFile } from "@/components/dialog-select-file" @@ -47,6 +47,7 @@ import { useComments, type LineComment } from "@/context/comments" import { extractPromptFromParts } from "@/utils/prompt" import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd" import { usePermission } from "@/context/permission" +import { decode64 } from "@/utils/base64" import { showToast } from "@opencode-ai/ui/toast" import { SessionHeader, @@ -2126,8 +2127,28 @@ export default function Page() { if (!isSvg()) return const c = state()?.content if (!c) return - if (c.encoding === "base64") return base64Decode(c.content) - return c.content + if (c.encoding !== "base64") return c.content + return decode64(c.content) + }) + + const svgDecodeFailed = createMemo(() => { + if (!isSvg()) return false + const c = state()?.content + if (!c) return false + if (c.encoding !== "base64") return false + return svgContent() === undefined + }) + + const svgToast = { shown: false } + createEffect(() => { + if (!svgDecodeFailed()) return + if (svgToast.shown) return + svgToast.shown = true + showToast({ + variant: "error", + title: language.t("toast.file.loadFailed.title"), + description: "Invalid base64 content.", + }) }) const svgPreviewUrl = createMemo(() => { if (!isSvg()) return -- cgit v1.2.3