diff --git a/apps/dashboard/src/components/layouts/dashboard-error-screen.tsx b/apps/dashboard/src/components/layouts/dashboard-error-screen.tsx
new file mode 100644
index 0000000..fce7310
--- /dev/null
+++ b/apps/dashboard/src/components/layouts/dashboard-error-screen.tsx
@@ -0,0 +1,182 @@
+import {
+ AlertCircleIcon,
+ LockIcon,
+ RefreshCwIcon,
+ SearchIcon,
+ WifiOffIcon,
+} from "@diffkit/icons";
+import { Button } from "@diffkit/ui/components/button";
+import { cn } from "@diffkit/ui/lib/utils";
+import { type ErrorComponentProps, useRouter } from "@tanstack/react-router";
+import type { ComponentType } from "react";
+import { useShowOrgSetupQueryState } from "#/lib/github-access-dialog-query";
+import { openGitHubAccessPrompt } from "#/lib/github-access-modal-store";
+
+type ErrorInfo = {
+ icon: ComponentType<{ size?: number; strokeWidth?: number }>;
+ iconClassName: string;
+ title: string;
+ description: string;
+ action: "retry" | "configure-access";
+};
+
+function getErrorInfo(error: Error): ErrorInfo {
+ const msg = error.message;
+ const lower = msg.toLowerCase();
+
+ if (lower.includes("rate limit") || lower.includes("429")) {
+ return {
+ icon: AlertCircleIcon,
+ iconClassName: "bg-amber-500/10 text-amber-500",
+ title: "Rate limit reached",
+ description:
+ "You've made too many requests. Wait a moment and try again.",
+ action: "retry",
+ };
+ }
+
+ if (
+ lower.includes("403") ||
+ lower.includes("forbidden") ||
+ lower.includes("not accessible by integration") ||
+ lower.includes("insufficient permissions")
+ ) {
+ return {
+ icon: LockIcon,
+ iconClassName: "bg-amber-500/10 text-amber-500",
+ title: "Access not configured",
+ description:
+ "DiffKit doesn't have access to this resource. Add the repository or organization in your GitHub app settings.",
+ action: "configure-access",
+ };
+ }
+
+ if (lower.includes("404") || lower.includes("not found")) {
+ return {
+ icon: SearchIcon,
+ iconClassName: "bg-muted-foreground/10 text-muted-foreground",
+ title: "Not found",
+ description:
+ "This resource doesn't exist or you don't have access to it.",
+ action: "retry",
+ };
+ }
+
+ if (
+ lower.includes("network") ||
+ lower.includes("fetch failed") ||
+ lower.includes("econnrefused") ||
+ lower.includes("enotfound") ||
+ lower.includes("failed to fetch")
+ ) {
+ return {
+ icon: WifiOffIcon,
+ iconClassName: "bg-amber-500/10 text-amber-500",
+ title: "Connection failed",
+ description:
+ "Could not reach the server. Check your internet connection and try again.",
+ action: "retry",
+ };
+ }
+
+ if (lower.includes("timeout") || lower.includes("timed out")) {
+ return {
+ icon: AlertCircleIcon,
+ iconClassName: "bg-amber-500/10 text-amber-500",
+ title: "Request timed out",
+ description:
+ "The request took too long to complete. Try again in a moment.",
+ action: "retry",
+ };
+ }
+
+ return {
+ icon: AlertCircleIcon,
+ iconClassName: "bg-destructive/10 text-destructive",
+ title: "Something went wrong",
+ description:
+ msg ||
+ "An unexpected error occurred. Please try again or refresh the page.",
+ action: "retry",
+ };
+}
+
+/** Strip the trailing ` - GET https://…` suffix that octokit appends. */
+function cleanErrorMessage(msg: string): string | null {
+ if (!msg) return null;
+ const cleaned = msg
+ .replace(/\s*-\s+(GET|POST|PUT|PATCH|DELETE|HEAD)\s+https?:\/\/\S+$/i, "")
+ .trim();
+ return cleaned || null;
+}
+
+export function DashboardErrorScreen({ error, reset }: ErrorComponentProps) {
+ const router = useRouter();
+ const {
+ icon: Icon,
+ iconClassName,
+ title,
+ description,
+ action,
+ } = getErrorInfo(error);
+ const detail = cleanErrorMessage(error.message);
+
+ return (
+
+
+
+
+
+
+
+
{title}
+
+ {description}
+
+
+
+ {detail && (
+
+ {detail}
+
+ )}
+
+
+ {action === "configure-access" ? : null}
+ }
+ onClick={() => {
+ reset();
+ router.invalidate();
+ }}
+ >
+ Try again
+
+
+
+
+ );
+}
+
+function ConfigureAccessButton() {
+ const [, setShowOrgSetup] = useShowOrgSetupQueryState();
+
+ return (
+
+ );
+}
diff --git a/apps/dashboard/src/components/layouts/error-screen.tsx b/apps/dashboard/src/components/layouts/error-screen.tsx
index 7dab3e6..324a509 100644
--- a/apps/dashboard/src/components/layouts/error-screen.tsx
+++ b/apps/dashboard/src/components/layouts/error-screen.tsx
@@ -1,40 +1,10 @@
-import { AlertCircleIcon, RefreshCwIcon } from "@diffkit/icons";
-import { Button } from "@diffkit/ui/components/button";
-import { type ErrorComponentProps, useRouter } from "@tanstack/react-router";
-
-export function ErrorScreen({ reset }: ErrorComponentProps) {
- const router = useRouter();
+import type { ErrorComponentProps } from "@tanstack/react-router";
+import { DashboardErrorScreen } from "./dashboard-error-screen";
+export function ErrorScreen(props: ErrorComponentProps) {
return (
-
-
-
-
-
-
- Something went wrong
-
-
- An unexpected error occurred. Please try again or refresh the page.
-
-
-
-
- }
- onClick={() => {
- reset();
- router.invalidate();
- }}
- >
- Try again
-
-
-
+
+
);
}
diff --git a/apps/dashboard/src/components/pulls/detail/pull-detail-activity.tsx b/apps/dashboard/src/components/pulls/detail/pull-detail-activity.tsx
index f5ce918..03927a1 100644
--- a/apps/dashboard/src/components/pulls/detail/pull-detail-activity.tsx
+++ b/apps/dashboard/src/components/pulls/detail/pull-detail-activity.tsx
@@ -523,7 +523,7 @@ function ReviewsSection({
repo,
pullNumber,
reviewId: review.id,
- message: "Dismissed via QuickHub",
+ message: "Dismissed via DiffKit",
},
})
.then((result) => {
diff --git a/apps/dashboard/src/lib/github.server.test.ts b/apps/dashboard/src/lib/github.server.test.ts
index 3739bab..283936b 100644
--- a/apps/dashboard/src/lib/github.server.test.ts
+++ b/apps/dashboard/src/lib/github.server.test.ts
@@ -93,7 +93,7 @@ describe("getGitHubClient", () => {
};
expect(options.auth).toBe("github-token");
- expect(options.userAgent).toBe("quickhub-dashboard");
+ expect(options.userAgent).toBe("diffkit-dashboard");
expect(options.retry).toEqual({ enabled: true });
expect(options.throttle.enabled).toBe(true);
expect(options.throttle.id).toBe("github-user:user-123");
diff --git a/apps/dashboard/src/lib/github.server.ts b/apps/dashboard/src/lib/github.server.ts
index c1024bb..b86a9bf 100644
--- a/apps/dashboard/src/lib/github.server.ts
+++ b/apps/dashboard/src/lib/github.server.ts
@@ -7,7 +7,7 @@ import {
} from "./github-app.server";
import { configureGitHubRequestPolicies } from "./github-request-policy";
-const GITHUB_CLIENT_USER_AGENT = "quickhub-dashboard";
+const GITHUB_CLIENT_USER_AGENT = "diffkit-dashboard";
const GITHUB_SECONDARY_RATE_LIMIT_FALLBACK_SECONDS = 60;
type GitHubThrottleRequestOptions = {
diff --git a/apps/dashboard/src/router.tsx b/apps/dashboard/src/router.tsx
index 6e50999..fe1c77d 100644
--- a/apps/dashboard/src/router.tsx
+++ b/apps/dashboard/src/router.tsx
@@ -1,5 +1,6 @@
import { createRouter as createTanStackRouter } from "@tanstack/react-router";
import { setupRouterSsrQueryIntegration } from "@tanstack/react-router-ssr-query";
+import { DashboardErrorScreen } from "#/components/layouts/dashboard-error-screen";
import {
AppQueryClientProvider,
createAppQueryClient,
@@ -17,6 +18,7 @@ export function getRouter() {
defaultPreload: "intent",
defaultPreloadStaleTime: 0,
defaultPendingMs: 0,
+ defaultErrorComponent: DashboardErrorScreen,
Wrap: ({ children }) => (
{children}
diff --git a/packages/icons/src/index.ts b/packages/icons/src/index.ts
index 3a380e1..57622ba 100644
--- a/packages/icons/src/index.ts
+++ b/packages/icons/src/index.ts
@@ -44,6 +44,7 @@ export {
Link02Icon as ExternalLinkIcon,
Loading03Icon as LoaderCircleIcon,
Location01Icon as LocationIcon,
+ LockIcon,
Mail01Icon as MailIcon,
Message01Icon as MessageIcon,
Moon02Icon as MoonIcon,
@@ -64,6 +65,7 @@ export {
UserCircleIcon,
UserGroupIcon as FollowersIcon,
ViewIcon,
+ WifiDisconnected01Icon as WifiOffIcon,
} from "@hugeicons/react";
export { GitHubLogo, GitHubWordmarkLogo } from "./brand-logos";
export { PenIcon } from "./pen-icon";