Revert "test(app): fail on unexpected error toasts"

This reverts commit eb26d40850.
This commit is contained in:
LukeParkerDev 2026-07-10 09:32:56 +10:00
parent 555a1299de
commit e8e5f7b8a0
61 changed files with 62 additions and 374 deletions

View file

@ -1,211 +0,0 @@
import { expect, test as base, type Page, type TestInfo } from "@playwright/test"
const marker = "__OPENCODE_E2E_ERROR_TOAST__"
type ErrorToastObservation = {
component: "legacy" | "v2"
title: string
description: string
text: string
url: string
timestamp: number
}
type ErrorToastExpectation = {
pattern: string | RegExp
seen: boolean
}
export type ErrorToastControl = {
expect: (pattern: string | RegExp) => void
allow: (pattern?: string | RegExp) => void
}
type ErrorToastState = ErrorToastControl & {
allowed: boolean
allowedPatterns: (string | RegExp)[]
expected: ErrorToastExpectation[]
}
type Fixtures = {
errorToasts: ErrorToastControl
}
export const test = base.extend<Fixtures>({
errorToasts: async ({}, use) => use(createErrorToastState()),
page: async ({ page, errorToasts }, use, testInfo) => {
const guard = await guardPage(page, testInfo, errorToasts as ErrorToastState)
try {
await use(page)
} finally {
await guard.finish()
}
},
})
export { expect }
export type { Browser, CDPSession, Locator, Page, Route, TestInfo } from "@playwright/test"
export async function guardPage(page: Page, testInfo: TestInfo, control = createErrorToastState()) {
const observations: ErrorToastObservation[] = []
const unexpected: ErrorToastObservation[] = []
let closing = false
const onConsole = (message: { text: () => string }) => {
const text = message.text()
if (!text.startsWith(marker)) return
const observation = JSON.parse(text.slice(marker.length)) as ErrorToastObservation
observations.push(observation)
console.error(`E2E_ERROR_TOAST ${JSON.stringify(observation)}`)
const expected = control.expected.find((item) => !item.seen && matches(item.pattern, observation.text))
if (expected) {
expected.seen = true
return
}
if (control.allowed || control.allowedPatterns.some((pattern) => matches(pattern, observation.text))) return
unexpected.push(observation)
if (closing) return
closing = true
void page.close().catch(() => {})
}
page.on("console", onConsole)
await page.addInitScript(installErrorToastObserver, marker)
await page.evaluate(installErrorToastObserver, marker)
return {
async finish() {
if (!page.isClosed()) {
await page
.evaluate(() => {
;(window as Window & { __flushErrorToastObserver?: () => void }).__flushErrorToastObserver?.()
})
.catch(() => {})
}
page.off("console", onConsole)
if (observations.length > 0) {
await testInfo.attach("error-toasts", {
body: JSON.stringify(observations, null, 2),
contentType: "application/json",
})
}
const missing = control.expected.filter((item) => !item.seen)
if (unexpected.length === 0 && missing.length === 0) return
const messages = [
...unexpected.map((item) => `Unexpected error toast: ${item.text}`),
...missing.map((item) => `Expected error toast was not shown: ${String(item.pattern)}`),
]
throw new Error(messages.join("\n"))
},
}
}
function createErrorToastState(): ErrorToastState {
const state: ErrorToastState = {
allowed: false,
allowedPatterns: [],
expected: [],
expect(pattern) {
state.expected.push({ pattern, seen: false })
},
allow(pattern) {
if (pattern !== undefined) {
state.allowedPatterns.push(pattern)
return
}
state.allowed = true
},
}
return state
}
function matches(pattern: string | RegExp, value: string) {
if (typeof pattern === "string") return value.includes(pattern)
pattern.lastIndex = 0
return pattern.test(value)
}
function installErrorToastObserver(marker: string) {
const owner = window as Window & {
__errorToastObserverInstalled?: boolean
__flushErrorToastObserver?: () => void
}
if (owner.__errorToastObserverInstalled) return
owner.__errorToastObserverInstalled = true
const selector = '[data-component="toast"][data-variant="error"], [data-component="toast-v2"][data-variant="error"]'
const seen = new WeakSet<Element>()
const pending = new Set<Node>()
let scheduled = false
const inspect = (node: Node) => {
const element = node instanceof HTMLElement ? node : node.parentElement
if (!element) return
const candidates = [
...(element.matches(selector) ? [element] : []),
...element.querySelectorAll<HTMLElement>(selector),
...(element.closest<HTMLElement>(selector) ? [element.closest<HTMLElement>(selector)!] : []),
]
candidates.forEach((toast) => {
if (seen.has(toast)) return
const text = toast.textContent?.replace(/\s+/g, " ").trim()
if (!text) return
seen.add(toast)
const component = toast.dataset.component === "toast-v2" ? "v2" : "legacy"
const title = toast.querySelector<HTMLElement>(`[data-slot="toast${component === "v2" ? "-v2" : ""}-title"]`)
const description = toast.querySelector<HTMLElement>(
`[data-slot="toast${component === "v2" ? "-v2" : ""}-description"]`,
)
console.debug(
marker +
JSON.stringify({
component,
title: title?.textContent?.trim() ?? "",
description: description?.textContent?.trim() ?? "",
text,
url: location.href,
timestamp: Date.now(),
}),
)
})
}
const scan = () => {
scheduled = false
pending.forEach(inspect)
pending.clear()
}
const schedule = (node: Node) => {
pending.add(node)
if (scheduled) return
scheduled = true
queueMicrotask(scan)
}
const start = () => {
const root = document.documentElement
if (!root) return
new MutationObserver((records) => {
records.forEach((record) => {
if (record.type !== "childList") schedule(record.target)
record.addedNodes.forEach(schedule)
})
}).observe(root, {
attributes: true,
attributeFilter: ["data-variant"],
childList: true,
characterData: true,
subtree: true,
})
schedule(root)
}
owner.__flushErrorToastObserver = () => {
const root = document.documentElement
if (root) pending.add(root)
scan()
}
if (document.documentElement) start()
else document.addEventListener("readystatechange", start, { once: true })
}

View file

@ -1,4 +1,4 @@
import { expect, guardPage, test as base, type Browser, type Page, type TestInfo } from "../fixtures"
import { expect, test as base, type Browser, type Page, type TestInfo } from "@playwright/test"
import { startChromeTrace } from "./chrome-trace"
type BenchmarkFixtures = {
@ -101,21 +101,16 @@ export async function withBenchmarkPage<T>(
browser: Browser,
name: string,
run: (page: Page) => Promise<T>,
testInfo: TestInfo,
testInfo?: TestInfo,
) {
const context = await browser.newContext()
try {
const page = await context.newPage()
const toastGuard = await guardPage(page, testInfo)
const diagnostics = await observePerformancePage(page, name)
try {
return await run(page)
} finally {
try {
await reportPerformancePage(name, diagnostics, testInfo)
} finally {
await toastGuard?.finish()
}
await reportPerformancePage(name, diagnostics, testInfo)
}
} finally {
await context.close()

View file

@ -1,4 +1,4 @@
import { expect, test } from "../../fixtures"
import { expect, test } from "@playwright/test"
import {
defineVisualRegions,
reportVisualStability,

View file

@ -1,4 +1,4 @@
import { expect, test } from "../../fixtures"
import { expect, test } from "@playwright/test"
import {
defineVisualRegions,
reportVisualStability,

View file

@ -1,4 +1,4 @@
import { test } from "../../fixtures"
import { test } from "@playwright/test"
import {
defineVisualRegions,
reportVisualStability,

View file

@ -1,4 +1,4 @@
import { test } from "../../fixtures"
import { test } from "@playwright/test"
import {
defineVisualRegions,
reportVisualStability,

View file

@ -1,4 +1,4 @@
import { expect, test } from "../../fixtures"
import { expect, test } from "@playwright/test"
import {
defineVisualRegions,
reportVisualStability,

View file

@ -1,4 +1,4 @@
import { expect, test } from "../../fixtures"
import { expect, test } from "@playwright/test"
import {
defineVisualRegions,
reportVisualStability,

View file

@ -1,4 +1,4 @@
import { expect, test } from "../../fixtures"
import { expect, test } from "@playwright/test"
import {
defineVisualRegions,
mapVisualRegions,

View file

@ -1,4 +1,4 @@
import { expect, test } from "../../fixtures"
import { expect, test } from "@playwright/test"
import {
analyzeVisualObservations,
defineVisualRegions,

View file

@ -1,4 +1,4 @@
import { expect, test } from "../../fixtures"
import { expect, test } from "@playwright/test"
import {
defineVisualRegions,
reportVisualStability,

View file

@ -1,4 +1,4 @@
import { test } from "../../fixtures"
import { test } from "@playwright/test"
import {
defineVisualRegions,
reportVisualStability,

View file

@ -1,4 +1,4 @@
import { expect, test } from "../../fixtures"
import { expect, test } from "@playwright/test"
import {
defineVisualRegions,
reportVisualStability,

View file

@ -1,4 +1,4 @@
import { expect, test } from "../../fixtures"
import { expect, test } from "@playwright/test"
import {
defineVisualRegions,
reportVisualStability,

View file

@ -1,4 +1,4 @@
import { expect, test } from "../../fixtures"
import { expect, test } from "@playwright/test"
import {
defineVisualRegions,
reportVisualStability,

View file

@ -1,4 +1,4 @@
import { expect, test, type Page, type Route } from "../fixtures"
import { expect, test, type Page, type Route } from "@playwright/test"
import { base64Encode } from "@opencode-ai/core/util/encode"
const serverA = "http://127.0.0.1:4096"
@ -45,9 +45,7 @@ test("closing the active server's last tab opens the remaining server tab", asyn
).toBe(true)
})
test("legacy session routes preserve an existing tab's server", async ({ page, errorToasts }) => {
// The legacy route bootstraps against the default server before redirecting to the persisted tab server.
errorToasts.allow(/server-b.*InvalidDirectory/)
test("legacy session routes preserve an existing tab's server", async ({ page }) => {
await mockServers(page, [])
await page.addInitScript(
({ serverB, sessionB }) => {
@ -83,12 +81,11 @@ async function mockServers(page: Page, requests: string[]) {
const url = new URL(route.request().url())
if (url.origin !== serverA && url.origin !== serverB) return route.fallback()
requests.push(url.toString())
const directory = url.searchParams.get("directory")
const current = url.origin === serverA ? sessionA : sessionB
const directory = url.searchParams.get("directory")
if (directory && directory !== current.directory) return json(route, { name: "InvalidDirectory" }, 500)
if (url.pathname === "/global/event" || url.pathname === "/event") return sse(route)
if (url.pathname === "/global/health") return json(route, { healthy: true })
if (url.pathname === "/session/status") return json(route, {})
if (url.pathname === "/session") return json(route, [current])
if (url.pathname === `/session/${current.id}`) return json(route, current)
if (/^\/session\/[^/]+$/.test(url.pathname)) return json(route, { name: "NotFoundError" }, 404)

View file

@ -1,50 +0,0 @@
import { glob, readFile } from "node:fs/promises"
import path from "node:path"
import { fileURLToPath } from "node:url"
import { expect, test } from "../fixtures"
test("requires every browser spec to use the error toast fixture", async () => {
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..")
const files = await Array.fromAsync(glob("**/*.spec.{ts,tsx}", { cwd: root }))
const unguarded = (
await Promise.all(
files.map(async (file) => {
const source = await readFile(path.join(root, file), "utf8")
if (/from\s+["'](?:\.\.\/)+fixtures["']/.test(source)) return
if (/from\s+["']\.\.\/benchmark["']/.test(source)) return
return file
}),
)
).filter((file): file is string => !!file)
expect(unguarded).toEqual([])
})
test("allows an explicitly expected error toast", async ({ page, errorToasts }) => {
errorToasts.expect("Expected request failure")
await page.goto(
`data:text/html,${encodeURIComponent(`
<div data-component="toast-v2" data-variant="error">
<div data-slot="toast-v2-title">Request failed</div>
<div data-slot="toast-v2-description">Expected request failure</div>
</div>
`)}`,
)
await expect(page.locator('[data-component="toast-v2"]')).toBeVisible()
})
test("fails immediately on an unexpected error toast", async ({ page }) => {
test.fail()
await page.goto(
`data:text/html,${encodeURIComponent(`
<div data-component="toast" data-variant="error">
<div data-slot="toast-title">Request failed</div>
<div data-slot="toast-description">Unexpected request failure</div>
</div>
`)}`,
)
await page.waitForTimeout(10_000)
})

View file

@ -1,4 +1,4 @@
import { expect, test, type Page } from "../fixtures"
import { expect, test, type Page } from "@playwright/test"
import { base64Encode } from "@opencode-ai/core/util/encode"
import { mockOpenCodeServer } from "../utils/mock-server"
import { expectAppVisible } from "../utils/waits"

View file

@ -1,4 +1,4 @@
import { expect, test, type Page, type Route } from "../fixtures"
import { expect, test, type Page, type Route } from "@playwright/test"
import { base64Encode } from "@opencode-ai/core/util/encode"
const serverA = "http://127.0.0.1:4096"

View file

@ -1,4 +1,4 @@
import { expect, test, type Page } from "../fixtures"
import { expect, test, type Page } from "@playwright/test"
import { base64Encode } from "@opencode-ai/core/util/encode"
import { mockOpenCodeServer } from "../utils/mock-server"
import { expectAppVisible, expectSessionTitle } from "../utils/waits"

View file

@ -1,4 +1,4 @@
import { expect, test, type Page } from "../fixtures"
import { expect, test, type Page } from "@playwright/test"
import { base64Encode } from "@opencode-ai/core/util/encode"
import { mockOpenCodeServer } from "../utils/mock-server"
import { expectAppVisible, expectSessionTitle } from "../utils/waits"

View file

@ -1,5 +1,5 @@
import { base64Encode } from "@opencode-ai/core/util/encode"
import { expect, test, type Page } from "../fixtures"
import { expect, test, type Page } from "@playwright/test"
import { mockOpenCodeServer } from "../utils/mock-server"
import { expectAppVisible, expectSessionTitle } from "../utils/waits"

View file

@ -1,4 +1,4 @@
import { test } from "../fixtures"
import { test } from "@playwright/test"
import { fixture, pageMessages } from "../smoke/session-timeline.fixture"
import { mockOpenCodeServer } from "../utils/mock-server"
import { expectAppVisible } from "../utils/waits"

View file

@ -1,4 +1,4 @@
import { expect, test } from "../fixtures"
import { expect, test } from "@playwright/test"
import { assistantMessage, setupTimeline, shell, userMessage } from "../performance/timeline-stability/fixture"
test("space activates a focused timeline button instead of scrolling", async ({ page }) => {

View file

@ -1,4 +1,4 @@
import { expect, test, type Locator, type Page } from "../fixtures"
import { expect, test, type Locator, type Page } from "@playwright/test"
import { mockOpenCodeServer } from "../utils/mock-server"
import { expectAppVisible, expectSessionTitle } from "../utils/waits"

View file

@ -1,4 +1,4 @@
import { expect, test, type Page } from "../fixtures"
import { expect, test, type Page } from "@playwright/test"
import { mockOpenCodeServer } from "../utils/mock-server"
import { expectAppVisible, expectSessionTitle } from "../utils/waits"
import {

View file

@ -1,4 +1,4 @@
import { expect, test } from "../fixtures"
import { expect, test } from "@playwright/test"
import {
assistantMessage,
partUpdated,

View file

@ -1,4 +1,4 @@
import { expect, test } from "../fixtures"
import { expect, test } from "@playwright/test"
import { assistantMessage, setupTimeline, toolPart, userMessage } from "../performance/timeline-stability/fixture"
test("renders completed write content", async ({ page }) => {

View file

@ -1,4 +1,4 @@
import { expect, test } from "../fixtures"
import { expect, test } from "@playwright/test"
import {
assistantMessage,
partUpdated,

View file

@ -1,5 +1,5 @@
import { base64Encode } from "@opencode-ai/core/util/encode"
import { expect, test, type Page } from "../fixtures"
import { expect, test, type Page } from "@playwright/test"
import {
assistantMessage,
directory,

View file

@ -1,4 +1,4 @@
import { expect, test } from "../fixtures"
import { expect, test } from "@playwright/test"
import {
assistantMessage,
completedAssistantInfo,

View file

@ -1,4 +1,4 @@
import { expect, test } from "../fixtures"
import { expect, test } from "@playwright/test"
import { assistantMessage, setupTimeline, toolPart, userMessage } from "../performance/timeline-stability/fixture"
for (const profile of [

View file

@ -1,4 +1,4 @@
import { expect, test } from "../fixtures"
import { expect, test } from "@playwright/test"
import {
assistantMessage,
setupTimeline,

View file

@ -1,4 +1,4 @@
import { expect, test } from "../fixtures"
import { expect, test } from "@playwright/test"
import {
assistantMessage,
reasoningPart,

View file

@ -1,4 +1,4 @@
import { expect, test } from "../fixtures"
import { expect, test } from "@playwright/test"
import {
assistantMessage,
completedAssistantInfo,

View file

@ -1,4 +1,4 @@
import { expect, test } from "../fixtures"
import { expect, test } from "@playwright/test"
import {
assistantMessage,
partUpdated,

View file

@ -1,4 +1,4 @@
import { expect, test } from "../fixtures"
import { expect, test } from "@playwright/test"
import {
assistantMessage,
partUpdated,

View file

@ -1,4 +1,4 @@
import { expect, test } from "../fixtures"
import { expect, test } from "@playwright/test"
import {
assistantMessage,
partUpdated,

View file

@ -1,5 +1,5 @@
import { base64Encode } from "@opencode-ai/core/util/encode"
import { expect, test, type Page } from "../fixtures"
import { expect, test, type Page } from "@playwright/test"
import { mockOpenCodeServer } from "../utils/mock-server"
import { expectSessionTitle } from "../utils/waits"

View file

@ -1,5 +1,5 @@
import { base64Encode } from "@opencode-ai/core/util/encode"
import { expect, test, type Page } from "../fixtures"
import { expect, test, type Page } from "@playwright/test"
import { mockOpenCodeServer } from "../utils/mock-server"
import { expectSessionTitle } from "../utils/waits"

View file

@ -1,4 +1,4 @@
import { expect, test, type Page, type Route } from "../fixtures"
import { expect, test, type Page, type Route } from "@playwright/test"
import { base64Encode } from "@opencode-ai/core/util/encode"
const server = "http://127.0.0.1:4096"
@ -58,7 +58,6 @@ async function mockServer(page: Page) {
if (url.origin !== server) return route.fallback()
if (url.pathname === "/global/event" || url.pathname === "/event") return sse(route)
if (url.pathname === "/global/health") return json(route, { healthy: true })
if (url.pathname === "/session/status") return json(route, {})
if (url.pathname === "/session") return json(route, sessions)
const byId = sessions.find((item) => url.pathname === `/session/${item.id}`)
if (byId) return json(route, byId)

View file

@ -1,5 +1,5 @@
import { base64Encode } from "@opencode-ai/core/util/encode"
import { expect, test } from "../fixtures"
import { expect, test } from "@playwright/test"
import { mockOpenCodeServer } from "../utils/mock-server"
import { expectSessionTitle } from "../utils/waits"

View file

@ -1,4 +1,4 @@
import { expect, test } from "../fixtures"
import { expect, test } from "@playwright/test"
import { mockOpenCodeServer } from "../utils/mock-server"
import { expectSessionTitle } from "../utils/waits"

View file

@ -1,5 +1,5 @@
import { base64Encode } from "@opencode-ai/core/util/encode"
import { expect, test, type Page } from "../fixtures"
import { expect, test, type Page } from "@playwright/test"
import { mockOpenCodeServer } from "../utils/mock-server"
import { expectSessionTitle } from "../utils/waits"

View file

@ -1,4 +1,4 @@
import { expect, test, type Page } from "../fixtures"
import { expect, test, type Page } from "@playwright/test"
import { base64Encode } from "@opencode-ai/core/util/encode"
import { fixture, pageMessages } from "./session-timeline.fixture"
import { trackPageErrors, expectNoSmokeErrors } from "../utils/errors"

View file

@ -6,14 +6,11 @@
"types": ["node", "bun"]
},
"include": [
"./fixtures.ts",
"./performance/timeline-stability/**/*.spec.ts",
"./performance/timeline-stability/fixture.test.ts",
"./performance/timeline-stability/fixture.ts",
"./performance/unit/visual-stability.test.ts",
"./regression/new-session-panel-corner.spec.ts",
"./regression/error-toast-guard.spec.ts",
"./regression/session-request-docks.spec.ts",
"./regression/session-timeline-context-resize.spec.ts",
"./utils/**/*.ts"
]

View file

@ -20,7 +20,6 @@ export interface MockServerConfig {
todos?: (sessionID: string) => unknown[]
permissions?: unknown[] | (() => unknown[])
questions?: unknown[] | (() => unknown[])
onQuestionReply?: (input: { requestID: string; answers?: string[][] }) => unknown | Promise<unknown>
fileList?: (path: string) => unknown | Promise<unknown>
fileContent?: (path: string) => unknown | Promise<unknown>
findFiles?: (input: { query: string; dirs?: string; limit?: number }) => unknown
@ -64,7 +63,8 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
return json(route, typeof config.questions === "function" ? config.questions() : (config.questions ?? []))
if (path === "/session/status") return json(route, config.sessionStatus ?? {})
if (path === "/vcs/diff" && config.vcsDiff) return json(route, config.vcsDiff)
if (path === "/file") return json(route, (await config.fileList?.(url.searchParams.get("path") ?? "")) ?? [])
if (path === "/file" && config.fileList)
return json(route, await config.fileList(url.searchParams.get("path") ?? ""))
if (path === "/file/content" && config.fileContent)
return json(route, await config.fileContent(url.searchParams.get("path") ?? ""))
if (path === "/find/file" && config.findFiles)
@ -110,18 +110,6 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
if (todoMatch) return json(route, config.todos?.(todoMatch[1]!) ?? [])
if (/^\/session\/[^/]+\/(children|diff)$/.test(path)) return json(route, [])
const permissionRespondMatch = path.match(/^\/session\/([^/]+)\/permissions\/([^/]+)$/)
if (permissionRespondMatch && route.request().method() === "POST") return json(route, true)
const questionReplyMatch = path.match(/^\/question\/([^/]+)\/reply$/)
if (questionReplyMatch && route.request().method() === "POST") {
const body = route.request().postDataJSON() as { answers?: string[][] }
return json(
route,
(await config.onQuestionReply?.({ requestID: questionReplyMatch[1]!, answers: body.answers })) ?? true,
)
}
const messagesMatch = path.match(/^\/session\/([^/]+)\/message$/)
if (messagesMatch) {
const token = url.searchParams.get("before") ?? undefined

View file

@ -162,7 +162,7 @@ export async function installSseTransport<T>(
const request = new Request(input, init)
const url = new URL(request.url)
if (url.origin !== server || (url.pathname !== "/global/event" && url.pathname !== "/event"))
return originalFetch(request)
return originalFetch(input, init)
const id = ++nextConnectionID
const record = {

View file

@ -161,7 +161,7 @@ export function CustomProviderForm() {
},
onError: (err) => {
const message = err instanceof Error ? err.message : String(err)
showToast({ variant: "error", title: language.t("common.requestFailed"), description: message })
showToast({ title: language.t("common.requestFailed"), description: message })
},
}))

View file

@ -72,7 +72,7 @@ export const DialogFork: Component = () => {
.client.session.fork({ sessionID, messageID: item.id })
.then((forked) => {
if (!forked.data) {
showToast({ variant: "error", title: language.t("common.requestFailed") })
showToast({ title: language.t("common.requestFailed") })
return
}
dialog.close()
@ -81,7 +81,7 @@ export const DialogFork: Component = () => {
})
.catch((err: unknown) => {
const message = err instanceof Error ? err.message : String(err)
showToast({ variant: "error", title: language.t("common.requestFailed"), description: message })
showToast({ title: language.t("common.requestFailed"), description: message })
})
}

View file

@ -327,7 +327,6 @@ export function createPromptSubmit(input: PromptSubmitInput) {
.then((x) => x.data)
.catch((err) => {
showToast({
variant: "error",
title: language.t("prompt.toast.worktreeCreateFailed.title"),
description: errorMessage(err),
})
@ -336,7 +335,6 @@ export function createPromptSubmit(input: PromptSubmitInput) {
if (!createdWorktree?.directory) {
showToast({
variant: "error",
title: language.t("prompt.toast.worktreeCreateFailed.title"),
description: language.t("common.requestFailed"),
})
@ -368,7 +366,6 @@ export function createPromptSubmit(input: PromptSubmitInput) {
.then((x) => x.data ?? undefined)
.catch((err) => {
showToast({
variant: "error",
title: language.t("prompt.toast.sessionCreateFailed.title"),
description: errorMessage(err),
})
@ -388,7 +385,6 @@ export function createPromptSubmit(input: PromptSubmitInput) {
}
if (!session) {
showToast({
variant: "error",
title: language.t("prompt.toast.promptSendFailed.title"),
description: language.t("prompt.toast.promptSendFailed.description"),
})
@ -453,7 +449,6 @@ export function createPromptSubmit(input: PromptSubmitInput) {
})
.catch((err) => {
showToast({
variant: "error",
title: language.t("prompt.toast.shellSendFailed.title"),
description: errorMessage(err),
})
@ -486,7 +481,6 @@ export function createPromptSubmit(input: PromptSubmitInput) {
})
.catch((err) => {
showToast({
variant: "error",
title: language.t("prompt.toast.commandSendFailed.title"),
description: formatServerError(err, language.t, language.t("common.requestFailed")),
})
@ -582,7 +576,6 @@ export function createPromptSubmit(input: PromptSubmitInput) {
sync().set("session_status", session.id, { type: "idle" })
}
showToast({
variant: "error",
title: language.t("prompt.toast.promptSendFailed.title"),
description: errorMessage(err),
})

View file

@ -113,7 +113,7 @@ const SettingsProvidersContent: Component<{ onBack?: () => void }> = (props) =>
.catch((err: unknown) => {
serverSync().set("config", "disabled_providers", before)
const message = err instanceof Error ? err.message : String(err)
showToast({ variant: "error", title: language.t("common.requestFailed"), description: message })
showToast({ title: language.t("common.requestFailed"), description: message })
})
}
@ -138,7 +138,7 @@ const SettingsProvidersContent: Component<{ onBack?: () => void }> = (props) =>
})
.catch((err: unknown) => {
const message = err instanceof Error ? err.message : String(err)
showToast({ variant: "error", title: language.t("common.requestFailed"), description: message })
showToast({ title: language.t("common.requestFailed"), description: message })
})
}

View file

@ -107,7 +107,7 @@ export const SettingsProvidersV2: Component<{ onBack?: () => void }> = (props) =
.catch((err: unknown) => {
serverSync().set("config", "disabled_providers", before)
const message = err instanceof Error ? err.message : String(err)
showToast({ variant: "error", title: language.t("common.requestFailed"), description: message })
showToast({ title: language.t("common.requestFailed"), description: message })
})
}
@ -132,7 +132,7 @@ export const SettingsProvidersV2: Component<{ onBack?: () => void }> = (props) =
})
.catch((err: unknown) => {
const message = err instanceof Error ? err.message : String(err)
showToast({ variant: "error", title: language.t("common.requestFailed"), description: message })
showToast({ title: language.t("common.requestFailed"), description: message })
})
}

View file

@ -145,7 +145,6 @@ export function TabNavItem(props: {
} catch (err) {
props.onTitleChangeFailed?.(original)
showToast({
variant: "error",
title: language.t("common.requestFailed"),
description: err instanceof Error ? err.message : undefined,
})

View file

@ -44,7 +44,7 @@ export function useUpdaterAction() {
})
}
if (state?.status === "error") {
showToast({ variant: "error", title: language.t("common.requestFailed"), description: state.message })
showToast({ title: language.t("common.requestFailed"), description: state.message })
}
},
}

View file

@ -518,7 +518,6 @@ export function NewHome() {
),
onError: (error) =>
showToast({
variant: "error",
title: language.t("common.requestFailed"),
description: errorMessage(error, language.t("common.requestFailed")),
}),

View file

@ -1391,7 +1391,6 @@ export default function LegacyLayout(props: ParentProps) {
.then((x) => x.data)
.catch((err) => {
showToast({
variant: "error",
title: language.t("workspace.delete.failed.title"),
description: errorMessage(err, language.t("common.requestFailed")),
})
@ -1465,7 +1464,6 @@ export default function LegacyLayout(props: ParentProps) {
.then((x) => x.data)
.catch((err) => {
showToast({
variant: "error",
title: language.t("workspace.reset.failed.title"),
description: errorMessage(err, language.t("common.requestFailed")),
})
@ -1828,7 +1826,6 @@ export default function LegacyLayout(props: ParentProps) {
.then((x) => x.data)
.catch((err) => {
showToast({
variant: "error",
title: language.t("workspace.create.failed.title"),
description: errorMessage(err, language.t("common.requestFailed")),
})

View file

@ -85,7 +85,7 @@ export function createSessionComposerController(options?: { closeMs?: number | (
.client.permission.respond({ sessionID: perm.sessionID, permissionID: perm.id, response })
.catch((err: unknown) => {
const description = err instanceof Error ? err.message : String(err)
showToast({ variant: "error", title: language.t("common.requestFailed"), description })
showToast({ title: language.t("common.requestFailed"), description })
})
.finally(() => {
setStore("responding", (id) => (id === perm.id ? undefined : id))

View file

@ -219,7 +219,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
const fail = (err: unknown) => {
const message = err instanceof Error ? err.message : String(err)
showToast({ variant: "error", title: language.t("common.requestFailed"), description: message })
showToast({ title: language.t("common.requestFailed"), description: message })
}
const replyMutation = useMutation(() => ({

View file

@ -15,12 +15,7 @@ import { createStore, produce } from "solid-js/store"
import { Dynamic } from "solid-js/web"
import { useNavigate } from "@solidjs/router"
import { useMutation } from "@tanstack/solid-query"
import {
createVirtualizer,
defaultRangeExtractor,
elementScroll,
type VirtualItem,
} from "@tanstack/solid-virtual"
import { createVirtualizer, defaultRangeExtractor, elementScroll, type VirtualItem } from "@tanstack/solid-virtual"
import { Accordion } from "@opencode-ai/ui/accordion"
import { Button } from "@opencode-ai/ui/button"
import { Card } from "@opencode-ai/ui/card"
@ -679,7 +674,6 @@ export function MessageTimeline(props: {
},
onError: (err) => {
showToast({
variant: "error",
title: language.t("common.requestFailed"),
description: errorMessage(err),
})
@ -714,7 +708,6 @@ export function MessageTimeline(props: {
)
.catch((err: unknown) =>
showToast({
variant: "error",
title: language.t("common.requestFailed"),
description: errorMessage(err),
}),
@ -827,7 +820,6 @@ export function MessageTimeline(props: {
})
.catch((err) => {
showToast({
variant: "error",
title: language.t("common.requestFailed"),
description: errorMessage(err),
})
@ -847,7 +839,6 @@ export function MessageTimeline(props: {
.then((x) => x.data)
.catch((err) => {
showToast({
variant: "error",
title: language.t("session.delete.failed.title"),
description: errorMessage(err),
})

View file

@ -90,7 +90,6 @@ export interface ToastV2Options {
title?: string
description?: string
icon?: JSX.Element
variant?: "default" | "success" | "error" | "loading"
duration?: number
persistent?: boolean
actions?: ToastV2Action[]
@ -101,12 +100,7 @@ export function showToastV2(options: ToastV2Options | string) {
return toaster.show((props) => {
const resolvedIcon = children(() => opts.icon)
return (
<ToastV2
toastId={props.toastId}
duration={opts.duration}
persistent={opts.persistent}
data-variant={opts.variant ?? "default"}
>
<ToastV2 toastId={props.toastId} duration={opts.duration} persistent={opts.persistent}>
<div data-slot="toast-v2-header">
<Show when={resolvedIcon()}>
<ToastV2.Icon>{resolvedIcon()}</ToastV2.Icon>