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", () => {...@@ -17,6 +17,28 @@ describe("parseDiffRouteSearch", () => {
17 });17 });
18 });18 });
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
20 it("drops turn and file values when diff is closed", () => {42 it("drops turn and file values when diff is closed", () => {
21 const parsed = parseDiffRouteSearch({43 const parsed = parseDiffRouteSearch({
22 diff: "0",44 diff: "0",
...@@ -50,4 +72,3 @@ describe("parseDiffRouteSearch", () => {...@@ -50,4 +72,3 @@ describe("parseDiffRouteSearch", () => {
50 });72 });
51 });73 });
52});74});
53
apps/web/src/diffRouteSearch.ts+5-1
...@@ -4,6 +4,10 @@ export interface DiffRouteSearch {...@@ -4,6 +4,10 @@ export interface DiffRouteSearch {
4 diffFilePath?: string;4 diffFilePath?: string;
5}5}
66
7function isDiffOpenValue(value: unknown): boolean {
8 return value === "1" || value === 1 || value === true;
9}
10
7function normalizeSearchString(value: unknown): string | undefined {11function normalizeSearchString(value: unknown): string | undefined {
8 if (typeof value !== "string") {12 if (typeof value !== "string") {
9 return undefined;13 return undefined;
...@@ -20,7 +24,7 @@ export function stripDiffSearchParams<T extends Record<string, unknown>>(...@@ -20,7 +24,7 @@ export function stripDiffSearchParams<T extends Record<string, unknown>>(
20}24}
2125
22export function parseDiffRouteSearch(search: Record<string, unknown>): DiffRouteSearch {26export function parseDiffRouteSearch(search: Record<string, unknown>): DiffRouteSearch {
23 const diff = search.diff === "1" ? "1" : undefined;27 const diff = isDiffOpenValue(search.diff) ? "1" : undefined;
24 const diffTurnId = diff ? normalizeSearchString(search.diffTurnId) : undefined;28 const diffTurnId = diff ? normalizeSearchString(search.diffTurnId) : undefined;
25 const diffFilePath = diff && diffTurnId ? normalizeSearchString(search.diffFilePath) : undefined;29 const diffFilePath = diff && diffTurnId ? normalizeSearchString(search.diffFilePath) : undefined;
2630
apps/web/src/routes/__root.tsx+80-2
...@@ -1,7 +1,14 @@...@@ -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";
2import { useEffect, useRef } from "react";7import { useEffect, useRef } from "react";
3import { QueryClient, useQueryClient } from "@tanstack/react-query";8import { QueryClient, useQueryClient } from "@tanstack/react-query";
49
10import { APP_DISPLAY_NAME } from "../branding";
11import { Button } from "../components/ui/button";
5import { AnchoredToastProvider, ToastProvider } from "../components/ui/toast";12import { AnchoredToastProvider, ToastProvider } from "../components/ui/toast";
6import { isElectron } from "../env";13import { isElectron } from "../env";
7import { useNativeApi } from "../hooks/useNativeApi";14import { useNativeApi } from "../hooks/useNativeApi";
...@@ -9,12 +16,12 @@ import { invalidateGitQueries } from "../lib/gitReactQuery";...@@ -9,12 +16,12 @@ import { invalidateGitQueries } from "../lib/gitReactQuery";
9import { DEFAULT_MODEL } from "../model-logic";16import { DEFAULT_MODEL } from "../model-logic";
10import { useStore } from "../store";17import { useStore } from "../store";
11import { onServerWelcome } from "../wsNativeApi";18import { onServerWelcome } from "../wsNativeApi";
12import { APP_DISPLAY_NAME } from "../branding";
1319
14export const Route = createRootRouteWithContext<{20export const Route = createRootRouteWithContext<{
15 queryClient: QueryClient;21 queryClient: QueryClient;
16}>()({22}>()({
17 component: RootRouteView,23 component: RootRouteView,
24 errorComponent: RootRouteErrorView,
18});25});
1926
20function RootRouteView() {27function RootRouteView() {
...@@ -44,6 +51,77 @@ function RootRouteView() {...@@ -44,6 +51,77 @@ function RootRouteView() {
44 );51 );
45}52}
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
47function EventRouter() {125function EventRouter() {
48 const api = useNativeApi();126 const api = useNativeApi();
49 const { dispatch } = useStore();127 const { dispatch } = useStore();
apps/web/src/routes/_chat.$threadId.tsx+6-8
...@@ -1,5 +1,5 @@...@@ -1,5 +1,5 @@
1import { createFileRoute, useNavigate } from "@tanstack/react-router";1import { 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
4import ChatView from "../components/ChatView";4import ChatView from "../components/ChatView";
5import { parseDiffRouteSearch, stripDiffSearchParams } from "../diffRouteSearch";5import { parseDiffRouteSearch, stripDiffSearchParams } from "../diffRouteSearch";
...@@ -97,13 +97,11 @@ function ChatThreadRouteView() {...@@ -97,13 +97,11 @@ function ChatThreadRouteView() {
97 return (97 return (
98 <>98 <>
99 <ChatView threadId={threadId} />99 <ChatView threadId={threadId} />
100 <Activity mode={diffOpen ? "visible" : "hidden"}>100 <DiffPanelWrapper sheet={shouldUseDiffSheet} diffOpen={diffOpen} onCloseDiff={closeDiff}>
101 <DiffPanelWrapper sheet={shouldUseDiffSheet} diffOpen={diffOpen} onCloseDiff={closeDiff}>101 <Suspense fallback={diffLoadingFallback}>
102 <Suspense fallback={diffLoadingFallback}>102 <DiffPanel mode={shouldUseDiffSheet ? "sheet" : "inline"} />
103 <DiffPanel mode={shouldUseDiffSheet ? "sheet" : "inline"} />103 </Suspense>
104 </Suspense>104 </DiffPanelWrapper>
105 </DiffPanelWrapper>
106 </Activity>
107 </>105 </>
108 );106 );
109}107}