+
{shortcutsConnect.dialog}
+ onOpenChange={(open) => {
setNewKey((s) => ({
open,
key: open ? s.key : "",
pluginId: open ? s.pluginId : null,
+ loading: open ? s.loading : false,
}))
- }
+ if (!open) void setConnectTarget(null)
+ }}
>
- Copy your key and run these steps to finish.
+ {newKey.loading
+ ? "Generating your key…"
+ : "Copy your key and run these steps to finish."}
@@ -3599,15 +3692,28 @@ export function IntegrationsView({
INSET,
)}
>
-
+ {newKey.loading ? (
+
+
+ Generating your key…
+
+ ) : (
+
+ )}
)
diff --git a/apps/web/components/integrations/plugins-detail.tsx b/apps/web/components/integrations/plugins-detail.tsx
index 0bd044ae..2f28e791 100644
--- a/apps/web/components/integrations/plugins-detail.tsx
+++ b/apps/web/components/integrations/plugins-detail.tsx
@@ -613,7 +613,7 @@ export function PluginsDetail() {
try {
const result = await autumn.attach({
planId: "api_pro",
- successUrl: `${window.location.origin}/?view=integrations`,
+ successUrl: `${window.location.origin}/integrations`,
})
if (result?.paymentUrl) {
window.open(result.paymentUrl, "_self")
diff --git a/apps/web/components/settings/connections-mcp.tsx b/apps/web/components/settings/connections-mcp.tsx
index 43bbcee2..ccd098d6 100644
--- a/apps/web/components/settings/connections-mcp.tsx
+++ b/apps/web/components/settings/connections-mcp.tsx
@@ -710,7 +710,7 @@ export default function ConnectionsMCP() {
router.push("/?view=integrations&cat=ai-clients")}
+ onClick={() => router.push("/integrations?cat=ai-clients")}
>
diff --git a/apps/web/components/settings/settings-content.tsx b/apps/web/components/settings/settings-content.tsx
index 578a88bd..a257c223 100644
--- a/apps/web/components/settings/settings-content.tsx
+++ b/apps/web/components/settings/settings-content.tsx
@@ -195,7 +195,7 @@ export function SettingsContent({
}
const handleIntegrations = () => {
- void router.push("/?view=integrations")
+ void router.push("/integrations")
}
const handleDeleteAccount = async () => {
diff --git a/apps/web/lib/integration-routes.ts b/apps/web/lib/integration-routes.ts
new file mode 100644
index 00000000..f525da55
--- /dev/null
+++ b/apps/web/lib/integration-routes.ts
@@ -0,0 +1,40 @@
+import type { ViewParamValue } from "@/lib/search-params"
+
+// Integration-family views that live under the real /integrations route.
+export const INTEGRATION_VIEWS = [
+ "integrations",
+ "mcp",
+ "plugins",
+ "chrome",
+ "connections",
+ "shortcuts",
+ "raycast",
+ "import",
+] as const
+
+export type IntegrationView = (typeof INTEGRATION_VIEWS)[number]
+
+// Sub-view cards — each is a nested route segment under /integrations.
+export const INTEGRATION_CARDS = INTEGRATION_VIEWS.filter(
+ (v) => v !== "integrations",
+) as Exclude[]
+
+export function isIntegrationView(view: string): view is IntegrationView {
+ return (INTEGRATION_VIEWS as readonly string[]).includes(view)
+}
+
+export function isIntegrationCard(slug: string): slug is IntegrationView {
+ return (INTEGRATION_CARDS as readonly string[]).includes(slug)
+}
+
+export function integrationViewToPath(view: IntegrationView): string {
+ return view === "integrations" ? "/integrations" : `/integrations/${view}`
+}
+
+export function pathToIntegrationView(pathname: string): ViewParamValue | null {
+ const trimmed = pathname.replace(/\/$/, "")
+ if (trimmed === "/integrations") return "integrations"
+ const slug = trimmed.match(/^\/integrations\/([^/]+)$/)?.[1]
+ if (slug && isIntegrationCard(slug)) return slug
+ return null
+}
diff --git a/apps/web/lib/view-mode-context.tsx b/apps/web/lib/view-mode-context.tsx
index e797b47a..d26fc731 100644
--- a/apps/web/lib/view-mode-context.tsx
+++ b/apps/web/lib/view-mode-context.tsx
@@ -1,24 +1,76 @@
"use client"
import { useQueryState } from "nuqs"
+import { usePathname, useRouter, useSearchParams } from "next/navigation"
import { viewParam, type ViewParamValue } from "@/lib/search-params"
+import {
+ integrationViewToPath,
+ isIntegrationView,
+ pathToIntegrationView,
+} from "@/lib/integration-routes"
import { analytics } from "@/lib/analytics"
-import { useCallback } from "react"
+import { useCallback, useEffect } from "react"
export type ViewMode = ViewParamValue
-type SetViewMode = (value: ViewMode | null) => Promise
+const TRACKED_VIEW_MODES = [
+ "dashboard",
+ "graph",
+ "list",
+ "integrations",
+ "chat",
+ "digests",
+] as const
+
+function isTrackedViewMode(
+ mode: ViewMode,
+): mode is (typeof TRACKED_VIEW_MODES)[number] {
+ return (TRACKED_VIEW_MODES as readonly string[]).includes(mode)
+}
export function useViewMode() {
- const [viewMode, _setViewMode] = useQueryState("view", viewParam)
+ const pathname = usePathname()
+ const router = useRouter()
+ const [paramView, setParamView] = useQueryState("view", viewParam)
+
+ // On /integrations[/card] the path is the source of truth; elsewhere the ?view param is.
+ const pathView = pathToIntegrationView(pathname)
+ const viewMode: ViewMode = pathView ?? paramView
const setViewMode = useCallback(
(mode: ViewMode) => {
- analytics.viewModeChanged(mode)
- ;(_setViewMode as SetViewMode)(mode)
+ if (isTrackedViewMode(mode)) analytics.viewModeChanged(mode)
+ if (isIntegrationView(mode)) {
+ router.push(integrationViewToPath(mode))
+ return
+ }
+ // Leaving (or already off) the integrations route for a non-integration view.
+ if (pathToIntegrationView(pathname)) {
+ router.push(mode === "dashboard" ? "/" : `/?view=${mode}`)
+ return
+ }
+ void setParamView(mode)
},
- [_setViewMode],
+ [router, pathname, setParamView],
)
return { viewMode, setViewMode, isInitialized: true }
}
+
+// Forwards legacy /?view=integrations (and sub-views) to the canonical /integrations route,
+// preserving any other query params. Call once near the app root.
+export function useLegacyViewRedirect() {
+ const pathname = usePathname()
+ const router = useRouter()
+ const searchParams = useSearchParams()
+
+ useEffect(() => {
+ if (pathname !== "/") return
+ const view = searchParams.get("view")
+ if (!view || !isIntegrationView(view)) return
+ const params = new URLSearchParams(searchParams.toString())
+ params.delete("view")
+ const qs = params.toString()
+ router.replace(integrationViewToPath(view) + (qs ? `?${qs}` : ""))
+ }, [pathname, searchParams, router])
+}
diff --git a/apps/web/middleware.ts b/apps/web/middleware.ts
index 2094e5aa..b0e7879f 100644
--- a/apps/web/middleware.ts
+++ b/apps/web/middleware.ts
@@ -36,6 +36,14 @@ export default async function proxy(request: Request) {
return NextResponse.next()
}
+ // Real integrations routes, public in guest mode (mirrors view=integrations / view=mcp).
+ if (
+ url.pathname === "/integrations" ||
+ url.pathname === "/integrations/mcp"
+ ) {
+ return NextResponse.next()
+ }
+
if (url.pathname.startsWith("/api/")) {
if (!sessionCookie) {
console.debug("[MIDDLEWARE] API route without session, returning 401")