diff --git a/apps/desktop/src/renderer/components/account/AccountPage.tsx b/apps/desktop/src/renderer/components/account/AccountPage.tsx index b8b179b89..3c7359ed0 100644 --- a/apps/desktop/src/renderer/components/account/AccountPage.tsx +++ b/apps/desktop/src/renderer/components/account/AccountPage.tsx @@ -55,6 +55,7 @@ import { openExternalUrl } from "../../lib/openExternal"; import { isWebClientMode } from "../../lib/webClientMode"; import { docs } from "../../onboarding/docsLinks"; import { useClampedFixedPosition } from "../../hooks/useClampedFixedPosition"; +import { settingsRouteFor } from "../settings/settingsManifest"; const REPO_BRIDGE_DISMISS_KEY = "ade.account.repoBridgeDismissed.v1"; const MACHINES_REFRESH_MS = 30_000; @@ -1247,7 +1248,7 @@ export function AccountPage() { diff --git a/apps/desktop/src/renderer/components/cto/useCtoModelOptions.ts b/apps/desktop/src/renderer/components/cto/useCtoModelOptions.ts index 979cf6343..d8eb6a793 100644 --- a/apps/desktop/src/renderer/components/cto/useCtoModelOptions.ts +++ b/apps/desktop/src/renderer/components/cto/useCtoModelOptions.ts @@ -2,6 +2,7 @@ import { useCallback, useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; import { getModelById, modelSupportsFastMode, selectSupportedReasoningEffort } from "../../../shared/modelRegistry"; import { deriveConfiguredModelIds } from "../../lib/modelOptions"; +import { settingsRouteFor } from "../settings/settingsManifest"; export type CtoModelSelection = { provider: string; @@ -74,7 +75,7 @@ export function useCtoModelOptions(): { }, []); const openProviderSettings = useCallback(() => { - navigate("/settings?tab=ai#ai-providers"); + navigate(settingsRouteFor("agents.providers")); }, [navigate]); return { availableModelIds, loadingModels, openProviderSettings }; diff --git a/apps/desktop/src/renderer/components/lanes/LanesPage.tsx b/apps/desktop/src/renderer/components/lanes/LanesPage.tsx index 6ec6acac3..44e4d203a 100644 --- a/apps/desktop/src/renderer/components/lanes/LanesPage.tsx +++ b/apps/desktop/src/renderer/components/lanes/LanesPage.tsx @@ -108,6 +108,7 @@ import type { import { eventMatchesBinding, getEffectiveBinding } from "../../lib/keybindings"; import { SmartTooltip } from "../ui/SmartTooltip"; import { docs } from "../../onboarding/docsLinks"; +import { settingsRouteFor } from "../settings/settingsManifest"; type RebaseScopePromptState = { laneId: string; @@ -2002,7 +2003,7 @@ export function LanesPage({ active = true }: { active?: boolean } = {}) { } }; - const openAutoRebaseSettings = useCallback(() => { navigate("/settings?tab=lane-templates"); }, [navigate]); + const openAutoRebaseSettings = useCallback(() => { navigate(settingsRouteFor("lanes-git.lane-templates")); }, [navigate]); const openRebaseDetails = useCallback((laneId?: string | null) => { const trimmedLaneId = typeof laneId === "string" ? laneId.trim() : ""; if (trimmedLaneId.length) { @@ -3656,8 +3657,8 @@ export function LanesPage({ active = true }: { active?: boolean } = {}) { prefill={createPrefill} onCreated={handleLaneCreated} onBusyChange={(busy) => { createBusyRef.current = busy; }} - onOpenLinearSettings={() => navigate("/settings?tab=general#linear-connection")} - onNavigateToTemplates={() => navigate("/settings?tab=lane-templates")} + onOpenLinearSettings={() => navigate(settingsRouteFor("integrations.linear"))} + onNavigateToTemplates={() => navigate(settingsRouteFor("lanes-git.lane-templates"))} /> {rebaseScopePrompt ? ( diff --git a/apps/desktop/src/renderer/components/prs/tabs/GitHubTab.tsx b/apps/desktop/src/renderer/components/prs/tabs/GitHubTab.tsx index ee94a57f5..e2212cea2 100644 --- a/apps/desktop/src/renderer/components/prs/tabs/GitHubTab.tsx +++ b/apps/desktop/src/renderer/components/prs/tabs/GitHubTab.tsx @@ -56,6 +56,7 @@ import { import { GitHubTabView } from "./GitHubTabView"; import { branchNameFromRef } from "./githubPrBranch"; import { useGitHubTabListModel } from "./useGitHubTabListModel"; +import { settingsRouteFor } from "../../settings/settingsManifest"; export type GitHubTabProps = { lanes: LaneSummary[]; @@ -943,7 +944,7 @@ export function GitHubTab({ syncedAt: snapshot?.syncedAt ?? null, onSync: () => { void handleSync(); }, error, - onConnectGitHub: () => navigate("/settings?tab=general#github-connection"), + onConnectGitHub: () => navigate(settingsRouteFor("integrations.github")), }} list={{ parentRef: listRef, diff --git a/apps/desktop/src/renderer/components/settings/AiFeaturesSection.test.tsx b/apps/desktop/src/renderer/components/settings/AiFeaturesSection.test.tsx index 1a0ebfcd6..c810eea46 100644 --- a/apps/desktop/src/renderer/components/settings/AiFeaturesSection.test.tsx +++ b/apps/desktop/src/renderer/components/settings/AiFeaturesSection.test.tsx @@ -133,7 +133,7 @@ describe("AiFeaturesSection", () => { fireEvent.click(screen.getByRole("button", { name: "Set up ai-feature-chat-auto-title" })); await waitFor(() => { - expect(screen.getByTestId("location").textContent).toBe("/settings?tab=ai#ai-providers"); + expect(screen.getByTestId("location").textContent).toBe("/settings?tab=agents#ai-providers"); }); }); diff --git a/apps/desktop/src/renderer/components/settings/settingsManifest.test.ts b/apps/desktop/src/renderer/components/settings/settingsManifest.test.ts index e7c45623e..0194752cd 100644 --- a/apps/desktop/src/renderer/components/settings/settingsManifest.test.ts +++ b/apps/desktop/src/renderer/components/settings/settingsManifest.test.ts @@ -14,6 +14,7 @@ import { searchSettingsEntries, settingsEntryPath, settingsGroupsForTab, + settingsRouteFor, } from "./settingsManifest"; /** @@ -49,6 +50,29 @@ describe("settings manifest", () => { } }); + it("builds a live route for every id in-app CTAs link to", () => { + // The banners, callouts and empty states across the app now name settings + // by manifest id instead of hand-writing `?tab=...#anchor`. If a setting is + // renamed or dropped, this list is where it fails — loudly — instead of a + // CTA quietly dumping the user on the settings root. + const ctaIds = [ + "integrations.github", + "integrations.linear", + "agents.providers", + "agents.background-jobs", + "agents.dictation", + "general.launch-prompt", + "lanes-git.lane-templates", + "storage.usage", + "storage.diagnostics", + ]; + for (const id of ctaIds) { + expect(settingsRouteFor(id), `${id} has no manifest entry`).not.toBe("/settings"); + } + expect(settingsRouteFor("integrations.github")).toBe("/settings?tab=integrations#github-connection"); + expect(settingsRouteFor("nope.missing")).toBe("/settings"); + }); + it("resolves current tab ids unchanged", () => { for (const id of SETTINGS_TAB_IDS) { expect(resolveSettingsTab(id)).toBe(id); diff --git a/apps/desktop/src/renderer/components/settings/settingsManifest.ts b/apps/desktop/src/renderer/components/settings/settingsManifest.ts index a04fd62ef..b5fd250ee 100644 --- a/apps/desktop/src/renderer/components/settings/settingsManifest.ts +++ b/apps/desktop/src/renderer/components/settings/settingsManifest.ts @@ -883,6 +883,22 @@ export function settingsEntryPath(entry: SettingEntry): string { return `/settings?tab=${entry.tab}#${entry.anchor}`; } +/** + * The route for a setting named by its manifest id — the form every in-app CTA + * (banners, callouts, empty states) should use. + * + * Hand-written `/settings?tab=general#github-connection` strings were the bug + * this replaces: when GitHub moved from General to Integrations, the tab in + * those literals kept pointing at General while the anchor moved, so the + * "Authorize" banner landed on General and scrolled nowhere. Deriving the whole + * route from the manifest means a setting can never move out from under a CTA + * again. Unknown ids fall back to the settings root rather than throwing. + */ +export function settingsRouteFor(entryId: string): string { + const entry = ENTRIES_BY_ID.get(entryId); + return entry ? settingsEntryPath(entry) : "/settings"; +} + export function settingsTabLabel(tab: SettingsTabId): string { return SETTINGS_TABS.find((candidate) => candidate.id === tab)?.label ?? tab; } diff --git a/apps/desktop/src/renderer/components/shared/useOpenProviderSignIn.ts b/apps/desktop/src/renderer/components/shared/useOpenProviderSignIn.ts index 4e49f1e33..0ba17833f 100644 --- a/apps/desktop/src/renderer/components/shared/useOpenProviderSignIn.ts +++ b/apps/desktop/src/renderer/components/shared/useOpenProviderSignIn.ts @@ -2,11 +2,12 @@ import { useCallback } from "react"; import { useNavigate } from "react-router-dom"; import type { AuthType, ProviderFamily } from "../../../shared/modelRegistry"; import { createClaudeLoginTerminalInWork } from "../work/ClaudeLoginPromptButton"; +import { settingsRouteFor } from "../settings/settingsManifest"; export function useOpenProviderSignIn(): (family?: ProviderFamily, authTypes?: readonly AuthType[]) => void { const navigate = useNavigate(); const openAiProvidersSettings = useCallback(() => { - navigate("/settings?tab=ai#ai-providers"); + navigate(settingsRouteFor("agents.providers")); }, [navigate]); return useCallback((family?: ProviderFamily, authTypes?: readonly AuthType[]) => { diff --git a/apps/desktop/src/renderer/components/terminals/SessionListPane.tsx b/apps/desktop/src/renderer/components/terminals/SessionListPane.tsx index 865f2e845..226302b5e 100644 --- a/apps/desktop/src/renderer/components/terminals/SessionListPane.tsx +++ b/apps/desktop/src/renderer/components/terminals/SessionListPane.tsx @@ -68,6 +68,7 @@ import { handoffLaunchTitle, type HandoffLaunchJob, } from "../../lib/handoffLaunchJobs"; +import { settingsRouteFor } from "../settings/settingsManifest"; const EMPTY_GRID_SETS: WorkGridSet[] = []; @@ -3102,8 +3103,8 @@ export const SessionListPane = React.memo(function SessionListPane({ open={createLaneOpen} onOpenChange={setCreateLaneOpen} behavior="close-on-create" - onNavigateToTemplates={() => navigate("/settings?tab=lane-templates")} - onOpenLinearSettings={() => navigate("/settings?tab=general#linear-connection")} + onNavigateToTemplates={() => navigate(settingsRouteFor("lanes-git.lane-templates"))} + onOpenLinearSettings={() => navigate(settingsRouteFor("integrations.linear"))} /> ) : null} {settleUndo ? ( diff --git a/apps/desktop/src/renderer/components/terminals/WorkSidebar.tsx b/apps/desktop/src/renderer/components/terminals/WorkSidebar.tsx index a159adc31..73b84e3e2 100644 --- a/apps/desktop/src/renderer/components/terminals/WorkSidebar.tsx +++ b/apps/desktop/src/renderer/components/terminals/WorkSidebar.tsx @@ -44,6 +44,7 @@ import { LaneDiffPane } from "../lanes/LaneDiffPane"; import { LaneGitActionsPane } from "../lanes/LaneGitActionsPane"; import { GlowMenu, type GlowMenuItem } from "../ui/GlowMenu"; import { cn } from "../ui/cn"; +import { settingsRouteFor } from "../settings/settingsManifest"; const WORK_SIDEBAR_TABS: Array> = [ { @@ -574,7 +575,7 @@ export function WorkSidebar({ navigate("/settings?tab=lane-templates")} + onOpenSettings={() => navigate(settingsRouteFor("lanes-git.lane-templates"))} onSelectFile={(path, mode) => { setSelectedPath(path); setSelectedMode(mode); diff --git a/apps/desktop/src/renderer/lib/githubIntegrationStatus.ts b/apps/desktop/src/renderer/lib/githubIntegrationStatus.ts index 68a683996..ed1da4b2c 100644 --- a/apps/desktop/src/renderer/lib/githubIntegrationStatus.ts +++ b/apps/desktop/src/renderer/lib/githubIntegrationStatus.ts @@ -195,7 +195,7 @@ export function githubAccountIssueCopy( return { title: "GitHub App not authorized", detail: "Authorize ADE with GitHub to turn on real-time pull request updates.", - action: "Authorize", + action: "Set up ADE GitHub App", }; }