fix(app): non-fatal error handling

This commit is contained in:
adamelmore
2026-01-27 06:27:27 -06:00
parent 743e83d9bf
commit 095328faf4
14 changed files with 354 additions and 212 deletions

View File

@@ -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 (
<Show when={params.dir}>
<Show when={directory()}>
<SDKProvider directory={directory()}>
<SyncProvider>
{iife(() => {

View File

@@ -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)
})

View File

@@ -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