From ecd6b8c47ccce5041ff784aacbab83a1a3c36896 Mon Sep 17 00:00:00 2001 From: justsomelegs <145564979+justsomelegs@users.noreply.github.com> Date: Wed, 13 May 2026 14:34:37 +0100 Subject: [PATCH 1/2] Add expandable runtime warning details --- .../Layers/ProviderRuntimeIngestion.ts | 2 +- .../components/chat/MessagesTimeline.test.tsx | 37 ++- .../src/components/chat/MessagesTimeline.tsx | 253 ++++++++++++------ apps/web/src/session-logic.test.ts | 23 ++ apps/web/src/session-logic.ts | 20 +- 5 files changed, 246 insertions(+), 89 deletions(-) diff --git a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts index 2c07ac91b1e..8661e326ae3 100644 --- a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts +++ b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts @@ -352,7 +352,7 @@ function runtimeEventToActivities( createdAt: event.createdAt, tone: "info", kind: "runtime.warning", - summary: "Runtime warning", + summary: "Runtime Warning", payload: { message: truncateDetail(event.payload.message), ...(event.payload.detail !== undefined ? { detail: event.payload.detail } : {}), diff --git a/apps/web/src/components/chat/MessagesTimeline.test.tsx b/apps/web/src/components/chat/MessagesTimeline.test.tsx index 12103194870..daef05631a3 100644 --- a/apps/web/src/components/chat/MessagesTimeline.test.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.test.tsx @@ -131,7 +131,7 @@ describe("MessagesTimeline", () => { expect(markup).toContain('data-user-message-collapsed="true"'); expect(markup).toContain('data-user-message-fade="true"'); expect(markup).toContain('data-user-message-footer="true"'); - }); + }, 20_000); it("does not render collapse controls for short user messages", async () => { const { MessagesTimeline } = await import("./MessagesTimeline"); @@ -144,7 +144,7 @@ describe("MessagesTimeline", () => { expect(markup).not.toContain("Show full message"); expect(markup).toContain('data-user-message-collapsible="false"'); - }); + }, 20_000); it("renders inline terminal labels with the composer chip UI", async () => { const { MessagesTimeline } = await import("./MessagesTimeline"); @@ -212,6 +212,39 @@ describe("MessagesTimeline", () => { expect(markup).toContain("Work log"); }); + it("renders a warning detail toggle without exposing the details by default", async () => { + const { MessagesTimeline } = await import("./MessagesTimeline"); + const markup = renderToStaticMarkup( + , + ); + + expect(markup).toContain("Runtime Warning"); + expect(markup).toContain('aria-label="Show runtime warning details"'); + expect(markup).not.toContain("slow_provider"); + expect(markup).not.toContain("Provider got slow"); + expect(markup).not.toContain(">Warning<"); + }); + it("formats changed file paths from the workspace root", async () => { const { MessagesTimeline } = await import("./MessagesTimeline"); const markup = renderToStaticMarkup( diff --git a/apps/web/src/components/chat/MessagesTimeline.tsx b/apps/web/src/components/chat/MessagesTimeline.tsx index 1540d5f344a..86e7df6f09c 100644 --- a/apps/web/src/components/chat/MessagesTimeline.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.tsx @@ -23,6 +23,7 @@ import ChatMarkdown from "../ChatMarkdown"; import { BotIcon, CheckIcon, + ChevronDownIcon, CircleAlertIcon, EyeIcon, GlobeIcon, @@ -51,6 +52,7 @@ import { } from "./MessagesTimeline.logic"; import { TerminalContextInlineChip } from "./TerminalContextInlineChip"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; +import { Collapsible, CollapsibleContent } from "../ui/collapsible"; import { deriveDisplayedUserMessageState, type ParsedTerminalContextEntry, @@ -1035,9 +1037,18 @@ function workToneClass(tone: "thinking" | "tool" | "info" | "error"): string { } function workEntryPreview( - workEntry: Pick, + workEntry: Pick< + TimelineWorkEntry, + "detail" | "command" | "changedFiles" | "runtimeWarningMessage" | "runtimeWarningDetail" + >, workspaceRoot: string | undefined, ) { + if ( + workEntry.runtimeWarningMessage !== undefined || + workEntry.runtimeWarningDetail !== undefined + ) { + return null; + } if (workEntry.command) return workEntry.command; if (workEntry.detail) return workEntry.detail; if ((workEntry.changedFiles?.length ?? 0) === 0) return null; @@ -1099,11 +1110,30 @@ function toolWorkEntryHeading(workEntry: TimelineWorkEntry): string { return capitalizePhrase(normalizeCompactToolLabel(workEntry.toolTitle)); } +function formatRuntimeWarningDetail(detail: unknown): string | null { + if (typeof detail === "string") { + const trimmed = detail.trim(); + return trimmed.length > 0 ? trimmed : null; + } + + if (detail === null || detail === undefined) { + return null; + } + + try { + const serialized = JSON.stringify(detail, null, 2); + return typeof serialized === "string" && serialized.length > 0 ? serialized : String(detail); + } catch { + return String(detail); + } +} + const SimpleWorkEntryRow = memo(function SimpleWorkEntryRow(props: { workEntry: TimelineWorkEntry; workspaceRoot: string | undefined; }) { const { workEntry, workspaceRoot } = props; + const [showRuntimeWarningDetail, setShowRuntimeWarningDetail] = useState(false); const iconConfig = workToneIcon(workEntry.tone); const EntryIcon = workEntryIcon(workEntry); const heading = toolWorkEntryHeading(workEntry); @@ -1118,104 +1148,159 @@ const SimpleWorkEntryRow = memo(function SimpleWorkEntryRow(props: { const displayText = preview ? `${heading} - ${preview}` : heading; const hasChangedFiles = (workEntry.changedFiles?.length ?? 0) > 0; const previewIsChangedFiles = hasChangedFiles && !workEntry.command && !workEntry.detail; + const runtimeWarningMessage = workEntry.runtimeWarningMessage?.trim() || null; + const runtimeWarningDetail = formatRuntimeWarningDetail(workEntry.runtimeWarningDetail); + const hasRuntimeWarningMeta = runtimeWarningMessage !== null || runtimeWarningDetail !== null; - return ( -
-
- - - -
- {rawCommand ? ( -
-

+

+ + +
+ {runtimeWarningMessage ? ( +

+ {runtimeWarningMessage} +

+ ) : null} + {runtimeWarningDetail ? ( +
+                  {runtimeWarningDetail}
+                
+ ) : null}
- ) : ( - - +
+
+ + ); + } + + return ( + +
+
+ + + +
+ {rawCommand ? ( +

{heading} - {preview && - {preview}} -

- - -

- {displayText} + {preview && ( + + + {" "} + - {preview} + + } + /> + +

+ {rawCommand} +
+
+ + )}

- - - )} +
+ ) : ( + + +

+ + {heading} + + {preview && - {preview}} +

+
+ +

+ {displayText} +

+
+
+ )} +
-
- {hasChangedFiles && !previewIsChangedFiles && ( -
- {workEntry.changedFiles?.slice(0, 4).map((filePath) => { - const displayPath = formatWorkspaceRelativePath(filePath, workspaceRoot); - return ( - - {displayPath} + {hasChangedFiles && !previewIsChangedFiles && ( +
+ {workEntry.changedFiles?.slice(0, 4).map((filePath) => { + const displayPath = formatWorkspaceRelativePath(filePath, workspaceRoot); + return ( + + {displayPath} + + ); + })} + {(workEntry.changedFiles?.length ?? 0) > 4 && ( + + +{(workEntry.changedFiles?.length ?? 0) - 4} - ); - })} - {(workEntry.changedFiles?.length ?? 0) > 4 && ( - - +{(workEntry.changedFiles?.length ?? 0) - 4} - - )} -
- )} -
+ )} +
+ )} +
+ ); }); diff --git a/apps/web/src/session-logic.test.ts b/apps/web/src/session-logic.test.ts index baf384d6af2..02dbd32b2d2 100644 --- a/apps/web/src/session-logic.test.ts +++ b/apps/web/src/session-logic.test.ts @@ -653,6 +653,29 @@ describe("deriveWorkLogEntries", () => { expect(entries[0]?.tone).toBe("error"); }); + it("keeps runtime warning rows generic while preserving message and structured details", () => { + const detail = { code: "slow_provider", retryInSeconds: 5 }; + const activities: OrchestrationThreadActivity[] = [ + makeActivity({ + id: "runtime-warning", + createdAt: "2026-02-23T00:00:03.000Z", + kind: "runtime.warning", + summary: "Runtime Warning", + tone: "info", + payload: { + message: "Provider got slow", + detail, + }, + }), + ]; + + const [entry] = deriveWorkLogEntries(activities, undefined); + expect(entry?.label).toBe("Runtime Warning"); + expect(entry?.detail).toBeUndefined(); + expect(entry?.runtimeWarningMessage).toBe("Provider got slow"); + expect(entry?.runtimeWarningDetail).toEqual(detail); + }); + it("filters by turn id when provided", () => { const activities: OrchestrationThreadActivity[] = [ makeActivity({ id: "turn-1", turnId: "turn-1", summary: "Tool call", kind: "tool.started" }), diff --git a/apps/web/src/session-logic.ts b/apps/web/src/session-logic.ts index a7767672fa1..15d7ded33db 100644 --- a/apps/web/src/session-logic.ts +++ b/apps/web/src/session-logic.ts @@ -52,6 +52,8 @@ export interface WorkLogEntry { createdAt: string; label: string; detail?: string; + runtimeWarningMessage?: string; + runtimeWarningDetail?: unknown; command?: string; rawCommand?: string; changedFiles?: ReadonlyArray; @@ -530,6 +532,12 @@ function toDerivedWorkLogEntry(activity: OrchestrationThreadActivity): DerivedWo payload.detail.length > 0 ? payload.detail : null; + const runtimeWarningMessage = + activity.kind === "runtime.warning" && + typeof payload?.message === "string" && + payload.message.length > 0 + ? payload.message + : null; const taskLabel = taskSummary || taskDetailAsLabel; const detail = isTaskActivity ? !taskDetailAsLabel && @@ -538,12 +546,14 @@ function toDerivedWorkLogEntry(activity: OrchestrationThreadActivity): DerivedWo payload.detail.length > 0 ? stripTrailingExitCode(payload.detail).output : null - : extractToolDetail(payload, title ?? activity.summary); + : activity.kind === "runtime.warning" + ? null + : extractToolDetail(payload, title ?? activity.summary); const toolCallId = isTaskActivity ? null : extractToolCallId(payload); const entry: DerivedWorkLogEntry = { id: activity.id, createdAt: activity.createdAt, - label: taskLabel || activity.summary, + label: activity.kind === "runtime.warning" ? activity.summary : taskLabel || activity.summary, tone: activity.kind === "task.progress" ? "thinking" @@ -557,6 +567,12 @@ function toDerivedWorkLogEntry(activity: OrchestrationThreadActivity): DerivedWo if (detail) { entry.detail = detail; } + if (runtimeWarningMessage) { + entry.runtimeWarningMessage = runtimeWarningMessage; + } + if (activity.kind === "runtime.warning" && payload?.detail !== undefined) { + entry.runtimeWarningDetail = payload.detail; + } if (commandPreview.command) { entry.command = commandPreview.command; } From 50fe1e2f78ca50322020c0accf7e10aee9afa2b0 Mon Sep 17 00:00:00 2001 From: justsomelegs <145564979+justsomelegs@users.noreply.github.com> Date: Wed, 13 May 2026 14:58:06 +0100 Subject: [PATCH 2/2] Align runtime warning preview checks --- .../components/chat/MessagesTimeline.test.tsx | 26 ++++++++ .../src/components/chat/MessagesTimeline.tsx | 59 +++++++++++-------- 2 files changed, 61 insertions(+), 24 deletions(-) diff --git a/apps/web/src/components/chat/MessagesTimeline.test.tsx b/apps/web/src/components/chat/MessagesTimeline.test.tsx index daef05631a3..1b3d6f437a5 100644 --- a/apps/web/src/components/chat/MessagesTimeline.test.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.test.tsx @@ -245,6 +245,32 @@ describe("MessagesTimeline", () => { expect(markup).not.toContain(">Warning<"); }); + it("renders whitespace-only runtime warning messages as a plain runtime warning row", async () => { + const { MessagesTimeline } = await import("./MessagesTimeline"); + const markup = renderToStaticMarkup( + , + ); + + expect(markup).toContain("Runtime Warning"); + expect(markup).not.toContain('aria-label="Show runtime warning details"'); + }); + it("formats changed file paths from the workspace root", async () => { const { MessagesTimeline } = await import("./MessagesTimeline"); const markup = renderToStaticMarkup( diff --git a/apps/web/src/components/chat/MessagesTimeline.tsx b/apps/web/src/components/chat/MessagesTimeline.tsx index 86e7df6f09c..077fa1cfd84 100644 --- a/apps/web/src/components/chat/MessagesTimeline.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.tsx @@ -1036,6 +1036,38 @@ function workToneClass(tone: "thinking" | "tool" | "info" | "error"): string { return "text-muted-foreground/40"; } +function normalizeRuntimeWarningMessage(message: string | undefined): string | null { + const trimmed = message?.trim(); + return trimmed && trimmed.length > 0 ? trimmed : null; +} + +function formatRuntimeWarningDetail(detail: unknown): string | null { + if (typeof detail === "string") { + const trimmed = detail.trim(); + return trimmed.length > 0 ? trimmed : null; + } + + if (detail === null || detail === undefined) { + return null; + } + + try { + const serialized = JSON.stringify(detail, null, 2); + return typeof serialized === "string" && serialized.length > 0 ? serialized : String(detail); + } catch { + return String(detail); + } +} + +function hasRenderableRuntimeWarningMeta( + workEntry: Pick, +): boolean { + return ( + normalizeRuntimeWarningMessage(workEntry.runtimeWarningMessage) !== null || + formatRuntimeWarningDetail(workEntry.runtimeWarningDetail) !== null + ); +} + function workEntryPreview( workEntry: Pick< TimelineWorkEntry, @@ -1043,10 +1075,7 @@ function workEntryPreview( >, workspaceRoot: string | undefined, ) { - if ( - workEntry.runtimeWarningMessage !== undefined || - workEntry.runtimeWarningDetail !== undefined - ) { + if (hasRenderableRuntimeWarningMeta(workEntry)) { return null; } if (workEntry.command) return workEntry.command; @@ -1110,24 +1139,6 @@ function toolWorkEntryHeading(workEntry: TimelineWorkEntry): string { return capitalizePhrase(normalizeCompactToolLabel(workEntry.toolTitle)); } -function formatRuntimeWarningDetail(detail: unknown): string | null { - if (typeof detail === "string") { - const trimmed = detail.trim(); - return trimmed.length > 0 ? trimmed : null; - } - - if (detail === null || detail === undefined) { - return null; - } - - try { - const serialized = JSON.stringify(detail, null, 2); - return typeof serialized === "string" && serialized.length > 0 ? serialized : String(detail); - } catch { - return String(detail); - } -} - const SimpleWorkEntryRow = memo(function SimpleWorkEntryRow(props: { workEntry: TimelineWorkEntry; workspaceRoot: string | undefined; @@ -1148,9 +1159,9 @@ const SimpleWorkEntryRow = memo(function SimpleWorkEntryRow(props: { const displayText = preview ? `${heading} - ${preview}` : heading; const hasChangedFiles = (workEntry.changedFiles?.length ?? 0) > 0; const previewIsChangedFiles = hasChangedFiles && !workEntry.command && !workEntry.detail; - const runtimeWarningMessage = workEntry.runtimeWarningMessage?.trim() || null; + const runtimeWarningMessage = normalizeRuntimeWarningMessage(workEntry.runtimeWarningMessage); const runtimeWarningDetail = formatRuntimeWarningDetail(workEntry.runtimeWarningDetail); - const hasRuntimeWarningMeta = runtimeWarningMessage !== null || runtimeWarningDetail !== null; + const hasRuntimeWarningMeta = hasRenderableRuntimeWarningMeta(workEntry); if (hasRuntimeWarningMeta) { return (