Skip to content

feat(client): honor ?project= deep links so shared eval URLs land on the right project - #3643

Merged
chelojimenez merged 2 commits into
mainfrom
worktree-project-deep-link
Aug 3, 2026
Merged

feat(client): honor ?project= deep links so shared eval URLs land on the right project#3643
chelojimenez merged 2 commits into
mainfrom
worktree-project-deep-link

Conversation

@chelojimenez

@chelojimenez chelojimenez commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Problem

Eval/suite/run URLs (/evals/suite/:suiteId/runs/:runId) carry no project information — the page renders whatever project the viewer's client-side picker happens to be on. Any shared link (Slack bot messages, PR descriptions, pasted URLs) silently shows an empty state for anyone parked on a different project. Found while dogfooding the Slack bot (#3629): clicking a run link landed on an empty evals list because the picker was on another project.

Fix

Client — new client/src/lib/project-deep-link.ts + a useProjectDeepLinkSwitch hook in App.tsx:

  • On load, a ?project=<id> param switches the active project via the same handleSwitchProject path the picker uses (membership-guarded, disconnects servers, persists), then strips the param via replaceState — following the existing ?plan=/?template= deep-link patterns.
  • Cross-org links work: the unfiltered projects:getMyProjects result (same Convex query, deduped) resolves the project's organization; the hook switches org first, then the project on a later render.
  • Unknown/no-access ids strip the param and toast, no switch. Param values must look like Convex ids (shape-checked) before anything reacts to them.
  • The param suppresses the first-run onboarding redirect (same clobber hazard as ?template: onboarding would navigate away and drop the param before it's consumed). Unlike ?template, membership can't be checked synchronously — but the handler always strips the param once data settles, including the no-access case, so suppression is transient by construction.
  • Deliberately not handleSidebarSwitchProject: that variant navigates to a tab target, which would clobber the deep-linked path.

Server — the v1 agent endpoint (server/routes/v1/agent.ts) now appends ?project=<projectId> to the suite URLs it returns in createdResources, so every link the Slack bot posts is self-describing.

The decision logic is a pure function (resolveProjectDeepLinkAction) so the ordering rules (clear / switch / switch-org / wait / not-found) are unit-tested without mounting App.

Testing

  • New client/src/lib/__tests__/project-deep-link.test.ts (11 tests: param parsing/shape rejection + all five action branches).
  • agent.test.ts updated to assert the ?project= suffix on created-resource URLs (22 pass).
  • Full v1 route suite: 267 pass. typecheck:client clean; server tsc has no errors in touched files.

Out of scope

Path-based routing (/projects/:projectId/...) is the structural fix but a full-app migration; this param approach is additive and doesn't block it.

🤖 Generated with Claude Code


Note

Medium Risk
Changes active project and organization on initial load via membership-guarded switching; mistakes could leave users on the wrong project or show incorrect toasts, but it reuses the standard switch path and does not bypass auth.

Overview
Shared eval/suite/run URLs have no project in the path, so recipients often saw an empty list when their picker was on another project. This PR adds ?project=<id> handling so links can name the intended project.

Client: New project-deep-link helpers validate Convex-shaped ids, decide switch vs wait vs not-found (including cross-org org-switch-first), and strip the query param via replaceState. useProjectDeepLinkSwitch in App.tsx runs that logic through existing handleSwitchProject (not sidebar navigation, so the deep-linked path is preserved). A valid ?project= also blocks first-run onboarding from clobbering the URL before the handler runs.

Server: The v1 agent’s suite URLs in createdResources now include ?project=<projectId> so Slack-bot links are self-describing.

Unit tests cover param parsing and all resolveProjectDeepLinkAction branches; agent.test.ts asserts the new URL shape.

Reviewed by Cursor Bugbot for commit 36398e4. Bugbot is set up for automated code reviews on this repo. Configure here.


Summary by cubic

Honor ?project=<id> deep links so shared eval/suite/run URLs open on the correct project. The client switches project/org on load and cleans the URL; the v1 agent appends the param to suite links it returns.

  • New Features

    • Client reads ?project=<id>, switches via handleSwitchProject, then strips the param.
    • Cross‑org links auto‑switch organizations; invalid/no‑access ids strip and toast.
    • Temporarily blocks first‑run onboarding until the param is handled; pure resolveProjectDeepLinkAction with unit tests.
  • Bug Fixes

    • Failed project switches now show a toast and only clear the param after handling.
    • Correct org‑less project behavior: not-found under an active org filter; wait when no org filter is active.

Written for commit 36398e4. Summary will update on new commits.

Review in cubic

…the right project

Eval/suite/run URLs carry no project segment, so a shared link rendered
whatever project the viewer's picker was parked on — an empty state for
everyone but the author. Now:

- client: a ?project=<id> param switches the active project on load
  (switching organization first for cross-org links), then strips the
  param; unknown/no-access ids strip with a toast. The param also
  suppresses the first-run onboarding redirect (same clobber hazard as
  ?template).
- server: the v1 agent endpoint appends ?project= to the suite deep
  links it returns in createdResources.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@dosubot dosubot Bot added size:L This PR changes 100-499 lines, ignoring generated files. enhancement New feature or request labels Aug 3, 2026
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@dosubot

dosubot Bot commented Aug 3, 2026

Copy link
Copy Markdown

📄 Knowledge review

Dosu skipped reviewing this PR because your organization has used its 200 included credits for the month. Your usage will reset on 2026-09-01. To have Dosu review this PR before then, ask your organization admin to upgrade to a pro account.


Leave Feedback Ask Dosu about inspector Add Dosu to your team

@cursor

cursor Bot commented Aug 3, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_a2f12d99-72d9-47a9-acdb-9a298bfaf233)

@chelojimenez

chelojimenez commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Internal preview

Preview URL will appear in Railway after the deploy finishes.
Deployed commit: 057ff42
PR head commit: 36398e4
Backend target: staging fallback.
Access is employee-only in non-production environments.

@coderabbitai

coderabbitai Bot commented Aug 3, 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: 0ab38a4a-b289-4689-b19a-05271055f847

📥 Commits

Reviewing files that changed from the base of the PR and between f6d316f and 36398e4.

📒 Files selected for processing (3)
  • mcpjam-inspector/client/src/App.tsx
  • mcpjam-inspector/client/src/lib/__tests__/project-deep-link.test.ts
  • mcpjam-inspector/client/src/lib/project-deep-link.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • mcpjam-inspector/client/src/App.tsx
  • mcpjam-inspector/client/src/lib/tests/project-deep-link.test.ts

Walkthrough

The client now supports ?project=<id> deep links. It validates and clears the parameter, switches projects or organizations when required, waits for transient data states, and reports inaccessible projects. Pending project links suppress first-run onboarding redirects. Server-generated suite URLs now include the route-scoped project ID, with updated coverage for the query parameter.

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

mcpjam-inspector/client/src/App.tsx

Oops! Something went wrong! :(

ESLint: 8.57.1

Error: ESLint configuration in --config is invalid:

  • Unexpected top-level property "__esModule".

    at ConfigValidator.validateConfigSchema (/soundcheck/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2177:19)
    at ConfigArrayFactory._normalizeConfigData (/soundcheck/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3019:19)
    at ConfigArrayFactory._loadConfigData (/soundcheck/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2984:21)
    at ConfigArrayFactory.loadFile (/soundcheck/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2850:40)
    at createCLIConfigArray (/soundcheck/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3660:35)
    at new CascadingConfigArrayFactory (/soundcheck/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3735:29)
    at new CLIEngine (/soundcheck/node_modules/eslint/lib/cli-engine/cli-engine.js:617:36)
    at new ESLint (/soundcheck/node_modules/eslint/lib/eslint/eslint.js:430:27)
    at Object.execute (/soundcheck/node_modules/eslint/lib/cli.js:410:24)
    at async main (/soundcheck/node_modules/eslint/bin/eslint.js:152:22)

mcpjam-inspector/client/src/lib/__tests__/project-deep-link.test.ts

Oops! Something went wrong! :(

ESLint: 8.57.1

Error: ESLint configuration in --config is invalid:

  • Unexpected top-level property "__esModule".

    at ConfigValidator.validateConfigSchema (/soundcheck/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2177:19)
    at ConfigArrayFactory._normalizeConfigData (/soundcheck/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3019:19)
    at ConfigArrayFactory._loadConfigData (/soundcheck/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2984:21)
    at ConfigArrayFactory.loadFile (/soundcheck/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2850:40)
    at createCLIConfigArray (/soundcheck/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3660:35)
    at new CascadingConfigArrayFactory (/soundcheck/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3735:29)
    at new CLIEngine (/soundcheck/node_modules/eslint/lib/cli-engine/cli-engine.js:617:36)
    at new ESLint (/soundcheck/node_modules/eslint/lib/eslint/eslint.js:430:27)
    at Object.execute (/soundcheck/node_modules/eslint/lib/cli.js:410:24)
    at async main (/soundcheck/node_modules/eslint/bin/eslint.js:152:22)

mcpjam-inspector/client/src/lib/project-deep-link.ts

Oops! Something went wrong! :(

ESLint: 8.57.1

Error: ESLint configuration in --config is invalid:

  • Unexpected top-level property "__esModule".

    at ConfigValidator.validateConfigSchema (/soundcheck/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2177:19)
    at ConfigArrayFactory._normalizeConfigData (/soundcheck/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3019:19)
    at ConfigArrayFactory._loadConfigData (/soundcheck/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2984:21)
    at ConfigArrayFactory.loadFile (/soundcheck/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2850:40)
    at createCLIConfigArray (/soundcheck/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3660:35)
    at new CascadingConfigArrayFactory (/soundcheck/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3735:29)
    at new CLIEngine (/soundcheck/node_modules/eslint/lib/cli-engine/cli-engine.js:617:36)
    at new ESLint (/soundcheck/node_modules/eslint/lib/eslint/eslint.js:430:27)
    at Object.execute (/soundcheck/node_modules/eslint/lib/cli.js:410:24)
    at async main (/soundcheck/node_modules/eslint/bin/eslint.js:152:22)


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.

@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: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@mcpjam-inspector/client/src/App.tsx`:
- Around line 892-897: Add a rejection handler to the promise chain in the
“switch-project” branch around handleSwitchProject so failures are surfaced
before clearProjectDeepLinkFromUrl still runs in finally. Reuse the surrounding
app’s established error-reporting or user-notification mechanism, while
preserving the existing handledRef update and successful project-switch
behavior.

In `@mcpjam-inspector/client/src/lib/project-deep-link.ts`:
- Around line 56-88: Update resolveProjectDeepLinkAction so a matching project
without organizationId returns { kind: "not-found" } unless it was already
handled by the active project checks. Preserve switch-organization for matches
with a different organizationId and wait only for projects belonging to the
active organization whose filtered visibility has not caught up.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a7e81bef-fe29-46d7-b7c6-85ffa9d71dd0

📥 Commits

Reviewing files that changed from the base of the PR and between 86cb60f and f6d316f.

📒 Files selected for processing (5)
  • mcpjam-inspector/client/src/App.tsx
  • mcpjam-inspector/client/src/lib/__tests__/project-deep-link.test.ts
  • mcpjam-inspector/client/src/lib/project-deep-link.ts
  • mcpjam-inspector/server/routes/v1/__tests__/agent.test.ts
  • mcpjam-inspector/server/routes/v1/agent.ts

Comment thread mcpjam-inspector/client/src/App.tsx
Comment thread mcpjam-inspector/client/src/lib/project-deep-link.ts

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No issues found across 5 files

Re-trigger cubic

…s projects

- switch-project now .catch()es before .finally(): a rejected
  handleSwitchProject was an unhandled rejection with the user silently
  left on the wrong project after the param was stripped.
- an org-less matched project returns not-found under an active org
  filter (it can never enter the filtered set, so 'wait' hung forever);
  with no org filter it still waits for the unfiltered set.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@chelojimenez

Copy link
Copy Markdown
Contributor Author

Review triage — both CodeRabbit findings verified as real and fixed in 36398e4:

  1. Unhandled rejection on failed switch (Major).finally() re-throws rejections, so a failed handleSwitchProject became an unhandled promise rejection with the user silently left on the wrong project after the param was stripped. Now .catch()es first (toast pointing at the project picker), then strips the param.
  2. Org-less project hung the resolver (Minor) — a matched project without organizationId can never enter an org-filtered project set, so the resolver returned wait forever and the param never stripped. Now: not-found under an active org filter, wait only when no filter applies (the unfiltered set includes it next render). Two new tests cover both branches.

13/13 lib tests, client typecheck clean. Cubic reported no issues.

@cursor

cursor Bot commented Aug 3, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_204ec1b2-3258-4cb3-8e24-34d34b5ae4ff)

@chelojimenez
chelojimenez merged commit daa524f into main Aug 3, 2026
12 of 13 checks passed
@chelojimenez
chelojimenez deleted the worktree-project-deep-link branch August 3, 2026 01:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant