fix(ui): load all agents so the picker and deep-links work past the first page#347
Conversation
…ks by name The agent picker fetched agents with an unpaginated list() call, so on accounts with more than one page of agents only the first page was shown. Those agents were invisible in the picker and, because the same list backed deep-link validation, deep-linking to one cleared the agent_name param and bounced back to the home grid. - useAgents now pages through the full list (limit 100, loop until a short page) with a safety bound + warning to avoid silent truncation. - Add useAgentByName to validate a deep-linked agent directly via retrieveByName, so a valid agent opens even if it's outside the loaded list. - agentex-ui-root only clears agent_name when it's present and invalid, and accepts an agent found either in the list or by name. - Add unit tests for the pagination loop and the by-name lookup. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
With the full agent list now loading, accounts with many agents produced a picker taller than the viewport. Because the home view is vertically centered with no overflow handling, the grid spilled off-screen top and bottom, hiding the title and the prompt input with no way to scroll. Cap the chip grid height and let it scroll internally so the title and input stay visible. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| // on agent_name changes, not on every `agents`/`agentByName` identity change. | ||
| useEffect(() => { | ||
| if (isLoading) return; | ||
| if (isLoading || isAgentByNameLoading) return; |
There was a problem hiding this comment.
this validates using isLoading, which is false when React Query has stale cached data and is refetching. A cached null by-name result or stale non-Ready list entry can clear agent_name before the fresh validation finishes. Can we use the enabled query’s isFetching / success state instead?
There was a problem hiding this comment.
Good catch, I switched both queries to isFetching so it won't validate against stale cached data mid-refetch
…ation on isFetching - useAgentByName resolves to null only on a 404 (APIError, status 404) and re-throws everything else, so a transient network/5xx/auth failure surfaces as a query error instead of masquerading as "not found". - agentex-ui-root: gate the deep-link validation on isFetching (not isLoading, which is false once a query has cached data) so it never validates against stale data mid-refetch; and only clear agent_name when the agent is genuinely invalid — skip the clear when the by-name lookup errored and the agent isn't in the loaded list (unknown != invalid). - Export AGENTS_PAGE_SIZE / MAX_AGENT_PAGES and import them in the test instead of duplicating the values. - Tests updated: 404 -> null, non-404 -> surfaced error. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
declan-scale
left a comment
There was a problem hiding this comment.
LGTM thanks, maybe want to make a low priority linear ticket for the improvement (or just implement real quick, I believe the threads fetching already has the infinite query pattern)
| @@ -20,7 +38,31 @@ export function useAgents(agentexClient: AgentexSDK) { | |||
| return useQuery({ | |||
There was a problem hiding this comment.
[nit] we could use an infinite query with a scroll trigger but until we see long loading times I think this pattern is ok
There was a problem hiding this comment.
Agreed, went with the simple page-through for now since the picker wants the full list available anyway. If we start seeing slow loads on large accounts, an infinite query with a scroll trigger is a good follow-up. Good call out
…r-pagination Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…switch - Account picker: extract a shared `iconTile` (loading + single-account collapsed tiles) and a shared `options` element (the SelectContent list previously duplicated across the collapsed and expanded switchers); single change handler `onAccountChange`. - Provider: `setSelectedAccountId` now clears `task_id` on an explicit switch (the open task is account-scoped and 404s under a new account). This preserves the reset that previously lived in agentex-ui-root's validation effect, which #347 reworked. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…itch A selected agent is account-scoped, but its name persisted in the URL across a switch — so a same-named agent in the new account stayed selected (and #347's localStorage restore could reinstate it). On an account change, clear agent_name and the remembered agent so the view resets to the grid. task_id is already cleared by the switch. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…itch A selected agent is account-scoped, but its name persisted in the URL across a switch — so a same-named agent in the new account stayed selected (and #347's localStorage restore could reinstate it). On an account change, clear agent_name and the remembered agent so the view resets to the grid. task_id is already cleared by the switch. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
What
useAgentsinstead of fetching only the default first page.useAgentByNameto validate a deep-linkedagent_namedirectly, so a valid agent opens even if it's outside the loaded list.agent_namewhen it's present and invalid; accept an agent found in the list or by name.Why
The picker fetched agents with an unpaginated
list()call, so on accounts with more than one page of agents only the first ~50 showed. Those agents were invisible in the picker and because the same list backed deep-link validation, deep-linking to one bounced back to the home grid. Loading the full list then surfaced a layout issue where a tall picker overflowed a vertically-centered container with no way to scroll.Testing
npm run typecheck+npm run lintclean.Before:
Screen.Recording.2026-07-07.at.11.44.28.AM.mov
After:
Screen.Recording.2026-07-07.at.11.41.26.AM.mov
Greptile Summary
This PR fixes the agent picker and deep-link routing by paginating through the full agent list (instead of stopping after the default first page), adding a
useAgentByNamehook for direct deep-link validation, and capping the picker's chip grid height to prevent layout overflow on large accounts.useAgentsnow loops through all pages up to aMAX_AGENT_PAGESsafety bound (100 × 100 = 10 000 agents max), warns loudly if truncated, and exports its constants so tests can import them rather than duplicating hardcoded values.useAgentByNamevalidates a deep-linked agent name directly against the backend: 404 resolves tonull, any other error is re-thrown so React Query surfaces it asisError, and the effect inagentex-ui-root.tsxrespectscouldNotDetermineto avoid clearing a valid deep link on a transient failure.AgentsListgainsmax-h-[60vh] overflow-y-autoso a long agent list scrolls within the centered container rather than pushing the title and input off-screen.Confidence Score: 5/5
Safe to merge — all changed paths are well-tested, the catch narrowing and couldNotDetermine guard are correctly implemented, and the pagination loop has a clear termination condition.
The pagination loop terminates reliably on a short page or at the safety bound, both hooks narrow their error handling correctly (404 → null, everything else re-thrown), and the validation effect is gated on both isFetching signals so it never acts on stale data. Unit tests cover all meaningful boundary cases. The previous review thread's concerns have been fully addressed.
No files require special attention.
Important Files Changed
Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart TD A[Page loads with agent_name in URL] --> B{isAgentsFetching\nOR isAgentByNameFetching?} B -- Yes --> C[Return early — wait for both queries to settle] B -- No --> D{agentName present\nin URL?} D -- No --> E{localAgentName\nin localStorage?} E -- Yes --> F[Restore: updateParams agent_name = localAgentName] E -- No --> G[No-op] D -- Yes --> H{Agent found\nin paginated list?} H -- Yes --> I[agentInList = agent] H -- No --> J{useAgentByName\nresult?} J -- Agent returned --> K[agentByName = Agent] J -- 404 → null --> L[agentByName = null] J -- Non-404 error --> M[isAgentByNameError = true\ncouldNotDetermine = true] I --> N{status === Ready?} K --> N L --> O[isAgentValid = false\ncouldNotDetermine = false] M --> P[Leave URL alone\npreserve deep link] N -- Yes --> Q[isAgentValid = true\nLeave URL alone — agent opens] N -- No --> O O --> R[Clear agent_name from URL\nsetLocalAgentName undefined]%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%% flowchart TD A[Page loads with agent_name in URL] --> B{isAgentsFetching\nOR isAgentByNameFetching?} B -- Yes --> C[Return early — wait for both queries to settle] B -- No --> D{agentName present\nin URL?} D -- No --> E{localAgentName\nin localStorage?} E -- Yes --> F[Restore: updateParams agent_name = localAgentName] E -- No --> G[No-op] D -- Yes --> H{Agent found\nin paginated list?} H -- Yes --> I[agentInList = agent] H -- No --> J{useAgentByName\nresult?} J -- Agent returned --> K[agentByName = Agent] J -- 404 → null --> L[agentByName = null] J -- Non-404 error --> M[isAgentByNameError = true\ncouldNotDetermine = true] I --> N{status === Ready?} K --> N L --> O[isAgentValid = false\ncouldNotDetermine = false] M --> P[Leave URL alone\npreserve deep link] N -- Yes --> Q[isAgentValid = true\nLeave URL alone — agent opens] N -- No --> O O --> R[Clear agent_name from URL\nsetLocalAgentName undefined]Reviews (3): Last reviewed commit: "Merge origin/main into vineetvora/agx1-3..." | Re-trigger Greptile