diff --git a/packages/app/e2e/fixtures.ts b/packages/app/e2e/fixtures.ts new file mode 100644 index 00000000000..a8fa0110916 --- /dev/null +++ b/packages/app/e2e/fixtures.ts @@ -0,0 +1,211 @@ +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({ + 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() + const pending = new Set() + 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(selector), + ...(element.closest(selector) ? [element.closest(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(`[data-slot="toast${component === "v2" ? "-v2" : ""}-title"]`) + const description = toast.querySelector( + `[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 }) +} diff --git a/packages/app/e2e/performance/benchmark.ts b/packages/app/e2e/performance/benchmark.ts index b9f8ea43411..1e4b82834ce 100644 --- a/packages/app/e2e/performance/benchmark.ts +++ b/packages/app/e2e/performance/benchmark.ts @@ -1,4 +1,4 @@ -import { expect, test as base, type Browser, type Page, type TestInfo } from "@playwright/test" +import { expect, guardPage, test as base, type Browser, type Page, type TestInfo } from "../fixtures" import { startChromeTrace } from "./chrome-trace" type BenchmarkFixtures = { @@ -101,16 +101,21 @@ export async function withBenchmarkPage( browser: Browser, name: string, run: (page: Page) => Promise, - 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 { - await reportPerformancePage(name, diagnostics, testInfo) + try { + await reportPerformancePage(name, diagnostics, testInfo) + } finally { + await toastGuard?.finish() + } } } finally { await context.close() diff --git a/packages/app/e2e/performance/timeline-stability/adverse.spec.ts b/packages/app/e2e/performance/timeline-stability/adverse.spec.ts index 7ce1be90645..bfd5929157f 100644 --- a/packages/app/e2e/performance/timeline-stability/adverse.spec.ts +++ b/packages/app/e2e/performance/timeline-stability/adverse.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from "@playwright/test" +import { expect, test } from "../../fixtures" import { defineVisualRegions, reportVisualStability, diff --git a/packages/app/e2e/performance/timeline-stability/context-matrix.spec.ts b/packages/app/e2e/performance/timeline-stability/context-matrix.spec.ts index b3438b77c06..799eeb44546 100644 --- a/packages/app/e2e/performance/timeline-stability/context-matrix.spec.ts +++ b/packages/app/e2e/performance/timeline-stability/context-matrix.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from "@playwright/test" +import { expect, test } from "../../fixtures" import { defineVisualRegions, reportVisualStability, diff --git a/packages/app/e2e/performance/timeline-stability/environment-matrix.spec.ts b/packages/app/e2e/performance/timeline-stability/environment-matrix.spec.ts index 3455438a736..7381250a4c2 100644 --- a/packages/app/e2e/performance/timeline-stability/environment-matrix.spec.ts +++ b/packages/app/e2e/performance/timeline-stability/environment-matrix.spec.ts @@ -1,4 +1,4 @@ -import { test } from "@playwright/test" +import { test } from "../../fixtures" import { defineVisualRegions, reportVisualStability, diff --git a/packages/app/e2e/performance/timeline-stability/file-matrix.spec.ts b/packages/app/e2e/performance/timeline-stability/file-matrix.spec.ts index e0f0d72233b..4445d26c75b 100644 --- a/packages/app/e2e/performance/timeline-stability/file-matrix.spec.ts +++ b/packages/app/e2e/performance/timeline-stability/file-matrix.spec.ts @@ -1,4 +1,4 @@ -import { test } from "@playwright/test" +import { test } from "../../fixtures" import { defineVisualRegions, reportVisualStability, diff --git a/packages/app/e2e/performance/timeline-stability/file-mutation.spec.ts b/packages/app/e2e/performance/timeline-stability/file-mutation.spec.ts index 798bf0df3b2..5dde1eb1375 100644 --- a/packages/app/e2e/performance/timeline-stability/file-mutation.spec.ts +++ b/packages/app/e2e/performance/timeline-stability/file-mutation.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from "@playwright/test" +import { expect, test } from "../../fixtures" import { defineVisualRegions, reportVisualStability, diff --git a/packages/app/e2e/performance/timeline-stability/interaction.spec.ts b/packages/app/e2e/performance/timeline-stability/interaction.spec.ts index 8cdf4fa8cfa..f1f845bc8d9 100644 --- a/packages/app/e2e/performance/timeline-stability/interaction.spec.ts +++ b/packages/app/e2e/performance/timeline-stability/interaction.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from "@playwright/test" +import { expect, test } from "../../fixtures" import { defineVisualRegions, reportVisualStability, diff --git a/packages/app/e2e/performance/timeline-stability/lifecycle.spec.ts b/packages/app/e2e/performance/timeline-stability/lifecycle.spec.ts index 40688429a33..27732039828 100644 --- a/packages/app/e2e/performance/timeline-stability/lifecycle.spec.ts +++ b/packages/app/e2e/performance/timeline-stability/lifecycle.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from "@playwright/test" +import { expect, test } from "../../fixtures" import { defineVisualRegions, mapVisualRegions, diff --git a/packages/app/e2e/performance/timeline-stability/oracle-browser.spec.ts b/packages/app/e2e/performance/timeline-stability/oracle-browser.spec.ts index 7891eba5bd9..d2ed1db746d 100644 --- a/packages/app/e2e/performance/timeline-stability/oracle-browser.spec.ts +++ b/packages/app/e2e/performance/timeline-stability/oracle-browser.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from "@playwright/test" +import { expect, test } from "../../fixtures" import { analyzeVisualObservations, defineVisualRegions, diff --git a/packages/app/e2e/performance/timeline-stability/scroll-interaction.spec.ts b/packages/app/e2e/performance/timeline-stability/scroll-interaction.spec.ts index e67545104d4..2d3a19ba3c0 100644 --- a/packages/app/e2e/performance/timeline-stability/scroll-interaction.spec.ts +++ b/packages/app/e2e/performance/timeline-stability/scroll-interaction.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from "@playwright/test" +import { expect, test } from "../../fixtures" import { defineVisualRegions, reportVisualStability, diff --git a/packages/app/e2e/performance/timeline-stability/shell-matrix.spec.ts b/packages/app/e2e/performance/timeline-stability/shell-matrix.spec.ts index bab1ca23b4f..b0c1991c4c8 100644 --- a/packages/app/e2e/performance/timeline-stability/shell-matrix.spec.ts +++ b/packages/app/e2e/performance/timeline-stability/shell-matrix.spec.ts @@ -1,4 +1,4 @@ -import { test } from "@playwright/test" +import { test } from "../../fixtures" import { defineVisualRegions, reportVisualStability, diff --git a/packages/app/e2e/performance/timeline-stability/tool-mutation.spec.ts b/packages/app/e2e/performance/timeline-stability/tool-mutation.spec.ts index 03690ec9214..37bbbb61fed 100644 --- a/packages/app/e2e/performance/timeline-stability/tool-mutation.spec.ts +++ b/packages/app/e2e/performance/timeline-stability/tool-mutation.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from "@playwright/test" +import { expect, test } from "../../fixtures" import { defineVisualRegions, reportVisualStability, diff --git a/packages/app/e2e/performance/timeline-stability/tools.spec.ts b/packages/app/e2e/performance/timeline-stability/tools.spec.ts index d28fdaa65f5..c723f9f2011 100644 --- a/packages/app/e2e/performance/timeline-stability/tools.spec.ts +++ b/packages/app/e2e/performance/timeline-stability/tools.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from "@playwright/test" +import { expect, test } from "../../fixtures" import { defineVisualRegions, reportVisualStability, diff --git a/packages/app/e2e/performance/timeline-stability/transition-matrix.spec.ts b/packages/app/e2e/performance/timeline-stability/transition-matrix.spec.ts index e999e8c50a6..52b82a84204 100644 --- a/packages/app/e2e/performance/timeline-stability/transition-matrix.spec.ts +++ b/packages/app/e2e/performance/timeline-stability/transition-matrix.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from "@playwright/test" +import { expect, test } from "../../fixtures" import { defineVisualRegions, reportVisualStability, diff --git a/packages/app/e2e/regression/cross-server-tab-close.spec.ts b/packages/app/e2e/regression/cross-server-tab-close.spec.ts index 159b5a50676..32f318b7bc4 100644 --- a/packages/app/e2e/regression/cross-server-tab-close.spec.ts +++ b/packages/app/e2e/regression/cross-server-tab-close.spec.ts @@ -1,4 +1,4 @@ -import { expect, test, type Page, type Route } from "@playwright/test" +import { expect, test, type Page, type Route } from "../fixtures" import { base64Encode } from "@opencode-ai/core/util/encode" const serverA = "http://127.0.0.1:4096" @@ -45,7 +45,9 @@ 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 }) => { +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/) await mockServers(page, []) await page.addInitScript( ({ serverB, sessionB }) => { @@ -81,11 +83,12 @@ 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 current = url.origin === serverA ? sessionA : sessionB const directory = url.searchParams.get("directory") + const current = url.origin === serverA ? sessionA : sessionB 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) diff --git a/packages/app/e2e/regression/error-toast-guard.spec.ts b/packages/app/e2e/regression/error-toast-guard.spec.ts new file mode 100644 index 00000000000..c4f8d648132 --- /dev/null +++ b/packages/app/e2e/regression/error-toast-guard.spec.ts @@ -0,0 +1,50 @@ +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(` +
+
Request failed
+
Expected request failure
+
+ `)}`, + ) + + 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(` +
+
Request failed
+
Unexpected request failure
+
+ `)}`, + ) + await page.waitForTimeout(10_000) +}) diff --git a/packages/app/e2e/regression/prompt-thinking-level.spec.ts b/packages/app/e2e/regression/prompt-thinking-level.spec.ts index 4219699f28c..011dcb89c9a 100644 --- a/packages/app/e2e/regression/prompt-thinking-level.spec.ts +++ b/packages/app/e2e/regression/prompt-thinking-level.spec.ts @@ -1,4 +1,4 @@ -import { expect, test, type Page } from "@playwright/test" +import { expect, test, type Page } from "../fixtures" import { base64Encode } from "@opencode-ai/core/util/encode" import { mockOpenCodeServer } from "../utils/mock-server" import { expectAppVisible } from "../utils/waits" diff --git a/packages/app/e2e/regression/remote-tab-busy.spec.ts b/packages/app/e2e/regression/remote-tab-busy.spec.ts index 119fc7ee2da..7e44d9d69d6 100644 --- a/packages/app/e2e/regression/remote-tab-busy.spec.ts +++ b/packages/app/e2e/regression/remote-tab-busy.spec.ts @@ -1,4 +1,4 @@ -import { expect, test, type Page, type Route } from "@playwright/test" +import { expect, test, type Page, type Route } from "../fixtures" import { base64Encode } from "@opencode-ai/core/util/encode" const serverA = "http://127.0.0.1:4096" diff --git a/packages/app/e2e/regression/review-image-flash.spec.ts b/packages/app/e2e/regression/review-image-flash.spec.ts index dd200384d49..e76b5c3f8c0 100644 --- a/packages/app/e2e/regression/review-image-flash.spec.ts +++ b/packages/app/e2e/regression/review-image-flash.spec.ts @@ -1,4 +1,4 @@ -import { expect, test, type Page } from "@playwright/test" +import { expect, test, type Page } from "../fixtures" import { base64Encode } from "@opencode-ai/core/util/encode" import { mockOpenCodeServer } from "../utils/mock-server" import { expectAppVisible, expectSessionTitle } from "../utils/waits" diff --git a/packages/app/e2e/regression/review-line-comment.spec.ts b/packages/app/e2e/regression/review-line-comment.spec.ts index 042f926c537..41116131596 100644 --- a/packages/app/e2e/regression/review-line-comment.spec.ts +++ b/packages/app/e2e/regression/review-line-comment.spec.ts @@ -1,4 +1,4 @@ -import { expect, test, type Page } from "@playwright/test" +import { expect, test, type Page } from "../fixtures" import { base64Encode } from "@opencode-ai/core/util/encode" import { mockOpenCodeServer } from "../utils/mock-server" import { expectAppVisible, expectSessionTitle } from "../utils/waits" diff --git a/packages/app/e2e/regression/review-tab-switch.spec.ts b/packages/app/e2e/regression/review-tab-switch.spec.ts index c2ea406c5ab..9993b2698bf 100644 --- a/packages/app/e2e/regression/review-tab-switch.spec.ts +++ b/packages/app/e2e/regression/review-tab-switch.spec.ts @@ -1,5 +1,5 @@ import { base64Encode } from "@opencode-ai/core/util/encode" -import { expect, test, type Page } from "@playwright/test" +import { expect, test, type Page } from "../fixtures" import { mockOpenCodeServer } from "../utils/mock-server" import { expectAppVisible, expectSessionTitle } from "../utils/waits" diff --git a/packages/app/e2e/regression/session-list-path-loading.spec.ts b/packages/app/e2e/regression/session-list-path-loading.spec.ts index 4a3855122a4..77375a5c55e 100644 --- a/packages/app/e2e/regression/session-list-path-loading.spec.ts +++ b/packages/app/e2e/regression/session-list-path-loading.spec.ts @@ -1,4 +1,4 @@ -import { test } from "@playwright/test" +import { test } from "../fixtures" import { fixture, pageMessages } from "../smoke/session-timeline.fixture" import { mockOpenCodeServer } from "../utils/mock-server" import { expectAppVisible } from "../utils/waits" diff --git a/packages/app/e2e/regression/session-timeline-accessibility.spec.ts b/packages/app/e2e/regression/session-timeline-accessibility.spec.ts index 598763c0225..1aa276079d4 100644 --- a/packages/app/e2e/regression/session-timeline-accessibility.spec.ts +++ b/packages/app/e2e/regression/session-timeline-accessibility.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from "@playwright/test" +import { expect, test } from "../fixtures" import { assistantMessage, setupTimeline, shell, userMessage } from "../performance/timeline-stability/fixture" test("space activates a focused timeline button instead of scrolling", async ({ page }) => { diff --git a/packages/app/e2e/regression/session-timeline-collapse-state.spec.ts b/packages/app/e2e/regression/session-timeline-collapse-state.spec.ts index 5b6e0b127b1..f84042199bd 100644 --- a/packages/app/e2e/regression/session-timeline-collapse-state.spec.ts +++ b/packages/app/e2e/regression/session-timeline-collapse-state.spec.ts @@ -1,4 +1,4 @@ -import { expect, test, type Locator, type Page } from "@playwright/test" +import { expect, test, type Locator, type Page } from "../fixtures" import { mockOpenCodeServer } from "../utils/mock-server" import { expectAppVisible, expectSessionTitle } from "../utils/waits" diff --git a/packages/app/e2e/regression/session-timeline-context-resize.spec.ts b/packages/app/e2e/regression/session-timeline-context-resize.spec.ts index a9a4738da92..99db4b779e5 100644 --- a/packages/app/e2e/regression/session-timeline-context-resize.spec.ts +++ b/packages/app/e2e/regression/session-timeline-context-resize.spec.ts @@ -1,4 +1,4 @@ -import { expect, test, type Page } from "@playwright/test" +import { expect, test, type Page } from "../fixtures" import { mockOpenCodeServer } from "../utils/mock-server" import { expectAppVisible, expectSessionTitle } from "../utils/waits" import { diff --git a/packages/app/e2e/regression/session-timeline-context-state.spec.ts b/packages/app/e2e/regression/session-timeline-context-state.spec.ts index 37878325e14..96ed413327d 100644 --- a/packages/app/e2e/regression/session-timeline-context-state.spec.ts +++ b/packages/app/e2e/regression/session-timeline-context-state.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from "@playwright/test" +import { expect, test } from "../fixtures" import { assistantMessage, partUpdated, diff --git a/packages/app/e2e/regression/session-timeline-file-projection.spec.ts b/packages/app/e2e/regression/session-timeline-file-projection.spec.ts index f07da121c66..55e35d4f21d 100644 --- a/packages/app/e2e/regression/session-timeline-file-projection.spec.ts +++ b/packages/app/e2e/regression/session-timeline-file-projection.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from "@playwright/test" +import { expect, test } from "../fixtures" import { assistantMessage, setupTimeline, toolPart, userMessage } from "../performance/timeline-stability/fixture" test("renders completed write content", async ({ page }) => { diff --git a/packages/app/e2e/regression/session-timeline-file-state.spec.ts b/packages/app/e2e/regression/session-timeline-file-state.spec.ts index cb228c13c7a..8ae8e2b2bcf 100644 --- a/packages/app/e2e/regression/session-timeline-file-state.spec.ts +++ b/packages/app/e2e/regression/session-timeline-file-state.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from "@playwright/test" +import { expect, test } from "../fixtures" import { assistantMessage, partUpdated, diff --git a/packages/app/e2e/regression/session-timeline-history-root.spec.ts b/packages/app/e2e/regression/session-timeline-history-root.spec.ts index 15375cafed5..1d6c8aaa9c9 100644 --- a/packages/app/e2e/regression/session-timeline-history-root.spec.ts +++ b/packages/app/e2e/regression/session-timeline-history-root.spec.ts @@ -1,5 +1,5 @@ import { base64Encode } from "@opencode-ai/core/util/encode" -import { expect, test, type Page } from "@playwright/test" +import { expect, test, type Page } from "../fixtures" import { assistantMessage, directory, diff --git a/packages/app/e2e/regression/session-timeline-lifecycle-state.spec.ts b/packages/app/e2e/regression/session-timeline-lifecycle-state.spec.ts index 3e2b171bca0..b55941fcb67 100644 --- a/packages/app/e2e/regression/session-timeline-lifecycle-state.spec.ts +++ b/packages/app/e2e/regression/session-timeline-lifecycle-state.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from "@playwright/test" +import { expect, test } from "../fixtures" import { assistantMessage, completedAssistantInfo, diff --git a/packages/app/e2e/regression/session-timeline-locale-projection.spec.ts b/packages/app/e2e/regression/session-timeline-locale-projection.spec.ts index 3901f8865e3..d9d250ef88d 100644 --- a/packages/app/e2e/regression/session-timeline-locale-projection.spec.ts +++ b/packages/app/e2e/regression/session-timeline-locale-projection.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from "@playwright/test" +import { expect, test } from "../fixtures" import { assistantMessage, setupTimeline, toolPart, userMessage } from "../performance/timeline-stability/fixture" for (const profile of [ diff --git a/packages/app/e2e/regression/session-timeline-projection.spec.ts b/packages/app/e2e/regression/session-timeline-projection.spec.ts index 9fd2ca8d0b6..4c7a584c6c3 100644 --- a/packages/app/e2e/regression/session-timeline-projection.spec.ts +++ b/packages/app/e2e/regression/session-timeline-projection.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from "@playwright/test" +import { expect, test } from "../fixtures" import { assistantMessage, setupTimeline, diff --git a/packages/app/e2e/regression/session-timeline-reasoning-projection.spec.ts b/packages/app/e2e/regression/session-timeline-reasoning-projection.spec.ts index 7c0864e5845..483e65faf64 100644 --- a/packages/app/e2e/regression/session-timeline-reasoning-projection.spec.ts +++ b/packages/app/e2e/regression/session-timeline-reasoning-projection.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from "@playwright/test" +import { expect, test } from "../fixtures" import { assistantMessage, reasoningPart, diff --git a/packages/app/e2e/regression/session-timeline-reducer-projection.spec.ts b/packages/app/e2e/regression/session-timeline-reducer-projection.spec.ts index ad35eef601c..5410b2767b6 100644 --- a/packages/app/e2e/regression/session-timeline-reducer-projection.spec.ts +++ b/packages/app/e2e/regression/session-timeline-reducer-projection.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from "@playwright/test" +import { expect, test } from "../fixtures" import { assistantMessage, completedAssistantInfo, diff --git a/packages/app/e2e/regression/session-timeline-tool-projection.spec.ts b/packages/app/e2e/regression/session-timeline-tool-projection.spec.ts index 99f1acf270b..e5813123ed8 100644 --- a/packages/app/e2e/regression/session-timeline-tool-projection.spec.ts +++ b/packages/app/e2e/regression/session-timeline-tool-projection.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from "@playwright/test" +import { expect, test } from "../fixtures" import { assistantMessage, partUpdated, diff --git a/packages/app/e2e/regression/session-timeline-tool-state.spec.ts b/packages/app/e2e/regression/session-timeline-tool-state.spec.ts index 63646f4454c..8d8af6f37da 100644 --- a/packages/app/e2e/regression/session-timeline-tool-state.spec.ts +++ b/packages/app/e2e/regression/session-timeline-tool-state.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from "@playwright/test" +import { expect, test } from "../fixtures" import { assistantMessage, partUpdated, diff --git a/packages/app/e2e/regression/session-timeline-transport.spec.ts b/packages/app/e2e/regression/session-timeline-transport.spec.ts index 850e966d0b0..2b05db85f98 100644 --- a/packages/app/e2e/regression/session-timeline-transport.spec.ts +++ b/packages/app/e2e/regression/session-timeline-transport.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from "@playwright/test" +import { expect, test } from "../fixtures" import { assistantMessage, partUpdated, diff --git a/packages/app/e2e/regression/session-todo-dock-navigation.spec.ts b/packages/app/e2e/regression/session-todo-dock-navigation.spec.ts index 603c411d551..1e574de9616 100644 --- a/packages/app/e2e/regression/session-todo-dock-navigation.spec.ts +++ b/packages/app/e2e/regression/session-todo-dock-navigation.spec.ts @@ -1,5 +1,5 @@ import { base64Encode } from "@opencode-ai/core/util/encode" -import { expect, test, type Page } from "@playwright/test" +import { expect, test, type Page } from "../fixtures" import { mockOpenCodeServer } from "../utils/mock-server" import { expectSessionTitle } from "../utils/waits" diff --git a/packages/app/e2e/regression/subagent-child-navigation.spec.ts b/packages/app/e2e/regression/subagent-child-navigation.spec.ts index 19d2c29af02..388b3bf0e1c 100644 --- a/packages/app/e2e/regression/subagent-child-navigation.spec.ts +++ b/packages/app/e2e/regression/subagent-child-navigation.spec.ts @@ -1,5 +1,5 @@ import { base64Encode } from "@opencode-ai/core/util/encode" -import { expect, test, type Page } from "@playwright/test" +import { expect, test, type Page } from "../fixtures" import { mockOpenCodeServer } from "../utils/mock-server" import { expectSessionTitle } from "../utils/waits" diff --git a/packages/app/e2e/regression/tab-navigate-mousedown.spec.ts b/packages/app/e2e/regression/tab-navigate-mousedown.spec.ts index 94afbc9a9d6..309d11610a8 100644 --- a/packages/app/e2e/regression/tab-navigate-mousedown.spec.ts +++ b/packages/app/e2e/regression/tab-navigate-mousedown.spec.ts @@ -1,4 +1,4 @@ -import { expect, test, type Page, type Route } from "@playwright/test" +import { expect, test, type Page, type Route } from "../fixtures" import { base64Encode } from "@opencode-ai/core/util/encode" const server = "http://127.0.0.1:4096" @@ -58,6 +58,7 @@ 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) diff --git a/packages/app/e2e/regression/terminal-composer-focus.spec.ts b/packages/app/e2e/regression/terminal-composer-focus.spec.ts index 2c2801d4b5b..1015dc0f75a 100644 --- a/packages/app/e2e/regression/terminal-composer-focus.spec.ts +++ b/packages/app/e2e/regression/terminal-composer-focus.spec.ts @@ -1,5 +1,5 @@ import { base64Encode } from "@opencode-ai/core/util/encode" -import { expect, test } from "@playwright/test" +import { expect, test } from "../fixtures" import { mockOpenCodeServer } from "../utils/mock-server" import { expectSessionTitle } from "../utils/waits" diff --git a/packages/app/e2e/regression/terminal-hidden.spec.ts b/packages/app/e2e/regression/terminal-hidden.spec.ts index 73821580af0..31482da478d 100644 --- a/packages/app/e2e/regression/terminal-hidden.spec.ts +++ b/packages/app/e2e/regression/terminal-hidden.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from "@playwright/test" +import { expect, test } from "../fixtures" import { mockOpenCodeServer } from "../utils/mock-server" import { expectSessionTitle } from "../utils/waits" diff --git a/packages/app/e2e/regression/terminal-tab-switch.spec.ts b/packages/app/e2e/regression/terminal-tab-switch.spec.ts index cbb72958ad3..a84dcca9a90 100644 --- a/packages/app/e2e/regression/terminal-tab-switch.spec.ts +++ b/packages/app/e2e/regression/terminal-tab-switch.spec.ts @@ -1,5 +1,5 @@ import { base64Encode } from "@opencode-ai/core/util/encode" -import { expect, test, type Page } from "@playwright/test" +import { expect, test, type Page } from "../fixtures" import { mockOpenCodeServer } from "../utils/mock-server" import { expectSessionTitle } from "../utils/waits" diff --git a/packages/app/e2e/smoke/session-timeline.spec.ts b/packages/app/e2e/smoke/session-timeline.spec.ts index a73cc0ccdd7..e44b25ae823 100644 --- a/packages/app/e2e/smoke/session-timeline.spec.ts +++ b/packages/app/e2e/smoke/session-timeline.spec.ts @@ -1,4 +1,4 @@ -import { expect, test, type Page } from "@playwright/test" +import { expect, test, type Page } from "../fixtures" import { base64Encode } from "@opencode-ai/core/util/encode" import { fixture, pageMessages } from "./session-timeline.fixture" import { trackPageErrors, expectNoSmokeErrors } from "../utils/errors" diff --git a/packages/app/e2e/tsconfig.json b/packages/app/e2e/tsconfig.json index 53aacbda02e..191295258cd 100644 --- a/packages/app/e2e/tsconfig.json +++ b/packages/app/e2e/tsconfig.json @@ -6,11 +6,14 @@ "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" ] diff --git a/packages/app/e2e/utils/mock-server.ts b/packages/app/e2e/utils/mock-server.ts index e1946b8fba0..26e5a48302c 100644 --- a/packages/app/e2e/utils/mock-server.ts +++ b/packages/app/e2e/utils/mock-server.ts @@ -20,6 +20,7 @@ export interface MockServerConfig { todos?: (sessionID: string) => unknown[] permissions?: unknown[] | (() => unknown[]) questions?: unknown[] | (() => unknown[]) + onQuestionReply?: (input: { requestID: string; answers?: string[][] }) => unknown | Promise fileList?: (path: string) => unknown | Promise fileContent?: (path: string) => unknown | Promise findFiles?: (input: { query: string; dirs?: string; limit?: number }) => unknown @@ -63,8 +64,7 @@ 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" && config.fileList) - return json(route, await config.fileList(url.searchParams.get("path") ?? "")) + if (path === "/file") 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,6 +110,18 @@ 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 diff --git a/packages/app/e2e/utils/sse-transport.ts b/packages/app/e2e/utils/sse-transport.ts index 186962998d1..55420485f39 100644 --- a/packages/app/e2e/utils/sse-transport.ts +++ b/packages/app/e2e/utils/sse-transport.ts @@ -162,7 +162,7 @@ export async function installSseTransport( 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(input, init) + return originalFetch(request) const id = ++nextConnectionID const record = { diff --git a/packages/app/src/components/dialog-custom-provider.tsx b/packages/app/src/components/dialog-custom-provider.tsx index 363a2e390a4..7bc8576739c 100644 --- a/packages/app/src/components/dialog-custom-provider.tsx +++ b/packages/app/src/components/dialog-custom-provider.tsx @@ -161,7 +161,7 @@ export function CustomProviderForm() { }, onError: (err) => { const message = err instanceof Error ? err.message : String(err) - showToast({ title: language.t("common.requestFailed"), description: message }) + showToast({ variant: "error", title: language.t("common.requestFailed"), description: message }) }, })) diff --git a/packages/app/src/components/dialog-fork.tsx b/packages/app/src/components/dialog-fork.tsx index 601f03084ce..4d50832840f 100644 --- a/packages/app/src/components/dialog-fork.tsx +++ b/packages/app/src/components/dialog-fork.tsx @@ -72,7 +72,7 @@ export const DialogFork: Component = () => { .client.session.fork({ sessionID, messageID: item.id }) .then((forked) => { if (!forked.data) { - showToast({ title: language.t("common.requestFailed") }) + showToast({ variant: "error", 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({ title: language.t("common.requestFailed"), description: message }) + showToast({ variant: "error", title: language.t("common.requestFailed"), description: message }) }) } diff --git a/packages/app/src/components/prompt-input/submit.ts b/packages/app/src/components/prompt-input/submit.ts index 05d978a947c..38c3f6b1fa2 100644 --- a/packages/app/src/components/prompt-input/submit.ts +++ b/packages/app/src/components/prompt-input/submit.ts @@ -327,6 +327,7 @@ export function createPromptSubmit(input: PromptSubmitInput) { .then((x) => x.data) .catch((err) => { showToast({ + variant: "error", title: language.t("prompt.toast.worktreeCreateFailed.title"), description: errorMessage(err), }) @@ -335,6 +336,7 @@ export function createPromptSubmit(input: PromptSubmitInput) { if (!createdWorktree?.directory) { showToast({ + variant: "error", title: language.t("prompt.toast.worktreeCreateFailed.title"), description: language.t("common.requestFailed"), }) @@ -366,6 +368,7 @@ 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), }) @@ -385,6 +388,7 @@ export function createPromptSubmit(input: PromptSubmitInput) { } if (!session) { showToast({ + variant: "error", title: language.t("prompt.toast.promptSendFailed.title"), description: language.t("prompt.toast.promptSendFailed.description"), }) @@ -449,6 +453,7 @@ export function createPromptSubmit(input: PromptSubmitInput) { }) .catch((err) => { showToast({ + variant: "error", title: language.t("prompt.toast.shellSendFailed.title"), description: errorMessage(err), }) @@ -481,6 +486,7 @@ 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")), }) @@ -576,6 +582,7 @@ 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), }) diff --git a/packages/app/src/components/settings-providers.tsx b/packages/app/src/components/settings-providers.tsx index bcd30edbc7d..5bc90434ae4 100644 --- a/packages/app/src/components/settings-providers.tsx +++ b/packages/app/src/components/settings-providers.tsx @@ -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({ title: language.t("common.requestFailed"), description: message }) + showToast({ variant: "error", 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({ title: language.t("common.requestFailed"), description: message }) + showToast({ variant: "error", title: language.t("common.requestFailed"), description: message }) }) } diff --git a/packages/app/src/components/settings-v2/providers.tsx b/packages/app/src/components/settings-v2/providers.tsx index f945fa33c64..0d6b17981ca 100644 --- a/packages/app/src/components/settings-v2/providers.tsx +++ b/packages/app/src/components/settings-v2/providers.tsx @@ -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({ title: language.t("common.requestFailed"), description: message }) + showToast({ variant: "error", 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({ title: language.t("common.requestFailed"), description: message }) + showToast({ variant: "error", title: language.t("common.requestFailed"), description: message }) }) } diff --git a/packages/app/src/components/titlebar-tab-nav.tsx b/packages/app/src/components/titlebar-tab-nav.tsx index a397046f9b3..98985f019f0 100644 --- a/packages/app/src/components/titlebar-tab-nav.tsx +++ b/packages/app/src/components/titlebar-tab-nav.tsx @@ -145,6 +145,7 @@ export function TabNavItem(props: { } catch (err) { props.onTitleChangeFailed?.(original) showToast({ + variant: "error", title: language.t("common.requestFailed"), description: err instanceof Error ? err.message : undefined, }) diff --git a/packages/app/src/components/updater-action.ts b/packages/app/src/components/updater-action.ts index 9c13c542b41..25be6167b2d 100644 --- a/packages/app/src/components/updater-action.ts +++ b/packages/app/src/components/updater-action.ts @@ -44,7 +44,7 @@ export function useUpdaterAction() { }) } if (state?.status === "error") { - showToast({ title: language.t("common.requestFailed"), description: state.message }) + showToast({ variant: "error", title: language.t("common.requestFailed"), description: state.message }) } }, } diff --git a/packages/app/src/pages/home.tsx b/packages/app/src/pages/home.tsx index c8a89672dfc..8b53e0746b7 100644 --- a/packages/app/src/pages/home.tsx +++ b/packages/app/src/pages/home.tsx @@ -518,6 +518,7 @@ export function NewHome() { ), onError: (error) => showToast({ + variant: "error", title: language.t("common.requestFailed"), description: errorMessage(error, language.t("common.requestFailed")), }), diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index fd9d16b90ad..ec32964e287 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -1391,6 +1391,7 @@ 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")), }) @@ -1464,6 +1465,7 @@ 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")), }) @@ -1826,6 +1828,7 @@ 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")), }) diff --git a/packages/app/src/pages/session/composer/session-composer-state.ts b/packages/app/src/pages/session/composer/session-composer-state.ts index 45f5e4cb26f..7284f41bd84 100644 --- a/packages/app/src/pages/session/composer/session-composer-state.ts +++ b/packages/app/src/pages/session/composer/session-composer-state.ts @@ -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({ title: language.t("common.requestFailed"), description }) + showToast({ variant: "error", title: language.t("common.requestFailed"), description }) }) .finally(() => { setStore("responding", (id) => (id === perm.id ? undefined : id)) diff --git a/packages/app/src/pages/session/composer/session-question-dock.tsx b/packages/app/src/pages/session/composer/session-question-dock.tsx index 445a9f47a08..b9ca648b521 100644 --- a/packages/app/src/pages/session/composer/session-question-dock.tsx +++ b/packages/app/src/pages/session/composer/session-question-dock.tsx @@ -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({ title: language.t("common.requestFailed"), description: message }) + showToast({ variant: "error", title: language.t("common.requestFailed"), description: message }) } const replyMutation = useMutation(() => ({ diff --git a/packages/app/src/pages/session/timeline/message-timeline.tsx b/packages/app/src/pages/session/timeline/message-timeline.tsx index d6cc7be972d..fc5efec6e85 100644 --- a/packages/app/src/pages/session/timeline/message-timeline.tsx +++ b/packages/app/src/pages/session/timeline/message-timeline.tsx @@ -15,7 +15,12 @@ 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" @@ -674,6 +679,7 @@ export function MessageTimeline(props: { }, onError: (err) => { showToast({ + variant: "error", title: language.t("common.requestFailed"), description: errorMessage(err), }) @@ -708,6 +714,7 @@ export function MessageTimeline(props: { ) .catch((err: unknown) => showToast({ + variant: "error", title: language.t("common.requestFailed"), description: errorMessage(err), }), @@ -820,6 +827,7 @@ export function MessageTimeline(props: { }) .catch((err) => { showToast({ + variant: "error", title: language.t("common.requestFailed"), description: errorMessage(err), }) @@ -839,6 +847,7 @@ export function MessageTimeline(props: { .then((x) => x.data) .catch((err) => { showToast({ + variant: "error", title: language.t("session.delete.failed.title"), description: errorMessage(err), }) diff --git a/packages/ui/src/v2/components/toast-v2.tsx b/packages/ui/src/v2/components/toast-v2.tsx index d2dc1b3605f..03a96a7ea6b 100644 --- a/packages/ui/src/v2/components/toast-v2.tsx +++ b/packages/ui/src/v2/components/toast-v2.tsx @@ -90,6 +90,7 @@ export interface ToastV2Options { title?: string description?: string icon?: JSX.Element + variant?: "default" | "success" | "error" | "loading" duration?: number persistent?: boolean actions?: ToastV2Action[] @@ -100,7 +101,12 @@ export function showToastV2(options: ToastV2Options | string) { return toaster.show((props) => { const resolvedIcon = children(() => opts.icon) return ( - +
{resolvedIcon()}