Compare commits

...

2 Commits

Author SHA1 Message Date
Jamison Lahman
c48dd49d1a what 2026-03-19 17:30:33 -07:00
Jamison Lahman
6cb4bc063e fix(fe): scroll stack trace exception into view 2026-03-19 17:10:06 -07:00
2 changed files with 113 additions and 2 deletions

View File

@@ -1,4 +1,4 @@
import { useState } from "react";
import { useEffect, useRef, useState } from "react";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { SvgChevronDown, SvgChevronRight } from "@opal/icons";
import { Button } from "@opal/components";
@@ -36,6 +36,16 @@ export const ErrorBanner = ({
resubmit?: () => void;
}) => {
const [isStackTraceExpanded, setIsStackTraceExpanded] = useState(false);
const stackTraceRef = useRef<HTMLPreElement>(null);
useEffect(() => {
if (isStackTraceExpanded && stackTraceRef.current) {
stackTraceRef.current.scrollIntoView({
behavior: "instant",
block: "end",
});
}
}, [isStackTraceExpanded]);
return (
<div className="text-red-700 mt-4 text-sm my-auto">
@@ -71,7 +81,10 @@ export const ErrorBanner = ({
/>
</div>
{isStackTraceExpanded && (
<pre className="mt-2 p-3 bg-neutral-100 dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded text-xs text-neutral-700 dark:text-neutral-300 overflow-auto max-h-48 whitespace-pre-wrap font-mono">
<pre
ref={stackTraceRef}
className="mt-2 p-3 bg-neutral-100 dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded text-xs text-neutral-700 dark:text-neutral-300 overflow-auto max-h-48 whitespace-pre-wrap font-mono"
>
{stackTrace}
</pre>
)}

View File

@@ -240,6 +240,36 @@ function buildMockSearchStream(options: SearchMockOptions): string {
return `${packets.map((p) => JSON.stringify(p)).join("\n")}\n`;
}
interface ErrorMockOptions {
error: string;
stack_trace?: string | null;
error_code?: string | null;
is_retryable?: boolean;
details?: Record<string, unknown> | null;
}
function buildMockErrorStream(options: ErrorMockOptions): string {
turnCounter += 1;
const userMessageId = turnCounter * 100 + 1;
const agentMessageId = turnCounter * 100 + 2;
const packets: Record<string, unknown>[] = [
{
user_message_id: userMessageId,
reserved_assistant_message_id: agentMessageId,
},
{
error: options.error,
stack_trace: options.stack_trace ?? null,
error_code: options.error_code ?? null,
is_retryable: options.is_retryable ?? true,
details: options.details ?? null,
},
];
return `${packets.map((p) => JSON.stringify(p)).join("\n")}\n`;
}
async function openChat(page: Page): Promise<void> {
await page.goto("/app");
await page.waitForLoadState("networkidle");
@@ -801,6 +831,74 @@ Set \`max_results\` to limit the number of returned documents.`;
});
});
test.describe.only("Error Banner and Stack Trace", () => {
const MOCK_STACK_TRACE = [
"Traceback (most recent call last):",
' File "/app/backend/onyx/chat/process_message.py", line 400, in stream_chat_message_objects',
' raise ValueError("Invalid configuration")',
' File "/app/backend/onyx/llm/factory.py", line 85, in get_llm',
" return self._get_llm(model_name)",
' File "/app/backend/onyx/llm/factory.py", line 92, in _get_llm',
' raise RuntimeError("Model not available")',
"RuntimeError: Model not available",
].join("\n");
test("expanding stack trace scrolls it into view", async ({ page }) => {
await openChat(page);
await page.route("**/api/chat/send-chat-message", async (route) => {
await route.fulfill({
status: 200,
contentType: "text/plain",
body: buildMockErrorStream({
error: "Failed to process your request",
stack_trace: MOCK_STACK_TRACE,
error_code: "VALIDATION_ERROR",
is_retryable: true,
details: { model: "gpt-4", provider: "openai" },
}),
});
});
// Send message manually (sendMessage waits for an AI message element,
// but error responses render an ErrorBanner instead).
await page
.locator("#onyx-chat-input-textarea")
.fill("trigger an error");
await page.locator("#onyx-chat-input-send-button").click();
// Wait for the error banner to appear
const stackTraceButton = page.getByRole("button", {
name: "Stack trace",
});
await expect(stackTraceButton).toBeVisible({ timeout: 15000 });
// The <pre> should not be visible yet
const stackTracePre = page.locator("pre.font-mono");
await expect(stackTracePre).not.toBeVisible();
// Click to expand the stack trace
await stackTraceButton.click();
await expect(stackTracePre).toBeVisible();
// Verify the stack trace <pre> element is scrolled into the viewport
const isInViewport = await stackTracePre.evaluate((el) => {
const rect = el.getBoundingClientRect();
return (
rect.bottom > 0 &&
rect.bottom <= window.innerHeight &&
rect.top >= 0
);
});
expect(isInViewport).toBe(true);
await screenshotChatContainer(
page,
`chat-error-stack-trace-expanded-${theme}`
);
});
});
test.describe("Message Interaction States", () => {
test("hovering over user message shows action buttons", async ({
page,