diff --git a/STAGING-v0.0.5.md b/STAGING-v0.0.5.md index c8fbcf0..05f11a5 100644 --- a/STAGING-v0.0.5.md +++ b/STAGING-v0.0.5.md @@ -51,6 +51,7 @@ branch → run relevant tests → record below. | 31 | #148 | fix/100-dedupe-eventsource | #100 | round-1 findings (P2 second-context vs lib/AGENTS.md; P2 cross-session event bleed) already fixed by author's fixup `3f73570` (session-scoped `publish` + documented stateless bus context + 5 vitest tests); round-2 P1 (throwing bus listener silently kills `connectSSE`'s UI switch — `publish` ran inside the try/catch before the switch) outstanding → fixed on PR branch in `cfbfb40` (per-listener try/catch in `SSEStreamContext.publish`, logs and continues) | branch repaired against staging in `8f74aab`: conflict only in `frontend/package-lock.json` (lockfile churn) — took staging's via `--theirs`, regenerated with `npm install` against merged package.json; `frontend/node_modules` verified real directory (not symlink) | branch: vitest **9/9** (4 api + 5 bus/notebook), tsc clean, prettier clean; post-merge staging: vitest **9/9**, tsc clean, prettier clean | `page.tsx` auto-merged — verified #148's `SSEStreamProvider`/`publish(sid, event)` AND #169's `takeSuggestedPrompt`/`Sparkles` wiring AND #87's `/api/sessions/{id}/download` UI all present; pushed `8f74aab` (contains P1 fix `cfbfb40`) verified as merge parent | | 32 | #150 | fix/98-memoize-chat | #98 | both P2 findings (shared `streamingItemId` string prop breaks `memo` bailout for ALL items on stream start/end) already fixed by author's fixup `b37907e` (per-item `isStreaming` boolean + manual-test tutorial `docs/manual-tests/chat-memoization.md`) | branch repaired against staging in `3e48748` — ort auto-merged CLEAN (no conflicts; branch was stacked on #148's original commit, staging already carried #148's fixup + P1 repair) | branch: vitest 9/9, tsc clean, prettier clean; post-merge staging: vitest 9/9, tsc clean, prettier clean | `page.tsx` merge kept memoized `ChatItemView` (`isStreaming={cur.id === streamingItemId}` verified) + #148 bus + #169/#87 wiring; pushed `3e48748` verified as merge parent | | 33 | #152 | fix/99-autoscroll | #99 | both findings already fixed by author's fixup `05cb539`: P1 smooth scroll after send un-pinned mid-animation (intermediate scroll events saw distance > threshold) → `behavior: 'auto'` unconditionally; P2 `AUTO_SCROLL_PIN_THRESHOLD_PX` re-created per render → hoisted to module level | branch repaired against staging in `8ca8e96`: one conflict in `page.tsx` — #152's `handleChatScroll` pin-tracking callback and staging's #169 suggested-prompt effect appended at the same anchor, kept both | branch: vitest 9/9, tsc clean, prettier clean; post-merge staging: vitest 9/9, tsc clean, prettier clean | `chatScrollRef` + `onScroll={handleChatScroll}` wiring verified at page.tsx:2110-2111; pushed `8ca8e96` verified as merge parent | +| 34 | #157 | fix/101-typed-sse | #101 | single P2 (`chart_config` mapped to `ChartConfig` with required `charts`, making the handler's defensive guard look dead) already fixed by author's fixup `c8a1bba` (wire-type `ChartConfigSSEData` with optional `charts` + `types.sse.test-d.ts` type-level assertions) | branch repaired against staging in `ba33265`: textual conflict in `MetricsTab.tsx` (#157 typed `ChartTooltip` props, staging's #161 exports it for the compare page — kept typed props AND `export`); semantic conflicts from the tightened union: extended `SSEEvent` with `budget_exceeded` (#165, new `BudgetExceededSSEData`) and the `notebook.kernel.state`/`notebook.structure.changed`/`notebook.cell.*` events (#148's typed bus requires them — payload interfaces reused from `lib/notebook/types.ts`, `StructureChangedEvent` moved there and re-exported from `useNotebookSSE`); `page.tsx` `budget_exceeded` case adapted to #157's per-case `const data = event.data` block; #148's `useNotebookSSE.test.tsx` cell.completed payload gained `exec_count: null` to satisfy the typed `CellCompletedEvent` | branch: vitest 9/9, tsc clean (incl. `types.sse.test-d.ts` via tsconfig `**/*.ts` include), prettier clean; post-merge staging: vitest 9/9, tsc clean, prettier clean | pushed `ba33265` verified as merge parent | ## Deferred / not merged diff --git a/frontend/src/app/error.tsx b/frontend/src/app/error.tsx new file mode 100644 index 0000000..39b011a --- /dev/null +++ b/frontend/src/app/error.tsx @@ -0,0 +1,57 @@ +'use client'; + +import { useEffect } from 'react'; +import { AlertCircle, RefreshCw } from 'lucide-react'; + +// Route-level error boundary (Next.js App Router convention: a file named +// `error.tsx` automatically wraps its route segment — here, the whole app, +// since this lives directly under `src/app/`). Catches render errors thrown +// anywhere in the tree below it that aren't already contained by a nested +// boundary (see `src/components/ErrorBoundary.tsx`, used around the +// workspace canvas and chat markdown renderers) and shows a recoverable +// screen instead of Next's default error overlay / a blank page. +export default function Error({ + error, + reset, +}: { + error: Error & { digest?: string }; + reset: () => void; +}) { + useEffect(() => { + console.error('Route-level error boundary caught:', error); + }, [error]); + + return ( +
+ Trainable hit an unexpected error. You can try again, or reload the page if it keeps + happening. +
+ {error.message && ( ++ {error.message} +
+ )} ++ Something went wrong before the app could start. Try again, or reload the page if it + keeps happening. +
+ {error.message && ( ++ {error.message} +
+ )} +
@@ -2845,9 +2855,11 @@ const ReportMarkdown = memo(function ReportMarkdown({
return (
-
- {content}
-
+
+
+ {content}
+
+
);
@@ -3785,6 +3797,11 @@ function SubAgentCard({ item }: { item: ChatItem }) {
const modelName = item.meta?.model
? item.meta.model.replace('claude-', '').replace(/-/g, ' ')
: '';
+ // Truncated once here so the markdown path and the error-boundary fallback
+ // render the same bounded text (a summary can be megabytes of agent output).
+ const summary = item.meta?.summary;
+ const truncatedSummary =
+ summary && summary.length > 800 ? summary.slice(0, 800) + '\n\n...' : summary;
useEffect(() => {
if (!isStart) return;
@@ -3844,15 +3861,17 @@ function SubAgentCard({ item }: { item: ChatItem }) {
{item.meta.task}
)}
- {item.meta?.summary && (
+ {truncatedSummary && (
Result:
-
- {item.meta.summary.length > 800
- ? item.meta.summary.slice(0, 800) + '\n\n...'
- : item.meta.summary}
-
+ (
+ {truncatedSummary}
+ )}
+ >
+ {truncatedSummary}
+
)}
@@ -4320,7 +4339,11 @@ const ChatItemView = memo(function ChatItemView({
{agentMeta && (
{agentMeta.label}
)}
- {item.content}
+ {item.content}}
+ >
+ {item.content}
+
{isStreaming && (
)}
diff --git a/frontend/src/components/ErrorBoundary.tsx b/frontend/src/components/ErrorBoundary.tsx
new file mode 100644
index 0000000..ba67551
--- /dev/null
+++ b/frontend/src/components/ErrorBoundary.tsx
@@ -0,0 +1,82 @@
+'use client';
+
+import { Component, type ReactNode } from 'react';
+import { AlertCircle, RefreshCw } from 'lucide-react';
+
+interface ErrorBoundaryProps {
+ children: ReactNode;
+ /** Rendered instead of `children` once a render error is caught. Receives
+ * the error and a `reset` callback that clears the boundary's error
+ * state and re-renders `children` — useful when the error was
+ * transient or the underlying data (e.g. a chat item, a workspace tab)
+ * has since changed. Falls back to a generic panel when omitted. */
+ fallback?: (error: Error, reset: () => void) => ReactNode;
+ /** Short noun used in the generic fallback's headline, e.g. "the
+ * workspace" or "this message". Ignored when `fallback` is provided. */
+ label?: string;
+ /** Called once when a render error is caught, e.g. to log/report it. */
+ onError?: (error: Error, info: { componentStack: string }) => void;
+}
+
+interface ErrorBoundaryState {
+ error: Error | null;
+}
+
+/**
+ * Generic React error boundary. Untyped/unvalidated SSE payloads and
+ * live-rendered agent-authored markdown + self-contained HTML artifacts
+ * mean a render throw is a real risk — without a boundary, one bad payload
+ * takes down the entire SPA with a blank screen (React unmounts the whole
+ * tree above the nearest boundary, and the root layout has none). Wrap the
+ * workspace canvas and individual markdown renderers with this so a crash
+ * is contained to that panel/bubble instead.
+ *
+ * Must be a class component — there is no hook equivalent for
+ * `getDerivedStateFromError`/`componentDidCatch` as of React 18.
+ */
+export class ErrorBoundary extends Component {
+ state: ErrorBoundaryState = { error: null };
+
+ static getDerivedStateFromError(error: Error): ErrorBoundaryState {
+ return { error };
+ }
+
+ componentDidCatch(error: Error, info: { componentStack: string }) {
+ console.error('ErrorBoundary caught a render error:', error, info.componentStack);
+ this.props.onError?.(error, info);
+ }
+
+ reset = () => {
+ this.setState({ error: null });
+ };
+
+ render() {
+ const { error } = this.state;
+ if (!error) return this.props.children;
+
+ if (this.props.fallback) return this.props.fallback(error, this.reset);
+
+ return (
+
+
+
+
+ {this.props.label
+ ? `Couldn't render ${this.props.label}.`
+ : 'Something went wrong rendering this.'}
+
+
+ {error.message}
+
+
+
+
+ );
+ }
+}