Skip to content

feat(frontend): /settings short link redirects to the scoped settings page - #5141

Merged
mmabrouk merged 1 commit into
big-agentsfrom
feat/settings-short-link
Jul 7, 2026
Merged

mmabrouk merged 1 commit into
big-agentsfrom
feat/settings-short-link

Conversation

@mmabrouk

@mmabrouk mmabrouk commented Jul 7, 2026 •

Copy link
Copy Markdown
Member

Context

Sharing a link to the settings page required knowing the workspace and project ids: /w/<ws>/p/<proj>/settings?tab=apiKeys. A bare https://<host>/settings?tab=apiKeys returned a 404, so there was no short, memorable URL you could give someone to open their own API keys page.

Changes

A bare /settings route now resolves the visitor's workspace and project client-side and forwards to the scoped page, preserving the full query string.

Before: /settings?tab=apiKeys renders a 404.

After: /settings?tab=apiKeys redirects to /w/<workspace_id>/p/<project_id>/settings?tab=apiKeys.

The new SettingsRedirect component follows the same reactive pattern as the sibling WorkspaceRedirect: derive the target from atoms in a memo, redirect in an effect that re-runs as they populate. The project comes from projectAtom (last-used, then default, then first non-demo) and the workspace id from the project's own workspace_id field. That last part matters: urlAtom's workspace and project ids are parsed from the URL, which a bare route does not have, so they stay empty here. Three new files: the component plus thin OSS and EE page entries at pages/settings/index.tsx.

Note for future bare-route redirects: a first version used a one-shot async wait (waitForWorkspaceContext) behind a pendingRef guard. Under React StrictMode's double-mount the first run gets cancelled and the second bails on the guard, so the redirect never fires. The reactive pattern avoids that class of bug entirely.

Scope / risk

Only /settings is covered; other bare paths (/observability, /playground, ...) still 404 on purpose. Playground URLs are app-scoped, so a bare path has no unambiguous target there. No existing route or resolver logic changed; the risk surface is the new page only. Logged-out visitors go through the normal auth flow first and land via the standard post-login redirect, not the deep link (known limitation, small follow-up if we want it).

Tests

  • Verified live on the dev box (EE, dev mode): /settings?tab=apiKeys and bare /settings both redirect to the scoped settings page with the API Keys tab rendered; query string preserved.
  • Logged-out: /settings?tab=apiKeys lands on /auth?redirectToPath=%2Fsettings%3Ftab%3DapiKeys, and after login the normal post-login flow takes over.
  • pnpm lint-fix passes for OSS and EE.

How to QA

Prerequisites: any running stack (local dev via run.sh, or the Hetzner dev box), logged in.

Steps:

  1. Visit https://<host>/settings?tab=apiKeys directly (paste in the address bar).
  2. Watch the URL after the spinner.

Expected result: you land on /w/<your-ws>/p/<your-project>/settings?tab=apiKeys with the API Keys tab active. The project is your last-used one.

Automated tests: none added; the component is a thin composition of already-tested selectors.

Edge cases: open the link in a fresh browser profile (logged out). You should get the login page, and after login land in your workspace normally (not on settings; see scope note).

https://claude.ai/code/session_01DjaJLpnLtnoRhgbYe7NTC3

@vercel

vercel Bot commented Jul 7, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agenta-documentation Ready Ready Preview, Comment Jul 7, 2026 5:57pm

Request Review

@dosubot dosubot Bot added size:XL This PR changes 500-999 lines, ignoring generated files. feature request New feature or request frontend labels Jul 7, 2026
@coderabbitai

coderabbitai Bot commented Jul 7, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 93a0da4d-2183-4e58-a026-c0846f71e82d

📥 Commits

Reviewing files that changed from the base of the PR and between 59ad253 and 8b69a97.

📒 Files selected for processing (3)
  • web/ee/src/pages/settings/index.tsx
  • web/oss/src/components/pages/SettingsRedirect/index.tsx
  • web/oss/src/pages/settings/index.tsx
✅ Files skipped from review due to trivial changes (1)
  • web/ee/src/pages/settings/index.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • web/oss/src/pages/settings/index.tsx

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Added a dedicated settings page experience that redirects users to the correct project settings view automatically.
    • Preserves the current URL query parameters during the redirect and shows a loading spinner while navigation completes.

Walkthrough

A new SettingsRedirect component resolves a settings target path from workspace and project context, redirects with the router while preserving query parameters, and is default-exported from both OSS and EE settings pages.

Changes

Settings Redirect Implementation

Layer / File(s) Summary
SettingsRedirect component logic
web/oss/src/components/pages/SettingsRedirect/index.tsx
Adds the redirect component, derives the target settings path from workspace/project state, performs a router-based redirect, and renders a loading spinner while waiting.
Settings page wiring
web/oss/src/pages/settings/index.tsx, web/ee/src/pages/settings/index.tsx
Both settings index pages import SettingsRedirect and export it as the default page component.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the new /settings redirect behavior and scoped settings page target.
Description check ✅ Passed The description is detailed and directly matches the implemented /settings redirect change.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/settings-short-link

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

let last = 0
let m: RegExpExecArray | null
TOKEN_RE.lastIndex = 0
while ((m = TOKEN_RE.exec(template)) !== null) {
"content-type": "application/json",
"content-length": Buffer.byteLength(payload),
});
res.end(payload);
if (!isPlainObject(cursor[key])) cursor[key] = {};
cursor = cursor[key] as Record<string, unknown>;
}
cursor[parts[parts.length - 1]] = value;
@mmabrouk
mmabrouk changed the base branch from main to big-agents July 7, 2026 17:46
@dosubot dosubot Bot added size:M This PR changes 30-99 lines, ignoring generated files. and removed size:XL This PR changes 500-999 lines, ignoring generated files. labels Jul 7, 2026
@mmabrouk

mmabrouk commented Jul 7, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 7, 2026 •

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
web/ee/src/pages/settings/index.tsx (1)

1-3: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Use the explicit OSS package path in web/ee/src/pages/settings/index.tsx

@/oss/* in EE resolves through web/ee/src/* first, so this page should import SettingsRedirect from @agenta/oss/src/... to keep the OSS dependency explicit.

🔧 Proposed fix
-import SettingsRedirect from "`@/oss/components/pages/SettingsRedirect`"
+import SettingsRedirect from "`@agenta/oss/src/components/pages/SettingsRedirect`"

Source: Coding guidelines


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 34ec8e4e-eae2-4dde-9d18-9042b719e27c

📥 Commits

Reviewing files that changed from the base of the PR and between 79906d5 and 59ad253.

📒 Files selected for processing (3)
  • web/ee/src/pages/settings/index.tsx
  • web/oss/src/components/pages/SettingsRedirect/index.tsx
  • web/oss/src/pages/settings/index.tsx

Comment on lines +22 to +70
useEffect(() => {
if (!router.isReady) return
if (pendingRef.current) return

pendingRef.current = true

let cancelled = false
const searchIndex = router.asPath.indexOf("?")
const search = searchIndex >= 0 ? router.asPath.slice(searchIndex) : ""

const redirect = async (target: string | null) => {
if (!target || cancelled) return
const normalizedTarget = target.split("?")[0]
const latestPath = router.asPath.split("?")[0]
if (normalizedTarget === latestPath) return
await router.replace(target)
}

const resolveTarget = async (): Promise<string | null> => {
if (directPath) return `${directPath}${search}`

const context = await waitForWorkspaceContext({
requireProjectId: true,
requireOrgData: true,
})
if (cancelled) return null

const resolvedPath = await buildPostLoginPathResolved(context)
if (cancelled) return null

// Fully resolved paths end in /apps; swap the segment for /settings.
if (resolvedPath.endsWith("/apps")) {
return `${resolvedPath.slice(0, -"/apps".length)}/settings${search}`
}

// Partial context (/w or /w/<ws>) — hand off to the workspace selection flow.
return resolvedPath
}

void resolveTarget()
.then(redirect)
.finally(() => {
pendingRef.current = false
})

return () => {
cancelled = true
}
}, [directPath, router, router.isReady])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

pendingRef isn't reset in cleanup — can permanently block the redirect.

pendingRef.current is only reset inside the async chain's .finally() (Line 64), not in the effect's cleanup (Lines 67-69). If directPath transitions from null to a real value (e.g. workspaceId/projectURL finish loading after mount) while the first invocation is still awaiting waitForWorkspaceContext, React runs cleanup for the old effect (setting cancelled = true) and then immediately invokes the new effect body. The new invocation hits if (pendingRef.current) return (Line 24) and exits early, since pendingRef.current is still true at that point. Meanwhile, the old chain's waitForWorkspaceContext eventually resolves, checks cancelled (now true), and returns null — so no redirect ever fires. No further dependency change occurs to retrigger the effect, leaving the spinner shown indefinitely.

This is a real risk on the very case this PR targets: cold navigation to /settings, where workspaceId/projectURL typically populate asynchronously after router.isReady becomes true.

🔧 Proposed fix: reset the guard synchronously in cleanup
         return () => {
             cancelled = true
+            pendingRef.current = false
         }
     }, [directPath, router, router.isReady])
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
if (!router.isReady) return
if (pendingRef.current) return
pendingRef.current = true
let cancelled = false
const searchIndex = router.asPath.indexOf("?")
const search = searchIndex >= 0 ? router.asPath.slice(searchIndex) : ""
const redirect = async (target: string | null) => {
if (!target || cancelled) return
const normalizedTarget = target.split("?")[0]
const latestPath = router.asPath.split("?")[0]
if (normalizedTarget === latestPath) return
await router.replace(target)
}
const resolveTarget = async (): Promise<string | null> => {
if (directPath) return `${directPath}${search}`
const context = await waitForWorkspaceContext({
requireProjectId: true,
requireOrgData: true,
})
if (cancelled) return null
const resolvedPath = await buildPostLoginPathResolved(context)
if (cancelled) return null
// Fully resolved paths end in /apps; swap the segment for /settings.
if (resolvedPath.endsWith("/apps")) {
return `${resolvedPath.slice(0, -"/apps".length)}/settings${search}`
}
// Partial context (/w or /w/<ws>) — hand off to the workspace selection flow.
return resolvedPath
}
void resolveTarget()
.then(redirect)
.finally(() => {
pendingRef.current = false
})
return () => {
cancelled = true
}
}, [directPath, router, router.isReady])
useEffect(() => {
if (!router.isReady) return
if (pendingRef.current) return
pendingRef.current = true
let cancelled = false
const searchIndex = router.asPath.indexOf("?")
const search = searchIndex >= 0 ? router.asPath.slice(searchIndex) : ""
const redirect = async (target: string | null) => {
if (!target || cancelled) return
const normalizedTarget = target.split("?")[0]
const latestPath = router.asPath.split("?")[0]
if (normalizedTarget === latestPath) return
await router.replace(target)
}
const resolveTarget = async (): Promise<string | null> => {
if (directPath) return `${directPath}${search}`
const context = await waitForWorkspaceContext({
requireProjectId: true,
requireOrgData: true,
})
if (cancelled) return null
const resolvedPath = await buildPostLoginPathResolved(context)
if (cancelled) return null
// Fully resolved paths end in /apps; swap the segment for /settings.
if (resolvedPath.endsWith("/apps")) {
return `${resolvedPath.slice(0, -"/apps".length)}/settings${search}`
}
// Partial context (/w or /w/<ws>) — hand off to the workspace selection flow.
return resolvedPath
}
void resolveTarget()
.then(redirect)
.finally(() => {
pendingRef.current = false
})
return () => {
cancelled = true
pendingRef.current = false
}
}, [directPath, router, router.isReady])

@mmabrouk
mmabrouk force-pushed the feat/settings-short-link branch from 59ad253 to 8b69a97 Compare July 7, 2026 17:55
@mmabrouk

mmabrouk commented Jul 7, 2026

Copy link
Copy Markdown
Member Author

Pushed a fix after live QA on the dev box: the first version's redirect never fired (bare routes have no workspace/project in urlAtom, and the pendingRef guard deadlocked under StrictMode double-mount). Rewrote it in the reactive WorkspaceRedirect style; verified both /settings and /settings?tab=apiKeys redirect correctly when logged in. @coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 7, 2026 •

Copy link
Copy Markdown
Contributor

@mmabrouk Thanks for the context — I’ll review the updated changes with the redirect fix in mind.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@mmabrouk mmabrouk left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@mmabrouk
mmabrouk merged commit e8e8237 into big-agents Jul 7, 2026
20 of 24 checks passed

This branch was successfully deployed

1 active deployment
Preview — 8b69a97e Deployed Jul 7, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature request New feature or request frontend size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants