From a051ba0e28bd63ad37fe05968d6a258d5ddf5c1c Mon Sep 17 00:00:00 2001
From: MaheshtheDev <38828053+MaheshtheDev@users.noreply.github.com>
Date: Thu, 30 Jul 2026 19:27:36 +0000
Subject: [PATCH] feat(web): give Configure sections real routes under
/configure (#1378)
Configure sections were local useState, so they could not be linked or bookmarked and always opened on the default section. Each section is now a route (/configure, /configure/models, /configure/workspace-prompt, /configure/proactivity, /configure/automations), mirroring the existing /integrations/[card] pattern. The shell renders from the segment layout so switching sections does not remount the app, and legacy /?view=configure links forward to /configure.
Fixes ENG-1140
---
.../app/(app)/configure/[section]/page.tsx | 17 +++++++++
apps/web/app/(app)/configure/layout.tsx | 15 ++++++++
apps/web/app/(app)/configure/page.tsx | 4 ++
apps/web/components/configure-view.tsx | 33 +++++++++--------
apps/web/lib/configure-routes.ts | 37 +++++++++++++++++++
apps/web/lib/view-mode-context.tsx | 30 +++++++++++----
6 files changed, 112 insertions(+), 24 deletions(-)
create mode 100644 apps/web/app/(app)/configure/[section]/page.tsx
create mode 100644 apps/web/app/(app)/configure/layout.tsx
create mode 100644 apps/web/app/(app)/configure/page.tsx
create mode 100644 apps/web/lib/configure-routes.ts
diff --git a/apps/web/app/(app)/configure/[section]/page.tsx b/apps/web/app/(app)/configure/[section]/page.tsx
new file mode 100644
index 00000000..5acd2b3a
--- /dev/null
+++ b/apps/web/app/(app)/configure/[section]/page.tsx
@@ -0,0 +1,17 @@
+import { notFound, redirect } from "next/navigation"
+import {
+ DEFAULT_CONFIGURE_SECTION,
+ isConfigureSection,
+} from "@/lib/configure-routes"
+
+export default async function ConfigureSectionPage({
+ params,
+}: {
+ params: Promise<{ section: string }>
+}) {
+ const { section } = await params
+ // Default section is canonical at /configure.
+ if (section === DEFAULT_CONFIGURE_SECTION) redirect("/configure")
+ if (!isConfigureSection(section)) notFound()
+ return null
+}
diff --git a/apps/web/app/(app)/configure/layout.tsx b/apps/web/app/(app)/configure/layout.tsx
new file mode 100644
index 00000000..340bd4c1
--- /dev/null
+++ b/apps/web/app/(app)/configure/layout.tsx
@@ -0,0 +1,15 @@
+import { AppExperience } from "@/components/app-experience"
+
+// Shell lives here so section nav doesn't remount the app.
+export default function ConfigureLayout({
+ children,
+}: {
+ children: React.ReactNode
+}) {
+ return (
+ <>
+
+ {children}
+ >
+ )
+}
diff --git a/apps/web/app/(app)/configure/page.tsx b/apps/web/app/(app)/configure/page.tsx
new file mode 100644
index 00000000..fe7bd566
--- /dev/null
+++ b/apps/web/app/(app)/configure/page.tsx
@@ -0,0 +1,4 @@
+// Shell renders in layout.tsx.
+export default function ConfigurePage() {
+ return null
+}
diff --git a/apps/web/components/configure-view.tsx b/apps/web/components/configure-view.tsx
index 21206920..98be696b 100644
--- a/apps/web/components/configure-view.tsx
+++ b/apps/web/components/configure-view.tsx
@@ -2,7 +2,8 @@
import { cn } from "@lib/utils"
import { Blocks, CalendarClock, Cpu, ScrollText } from "lucide-react"
-import { useState } from "react"
+import Link from "next/link"
+import { usePathname } from "next/navigation"
import CompanyBrainConnections from "@/components/settings/company-brain-connections"
import CompanyBrainModels from "@/components/settings/company-brain-models"
import CompanyBrainProactivity from "@/components/settings/company-brain-proactivity"
@@ -11,15 +12,14 @@ import { ProactivenessIcon } from "@/components/settings/proactiveness-icon"
import { WorkspacePrompt } from "@/components/settings/workspace-prompt"
import { ErrorBoundary } from "@/components/error-boundary"
import { useAuth } from "@lib/auth-context"
+import {
+ type ConfigureSection,
+ configureSectionToPath,
+ DEFAULT_CONFIGURE_SECTION,
+ pathToConfigureSection,
+} from "@/lib/configure-routes"
import { dmSans125ClassName } from "@/lib/fonts"
-type ConfigureSection =
- | "company-brain"
- | "models"
- | "workspace-prompt"
- | "proactivity"
- | "automations"
-
const SECTIONS: {
id: ConfigureSection
label: string
@@ -27,7 +27,7 @@ const SECTIONS: {
icon: React.ComponentType<{ className?: string }>
}[] = [
{
- id: "company-brain",
+ id: "tools",
label: "Integrations",
description:
"Connect the tools your brain works with. Your account covers your own actions and reads; workspace accounts are a shared fallback.",
@@ -65,8 +65,10 @@ const SECTIONS: {
export function ConfigureView() {
const { org } = useAuth()
- const [activeSection, setActiveSection] =
- useState("company-brain")
+ const pathname = usePathname()
+ // Reachable via ?view=configure too, where the path carries no section.
+ const activeSection =
+ pathToConfigureSection(pathname) ?? DEFAULT_CONFIGURE_SECTION
const active = SECTIONS.find((section) => section.id === activeSection)
if (!active) return null
@@ -90,11 +92,10 @@ export function ConfigureView() {
const isActive = section.id === activeSection
const Icon = section.icon
return (
-
+
)
})}
@@ -135,7 +136,7 @@ export function ConfigureView() {
}
>
- {activeSection === "company-brain" ? (
+ {activeSection === "tools" ? (
) : activeSection === "models" ? (
diff --git a/apps/web/lib/configure-routes.ts b/apps/web/lib/configure-routes.ts
new file mode 100644
index 00000000..cdef17dd
--- /dev/null
+++ b/apps/web/lib/configure-routes.ts
@@ -0,0 +1,37 @@
+// Sections under the /configure route.
+// "tools" is the "Integrations" section, slugged to avoid clashing with /integrations.
+export const CONFIGURE_SECTIONS = [
+ "tools",
+ "models",
+ "workspace-prompt",
+ "proactivity",
+ "automations",
+] as const
+
+export type ConfigureSection = (typeof CONFIGURE_SECTIONS)[number]
+
+export const DEFAULT_CONFIGURE_SECTION: ConfigureSection = "tools"
+
+export function isConfigureSection(slug: string): slug is ConfigureSection {
+ return (CONFIGURE_SECTIONS as readonly string[]).includes(slug)
+}
+
+export function configureSectionToPath(section: ConfigureSection): string {
+ return section === DEFAULT_CONFIGURE_SECTION
+ ? "/configure"
+ : `/configure/${section}`
+}
+
+export function pathToConfigureSection(
+ pathname: string,
+): ConfigureSection | null {
+ const trimmed = pathname.replace(/\/$/, "")
+ if (trimmed === "/configure") return DEFAULT_CONFIGURE_SECTION
+ const slug = trimmed.match(/^\/configure\/([^/]+)$/)?.[1]
+ if (slug && isConfigureSection(slug)) return slug
+ return null
+}
+
+export function isConfigurePath(pathname: string): boolean {
+ return pathToConfigureSection(pathname) !== null
+}
diff --git a/apps/web/lib/view-mode-context.tsx b/apps/web/lib/view-mode-context.tsx
index d26fc731..0bc5f218 100644
--- a/apps/web/lib/view-mode-context.tsx
+++ b/apps/web/lib/view-mode-context.tsx
@@ -8,6 +8,7 @@ import {
isIntegrationView,
pathToIntegrationView,
} from "@/lib/integration-routes"
+import { isConfigurePath } from "@/lib/configure-routes"
import { analytics } from "@/lib/analytics"
import { useCallback, useEffect } from "react"
@@ -33,8 +34,11 @@ export function useViewMode() {
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)
+ // On /integrations[/card] and /configure[/section] the path is the source of truth;
+ // elsewhere the ?view param is.
+ const pathView: ViewMode | null =
+ pathToIntegrationView(pathname) ??
+ (isConfigurePath(pathname) ? "configure" : null)
const viewMode: ViewMode = pathView ?? paramView
const setViewMode = useCallback(
@@ -44,8 +48,12 @@ export function useViewMode() {
router.push(integrationViewToPath(mode))
return
}
- // Leaving (or already off) the integrations route for a non-integration view.
- if (pathToIntegrationView(pathname)) {
+ if (mode === "configure") {
+ router.push("/configure")
+ return
+ }
+ // Leaving (or already off) a path-owned route for a param-owned view.
+ if (pathToIntegrationView(pathname) || isConfigurePath(pathname)) {
router.push(mode === "dashboard" ? "/" : `/?view=${mode}`)
return
}
@@ -57,8 +65,8 @@ export function useViewMode() {
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.
+// Forwards legacy /?view=integrations (and sub-views) and /?view=configure to their
+// canonical routes, preserving any other query params. Call once near the app root.
export function useLegacyViewRedirect() {
const pathname = usePathname()
const router = useRouter()
@@ -67,10 +75,16 @@ export function useLegacyViewRedirect() {
useEffect(() => {
if (pathname !== "/") return
const view = searchParams.get("view")
- if (!view || !isIntegrationView(view)) return
+ if (!view) return
+ const target = isIntegrationView(view)
+ ? integrationViewToPath(view)
+ : view === "configure"
+ ? "/configure"
+ : null
+ if (!target) return
const params = new URLSearchParams(searchParams.toString())
params.delete("view")
const qs = params.toString()
- router.replace(integrationViewToPath(view) + (qs ? `?${qs}` : ""))
+ router.replace(target + (qs ? `?${qs}` : ""))
}, [pathname, searchParams, router])
}