Skip to content

Fix empty breadcrumb on /[tenantId]/profile page and replace prop-drilled permission flags (readOnly, canEdit, canUse) with direct hook call useProjectPermissionsQuery()#2792

Merged
dimaMachina merged 18 commits intomainfrom
prd-6346
Mar 20, 2026

Conversation

@dimaMachina
Copy link
Collaborator

No description provided.

@changeset-bot
Copy link

changeset-bot bot commented Mar 20, 2026

🦋 Changeset detected

Latest commit: 14bc1ac

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 10 packages
Name Type
@inkeep/agents-manage-ui Patch
@inkeep/agents-api Patch
@inkeep/agents-cli Patch
@inkeep/agents-core Patch
@inkeep/agents-email Patch
@inkeep/agents-mcp Patch
@inkeep/agents-sdk Patch
@inkeep/agents-work-apps Patch
@inkeep/ai-sdk-provider Patch
@inkeep/create-agents Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel
Copy link

vercel bot commented Mar 20, 2026

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

Project Deployment Actions Updated (UTC)
agents-api Ready Ready Preview, Comment Mar 20, 2026 10:18pm
agents-manage-ui Ready Ready Preview, Comment Mar 20, 2026 10:18pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
agents-docs Skipped Skipped Mar 20, 2026 10:18pm

Request Review

@pullfrog
Copy link
Contributor

pullfrog bot commented Mar 20, 2026

TL;DR — Replaces prop-drilled permission flags (readOnly, canEdit, canUse, canManage) with direct useProjectPermissionsQuery() hook calls in client components, eliminating server→client permission threading across ~15 files. Also fixes the empty breadcrumb on the /[tenantId]/profile page by extracting the header into a layout.

Key changes

  • Wrap fetchProjectPermissions with cache() — extracts the fetch logic into a private $fetchProjectPermissions function and re-exports it through cache(), making the caching boundary explicit
  • Replace prop-drilled permissions with useProjectPermissionsQuery — client components (AgentItem, ApiKeysTable, AppsTable, SkillForm, ArtifactComponentForm, DataComponentForm, ResourceMembersPage) call the hook directly instead of receiving permission booleans as props
  • Collapse server page wrappers into re-exports — pages like members, skills edit, and modal skills edit that existed solely to fetch permissions and forward them are replaced with single-line export { Component as default } re-exports
  • Inline destructuring in server pages — remaining server pages that still call fetchProjectPermissions destructure { canEdit } or { canUse } directly from the Promise.all result instead of through an intermediate variable
  • Extract profile PageHeader into layout — moves the PageHeader from the profile page component into a new profile/layout.tsx, fixing breadcrumb rendering and adding 'profile' to STATIC_LABELS

Summary | 26 files | 18 commits | base: mainprd-6346


fetchProjectPermissions wrapped with cache()

Before: fetchProjectPermissions was an anonymous async arrow function passed directly to cache().
After: The fetch logic lives in a named $fetchProjectPermissions function, re-exported as cache($fetchProjectPermissions).

Extracting the inner function makes the caching boundary explicit and keeps the implementation separate from the memoization concern. React's request-scoped cache() ensures multiple server components calling fetchProjectPermissions within a single render pass share a single network request.

projects.ts


Permissions consumed via useProjectPermissionsQuery instead of props

Before: Server pages fetched fetchProjectPermissions, destructured canEdit/canUse/canManage, and prop-drilled the values into client components.
After: Each client component calls useProjectPermissionsQuery() directly and destructures the permission it needs. The corresponding props are removed from every interface.

This eliminates a class of prop-threading boilerplate across the manage UI. Any component rendered under a project route can access permissions without the parent page having to fetch and forward them.

Which props were removed?
Component Removed prop
AgentItem canEdit
ApiKeysTable canUse
AppsTable canUse
ArtifactComponentForm readOnly
DataComponentForm readOnly
SkillForm readOnly
SkillEditModal readOnly
ResourceMembersPage canManage
ProjectMembersWrapper canManage, projectId, tenantId

api-keys-table.tsx · apps-table.tsx · skill-form.tsx · resource-members-page.tsx


Server pages simplified to re-exports

Before: Several page files (members, skills edit, modal skills edit) were async server components that existed solely to fetch permissions and pass them as props to a single child component.
After: These pages are replaced with one-line re-exports like export { Component as default }, and ProjectMembersWrapper now accepts PageProps directly (using React's use() to unwrap params).

members/page.tsx · skills/edit/page.tsx · project-members-wrapper.tsx


Profile page header extracted into layout

Before: The profile page was a 'use client' component that rendered its own PageHeader inline, duplicating it in both the loading skeleton and the loaded state — and producing an empty breadcrumb.
After: A new profile/layout.tsx server component renders the PageHeader once, and the client page component only renders the form content beneath it.

This fixes breadcrumb rendering on the profile page by placing the header in the layout — where Next.js can render it as a server component — and adds 'profile' to STATIC_LABELS for consistent label reuse.

profile/layout.tsx · profile/page.tsx · theme.ts

Pullfrog  | View workflow run | Triggered by Pullfrogpullfrog.com𝕏

Copy link
Contributor

@pullfrog pullfrog bot left a comment

Choose a reason for hiding this comment

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

Clean refactor that centralizes permissions into the project layout + React Query hydration boundary so child pages/components consume useProjectPermissionsQuery() instead of threading props. One real issue with the double cache() wrapping in projects.ts.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow runpullfrog.com𝕏

Comment on lines +110 to +122
@@ -118,3 +118,5 @@ export const fetchProjectPermissions = cache(
return response.data;
}
);

export const fetchProjectPermissions = cache($fetchProjectPermissions);
Copy link
Contributor

Choose a reason for hiding this comment

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

fetchProjectPermissions is now double-wrapped in cache(). The inner $fetchProjectPermissions is already cache(async (...) => { ... }), and then the outer export wraps it again: cache($fetchProjectPermissions). This is a no-op at best (React dedupes on the same function reference) but misleading — every other function in this file uses the $fn / cache($fn) pattern with a plain async inner function. Should be:

const $fetchProjectPermissions = async (tenantId: string, projectId: string): Promise<ProjectPermissions> => {
  // ...
};

export const fetchProjectPermissions = cache($fetchProjectPermissions);

This matches the $fetchProjects / $fetchProject pattern above.

const { user } = useAuthSession();
const {
data: { canEdit },
} = useProjectPermissionsQuery();
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: ResourceMembersPage is documented as a generic component reusable for agents, MCP servers, etc. Hardcoding useProjectPermissionsQuery() here couples it to project-level permissions. If this is intentional (i.e., no other resource types are planned), the JSDoc on lines 67-69 should be updated. Otherwise, canManage/canEdit should stay as a prop for reusability.

@vercel vercel bot temporarily deployed to Preview – agents-docs March 20, 2026 21:48 Inactive
@github-actions github-actions bot deleted a comment from claude bot Mar 20, 2026
@dimaMachina
Copy link
Collaborator Author

@claude --review and rewrite my changeset based on changes in this PR

@github-actions github-actions bot deleted a comment from claude bot Mar 20, 2026
Co-authored-by: Dimitri POSTOLOV <dmytropostolov@gmail.com>
Copy link
Contributor

@claude claude bot left a comment

Choose a reason for hiding this comment

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

PR Review Summary

(2) Total Issues | Risk: Low

🟡 Minor (2) 🟡

Inline Comments:

  • 🟡 Minor: layout.tsx:10 Layout function named Page instead of Layout
  • 🟡 Minor: projects.ts:110-113 Arrow function style inconsistent with codebase pattern

💭 Consider (2) 💭

💭 1) project-members-wrapper.tsx:40-43 Component accepts PageProps instead of explicit props

Issue: ProjectMembersWrapper now receives PageProps<...> and uses use(params) to unwrap route params. Components in /components/ typically receive explicit props, while only page files in /app/ receive PageProps.

Why: This couples the component to a specific route structure and Next.js page patterns. The component's JSDoc mentions future reuse for AgentMembersWrapper and McpServerMembersWrapper — with PageProps typing, reuse requires matching route structures. However, this is a valid architectural choice that enables the clean single-line re-export pattern in page.tsx.

Fix: If maintaining strict separation of concerns matters for future reuse, keep explicit tenantId: string and projectId: string props. If the single-line re-export pattern is preferred, document this as the new standard for similar wrapper components.

Refs:

💭 2) resource-members-page.tsx:85-87 Generic component hardcodes project permissions

Issue: ResourceMembersPage is documented as generic ("can be used for any resource type") but calls useProjectPermissionsQuery() directly. This couples a supposedly generic component to project-specific logic.

Why: This may limit future reusability when implementing AgentMembersWrapper or McpServerMembersWrapper (as noted in JSDoc comments), which might need different permission models. However, the current approach simplifies the immediate refactoring and may be fine if all future member pages share project-level permissions.

Fix: Either rename to ProjectResourceMembersPage to reflect actual scope, or restore the canManage prop if true genericness is desired.

Refs:


✅ APPROVE

Summary: Clean refactoring that centralizes permission consumption via useProjectPermissionsQuery() with proper server-side hydration in the project layout. The cache() wrapping and React Query deduplication work correctly together. Two minor naming/style inconsistencies are easily fixable. The architectural "Consider" items are valid observations but represent reasonable tradeoffs — the pattern simplifies pages at the cost of some component-level abstraction purity. The prior feedback about double cache() wrapping has been addressed in commit 9517ddcb3. 🎉

Comment on lines +40 to +43
export const ProjectMembersWrapper: FC<PageProps<'/[tenantId]/projects/[projectId]/members'>> = ({
params,
}) => {
const { tenantId, projectId } = use(params);
Copy link
Contributor

Choose a reason for hiding this comment

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

💭 Consider: Component receiving PageProps directly blurs page/component boundary

Issue: ProjectMembersWrapper now receives PageProps<'...'> and uses use(params) to extract route params. Components in /components/ typically receive explicit props, while only page files in /app/ receive PageProps.

Why: This couples the component to a specific route structure. The component's own comments mention future reuse for "AgentMembersWrapper" and "McpServerMembersWrapper" — with PageProps typing, reuse would require matching route structures.

Fix: Consider keeping explicit props for reusability:

interface ProjectMembersWrapperProps {
  tenantId: string;
  projectId: string;
}

export const ProjectMembersWrapper: FC<ProjectMembersWrapperProps> = ({
  tenantId,
  projectId,
}) => {
  // ...
}

Then the page.tsx can:

export default async function Page({ params }: PageProps<...>) {
  const { tenantId, projectId } = await params;
  return <ProjectMembersWrapper tenantId={tenantId} projectId={projectId} />;
}

@github-actions github-actions bot deleted a comment from claude bot Mar 20, 2026
@dimaMachina dimaMachina changed the title cache fetchProjectPermissions result Fix empty breadcrumb on /[tenantId]/profile page and replace prop-drilled permission flags (readOnly, canEdit, canUse) with direct hook call useProjectPermissionsQuery() Mar 20, 2026
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
@vercel vercel bot temporarily deployed to Preview – agents-docs March 20, 2026 22:12 Inactive
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
@vercel vercel bot temporarily deployed to Preview – agents-docs March 20, 2026 22:12 Inactive
Copy link
Contributor

@claude claude bot left a comment

Choose a reason for hiding this comment

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

PR Review Summary

(0) Total Issues | Risk: Low

This is a re-review scoped to the 7 commits since the prior review (commit 01e2abfae49c).

Prior Feedback Resolution ✅

All issues from the previous review have been addressed:

Prior Issue Status
🟠 Double cache() wrapping in projects.ts ✅ Fixed — now uses correct $fn/cache($fn) pattern
🟡 Layout function named Page instead of Layout ✅ Fixed — renamed to Layout
🟡 Arrow function style inconsistent ✅ Fixed — uses async function $fetchProjectPermissions
💭 AgentItem N hook subscriptions concern ✅ Addressed — reverted to prop-based pattern

Delta Changes Summary

The 7 commits since last review made targeted fixes:

  1. projects.ts — Refactored fetchProjectPermissions to use the standard $fn/cache($fn) pattern consistent with $fetchProjects, $fetchProject, and all other cached functions in the API layer.

  2. layout.tsx — Renamed the default export from Page to Layout for consistency with other layout files (login/layout.tsx, no-organization/layout.tsx).

  3. agent-item.tsx + agents/page.tsx — Reverted to prop-based permission passing for list items. The parent page now fetches canEdit once and passes it to each AgentItem, avoiding N hook subscriptions in list rendering.

  4. Changeset — Updated with accurate description of the PR's changes.

Code Quality

The refactoring is clean and consistent:

  • Server-side cache() wrapping deduplicates permission fetches within a single request
  • Client components consume useProjectPermissionsQuery() for permission-aware UI
  • List components (AgentItem) receive permissions via props to avoid subscription overhead
  • Profile page breadcrumb fix properly extracts PageHeader to layout

✅ APPROVE

Summary: All prior review feedback has been addressed. The permission centralization pattern is now consistent across the codebase — server pages fetch once via fetchProjectPermissions (with React cache() deduplication), and client components either receive permissions via props (for list items) or consume useProjectPermissionsQuery() (for standalone forms). Clean refactor, ready to ship! 🚀

@github-actions github-actions bot deleted a comment from claude bot Mar 20, 2026
@dimaMachina dimaMachina added this pull request to the merge queue Mar 20, 2026
Merged via the queue into main with commit 028a801 Mar 20, 2026
35 of 36 checks passed
@dimaMachina dimaMachina deleted the prd-6346 branch March 20, 2026 22:33
dimaMachina added a commit that referenced this pull request Mar 20, 2026
…rilled permission flags (`readOnly`, `canEdit`, `canUse`) with direct hook call `useProjectPermissionsQuery()` (#2792)

* upd

* upd

* format

* format

* format

* format

* format

* format

* format

* format

* format

* fix review

* fix breadcrumb on profile page

* Apply suggestions from code review

Co-authored-by: Dimitri POSTOLOV <dmytropostolov@gmail.com>

* Update agents-manage-ui/src/lib/api/projects.ts

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>

* Update agents-manage-ui/src/app/[tenantId]/profile/layout.tsx

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>

* style: auto-format with biome

* fix review

---------

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
@itoqa
Copy link

itoqa bot commented Mar 20, 2026

Ito Test Report ❌

14 test cases ran. 1 failed, 13 passed.

Across nine report shards, 14 includable test cases were executed with 13 passing and 1 failing, indicating the profile, project/apps/skills/components routes, permission transitions, deep-linking, modal/back-forward flows, listing pages, and mobile members/profile responsiveness were all stable and behaved as expected without crashes or 404s. The single critical finding was a high-severity, pre-existing bug in the skill creation flow at /default/projects/activities-planner/skills/new where rapid repeated Save clicks can fire duplicate create mutations before submit locking engages, while the security check confirmed injection payloads remained inert with no script execution.

❌ Failed (1)
Category Summary Screenshot
Adversarial ⚠️ Rapid Save clicks can trigger multiple create requests before submit lock engages. ADV-1
⚠️ Rapid Save can trigger duplicate skill create requests
  • What failed: Multiple create mutations can be fired from one rapid-click burst, causing duplicate POST attempts and backend error behavior instead of a single-flight submit.
  • Impact: Rapid user interaction can produce duplicate write attempts and inconsistent save outcomes. This risks data integrity issues and user-visible failure states during skill creation.
  • Introduced by this PR: No – pre-existing bug (code not changed in this PR)
  • Steps to reproduce:
    1. Open /default/projects/activities-planner/skills/new.
    2. Fill all required fields with valid values.
    3. Click Save rapidly multiple times in quick succession.
    4. Observe multiple create requests instead of a single-flight submit.
  • Code analysis: The submit button disable state is derived from React form state (isSubmitting) and applied after render, but there is no synchronous in-handler guard to stop re-entrant submits; the mutation path also has no dedupe/single-flight protection before issuing create requests.
  • Why this is likely a bug: The production submit path lacks a re-entrancy/single-flight guard, so rapid clicks can legitimately invoke concurrent create calls before UI disable state fully applies.

Relevant code:

agents-manage-ui/src/components/skills/form/skill-form.tsx (lines 66-74)

const { isSubmitting, isValid } = form.formState;
const isDisabled = isSubmitting || !isValid;

const onSubmit = form.handleSubmit(async (data) => {
  await upsertSkill({
    skillId: initialData ? data.name : undefined,
    data,
  });

agents-manage-ui/src/components/skills/form/skill-form.tsx (lines 180-184)

{!readOnly && (
  <div className="flex w-full justify-between">
    <Button type="submit" disabled={isDisabled}>
      Save
    </Button>

agents-manage-ui/src/lib/query/skills.ts (lines 86-91)

return useMutation<Skill, Error, UpsertSkillInput>({
  async mutationFn({ skillId, data }) {
    const result = skillId
      ? await updateSkill(tenantId, projectId, skillId, data)
      : await createSkill(tenantId, projectId, data);
✅ Passed (13)
Category Summary Screenshot
Adversarial Injection payloads stayed inert after reload; no script execution observed. ADV-3
Edge Fast 3G reload on Apps converged to correct permission-gated action visibility within 5s without stale hidden state. EDGE-1
Edge Rapid cross-project URL/back-forward switching preserved active-project permission state with no leakage from the prior project context. EDGE-2
Edge Fresh deep-link entry and hard refresh for /members and /skills/edge-skill/edit remained stable with no blank-shell crash. EDGE-3
Edge Modal back-forward-refresh sequence ended in a consistent skills-list route/UI state with no persistent edit-form residue. EDGE-4
Edge Re-test validated mobile profile and members responsiveness; earlier blocking behavior was environment/tooling-related rather than a product defect. EDGE-5
Happy-path On /default/profile the breadcrumb rendered as Profile and the page showed a single Profile heading with the expected description text. ROUTE-1
Happy-path Seed project was created locally, then required listing pages loaded with expected create/new actions and no crash/404. ROUTE-10
Happy-path On /default/profile at desktop 1280x800, hard reload under Slow 3G showed exactly one visible Profile heading during loading with skeleton placeholders present; after data resolved, skeletons disappeared and page remained stable with single header. ROUTE-2
Happy-path Skill edit route showed persisted values with editable controls and enabled Save for admin. ROUTE-4
Happy-path Intercepted skill modal opened as "Edit skill" and closed back to skills list without URL corruption. ROUTE-5
Happy-path Apps page rendered with visible New App CTA and no contradictory row-action state in empty table. ROUTE-6
Happy-path Verified data component edit view in admin context shows Save and Delete Component controls enabled, matching editable project permissions. ROUTE-9

Commit: 14bc1ac

View Full Run


Tell us how we did: Give Ito Feedback

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant