Conversation
Reviewer's GuideSyncs the Paperclip sidecar UI with upstream while tightening hostedMode protections so adapter/agent infrastructure details and configuration UIs are hidden or redirected in hosted deployments. Sequence diagram for hostedMode redirect on agent detail pagesequenceDiagram
actor User
participant Router
participant AgentDetail
participant HostedModeHook as useHostedMode
participant HomePage
User->>Router: Navigate to /agents/:agentId
Router->>AgentDetail: Render AgentDetail
AgentDetail->>HostedModeHook: get isHosted
HostedModeHook-->>AgentDetail: isHosted = true
AgentDetail->>Router: Return Navigate to /
Router->>HomePage: Render HomePage
HomePage-->>User: Show home UI (no agent config)
Class diagram for components gated by hostedModeclassDiagram
class useHostedMode {
+boolean isHosted
}
class JoinRequestAdapterSpan {
+JoinRequestAdapterSpan(adapterType string)
}
class JoinRequestInboxRow {
+joinRequest
+onApprove()
+onReject()
}
class HireAgentPayload {
+HireAgentPayload(payload Record_string_unknown)
}
class AgentConfigForm {
+AgentConfigForm(props AgentConfigFormProps)
}
class AgentDetail {
+AgentDetail()
}
class InviteLandingPage {
+InviteLandingPage()
}
class OrgChart {
+OrgChart()
}
class RoutineDetail {
+RoutineDetail()
}
JoinRequestAdapterSpan ..> useHostedMode : uses
JoinRequestInboxRow ..> JoinRequestAdapterSpan : renders
HireAgentPayload ..> useHostedMode : uses
AgentConfigForm ..> useHostedMode : uses
AgentDetail ..> useHostedMode : uses
InviteLandingPage ..> useHostedMode : uses
OrgChart ..> useHostedMode : uses
RoutineDetail ..> useHostedMode : uses
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
📝 WalkthroughWalkthroughThe changes introduce hosted mode detection across multiple UI components in Paperclip. When hosted mode is enabled via Changes
Estimated code review effort🎯 2 (Simple) | ⏱️ ~12 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Hey - I've left some high level feedback:
- The repeated
useHostedModechecks and early returns/redirects across multiple pages suggest an opportunity to introduce a shared route/component-level guard (e.g., aHostedInfraGuardwrapper) to centralize this logic and keep views more focused on their domain concerns. - The new
JoinRequestAdapterSpancomponent does only a hosted-mode check plus a simple span; consider either inlining theuseHostedModelogic where it’s used or generalizing this into a reusableHostedHidden/HostedVisibilityhelper to avoid very fine-grained single-use components.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The repeated `useHostedMode` checks and early returns/redirects across multiple pages suggest an opportunity to introduce a shared route/component-level guard (e.g., a `HostedInfraGuard` wrapper) to centralize this logic and keep views more focused on their domain concerns.
- The new `JoinRequestAdapterSpan` component does only a hosted-mode check plus a simple span; consider either inlining the `useHostedMode` logic where it’s used or generalizing this into a reusable `HostedHidden`/`HostedVisibility` helper to avoid very fine-grained single-use components.Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
Greptile SummaryThis PR adds
Confidence Score: 2/5Not safe to merge — P0 Rules of Hooks violation will crash the app on cold loads in hosted mode Four of the seven changed components use an early-return guard after calling useHostedMode() but before their remaining hooks. Since useHostedMode resolves asynchronously, isHosted starts false and transitions to true on re-render, causing React to detect a hooks-count mismatch and throw. This is a confirmed runtime crash path in hosted deployments. AgentConfigForm.tsx, AgentDetail.tsx, RoutineDetail.tsx, InviteLandingPage.tsx — all need the wrapper/inner-component pattern before this is safe to merge
|
| Filename | Overview |
|---|---|
| sidecars/paperclip/ui/src/components/AgentConfigForm.tsx | Adds hostedMode guard but violates React Rules of Hooks — early return fires after first render causes hooks-count crash in hosted mode |
| sidecars/paperclip/ui/src/pages/AgentDetail.tsx | Same Rules of Hooks violation — early return before 15+ hook calls that only run on first render |
| sidecars/paperclip/ui/src/pages/RoutineDetail.tsx | Same Rules of Hooks violation — early return before many hook calls; crashes on health query resolution in hosted mode |
| sidecars/paperclip/ui/src/pages/InviteLandingPage.tsx | Same Rules of Hooks violation — early Navigate return before useState/useQuery/useMutation calls |
| sidecars/paperclip/ui/src/pages/OrgChart.tsx | hostedMode guard applied correctly — isHosted used inline in JSX, all hooks called unconditionally |
| sidecars/paperclip/ui/src/components/ApprovalPayload.tsx | hostedMode guard applied correctly — isHosted used inline in JSX conditional, no early return before other hooks |
| sidecars/paperclip/ui/src/pages/Inbox.tsx | hostedMode guard applied correctly via JoinRequestAdapterSpan helper — only useHostedMode called before the conditional return |
Sequence Diagram
sequenceDiagram
participant Browser
participant Component as AgentDetail / AgentConfigForm / RoutineDetail / InviteLanding
participant useHostedMode
participant ReactQuery as TanStack Query (health cache)
Browser->>Component: Mount (cold load)
Component->>useHostedMode: call hook
useHostedMode->>ReactQuery: useQuery(queryKeys.health) — cache MISS
ReactQuery-->>useHostedMode: { data: undefined, isSuccess: false }
useHostedMode-->>Component: { isHosted: false, modeKnown: false }
Note over Component: isHosted=false → NO early return<br/>Calls 10+ more hooks (useState, useQuery, …)
Component-->>Browser: Renders full page
ReactQuery->>ReactQuery: Health API resolves → hosted_proxy
ReactQuery->>Component: Re-render triggered
Component->>useHostedMode: call hook (hook #1)
useHostedMode-->>Component: { isHosted: true }
Note over Component: isHosted=true → early return fires<br/>Hooks #2–N never called ❌
Component-->>Browser: React throws Rendered fewer hooks than expected
Prompt To Fix All With AI
This is a comment left during a code review.
Path: sidecars/paperclip/ui/src/components/AgentConfigForm.tsx
Line: 165-167
Comment:
**React Rules of Hooks violation — early return skips subsequent hook calls**
`useHostedMode()` resolves via a `useQuery` call, so on the first render `isHosted` is `false` (cache miss → `health` is `undefined`). After the health query settles in hosted mode, `isHosted` flips to `true` and the early `return null` fires — but by then React has already recorded a larger hooks list from the first render. React throws "Rendered fewer hooks than expected" and crashes the component tree.
The same pattern is present in `AgentDetail.tsx`, `RoutineDetail.tsx`, and `InviteLandingPage.tsx`.
The idiomatic fix is a thin wrapper that calls only `useHostedMode`, renders `null`/redirects, or delegates to an inner component that owns all remaining hooks:
```tsx
export function AgentConfigForm(props: AgentConfigFormProps) {
const { isHosted } = useHostedMode();
if (isHosted) return null;
return <AgentConfigFormInner {...props} />;
}
function AgentConfigFormInner(props: AgentConfigFormProps) {
const { mode, adapterModels: externalModels } = props;
const { selectedCompanyId } = useCompany();
// ... all remaining hooks and JSX
}
```
Apply the same split to `AgentDetail`, `RoutineDetail`, and `InviteLandingPage`.
How can I resolve this? If you propose a fix, please make it concise.Reviews (1): Last reviewed commit: "fix: add hostedMode guards for new upstr..." | Re-trigger Greptile
| const { isHosted } = useHostedMode(); | ||
| // Hide agent configuration form in hosted mode — infrastructure is managed server-side | ||
| if (isHosted) return null; |
There was a problem hiding this comment.
React Rules of Hooks violation — early return skips subsequent hook calls
useHostedMode() resolves via a useQuery call, so on the first render isHosted is false (cache miss → health is undefined). After the health query settles in hosted mode, isHosted flips to true and the early return null fires — but by then React has already recorded a larger hooks list from the first render. React throws "Rendered fewer hooks than expected" and crashes the component tree.
The same pattern is present in AgentDetail.tsx, RoutineDetail.tsx, and InviteLandingPage.tsx.
The idiomatic fix is a thin wrapper that calls only useHostedMode, renders null/redirects, or delegates to an inner component that owns all remaining hooks:
export function AgentConfigForm(props: AgentConfigFormProps) {
const { isHosted } = useHostedMode();
if (isHosted) return null;
return <AgentConfigFormInner {...props} />;
}
function AgentConfigFormInner(props: AgentConfigFormProps) {
const { mode, adapterModels: externalModels } = props;
const { selectedCompanyId } = useCompany();
// ... all remaining hooks and JSX
}Apply the same split to AgentDetail, RoutineDetail, and InviteLandingPage.
Prompt To Fix With AI
This is a comment left during a code review.
Path: sidecars/paperclip/ui/src/components/AgentConfigForm.tsx
Line: 165-167
Comment:
**React Rules of Hooks violation — early return skips subsequent hook calls**
`useHostedMode()` resolves via a `useQuery` call, so on the first render `isHosted` is `false` (cache miss → `health` is `undefined`). After the health query settles in hosted mode, `isHosted` flips to `true` and the early `return null` fires — but by then React has already recorded a larger hooks list from the first render. React throws "Rendered fewer hooks than expected" and crashes the component tree.
The same pattern is present in `AgentDetail.tsx`, `RoutineDetail.tsx`, and `InviteLandingPage.tsx`.
The idiomatic fix is a thin wrapper that calls only `useHostedMode`, renders `null`/redirects, or delegates to an inner component that owns all remaining hooks:
```tsx
export function AgentConfigForm(props: AgentConfigFormProps) {
const { isHosted } = useHostedMode();
if (isHosted) return null;
return <AgentConfigFormInner {...props} />;
}
function AgentConfigFormInner(props: AgentConfigFormProps) {
const { mode, adapterModels: externalModels } = props;
const { selectedCompanyId } = useCompany();
// ... all remaining hooks and JSX
}
```
Apply the same split to `AgentDetail`, `RoutineDetail`, and `InviteLandingPage`.
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Pull request overview
Automated upstream sync for the Paperclip sidecar UI, with additional hosted-mode gating to prevent infrastructure/configuration surfaces (and adapter details) from appearing when running in hosted_proxy deployments.
Changes:
- Added hosted-mode redirects for infrastructure/configuration pages (routine detail, agent detail, invite landing).
- Hid adapter type labels/details in OrgChart, Inbox join requests, and approval payload UI when hosted.
- Suppressed the agent configuration form entirely in hosted mode.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| sidecars/paperclip/ui/src/pages/RoutineDetail.tsx | Redirects away from routine configuration in hosted mode. |
| sidecars/paperclip/ui/src/pages/OrgChart.tsx | Hides agent adapter type label when hosted. |
| sidecars/paperclip/ui/src/pages/InviteLanding.tsx | Redirects away from invite/user bootstrapping in hosted mode. |
| sidecars/paperclip/ui/src/pages/Inbox.tsx | Hides join-request adapter type display when hosted. |
| sidecars/paperclip/ui/src/pages/AgentDetail.tsx | Redirects away from agent configuration in hosted mode. |
| sidecars/paperclip/ui/src/components/ApprovalPayload.tsx | Hides adapter type from approval payloads when hosted. |
| sidecars/paperclip/ui/src/components/AgentConfigForm.tsx | Suppresses agent config form rendering when hosted. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const { isHosted } = useHostedMode(); | ||
| // Redirect to home in hosted mode — routine configuration is infrastructure |
There was a problem hiding this comment.
This hosted-mode guard only checks isHosted. Since isHosted is false until the health query resolves (health is initially undefined), hosted users can briefly render routine infrastructure UI before the redirect occurs. Consider also using modeKnown to fail closed (render nothing/loading until known, then redirect when hosted).
| const { isHosted } = useHostedMode(); | |
| // Redirect to home in hosted mode — routine configuration is infrastructure | |
| const { isHosted, modeKnown } = useHostedMode(); | |
| // Fail closed until hosted mode is known, then redirect in hosted mode — | |
| // routine configuration is infrastructure | |
| if (!modeKnown) return <PageSkeleton />; |
| const { isHosted } = useHostedMode(); | ||
| // Redirect to home in hosted mode — user management and agent bootstrapping is server-side | ||
| if (isHosted) return <Navigate to="/" replace />; |
There was a problem hiding this comment.
This hosted-mode redirect relies only on isHosted, which is false until the health query resolves. In hosted deployments this can briefly show invite/user-management bootstrapping UI before redirecting. Use modeKnown to fail closed until deployment mode is known (then redirect if hosted).
| } | ||
|
|
||
| export function AgentDetail() { | ||
| const { isHosted } = useHostedMode(); |
There was a problem hiding this comment.
This hosted-mode redirect checks only isHosted. Because isHosted is false until the health query resolves, hosted users can briefly see agent configuration UI on first paint. Consider gating on modeKnown (render null/loading until known, then redirect if hosted).
| const { isHosted } = useHostedMode(); | |
| const { isHosted, modeKnown } = useHostedMode(); | |
| // Avoid rendering agent configuration UI until hosted mode is known. | |
| if (!modeKnown) return null; |
| {/* Hide adapter type in hosted mode — agent infrastructure is managed server-side */} | ||
| {agent && !isHosted && ( |
There was a problem hiding this comment.
This hides the adapter label only when isHosted is true. Since isHosted is false until the health query resolves, hosted users may briefly see adapter infrastructure labels on first paint. Consider also using modeKnown and hiding while !modeKnown (fail closed).
| {/* Hide adapter type in hosted mode — agent infrastructure is managed server-side */} | |
| {agent && !isHosted && ( | |
| {/* Hide adapter type until mode is known, and in hosted mode — agent infrastructure is managed server-side */} | |
| {agent && modeKnown && !isHosted && ( |
| const { isHosted } = useHostedMode(); | ||
| if (isHosted) return null; |
There was a problem hiding this comment.
JoinRequestAdapterSpan only hides when isHosted is true. Because isHosted is false until the health query resolves, hosted users may briefly see adapter type here on first paint. Consider using modeKnown and returning null when !modeKnown || isHosted (fail closed).
| const { isHosted } = useHostedMode(); | |
| if (isHosted) return null; | |
| const { isHosted, modeKnown } = useHostedMode(); | |
| if (!modeKnown || isHosted) return null; |
| {/* Hide adapter type in hosted mode — agent infrastructure is managed server-side */} | ||
| {!isHosted && !!payload.adapterType && ( | ||
| <div className="flex items-center gap-2"> | ||
| <span className="text-muted-foreground w-20 sm:w-24 shrink-0 text-xs">Adapter</span> |
There was a problem hiding this comment.
This hides adapterType only when isHosted is true. Because isHosted is false until the health query resolves, hosted users may briefly see adapter type here on first paint. Consider also using modeKnown and hiding while !modeKnown (fail closed).
| const { isHosted } = useHostedMode(); | ||
| // Hide agent configuration form in hosted mode — infrastructure is managed server-side | ||
| if (isHosted) return null; |
There was a problem hiding this comment.
This returns null only when isHosted is true. Because isHosted is false until the health query resolves, hosted users can briefly see the agent config form on first render. Consider also gating on modeKnown (fail closed until deployment mode is known).
| const { isHosted } = useHostedMode(); | |
| // Hide agent configuration form in hosted mode — infrastructure is managed server-side | |
| if (isHosted) return null; | |
| const { isHosted, modeKnown } = useHostedMode(); | |
| // Hide agent configuration form in hosted mode — infrastructure is managed server-side. | |
| // Fail closed until deployment mode is known to avoid briefly rendering the form for hosted users. | |
| if (!modeKnown || isHosted) return null; |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (1)
sidecars/paperclip/ui/src/pages/Inbox.tsx (1)
631-635: LGTM — isolating the hook in a tiny subcomponent is a clean pattern.Extracting
JoinRequestAdapterSpanavoids pollutingInbox's hook list and keeps the guard local. Same fail-closed suggestion as other files: consider usingmodeKnownto avoid a brief flash ofadapter: …on first paint.- const { isHosted } = useHostedMode(); - if (isHosted) return null; + const { isHosted, modeKnown } = useHostedMode(); + if (!modeKnown || isHosted) return null;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@sidecars/paperclip/ui/src/pages/Inbox.tsx` around lines 631 - 635, JoinRequestAdapterSpan currently reads isHosted from useHostedMode and can briefly render before the hosted-mode is known; change it to also read modeKnown from useHostedMode and short-circuit until modeKnown is true. In other words, inside JoinRequestAdapterSpan (which calls useHostedMode), return null when modeKnown is false or when isHosted is true, and only render the <span>adapter: {adapterType}</span> when modeKnown is true and isHosted is false.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@sidecars/paperclip/ui/src/components/AgentConfigForm.tsx`:
- Around line 164-168: AgentConfigForm currently calls useHostedMode() and
returns early if isHosted, which causes a Rules of Hooks violation when isHosted
transitions and skips the subsequent hooks; fix by removing the early return and
instead either (A) let the parent component (AgentDetail) fully gate rendering
so AgentConfigForm is never mounted in hosted mode, or (B) keep useHostedMode()
but move the hosted-mode gate to after all hook declarations and use the
mode-known flag (e.g., modeKnown from useHostedMode) to render a closed state
until the mode is resolved; update AgentConfigForm (and its useHostedMode usage)
accordingly to avoid conditional early returns before hook calls.
In `@sidecars/paperclip/ui/src/pages/AgentDetail.tsx`:
- Around line 616-619: AgentDetail currently calls useHostedMode() and returns
early before running the rest of its hooks which causes a Rules of Hooks
violation when isHosted changes; to fix, remove the in-component early return
(the Navigate return) or move that hosted-mode guard to after all hook calls in
AgentDetail so hooks execute in the same order on every render, or better yet
implement a route-level guard (e.g., a HostedModeGate used in the router) so
AgentDetail (and RoutineDetail, InviteLanding, NewAgent) never mounts in hosted
mode; locate useHostedMode, the Navigate return, and the subsequent hooks
(useParams, useCompany, usePanel, useDialog, useBreadcrumbs, useQueryClient,
useNavigate, useState, useSidebar, useMemo, useCallback, useQuery, useEffect)
and apply one of these fixes.
In `@sidecars/paperclip/ui/src/pages/InviteLanding.tsx`:
- Around line 40-44: The early return in InviteLandingPage causes a Hooks order
violation because isHosted can flip after the first render; call useHostedMode()
at the top as before but do not early return — instead read both isHosted and a
modeKnown (or similar) flag from useHostedMode/useQuery (the same
queryKeys.health/healthQuery used elsewhere) so hooks always run; run all local
hooks (useState ×6, useQuery ×3, useMemo, useEffect, useMutation)
unconditionally, then after those hooks check if modeKnown && isHosted and if so
return <Navigate to="/" replace />; if modeKnown is false render a
loading/placeholder to avoid flashing the invite UI in hosted mode or move the
redirect into the router level instead.
In `@sidecars/paperclip/ui/src/pages/OrgChart.tsx`:
- Line 135: useHostedMode() currently exposes isHosted which is false until the
health query resolves causing a brief incorrect render; switch to the
fail-closed pattern by also using the modeKnown flag from useHostedMode and only
render the adapter-type UI when modeKnown is true. Update OrgChart.tsx (and the
same pattern in ApprovalPayload.tsx and Inbox.tsx for JoinRequestAdapterSpan) to
check modeKnown before reading isHosted or rendering the adapter span so the
component waits for the health/query result instead of flashing a wrong adapter
type.
In `@sidecars/paperclip/ui/src/pages/RoutineDetail.tsx`:
- Around line 252-256: The early return in RoutineDetail using isHosted causes a
Hooks ordering crash because useHostedMode (backed by useQuery) can change after
initial render; declare all hooks first (e.g., useParams, useRef, useState,
useEffect, useMemo, your useQuery/useMutation calls) and only perform the
redirect after those hooks, using useHostedMode().modeKnown to "fail closed"
(i.e., if modeKnown is false do not redirect yet; when modeKnown is true and
isHosted is true then return <Navigate ... />). Alternatively, move this guard
out of RoutineDetail and implement a router-level HostedModeGuard that prevents
the component subtree from mounting in hosted mode.
---
Nitpick comments:
In `@sidecars/paperclip/ui/src/pages/Inbox.tsx`:
- Around line 631-635: JoinRequestAdapterSpan currently reads isHosted from
useHostedMode and can briefly render before the hosted-mode is known; change it
to also read modeKnown from useHostedMode and short-circuit until modeKnown is
true. In other words, inside JoinRequestAdapterSpan (which calls useHostedMode),
return null when modeKnown is false or when isHosted is true, and only render
the <span>adapter: {adapterType}</span> when modeKnown is true and isHosted is
false.
🪄 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: defaults
Review profile: CHILL
Plan: Pro
Run ID: a7739f15-daa1-4260-bfd8-730c44d24f61
📒 Files selected for processing (7)
sidecars/paperclip/ui/src/components/AgentConfigForm.tsxsidecars/paperclip/ui/src/components/ApprovalPayload.tsxsidecars/paperclip/ui/src/pages/AgentDetail.tsxsidecars/paperclip/ui/src/pages/Inbox.tsxsidecars/paperclip/ui/src/pages/InviteLanding.tsxsidecars/paperclip/ui/src/pages/OrgChart.tsxsidecars/paperclip/ui/src/pages/RoutineDetail.tsx
| export function AgentConfigForm(props: AgentConfigFormProps) { | ||
| const { isHosted } = useHostedMode(); | ||
| // Hide agent configuration form in hosted mode — infrastructure is managed server-side | ||
| if (isHosted) return null; | ||
|
|
There was a problem hiding this comment.
Critical: Rules of Hooks violation — early return precedes many hooks.
Same issue as RoutineDetail and InviteLanding: useHostedMode() is itself a useQuery subscription, so isHosted transitions from false → true after /health resolves. When that transition occurs, return null skips all the hooks below this point (useState ×5, useQuery ×4, useMutation ×3, useEffect ×2, useRef, useMemo, useCallback ×2), triggering a hook-count mismatch crash. This will manifest the first time a hosted deployment loads any page that mounts AgentConfigForm (e.g., AgentDetail).
Also, because isHosted is initially false, the form's queries will fire once (hitting adapterModels, detectModel, secrets.list, etc.) before the component bails — not ideal if those endpoints aren't reachable in hosted mode.
Fix: have the parent (AgentDetail.tsx) gate rendering (parent already early-returns in hosted mode per the AI summary, so in practice this may be unreachable today — but the guard here still crashes if ever reached). Alternatively, move the if (isHosted) check after all hooks are declared and use modeKnown to fail closed. If AgentDetail already redirects in hosted mode, consider simply deleting this guard to avoid the hook-ordering hazard entirely.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@sidecars/paperclip/ui/src/components/AgentConfigForm.tsx` around lines 164 -
168, AgentConfigForm currently calls useHostedMode() and returns early if
isHosted, which causes a Rules of Hooks violation when isHosted transitions and
skips the subsequent hooks; fix by removing the early return and instead either
(A) let the parent component (AgentDetail) fully gate rendering so
AgentConfigForm is never mounted in hosted mode, or (B) keep useHostedMode() but
move the hosted-mode gate to after all hook declarations and use the mode-known
flag (e.g., modeKnown from useHostedMode) to render a closed state until the
mode is resolved; update AgentConfigForm (and its useHostedMode usage)
accordingly to avoid conditional early returns before hook calls.
| const { isHosted } = useHostedMode(); | ||
| // Redirect to home in hosted mode — agent configuration is infrastructure | ||
| if (isHosted) return <Navigate to="/" replace />; | ||
|
|
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Confirm hook-ordering pattern in the sibling files mentioned by the summary,
# and check whether a route-level hosted-mode guard already exists.
fd -t f 'useHostedMode.ts' sidecars/paperclip/ui/src | head
rg -nP -C2 'useHostedMode\(\)' sidecars/paperclip/ui/src
rg -nP -C3 'isHosted\s*\)\s*return\s*<Navigate' sidecars/paperclip/ui/src
# Router config: is there a central place to gate routes?
fd -t f -e tsx -e ts . sidecars/paperclip/ui/src | xargs rg -nP -l 'createBrowserRouter|RouterProvider|<Route\b' 2>/dev/nullRepository: wopr-network/platform
Length of output: 26129
🏁 Script executed:
# View useHostedMode implementation to understand hook stability
cat -n sidecars/paperclip/ui/src/hooks/useHostedMode.tsRepository: wopr-network/platform
Length of output: 867
🏁 Script executed:
# View AgentDetail.tsx to see the actual hook sequence and count
head -n 700 sidecars/paperclip/ui/src/pages/AgentDetail.tsx | tail -n 100Repository: wopr-network/platform
Length of output: 4124
🏁 Script executed:
# Check if a route-level hosted guard exists anywhere
rg -n 'HostedModeGate|HostedRoute|privateRoute|ProtectedRoute' sidecars/paperclip/ui/src --type tsx --type tsRepository: wopr-network/platform
Length of output: 92
🏁 Script executed:
# Find the router configuration to check for route-level guards
rg -n 'createBrowserRouter|createHashRouter' sidecars/paperclip/ui/src -A 20 --type tsx --type tsRepository: wopr-network/platform
Length of output: 92
🏁 Script executed:
# Check if AgentDetail is wrapped in a component that prevents re-renders
rg -B5 -A2 'AgentDetail' sidecars/paperclip/ui/src/App.tsx | head -40Repository: wopr-network/platform
Length of output: 1056
🏁 Script executed:
# Check useQuery behavior and if React Query memoizes the hook return
rg -n 'staleTime|gcTime|retry' sidecars/paperclip/ui/src/hooks/useHostedMode.tsRepository: wopr-network/platform
Length of output: 109
🏁 Script executed:
# Verify the actual hook count after the early return in AgentDetail
sed -n '615,750p' sidecars/paperclip/ui/src/pages/AgentDetail.tsx | grep -c 'use[A-Z]'Repository: wopr-network/platform
Length of output: 67
Rules of Hooks violation: early return precedes many subsequent hooks.
AgentDetail calls useHostedMode() and then conditionally returns before the block of hooks below (useParams, useCompany, usePanel, useDialog, useBreadcrumbs, useQueryClient, useNavigate, useState×multiple, useSidebar, useMemo, useCallback, useQuery×multiple, useEffect×multiple). Since useHostedMode uses useQuery with a 5-minute stale time, the isHosted value can change between renders (e.g., after cache expires and refetch occurs). If the condition changes for a mounted instance, React will throw "Rendered fewer hooks than previous render" and crash the page.
Two safer options:
- Hoist the hosted-mode gate to the router level so
AgentDetailnever mounts in hosted mode (preferred — prevents agent data fetching and aligns with the "infra UI must not load" intent). - Keep the guard in-component but place it after all hooks have run.
The same root cause applies to RoutineDetail, InviteLanding, NewAgent, and other pages using this pattern. Consider consolidating behind a single route-level <HostedModeGate> or equivalent guard to prevent multiple components from facing this violation.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@sidecars/paperclip/ui/src/pages/AgentDetail.tsx` around lines 616 - 619,
AgentDetail currently calls useHostedMode() and returns early before running the
rest of its hooks which causes a Rules of Hooks violation when isHosted changes;
to fix, remove the in-component early return (the Navigate return) or move that
hosted-mode guard to after all hook calls in AgentDetail so hooks execute in the
same order on every render, or better yet implement a route-level guard (e.g., a
HostedModeGate used in the router) so AgentDetail (and RoutineDetail,
InviteLanding, NewAgent) never mounts in hosted mode; locate useHostedMode, the
Navigate return, and the subsequent hooks (useParams, useCompany, usePanel,
useDialog, useBreadcrumbs, useQueryClient, useNavigate, useState, useSidebar,
useMemo, useCallback, useQuery, useEffect) and apply one of these fixes.
| export function InviteLandingPage() { | ||
| const { isHosted } = useHostedMode(); | ||
| // Redirect to home in hosted mode — user management and agent bootstrapping is server-side | ||
| if (isHosted) return <Navigate to="/" replace />; | ||
|
|
There was a problem hiding this comment.
Critical: Rules of Hooks violation — same pattern as RoutineDetail.
isHosted starts false and may flip to true once the health query resolves. When it flips, this early return skips every hook below (useState ×6, useQuery ×3, useMemo, useEffect, useMutation), producing the React "Rendered fewer hooks than during the previous render" error.
Additionally, this component already issues healthApi.get() via its own healthQuery; useHostedMode uses the same queryKeys.health so they share cache, but the guard still happens after the first render cycle — causing a flash of the invite UI in hosted mode.
Fix: redirect at the router level, or restructure so the hosted check runs after all hooks and use modeKnown to fail closed (see the RoutineDetail comment for shape).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@sidecars/paperclip/ui/src/pages/InviteLanding.tsx` around lines 40 - 44, The
early return in InviteLandingPage causes a Hooks order violation because
isHosted can flip after the first render; call useHostedMode() at the top as
before but do not early return — instead read both isHosted and a modeKnown (or
similar) flag from useHostedMode/useQuery (the same queryKeys.health/healthQuery
used elsewhere) so hooks always run; run all local hooks (useState ×6, useQuery
×3, useMemo, useEffect, useMutation) unconditionally, then after those hooks
check if modeKnown && isHosted and if so return <Navigate to="/" replace />; if
modeKnown is false render a loading/placeholder to avoid flashing the invite UI
in hosted mode or move the redirect into the router level instead.
| // ── Main component ────────────────────────────────────────────────────── | ||
|
|
||
| export function OrgChart() { | ||
| const { isHosted } = useHostedMode(); |
There was a problem hiding this comment.
Minor: brief adapter-type flash before /health resolves.
useHostedMode() returns isHosted === false until the health query resolves, so on first paint the adapter type may render briefly before being hidden. For hosted-mode leak prevention, prefer fail-closed using modeKnown:
- const { isHosted } = useHostedMode();
+ const { isHosted, modeKnown } = useHostedMode();
...
- {agent && !isHosted && (
+ {agent && modeKnown && !isHosted && (Same pattern applies to ApprovalPayload.tsx and Inbox.tsx's JoinRequestAdapterSpan.
Also applies to: 427-432
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@sidecars/paperclip/ui/src/pages/OrgChart.tsx` at line 135, useHostedMode()
currently exposes isHosted which is false until the health query resolves
causing a brief incorrect render; switch to the fail-closed pattern by also
using the modeKnown flag from useHostedMode and only render the adapter-type UI
when modeKnown is true. Update OrgChart.tsx (and the same pattern in
ApprovalPayload.tsx and Inbox.tsx for JoinRequestAdapterSpan) to check modeKnown
before reading isHosted or rendering the adapter span so the component waits for
the health/query result instead of flashing a wrong adapter type.
| export function RoutineDetail() { | ||
| const { isHosted } = useHostedMode(); | ||
| // Redirect to home in hosted mode — routine configuration is infrastructure | ||
| if (isHosted) return <Navigate to="/" replace />; | ||
|
|
There was a problem hiding this comment.
Critical: Rules of Hooks violation — early return before subsequent hooks.
useHostedMode() is itself backed by useQuery, so isHosted starts as false on first render (before /health resolves) and can flip to true on a later render. When that flip happens, this early return skips all the hooks below (useParams, useRef, useState, useEffect, useMemo, multiple useQuery / useMutation), so React will throw: "Rendered fewer hooks than during the previous render." This is a hard crash, not a lint warning.
Secondary issue: because isHosted is false until health resolves, the full routine detail (including its queries that hit infra endpoints) will render/fire for a moment before the redirect — defeating the hostedMode guard's intent. useHostedMode already exposes modeKnown precisely to fail closed.
Fix: gate the redirect at the route/router level (so the child component never mounts), or perform the guard after all hooks are declared and use modeKnown to fail closed. Example of the in-component shape:
Proposed shape
export function RoutineDetail() {
- const { isHosted } = useHostedMode();
- // Redirect to home in hosted mode — routine configuration is infrastructure
- if (isHosted) return <Navigate to="/" replace />;
-
- const { routineId } = useParams<{ routineId: string }>();
+ const { isHosted, modeKnown } = useHostedMode();
+ const { routineId } = useParams<{ routineId: string }>();
// ... all other hooks unchanged ...
+
+ // After all hooks: fail closed until we know the mode, redirect when hosted.
+ if (!modeKnown || isHosted) {
+ return isHosted || !modeKnown ? <Navigate to="/" replace /> : null;
+ }Preferred alternative: wrap the route element in a <HostedModeGuard> at the router so the whole subtree (including its queries) never mounts in hosted mode.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@sidecars/paperclip/ui/src/pages/RoutineDetail.tsx` around lines 252 - 256,
The early return in RoutineDetail using isHosted causes a Hooks ordering crash
because useHostedMode (backed by useQuery) can change after initial render;
declare all hooks first (e.g., useParams, useRef, useState, useEffect, useMemo,
your useQuery/useMutation calls) and only perform the redirect after those
hooks, using useHostedMode().modeKnown to "fail closed" (i.e., if modeKnown is
false do not redirect yet; when modeKnown is true and isHosted is true then
return <Navigate ... />). Alternatively, move this guard out of RoutineDetail
and implement a router-level HostedModeGuard that prevents the component subtree
from mounting in hosted mode.
Automated upstream sync — Paperclip
Synced with latest from paperclipai/paperclip upstream.
What this does
Verify
Summary by Sourcery
Tighten hosted mode restrictions in the Paperclip UI to hide agent infrastructure details and configuration surfaces.
Bug Fixes:
Enhancements:
Summary by CodeRabbit