authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-19 18:42:39-08:00
committergravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-19 18:42:39-08:00
logd8412eca9f68256c42d0f86f80f884a12e5ec8f7
tree543e5db0ff83485bd2cd34d75bc3d6ff56f99c94
parent749a2076d8ad713eef9ef575ff40437abd3adcd8

error component


4 files changed, 113 insertions(+), 12 deletions(-)

apps/web/src/diffRouteSearch.test.ts+22-1
......@@ -17,6 +17,28 @@ describe("parseDiffRouteSearch", () => {
1717 });
1818 });
1919
20 it("treats numeric and boolean diff toggles as open", () => {
21 expect(
22 parseDiffRouteSearch({
23 diff: 1,
24 diffTurnId: "turn-1",
25 }),
26 ).toEqual({
27 diff: "1",
28 diffTurnId: "turn-1",
29 });
30
31 expect(
32 parseDiffRouteSearch({
33 diff: true,
34 diffTurnId: "turn-1",
35 }),
36 ).toEqual({
37 diff: "1",
38 diffTurnId: "turn-1",
39 });
40 });
41
2042 it("drops turn and file values when diff is closed", () => {
2143 const parsed = parseDiffRouteSearch({
2244 diff: "0",
......@@ -50,4 +72,3 @@ describe("parseDiffRouteSearch", () => {
5072 });
5173 });
5274});
53
apps/web/src/diffRouteSearch.ts+5-1
......@@ -4,6 +4,10 @@ export interface DiffRouteSearch {
44 diffFilePath?: string;
55}
66
7function isDiffOpenValue(value: unknown): boolean {
8 return value === "1" || value === 1 || value === true;
9}
10
711function normalizeSearchString(value: unknown): string | undefined {
812 if (typeof value !== "string") {
913 return undefined;
......@@ -20,7 +24,7 @@ export function stripDiffSearchParams<T extends Record<string, unknown>>(
2024}
2125
2226export function parseDiffRouteSearch(search: Record<string, unknown>): DiffRouteSearch {
23 const diff = search.diff === "1" ? "1" : undefined;
27 const diff = isDiffOpenValue(search.diff) ? "1" : undefined;
2428 const diffTurnId = diff ? normalizeSearchString(search.diffTurnId) : undefined;
2529 const diffFilePath = diff && diffTurnId ? normalizeSearchString(search.diffFilePath) : undefined;
2630
apps/web/src/routes/__root.tsx+80-2
......@@ -1,7 +1,14 @@
1import { Outlet, createRootRouteWithContext, useParams } from "@tanstack/react-router";
1import {
2 Outlet,
3 createRootRouteWithContext,
4 type ErrorComponentProps,
5 useParams,
6} from "@tanstack/react-router";
27import { useEffect, useRef } from "react";
38import { QueryClient, useQueryClient } from "@tanstack/react-query";
49
10import { APP_DISPLAY_NAME } from "../branding";
11import { Button } from "../components/ui/button";
512import { AnchoredToastProvider, ToastProvider } from "../components/ui/toast";
613import { isElectron } from "../env";
714import { useNativeApi } from "../hooks/useNativeApi";
......@@ -9,12 +16,12 @@ import { invalidateGitQueries } from "../lib/gitReactQuery";
916import { DEFAULT_MODEL } from "../model-logic";
1017import { useStore } from "../store";
1118import { onServerWelcome } from "../wsNativeApi";
12import { APP_DISPLAY_NAME } from "../branding";
1319
1420export const Route = createRootRouteWithContext<{
1521 queryClient: QueryClient;
1622}>()({
1723 component: RootRouteView,
24 errorComponent: RootRouteErrorView,
1825});
1926
2027function RootRouteView() {
......@@ -44,6 +51,77 @@ function RootRouteView() {
4451 );
4552}
4653
54function RootRouteErrorView({ error, reset }: ErrorComponentProps) {
55 const message = errorMessage(error);
56 const details = errorDetails(error);
57
58 return (
59 <div className="relative flex min-h-screen items-center justify-center overflow-hidden bg-background px-4 py-10 text-foreground sm:px-6">
60 <div className="pointer-events-none absolute inset-0 opacity-80">
61 <div className="absolute inset-x-0 top-0 h-44 bg-[radial-gradient(44rem_16rem_at_top,color-mix(in_srgb,var(--color-red-500)_16%,transparent),transparent)]" />
62 <div className="absolute inset-0 bg-[linear-gradient(145deg,color-mix(in_srgb,var(--background)_90%,var(--color-black))_0%,var(--background)_55%)]" />
63 </div>
64
65 <section className="relative w-full max-w-xl rounded-2xl border border-border/80 bg-card/90 p-6 shadow-2xl shadow-black/20 backdrop-blur-md sm:p-8">
66 <p className="text-[11px] font-semibold tracking-[0.18em] text-muted-foreground uppercase">
67 {APP_DISPLAY_NAME}
68 </p>
69 <h1 className="mt-3 text-2xl font-semibold tracking-tight sm:text-3xl">
70 Something went wrong.
71 </h1>
72 <p className="mt-2 text-sm leading-relaxed text-muted-foreground">{message}</p>
73
74 <div className="mt-5 flex flex-wrap gap-2">
75 <Button size="sm" onClick={() => reset()}>
76 Try again
77 </Button>
78 <Button size="sm" variant="outline" onClick={() => window.location.reload()}>
79 Reload app
80 </Button>
81 </div>
82
83 <details className="group mt-5 overflow-hidden rounded-lg border border-border/70 bg-background/55">
84 <summary className="cursor-pointer list-none px-3 py-2 text-xs font-medium text-muted-foreground">
85 <span className="group-open:hidden">Show error details</span>
86 <span className="hidden group-open:inline">Hide error details</span>
87 </summary>
88 <pre className="max-h-56 overflow-auto border-t border-border/70 bg-background/80 px-3 py-2 text-xs text-foreground/85">
89 {details}
90 </pre>
91 </details>
92 </section>
93 </div>
94 );
95}
96
97function errorMessage(error: unknown): string {
98 if (error instanceof Error && error.message.trim().length > 0) {
99 return error.message;
100 }
101
102 if (typeof error === "string" && error.trim().length > 0) {
103 return error;
104 }
105
106 return "An unexpected router error occurred.";
107}
108
109function errorDetails(error: unknown): string {
110 if (error instanceof Error) {
111 return error.stack ?? error.message;
112 }
113
114 if (typeof error === "string") {
115 return error;
116 }
117
118 try {
119 return JSON.stringify(error, null, 2);
120 } catch {
121 return "No additional error details are available.";
122 }
123}
124
47125function EventRouter() {
48126 const api = useNativeApi();
49127 const { dispatch } = useStore();
apps/web/src/routes/_chat.$threadId.tsx+6-8
......@@ -1,5 +1,5 @@
11import { createFileRoute, useNavigate } from "@tanstack/react-router";
2import { Activity, Suspense, lazy, type ReactNode, useCallback, useEffect } from "react";
2import { Suspense, lazy, type ReactNode, useCallback, useEffect } from "react";
33
44import ChatView from "../components/ChatView";
55import { parseDiffRouteSearch, stripDiffSearchParams } from "../diffRouteSearch";
......@@ -97,13 +97,11 @@ function ChatThreadRouteView() {
9797 return (
9898 <>
9999 <ChatView threadId={threadId} />
100 <Activity mode={diffOpen ? "visible" : "hidden"}>
101 <DiffPanelWrapper sheet={shouldUseDiffSheet} diffOpen={diffOpen} onCloseDiff={closeDiff}>
102 <Suspense fallback={diffLoadingFallback}>
103 <DiffPanel mode={shouldUseDiffSheet ? "sheet" : "inline"} />
104 </Suspense>
105 </DiffPanelWrapper>
106 </Activity>
100 <DiffPanelWrapper sheet={shouldUseDiffSheet} diffOpen={diffOpen} onCloseDiff={closeDiff}>
101 <Suspense fallback={diffLoadingFallback}>
102 <DiffPanel mode={shouldUseDiffSheet ? "sheet" : "inline"} />
103 </Suspense>
104 </DiffPanelWrapper>
107105 </>
108106 );
109107}