From 9daf46b3272ab7ba45460fc9cd0bb61d1ae78068 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Tue, 6 Oct 2026 13:07:18 +1000 Subject: [PATCH] fix(session-ui): align provider error icon with first line (#52859) Co-authored-by: iamdavidhill <1879069+iamdavidhill@users.noreply.github.com> Co-authored-by: LukeParkerDev <10430890+Hona@users.noreply.github.com> --- .../component-tests/session-timeline.spec.ts | 24 +++++++++ .../src/components/session-error.tsx | 2 +- .../src/timeline/error-card.stories.tsx | 54 +++++++++++++++++++ 3 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 packages/session-ui/src/timeline/error-card.stories.tsx diff --git a/packages/session-ui/component-tests/session-timeline.spec.ts b/packages/session-ui/component-tests/session-timeline.spec.ts index ee055fd965d..b5956d93f7e 100644 --- a/packages/session-ui/component-tests/session-timeline.spec.ts +++ b/packages/session-ui/component-tests/session-timeline.spec.ts @@ -60,6 +60,30 @@ story("aligns the retry icon with the error label", async ({ mount }) => { expect(iconY).toBe(labelY) }) +story("centers the error icon on the first line of short and wrapped errors", async ({ mount }) => { + const timeline = await mount("current-session-error-card--provider-errors") + const cards = timeline.locator('[data-kind="session-error-card"]') + + await expect(cards).toHaveCount(2) + + const geometry = await cards.evaluateAll((elements) => + elements.map((card) => { + const icon = card.querySelector('[data-slot="icon-svg"]')!.getBoundingClientRect() + const message = card.querySelector('[data-slot="icon-svg"] + div')! + const line = parseFloat(getComputedStyle(message).lineHeight) + const box = message.getBoundingClientRect() + + return { + lines: Math.round(box.height / line), + offset: icon.top + icon.height / 2 - (box.top + line / 2), + } + }), + ) + + expect(geometry.map((card) => card.lines > 1)).toEqual([false, true]) + geometry.forEach((card) => expect(card.offset).toBeCloseTo(0, 0)) +}) + // Moved from packages/app/e2e/regression/session-timeline-context-state.spec.ts story("preserves a collapsed context group through count and status updates", async ({ mount }) => { const timeline = await mount("current-session-research-agents--agent-research", { args: { scenario: "exploration" } }) diff --git a/packages/session-ui/src/components/session-error.tsx b/packages/session-ui/src/components/session-error.tsx index d497269f4da..12608a14af5 100644 --- a/packages/session-ui/src/components/session-error.tsx +++ b/packages/session-ui/src/components/session-error.tsx @@ -25,7 +25,7 @@ export function SessionErrorMessage(props: { message: string }) { export function SessionError(props: { message: string }) { return ( -
+
diff --git a/packages/session-ui/src/timeline/error-card.stories.tsx b/packages/session-ui/src/timeline/error-card.stories.tsx new file mode 100644 index 00000000000..1b395a79827 --- /dev/null +++ b/packages/session-ui/src/timeline/error-card.stories.tsx @@ -0,0 +1,54 @@ +import type { SessionDocument } from "../document" +import { CURRENT_SESSION_ID, STORY_MODEL, STORY_TIME, thinkingDocument } from "../storybook/current-session-fixtures" +import { CurrentSessionTimelineStory } from "../storybook/current-session-story" +import { SessionTimeline } from "./session-timeline" + +export default { + title: "OpenCode/Conversation/Error card", + id: "current-session-error-card", + component: SessionTimeline, + parameters: { layout: "fullscreen" }, +} + +const document = { + sessionID: CURRENT_SESSION_ID, + messages: [ + ...thinkingDocument.messages, + { + id: "msg_story_error_short", + type: "assistant", + agent: "build", + model: STORY_MODEL, + content: [], + error: { type: "provider.content-filter", message: "Provider blocked the response: Request declined." }, + time: { created: STORY_TIME + 33_000, completed: STORY_TIME + 34_000 }, + }, + { id: "msg_story_error_retry", type: "user", text: "Try again.", time: { created: STORY_TIME + 34_500 } }, + { + id: "msg_story_error_long", + type: "assistant", + agent: "build", + model: STORY_MODEL, + content: [], + error: { + type: "provider.content-filter", + message: + "Provider blocked the response: This request could not be completed. Check the provider's documentation for the applicable policy, then try a different approach or model.", + }, + time: { created: STORY_TIME + 35_000, completed: STORY_TIME + 36_000 }, + }, + ], + status: { type: "idle" }, + diffs: [], +} satisfies SessionDocument + +export const ProviderErrors = { + render: () => ( + + ), +}