From 8c88c217175ed58bcda8a1365b7260516564dcc4 Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Tue, 3 Mar 2026 12:44:41 +0000 Subject: [PATCH 1/3] docs: add plan.md --- plan.md | 144 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 plan.md diff --git a/plan.md b/plan.md new file mode 100644 index 000000000..11cd6003c --- /dev/null +++ b/plan.md @@ -0,0 +1,144 @@ +# Fix: Eliminate redundant `unifiedSessions.list` query invocations + +## Problem + +`useSidebarSessions` — which calls `useQuery` for `unifiedSessions.list` — is called inside +`CloudChatContainer`. That component re-renders ~6 times during its session-loading lifecycle due +to cascading `useState` / `useAtomValue` updates, and React Strict Mode doubles that to ~12. + +Each re-render within a single React tick adds another entry to tRPC's batching layer, which +collects all `useQuery` calls in a tick and sends them as one HTTP request. React Query +deduplicates *concurrent* requests with the same key, but here it sees a single in-flight batched +request containing 12 sub-operations — so deduplication does not fire. + +The fix is to **lift `useSidebarSessions` one level up into `CloudChatPage`**, which is a +stateless pass-through component that never re-renders on its own. The `sessions` list and +`refetchSessions` callback are then passed down as props. + +The fix applies identically to both `cloud-agent` (v1) and `cloud-agent-next` (v2). + +--- + +## Files to change + +### 1. `src/components/cloud-agent/CloudChatPage.tsx` + +**Before** — thin wrapper, no logic: +```tsx +export default function CloudChatPage(props: CloudChatPageProps) { + return ; +} +``` + +**After** — call `useSidebarSessions` here, pass results as props: +```tsx +import { useSidebarSessions } from './hooks/useSidebarSessions'; + +export default function CloudChatPage({ organizationId }: CloudChatPageProps) { + const { sessions, refetchSessions } = useSidebarSessions({ + organizationId: organizationId ?? null, + }); + return ( + + ); +} +``` + +### 2. `src/components/cloud-agent-next/CloudChatPage.tsx` + +Identical change to the above (different import paths, otherwise the same). + +--- + +### 3. `src/components/cloud-agent/CloudChatContainer.tsx` + +**A. Extend `CloudChatContainerProps`:** +```ts +// Before +type CloudChatContainerProps = { + organizationId?: string; +}; + +// After +type CloudChatContainerProps = { + organizationId?: string; + sessions: StoredSession[]; + refetchSessions: () => void; +}; +``` + +**B. Update function signature** to destructure the new props: +```ts +// Before +export function CloudChatContainer({ organizationId }: CloudChatContainerProps) { + +// After +export function CloudChatContainer({ organizationId, sessions, refetchSessions }: CloudChatContainerProps) { +``` + +**C. Remove `useSidebarSessions` call** (lines 237–241) and its import (line 43): + +Lines to delete: +```ts +// import +import { useSidebarSessions } from './hooks/useSidebarSessions'; + +// usage +// Sidebar sessions (scoped to organization when in org context, personal-only when undefined) +// Pass null for personal chat to filter out org sessions, or the org ID for org chat +const { sessions, refetchSessions } = useSidebarSessions({ + organizationId: organizationId ?? null, +}); +``` + +Everything else (`handleStreamComplete` using `refetchSessions`, `useSessionDeletion` receiving +`refetchSessions`, `sessions` prop on `CloudChatPresentation`) stays exactly as-is — they simply +consume the prop value instead of the locally-derived value. + +**D. Add `StoredSession` to imports** (it must be imported at the type level for the prop type): +```ts +import type { AgentMode, SessionStartConfig, StoredSession } from './types'; +``` +(Currently `StoredSession` is only imported inside `useSidebarSessions`, not in the container.) + +--- + +### 4. `src/components/cloud-agent-next/CloudChatContainer.tsx` + +Identical changes A–D above (different import paths, otherwise the same). + +--- + +## What does NOT change + +- `useSidebarSessions` itself — no changes. +- `CloudChatPageWrapper` / `CloudChatPageWrapperNext` — no changes; they already pass + `organizationId` down to `CloudChatPage`, which now passes it to both `useSidebarSessions` and + `CloudChatContainer`. +- `useSessionDeletion`, `CloudChatPresentation`, all other hooks — no changes; they continue + receiving `refetchSessions` / `sessions` exactly as before. + +--- + +## Why `CloudChatPage` is the right lift-point + +- Zero state, zero effects, zero atoms — it re-renders only when `organizationId` changes + (i.e. on navigation), not during the session-loading lifecycle. +- Already owns `organizationId`, so `useSidebarSessions({ organizationId: organizationId ?? null })` + requires no new prop threading. +- Sits inside the `` boundary in `CloudChatPageWrapper`, which is correct — the sessions + query should suspend/load after the Suspense boundary hydrates. +- No new files, no new abstractions, minimal diff. + +--- + +## Expected outcome + +| Scenario | Before | After | +|---|---|---| +| Dev (StrictMode on) | ~12 sub-operations in one batch | ~2 (one mount + StrictMode double) | +| Production | ~6 sub-operations in one batch | ~1 | From c25a799ec1cd65ff1e5053d895d24fdc3bc72aa8 Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Tue, 3 Mar 2026 12:48:13 +0000 Subject: [PATCH 2/3] refactor(cloud-chat): lift sidebar sessions loading to page and pass as props Move logic: - move useSidebarSessions call from CloudChatContainer to CloudChatPage for both packages - CloudChatContainer now accepts sessions: StoredSession[] and refetchSessions: () => void; remove internal fetch - CloudChatPage now uses useSidebarSessions and passes sessions and refetchSessions to container - update type imports to include StoredSession BREAKING CHANGE: CloudChatContainer API changed to require sessions and refetchSessions props; CloudChatPage wires loading to avoid re-renders --- .../cloud-agent-next/CloudChatContainer.tsx | 13 ++++------- .../cloud-agent-next/CloudChatPage.tsx | 23 ++++++++++++------- .../cloud-agent/CloudChatContainer.tsx | 13 ++++------- src/components/cloud-agent/CloudChatPage.tsx | 23 ++++++++++++------- 4 files changed, 38 insertions(+), 34 deletions(-) diff --git a/src/components/cloud-agent-next/CloudChatContainer.tsx b/src/components/cloud-agent-next/CloudChatContainer.tsx index 743da2028..da3fc77dc 100644 --- a/src/components/cloud-agent-next/CloudChatContainer.tsx +++ b/src/components/cloud-agent-next/CloudChatContainer.tsx @@ -47,7 +47,6 @@ import { useCloudAgentStream } from './useCloudAgentStream'; import { useAutoScroll } from './hooks/useAutoScroll'; import { useCelebrationSound } from '@/hooks/useCelebrationSound'; import { useNotificationSound } from '@/hooks/useNotificationSound'; -import { useSidebarSessions } from './hooks/useSidebarSessions'; import { useOrganizationModels } from './hooks/useOrganizationModels'; import { useSessionDeletion } from './hooks/useSessionDeletion'; import { useResumeConfigModal } from './hooks/useResumeConfigModal'; @@ -59,7 +58,7 @@ import { useSlashCommandSets } from '@/hooks/useSlashCommandSets'; import { CloudChatPresentation } from './CloudChatPresentation'; import { QuestionContextProvider } from './QuestionContext'; import type { ResumeConfig } from './ResumeConfigModal'; -import type { AgentMode, SessionStartConfig } from './types'; +import type { AgentMode, SessionStartConfig, StoredSession } from './types'; /** Normalize legacy mode strings ('build' → 'code', 'architect' → 'plan') from DB/DO */ function normalizeMode(mode: string): AgentMode { @@ -70,9 +69,11 @@ function normalizeMode(mode: string): AgentMode { type CloudChatContainerProps = { organizationId?: string; + sessions: StoredSession[]; + refetchSessions: () => void; }; -export function CloudChatContainer({ organizationId }: CloudChatContainerProps) { +export function CloudChatContainer({ organizationId, sessions, refetchSessions }: CloudChatContainerProps) { const router = useRouter(); const searchParams = useSearchParams(); const trpc = useTRPC(); @@ -267,12 +268,6 @@ export function CloudChatContainer({ organizationId }: CloudChatContainerProps) setIsSessionInitiated(true); }, []); - // Sidebar sessions (scoped to organization when in org context, personal-only when undefined) - // Pass null for personal chat to filter out org sessions, or the org ID for org chat - const { sessions, refetchSessions } = useSidebarSessions({ - organizationId: organizationId ?? null, - }); - // Callback for stream completion const handleStreamComplete = useCallback(() => { playCelebrationSound(); diff --git a/src/components/cloud-agent-next/CloudChatPage.tsx b/src/components/cloud-agent-next/CloudChatPage.tsx index c73281291..300e1646f 100644 --- a/src/components/cloud-agent-next/CloudChatPage.tsx +++ b/src/components/cloud-agent-next/CloudChatPage.tsx @@ -1,24 +1,31 @@ /** * Cloud Chat Page * - * Simple wrapper that exports the CloudChatContainer component. - * All business logic, hooks, and state management are in CloudChatContainer. - * All rendering logic is in CloudChatPresentation. + * Owns the sidebar session query so it runs in a stable component that does not + * re-render during the session-loading lifecycle, eliminating redundant + * unifiedSessions.list invocations that would otherwise be batched by tRPC. */ 'use client'; import { CloudChatContainer } from './CloudChatContainer'; +import { useSidebarSessions } from './hooks/useSidebarSessions'; type CloudChatPageProps = { organizationId?: string; }; -/** - * Main export - renders the cloud chat container - */ -export default function CloudChatPage(props: CloudChatPageProps) { - return ; +export default function CloudChatPage({ organizationId }: CloudChatPageProps) { + const { sessions, refetchSessions } = useSidebarSessions({ + organizationId: organizationId ?? null, + }); + return ( + + ); } // Named export for compatibility diff --git a/src/components/cloud-agent/CloudChatContainer.tsx b/src/components/cloud-agent/CloudChatContainer.tsx index 47c2a862f..068ee1ab7 100644 --- a/src/components/cloud-agent/CloudChatContainer.tsx +++ b/src/components/cloud-agent/CloudChatContainer.tsx @@ -40,7 +40,6 @@ import { import { useCloudAgentStreamV2 } from './useCloudAgentStreamV2'; import { useAutoScroll } from './hooks/useAutoScroll'; import { useCelebrationSound } from '@/hooks/useCelebrationSound'; -import { useSidebarSessions } from './hooks/useSidebarSessions'; import { useOrganizationModels } from './hooks/useOrganizationModels'; import { useSessionDeletion } from './hooks/useSessionDeletion'; import { useResumeConfigModal } from './hooks/useResumeConfigModal'; @@ -51,10 +50,12 @@ import { buildPrepareSessionRepoParams } from './utils/git-utils'; import { useSlashCommandSets } from '@/hooks/useSlashCommandSets'; import { CloudChatPresentation } from './CloudChatPresentation'; import type { ResumeConfig } from './ResumeConfigModal'; -import type { AgentMode, SessionStartConfig } from './types'; +import type { AgentMode, SessionStartConfig, StoredSession } from './types'; type CloudChatContainerProps = { organizationId?: string; + sessions: StoredSession[]; + refetchSessions: () => void; }; /** @@ -85,7 +86,7 @@ type ResumeConfigState = | { status: 'persisted'; config: ResumeConfig } | { status: 'failed'; config: ResumeConfig; error: Error }; -export function CloudChatContainer({ organizationId }: CloudChatContainerProps) { +export function CloudChatContainer({ organizationId, sessions, refetchSessions }: CloudChatContainerProps) { const router = useRouter(); const searchParams = useSearchParams(); const trpc = useTRPC(); @@ -234,12 +235,6 @@ export function CloudChatContainer({ organizationId }: CloudChatContainerProps) setIsSessionInitiated(true); }, []); - // Sidebar sessions (scoped to organization when in org context, personal-only when undefined) - // Pass null for personal chat to filter out org sessions, or the org ID for org chat - const { sessions, refetchSessions } = useSidebarSessions({ - organizationId: organizationId ?? null, - }); - // Callback for stream completion const handleStreamComplete = useCallback(() => { playCelebrationSound(); diff --git a/src/components/cloud-agent/CloudChatPage.tsx b/src/components/cloud-agent/CloudChatPage.tsx index c73281291..300e1646f 100644 --- a/src/components/cloud-agent/CloudChatPage.tsx +++ b/src/components/cloud-agent/CloudChatPage.tsx @@ -1,24 +1,31 @@ /** * Cloud Chat Page * - * Simple wrapper that exports the CloudChatContainer component. - * All business logic, hooks, and state management are in CloudChatContainer. - * All rendering logic is in CloudChatPresentation. + * Owns the sidebar session query so it runs in a stable component that does not + * re-render during the session-loading lifecycle, eliminating redundant + * unifiedSessions.list invocations that would otherwise be batched by tRPC. */ 'use client'; import { CloudChatContainer } from './CloudChatContainer'; +import { useSidebarSessions } from './hooks/useSidebarSessions'; type CloudChatPageProps = { organizationId?: string; }; -/** - * Main export - renders the cloud chat container - */ -export default function CloudChatPage(props: CloudChatPageProps) { - return ; +export default function CloudChatPage({ organizationId }: CloudChatPageProps) { + const { sessions, refetchSessions } = useSidebarSessions({ + organizationId: organizationId ?? null, + }); + return ( + + ); } // Named export for compatibility From 037d6a6f51a643a0e0d114d6c5796db0cdd9cdac Mon Sep 17 00:00:00 2001 From: Evgeny Shurakov Date: Tue, 3 Mar 2026 16:13:10 +0100 Subject: [PATCH 3/3] style: apply prettier formatting and remove plan.md --- plan.md | 144 ------------------ .../cloud-agent-next/CloudChatContainer.tsx | 6 +- .../cloud-agent-next/CloudChatPage.tsx | 6 +- .../cloud-agent/CloudChatContainer.tsx | 6 +- src/components/cloud-agent/CloudChatPage.tsx | 6 +- 5 files changed, 16 insertions(+), 152 deletions(-) delete mode 100644 plan.md diff --git a/plan.md b/plan.md deleted file mode 100644 index 11cd6003c..000000000 --- a/plan.md +++ /dev/null @@ -1,144 +0,0 @@ -# Fix: Eliminate redundant `unifiedSessions.list` query invocations - -## Problem - -`useSidebarSessions` — which calls `useQuery` for `unifiedSessions.list` — is called inside -`CloudChatContainer`. That component re-renders ~6 times during its session-loading lifecycle due -to cascading `useState` / `useAtomValue` updates, and React Strict Mode doubles that to ~12. - -Each re-render within a single React tick adds another entry to tRPC's batching layer, which -collects all `useQuery` calls in a tick and sends them as one HTTP request. React Query -deduplicates *concurrent* requests with the same key, but here it sees a single in-flight batched -request containing 12 sub-operations — so deduplication does not fire. - -The fix is to **lift `useSidebarSessions` one level up into `CloudChatPage`**, which is a -stateless pass-through component that never re-renders on its own. The `sessions` list and -`refetchSessions` callback are then passed down as props. - -The fix applies identically to both `cloud-agent` (v1) and `cloud-agent-next` (v2). - ---- - -## Files to change - -### 1. `src/components/cloud-agent/CloudChatPage.tsx` - -**Before** — thin wrapper, no logic: -```tsx -export default function CloudChatPage(props: CloudChatPageProps) { - return ; -} -``` - -**After** — call `useSidebarSessions` here, pass results as props: -```tsx -import { useSidebarSessions } from './hooks/useSidebarSessions'; - -export default function CloudChatPage({ organizationId }: CloudChatPageProps) { - const { sessions, refetchSessions } = useSidebarSessions({ - organizationId: organizationId ?? null, - }); - return ( - - ); -} -``` - -### 2. `src/components/cloud-agent-next/CloudChatPage.tsx` - -Identical change to the above (different import paths, otherwise the same). - ---- - -### 3. `src/components/cloud-agent/CloudChatContainer.tsx` - -**A. Extend `CloudChatContainerProps`:** -```ts -// Before -type CloudChatContainerProps = { - organizationId?: string; -}; - -// After -type CloudChatContainerProps = { - organizationId?: string; - sessions: StoredSession[]; - refetchSessions: () => void; -}; -``` - -**B. Update function signature** to destructure the new props: -```ts -// Before -export function CloudChatContainer({ organizationId }: CloudChatContainerProps) { - -// After -export function CloudChatContainer({ organizationId, sessions, refetchSessions }: CloudChatContainerProps) { -``` - -**C. Remove `useSidebarSessions` call** (lines 237–241) and its import (line 43): - -Lines to delete: -```ts -// import -import { useSidebarSessions } from './hooks/useSidebarSessions'; - -// usage -// Sidebar sessions (scoped to organization when in org context, personal-only when undefined) -// Pass null for personal chat to filter out org sessions, or the org ID for org chat -const { sessions, refetchSessions } = useSidebarSessions({ - organizationId: organizationId ?? null, -}); -``` - -Everything else (`handleStreamComplete` using `refetchSessions`, `useSessionDeletion` receiving -`refetchSessions`, `sessions` prop on `CloudChatPresentation`) stays exactly as-is — they simply -consume the prop value instead of the locally-derived value. - -**D. Add `StoredSession` to imports** (it must be imported at the type level for the prop type): -```ts -import type { AgentMode, SessionStartConfig, StoredSession } from './types'; -``` -(Currently `StoredSession` is only imported inside `useSidebarSessions`, not in the container.) - ---- - -### 4. `src/components/cloud-agent-next/CloudChatContainer.tsx` - -Identical changes A–D above (different import paths, otherwise the same). - ---- - -## What does NOT change - -- `useSidebarSessions` itself — no changes. -- `CloudChatPageWrapper` / `CloudChatPageWrapperNext` — no changes; they already pass - `organizationId` down to `CloudChatPage`, which now passes it to both `useSidebarSessions` and - `CloudChatContainer`. -- `useSessionDeletion`, `CloudChatPresentation`, all other hooks — no changes; they continue - receiving `refetchSessions` / `sessions` exactly as before. - ---- - -## Why `CloudChatPage` is the right lift-point - -- Zero state, zero effects, zero atoms — it re-renders only when `organizationId` changes - (i.e. on navigation), not during the session-loading lifecycle. -- Already owns `organizationId`, so `useSidebarSessions({ organizationId: organizationId ?? null })` - requires no new prop threading. -- Sits inside the `` boundary in `CloudChatPageWrapper`, which is correct — the sessions - query should suspend/load after the Suspense boundary hydrates. -- No new files, no new abstractions, minimal diff. - ---- - -## Expected outcome - -| Scenario | Before | After | -|---|---|---| -| Dev (StrictMode on) | ~12 sub-operations in one batch | ~2 (one mount + StrictMode double) | -| Production | ~6 sub-operations in one batch | ~1 | diff --git a/src/components/cloud-agent-next/CloudChatContainer.tsx b/src/components/cloud-agent-next/CloudChatContainer.tsx index da3fc77dc..914e0030d 100644 --- a/src/components/cloud-agent-next/CloudChatContainer.tsx +++ b/src/components/cloud-agent-next/CloudChatContainer.tsx @@ -73,7 +73,11 @@ type CloudChatContainerProps = { refetchSessions: () => void; }; -export function CloudChatContainer({ organizationId, sessions, refetchSessions }: CloudChatContainerProps) { +export function CloudChatContainer({ + organizationId, + sessions, + refetchSessions, +}: CloudChatContainerProps) { const router = useRouter(); const searchParams = useSearchParams(); const trpc = useTRPC(); diff --git a/src/components/cloud-agent-next/CloudChatPage.tsx b/src/components/cloud-agent-next/CloudChatPage.tsx index 300e1646f..46f465e58 100644 --- a/src/components/cloud-agent-next/CloudChatPage.tsx +++ b/src/components/cloud-agent-next/CloudChatPage.tsx @@ -1,9 +1,9 @@ /** * Cloud Chat Page * - * Owns the sidebar session query so it runs in a stable component that does not - * re-render during the session-loading lifecycle, eliminating redundant - * unifiedSessions.list invocations that would otherwise be batched by tRPC. + * Owns the sidebar session query so it runs outside CloudChatContainer, + * whose frequent internal state changes would otherwise cause redundant + * unifiedSessions.list invocations batched by tRPC. */ 'use client'; diff --git a/src/components/cloud-agent/CloudChatContainer.tsx b/src/components/cloud-agent/CloudChatContainer.tsx index 068ee1ab7..6ffd5987a 100644 --- a/src/components/cloud-agent/CloudChatContainer.tsx +++ b/src/components/cloud-agent/CloudChatContainer.tsx @@ -86,7 +86,11 @@ type ResumeConfigState = | { status: 'persisted'; config: ResumeConfig } | { status: 'failed'; config: ResumeConfig; error: Error }; -export function CloudChatContainer({ organizationId, sessions, refetchSessions }: CloudChatContainerProps) { +export function CloudChatContainer({ + organizationId, + sessions, + refetchSessions, +}: CloudChatContainerProps) { const router = useRouter(); const searchParams = useSearchParams(); const trpc = useTRPC(); diff --git a/src/components/cloud-agent/CloudChatPage.tsx b/src/components/cloud-agent/CloudChatPage.tsx index 300e1646f..46f465e58 100644 --- a/src/components/cloud-agent/CloudChatPage.tsx +++ b/src/components/cloud-agent/CloudChatPage.tsx @@ -1,9 +1,9 @@ /** * Cloud Chat Page * - * Owns the sidebar session query so it runs in a stable component that does not - * re-render during the session-loading lifecycle, eliminating redundant - * unifiedSessions.list invocations that would otherwise be batched by tRPC. + * Owns the sidebar session query so it runs outside CloudChatContainer, + * whose frequent internal state changes would otherwise cause redundant + * unifiedSessions.list invocations batched by tRPC. */ 'use client';