From 0e901b000383a84f14412712ccdffbdaf0a92f01 Mon Sep 17 00:00:00 2001 From: v0 Date: Thu, 26 Feb 2026 05:36:14 +0000 Subject: [PATCH 1/6] fix: render error stack as readable text in attribute-panel Update 'error' display to show stack trace as '
' text.

Slack-Thread: https://vercel.slack.com/archives/C09G3EQAL84/p1772083947691689?thread_ts=1772083947.691689&cid=C09G3EQAL84
Co-authored-by: Pranay Prakash <1797812+pranaygp@users.noreply.github.com>
---
 .../components/sidebar/attribute-panel.tsx    | 89 +++++++++++++++++++
 1 file changed, 89 insertions(+)

diff --git a/packages/web-shared/src/components/sidebar/attribute-panel.tsx b/packages/web-shared/src/components/sidebar/attribute-panel.tsx
index e7a9fc2bf0..62698d357a 100644
--- a/packages/web-shared/src/components/sidebar/attribute-panel.tsx
+++ b/packages/web-shared/src/components/sidebar/attribute-panel.tsx
@@ -113,6 +113,74 @@ function ConversationWithTabs({
   );
 }
 
+/**
+ * Tabbed view for error bodies that have a `stack` field.
+ * Shows the stack trace as readable 
 text by default,
+ * with a "Raw" tab to view the full JSON object.
+ */
+function ErrorBodyWithTabs({
+  value,
+}: {
+  value: Record;
+}) {
+  const [activeTab, setActiveTab] = useState<'stack' | 'raw'>('stack');
+  const stack = value.stack as string;
+  const message =
+    typeof value.message === 'string' ? value.message : undefined;
+
+  return (
+    
+
+ setActiveTab('stack')} + > + Stack + + setActiveTab('raw')} + > + Raw + +
+ + {activeTab === 'stack' ? ( +
+ {message && ( +

+ {message} +

+ )} +
+            {stack}
+          
+
+ ) : ( +
{JsonBlock(value)}
+ )} +
+ ); +} + /** * Render a value with the shared DataInspector (ObjectInspector with * custom theming, nodeRenderer for StreamRef/ClassInstanceRef, etc.) @@ -396,6 +464,27 @@ const attributeToDisplayFn: Record< }, error: (value: unknown) => { if (!hasDisplayContent(value)) return null; + + // If the error object has a `stack` field, render it with a Stack/Raw tab + // switcher so the stack trace is readable as plain text by default. + const hasStack = + value != null && + typeof value === 'object' && + 'stack' in value && + typeof (value as Record).stack === 'string'; + + if (hasStack) { + return ( + + } /> + + ); + } + return ( Date: Thu, 26 Feb 2026 05:41:07 +0000 Subject: [PATCH 2/6] feat: create new changeset for PR updates Add new changeset following format and style guidelines. Slack-Thread: https://vercel.slack.com/archives/C09G3EQAL84/p1772083947691689?thread_ts=1772083947.691689&cid=C09G3EQAL84 Co-authored-by: Pranay Prakash <1797812+pranaygp@users.noreply.github.com> --- .changeset/render-error-stack-text.md | 5 +++++ .../src/components/sidebar/attribute-panel.tsx | 11 +++-------- 2 files changed, 8 insertions(+), 8 deletions(-) create mode 100644 .changeset/render-error-stack-text.md diff --git a/.changeset/render-error-stack-text.md b/.changeset/render-error-stack-text.md new file mode 100644 index 0000000000..d9122bd358 --- /dev/null +++ b/.changeset/render-error-stack-text.md @@ -0,0 +1,5 @@ +--- +"@workflow/web-shared": patch +--- + +Render error stack traces as readable pre-formatted text with Stack/Raw tab switcher diff --git a/packages/web-shared/src/components/sidebar/attribute-panel.tsx b/packages/web-shared/src/components/sidebar/attribute-panel.tsx index 62698d357a..7851721065 100644 --- a/packages/web-shared/src/components/sidebar/attribute-panel.tsx +++ b/packages/web-shared/src/components/sidebar/attribute-panel.tsx @@ -10,8 +10,8 @@ import { extractConversation, isDoStreamStep } from '../../lib/utils'; import { StreamClickContext } from '../ui/data-inspector'; import { ErrorCard } from '../ui/error-card'; import { Skeleton } from '../ui/skeleton'; -import { CopyableDataBlock } from './copyable-data-block'; import { ConversationView } from './conversation-view'; +import { CopyableDataBlock } from './copyable-data-block'; import { DetailCard } from './detail-card'; /** @@ -118,15 +118,10 @@ function ConversationWithTabs({ * Shows the stack trace as readable
 text by default,
  * with a "Raw" tab to view the full JSON object.
  */
-function ErrorBodyWithTabs({
-  value,
-}: {
-  value: Record;
-}) {
+function ErrorBodyWithTabs({ value }: { value: Record }) {
   const [activeTab, setActiveTab] = useState<'stack' | 'raw'>('stack');
   const stack = value.stack as string;
-  const message =
-    typeof value.message === 'string' ? value.message : undefined;
+  const message = typeof value.message === 'string' ? value.message : undefined;
 
   return (
     
Date: Thu, 26 Feb 2026 05:48:04 +0000 Subject: [PATCH 3/6] feat: enhance tab state and accessibility Add shared 'TabbedContainer' and fix ARIA roles/keyboard nav; reset tab state on error change. Co-authored-by: Pranay Prakash <1797812+pranaygp@users.noreply.github.com> --- .../components/sidebar/attribute-panel.tsx | 166 ++++++++++++------ 1 file changed, 108 insertions(+), 58 deletions(-) diff --git a/packages/web-shared/src/components/sidebar/attribute-panel.tsx b/packages/web-shared/src/components/sidebar/attribute-panel.tsx index 7851721065..7b5d86f5d3 100644 --- a/packages/web-shared/src/components/sidebar/attribute-panel.tsx +++ b/packages/web-shared/src/components/sidebar/attribute-panel.tsx @@ -3,8 +3,8 @@ import { parseStepName, parseWorkflowName } from '@workflow/utils/parse-name'; import type { Event, Hook, Step, WorkflowRun } from '@workflow/world'; import type { ModelMessage } from 'ai'; -import type { ReactNode } from 'react'; -import { useCallback, useMemo, useState } from 'react'; +import type { KeyboardEvent, ReactNode } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { toast } from 'sonner'; import { extractConversation, isDoStreamStep } from '../../lib/utils'; import { StreamClickContext } from '../ui/data-inspector'; @@ -21,14 +21,23 @@ function TabButton({ active, onClick, children, + role, + 'aria-selected': ariaSelected, + tabIndex, }: { active: boolean; onClick: () => void; children: ReactNode; + role?: string; + 'aria-selected'?: boolean; + tabIndex?: number; }) { return (
)} - + ); } +const errorTabs = [ + { id: 'stack' as const, label: 'Stack' }, + { id: 'raw' as const, label: 'Raw' }, +]; + /** * Tabbed view for error bodies that have a `stack` field. * Shows the stack trace as readable
 text by default,
@@ -123,35 +189,19 @@ function ErrorBodyWithTabs({ value }: { value: Record }) {
   const stack = value.stack as string;
   const message = typeof value.message === 'string' ? value.message : undefined;
 
+  // Reset to "Stack" tab when the error value changes (e.g. selecting a
+  // different run/error) so users always see the stack trace first.
+  useEffect(() => {
+    setActiveTab('stack');
+  }, [stack, message]);
+
   return (
-    
-
- setActiveTab('stack')} - > - Stack - - setActiveTab('raw')} - > - Raw - -
- {activeTab === 'stack' ? (
{message && ( @@ -172,7 +222,7 @@ function ErrorBodyWithTabs({ value }: { value: Record }) { ) : (
{JsonBlock(value)}
)} -
+ ); } From db47e99932992b33192b3a9679e7a563f58cca17 Mon Sep 17 00:00:00 2001 From: v0 Date: Thu, 26 Feb 2026 06:03:27 +0000 Subject: [PATCH 4/6] feat: render error stack traces as readable text and add copy button Slack-Thread: https://vercel.slack.com/archives/C09G3EQAL84/p1772083947691689?thread_ts=1772083947.691689&cid=C09G3EQAL84 Co-authored-by: Pranay Prakash <1797812+pranaygp@users.noreply.github.com> --- .changeset/render-error-stack-text.md | 2 +- .../components/sidebar/attribute-panel.tsx | 114 +++++++++--------- 2 files changed, 56 insertions(+), 60 deletions(-) diff --git a/.changeset/render-error-stack-text.md b/.changeset/render-error-stack-text.md index d9122bd358..43e54c679b 100644 --- a/.changeset/render-error-stack-text.md +++ b/.changeset/render-error-stack-text.md @@ -2,4 +2,4 @@ "@workflow/web-shared": patch --- -Render error stack traces as readable pre-formatted text with Stack/Raw tab switcher +Render error stack traces as readable pre-formatted text instead of raw JSON diff --git a/packages/web-shared/src/components/sidebar/attribute-panel.tsx b/packages/web-shared/src/components/sidebar/attribute-panel.tsx index 7b5d86f5d3..faf4d69cae 100644 --- a/packages/web-shared/src/components/sidebar/attribute-panel.tsx +++ b/packages/web-shared/src/components/sidebar/attribute-panel.tsx @@ -3,8 +3,9 @@ import { parseStepName, parseWorkflowName } from '@workflow/utils/parse-name'; import type { Event, Hook, Step, WorkflowRun } from '@workflow/world'; import type { ModelMessage } from 'ai'; +import { Copy } from 'lucide-react'; import type { KeyboardEvent, ReactNode } from 'react'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useMemo, useState } from 'react'; import { toast } from 'sonner'; import { extractConversation, isDoStreamStep } from '../../lib/utils'; import { StreamClickContext } from '../ui/data-inspector'; @@ -21,23 +22,17 @@ function TabButton({ active, onClick, children, - role, - 'aria-selected': ariaSelected, - tabIndex, }: { active: boolean; onClick: () => void; children: ReactNode; - role?: string; - 'aria-selected'?: boolean; - tabIndex?: number; }) { return ( + + {message && ( +

+ {message} +

)} - +
+        {stack}
+      
+
); } @@ -510,8 +506,8 @@ const attributeToDisplayFn: Record< error: (value: unknown) => { if (!hasDisplayContent(value)) return null; - // If the error object has a `stack` field, render it with a Stack/Raw tab - // switcher so the stack trace is readable as plain text by default. + // If the error object has a `stack` field, render it as readable + // pre-formatted text. Otherwise fall back to the raw JSON viewer. const hasStack = value != null && typeof value === 'object' && @@ -525,7 +521,7 @@ const attributeToDisplayFn: Record< summaryClassName="text-base py-2" contentClassName="mt-0" > - } /> + } /> ); } From 82e012a3af4291cc1d9da4aa45728bff808c4a27 Mon Sep 17 00:00:00 2001 From: v0 Date: Thu, 26 Feb 2026 06:30:42 +0000 Subject: [PATCH 5/6] feat: render structured error stack traces in multiple UI components Slack-Thread: https://vercel.slack.com/archives/C09G3EQAL84/p1772083947691689?thread_ts=1772083947.691689&cid=C09G3EQAL84 Co-authored-by: Pranay Prakash <1797812+pranaygp@users.noreply.github.com> --- .changeset/render-error-stack-text.md | 2 +- .../src/components/event-list-view.tsx | 55 ++++++++++++- .../components/sidebar/attribute-panel.tsx | 71 ++-------------- .../src/components/sidebar/events-list.tsx | 48 ++++++++++- .../src/components/ui/error-stack-block.tsx | 80 +++++++++++++++++++ 5 files changed, 186 insertions(+), 70 deletions(-) create mode 100644 packages/web-shared/src/components/ui/error-stack-block.tsx diff --git a/.changeset/render-error-stack-text.md b/.changeset/render-error-stack-text.md index 43e54c679b..3e23db6e89 100644 --- a/.changeset/render-error-stack-text.md +++ b/.changeset/render-error-stack-text.md @@ -2,4 +2,4 @@ "@workflow/web-shared": patch --- -Render error stack traces as readable pre-formatted text instead of raw JSON +Render structured error stack traces as readable pre-formatted text everywhere errors are displayed (attribute panel, sidebar events, and events tab) diff --git a/packages/web-shared/src/components/event-list-view.tsx b/packages/web-shared/src/components/event-list-view.tsx index 958f2d478c..13755f15f1 100644 --- a/packages/web-shared/src/components/event-list-view.tsx +++ b/packages/web-shared/src/components/event-list-view.tsx @@ -8,8 +8,22 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Virtuoso, type VirtuosoHandle } from 'react-virtuoso'; import { formatDuration } from '../lib/utils'; import { DataInspector } from './ui/data-inspector'; +import { + ErrorStackBlock, + isStructuredErrorWithStack, +} from './ui/error-stack-block'; import { Skeleton } from './ui/skeleton'; +/** + * Event types whose eventData contains an error field with a StructuredError. + */ +const ERROR_EVENT_TYPES = new Set([ + 'step_failed', + 'step_retrying', + 'run_failed', + 'workflow_failed', +]); + const BUTTON_RESET_STYLE: React.CSSProperties = { appearance: 'none', WebkitAppearance: 'none', @@ -449,7 +463,36 @@ function deepParseJson(value: unknown): unknown { return value; } -function PayloadBlock({ data }: { data: unknown }): ReactNode { +/** + * Extracts a structured error with a stack trace from event data, if present. + * Returns the error object to render with ErrorStackBlock, or null if not applicable. + */ +function extractStructuredError( + data: unknown, + eventType?: string +): (Record & { stack: string }) | null { + if (!eventType || !ERROR_EVENT_TYPES.has(eventType)) return null; + if (data == null || typeof data !== 'object') return null; + const record = data as Record; + // Check the nested `error` field first (the StructuredError) + if (isStructuredErrorWithStack(record.error)) return record.error; + // Some error formats put the stack at the top level of eventData + if (isStructuredErrorWithStack(record)) return record; + return null; +} + +function PayloadBlock({ + data, + eventType, +}: { + data: unknown; + eventType?: string; +}): ReactNode { + const structuredError = useMemo( + () => extractStructuredError(data, eventType), + [data, eventType] + ); + const [copied, setCopied] = useState(false); const resetCopiedTimeoutRef = useRef(null); const cleaned = useMemo(() => deepParseJson(data), [data]); @@ -487,6 +530,14 @@ function PayloadBlock({ data }: { data: unknown }): ReactNode { [formatted] ); + if (structuredError) { + return ( +
+ +
+ ); + } + return (
+ ) : loadError ? (
}) { - const stack = value.stack as string; - const message = typeof value.message === 'string' ? value.message : undefined; - const copyText = message ? `${message}\n\n${stack}` : stack; - - return ( -
- - - {message && ( -

- {message} -

- )} -
-        {stack}
-      
-
- ); -} - /** * Render a value with the shared DataInspector (ObjectInspector with * custom theming, nodeRenderer for StreamRef/ClassInstanceRef, etc.) @@ -508,13 +453,7 @@ const attributeToDisplayFn: Record< // If the error object has a `stack` field, render it as readable // pre-formatted text. Otherwise fall back to the raw JSON viewer. - const hasStack = - value != null && - typeof value === 'object' && - 'stack' in value && - typeof (value as Record).stack === 'string'; - - if (hasStack) { + if (isStructuredErrorWithStack(value)) { return ( - +
)} ); } +/** + * Renders event data, using ErrorStackBlock for error events that contain + * a structured error with a stack trace, and CopyableDataBlock otherwise. + */ +function EventDataBlock({ + eventType, + data, +}: { + eventType: string; + data: unknown; +}) { + // For error events (step_failed, step_retrying), the eventData has the shape + // { error: StructuredError, stack?: string, ... }. Check both the top-level + // value and the nested `error` field for a stack trace. + if ( + ERROR_EVENT_TYPES.has(eventType) && + data != null && + typeof data === 'object' + ) { + const record = data as Record; + + // Check the nested `error` field first (the StructuredError) + if (isStructuredErrorWithStack(record.error)) { + return ; + } + + // Some error formats put the stack at the top level of eventData + if (isStructuredErrorWithStack(record)) { + return ; + } + } + + // For non-error events or errors without a stack, fall back to the + // generic JSON viewer. + return ; +} + export function EventsList({ events, isLoading = false, diff --git a/packages/web-shared/src/components/ui/error-stack-block.tsx b/packages/web-shared/src/components/ui/error-stack-block.tsx new file mode 100644 index 0000000000..c9e1066580 --- /dev/null +++ b/packages/web-shared/src/components/ui/error-stack-block.tsx @@ -0,0 +1,80 @@ +'use client'; + +import { Copy } from 'lucide-react'; +import { toast } from 'sonner'; + +/** + * Check whether `value` looks like a structured error object with a `stack` + * field that we can render as pre-formatted text. + */ +export function isStructuredErrorWithStack( + value: unknown +): value is Record & { stack: string } { + return ( + value != null && + typeof value === 'object' && + 'stack' in value && + typeof (value as Record).stack === 'string' + ); +} + +/** + * Renders an error with a `stack` field as readable pre-formatted text, + * styled to match the CopyableDataBlock component. The error message is + * displayed at the top with a visual separator from the stack trace. + * The entire block is copyable via a copy button. + */ +export function ErrorStackBlock({ + value, +}: { + value: Record & { stack: string }; +}) { + const stack = value.stack; + const message = typeof value.message === 'string' ? value.message : undefined; + const copyText = message ? `${message}\n\n${stack}` : stack; + + return ( +
+ + + {message && ( +

+ {message} +

+ )} +
+        {stack}
+      
+
+ ); +} From 88b959cb58a509e0f7fc4804e8cb43fde564b1dc Mon Sep 17 00:00:00 2001 From: v0 Date: Thu, 26 Feb 2026 06:37:05 +0000 Subject: [PATCH 6/6] fix: resolve TypeScript type and import errors Remove unnecessary type cast and add missing toast import. Co-authored-by: Pranay Prakash <1797812+pranaygp@users.noreply.github.com> --- packages/web-shared/src/components/sidebar/attribute-panel.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/web-shared/src/components/sidebar/attribute-panel.tsx b/packages/web-shared/src/components/sidebar/attribute-panel.tsx index 3665b28f21..03a5d7939b 100644 --- a/packages/web-shared/src/components/sidebar/attribute-panel.tsx +++ b/packages/web-shared/src/components/sidebar/attribute-panel.tsx @@ -5,6 +5,7 @@ import type { Event, Hook, Step, WorkflowRun } from '@workflow/world'; import type { ModelMessage } from 'ai'; import type { KeyboardEvent, ReactNode } from 'react'; import { useCallback, useMemo, useState } from 'react'; +import { toast } from 'sonner'; import { extractConversation, isDoStreamStep } from '../../lib/utils'; import { StreamClickContext } from '../ui/data-inspector'; import { ErrorCard } from '../ui/error-card'; @@ -460,7 +461,7 @@ const attributeToDisplayFn: Record< summaryClassName="text-base py-2" contentClassName="mt-0" > - } /> + ); }