Skip to content

fix: phase 1#476

Merged
urjitc merged 2 commits into
mainfrom
fix/thread-runtime
Apr 28, 2026
Merged

fix: phase 1#476
urjitc merged 2 commits into
mainfrom
fix/thread-runtime

Conversation

@urjitc

@urjitc urjitc commented Apr 28, 2026

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features

    • Thread list now orders active threads by activity, with most recently started conversations appearing first.
  • Bug Fixes

    • Improved chat thread scroll behavior with better auto-positioning and message pinning when switching threads.
  • Improvements

    • Enhanced error handling and user-facing error messages in chat conversations.
    • Refined chat state management for improved reliability and thread lifecycle handling.

@vercel

vercel Bot commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

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

Project Deployment Actions Updated (UTC)
thinkex Canceled Canceled Apr 28, 2026 6:13am

Request Review

@coderabbitai

coderabbitai Bot commented Apr 28, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@urjitc has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 45 minutes and 22 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d24411c0-15a6-45b8-88f2-730b3131f5e0

📥 Commits

Reviewing files that changed from the base of the PR and between 755eaf3 and 906bb6c.

📒 Files selected for processing (13)
  • src/app/api/chat/route.ts
  • src/components/chat/ChatProvider.tsx
  • src/components/chat/ChatRuntimes.tsx
  • src/components/chat/Messages.tsx
  • src/components/chat/ThreadBody.tsx
  • src/components/chat/ThreadListDropdown.tsx
  • src/components/chat/use-thread-viewport-controller.ts
  • src/components/layout/DashboardLayout.tsx
  • src/lib/ai/supermemory.ts
  • src/lib/chat/__tests__/thread-order.test.ts
  • src/lib/chat/__tests__/thread-viewport-policy.test.ts
  • src/lib/chat/thread-order.ts
  • src/lib/chat/thread-runtime-state.ts
📝 Walkthrough

Walkthrough

The PR consolidates chat runtime lifecycle management from a separate ChatRuntimes.tsx context into ChatProvider.tsx, which now owns per-thread runtime instances and handles thread hydration, error management, and status tracking. It introduces a viewport controller for message scrolling logic, adds thread ordering by runtime activity, and provides new hooks useChatThreadManager and useThreadStatus. It also adds Supermemory workspace migration helpers. The ChatRuntimes provider is removed entirely from the component hierarchy.

Changes

Cohort / File(s) Summary
Chat Provider Runtime Consolidation
src/components/chat/ChatProvider.tsx, src/components/chat/ChatRuntimes.tsx, src/components/layout/DashboardLayout.tsx
Moves per-thread runtime lifecycle ownership and status tracking from ChatRuntimes context into ChatProvider. Adds runtime creation deduplication, structured error handling with toast display, and new hooks useChatThreadManager and useThreadStatus. Removes ChatRuntimesProvider from layout hierarchy.
Message Viewport & Scrolling
src/components/chat/Messages.tsx, src/components/chat/use-thread-viewport-controller.ts
Extracts viewport measurement and scroll auto-positioning logic into new useThreadViewportController hook. Removes local state and lifecycle effects from Messages, delegates to controller for ref management and scroll actions.
Thread Rendering & Selection
src/components/chat/ThreadBody.tsx
Adds threadId key to Messages component to force remount on thread switches instead of component reuse.
Thread List Ordering & Management
src/components/chat/ThreadListDropdown.tsx
Switches from query-order to activity-based ordering via orderThreadsByRuntimeActivity. Updates runtime disposal calls to use useChatThreadManager. Adjusts edit-mode lifecycle to explicitly set title on enter rather than via effect.
Chat Runtime State Utilities
src/lib/chat/thread-runtime-state.ts, src/lib/chat/thread-order.ts
Adds type definitions for ActiveChatStatus and ThreadStatus. Implements isRunningThreadStatus helper and orderThreadsByRuntimeActivity sorter for activity-based thread ordering.
Viewport Controller Utilities
src/components/chat/use-thread-viewport-controller.ts
Introduces resolveThreadViewportAction and useThreadViewportController for scroll positioning logic, supporting pin-to-user, snap-to-bottom, and idle states based on thread status transitions.
Supermemory Workspace Migration
src/lib/ai/supermemory-workspace-migration.ts
Adds workspace migration helpers including isThreadUuidCustomId, workspaceScopedContainerTag, and routeDocumentToWorkspaceTag for scoped container routing.
Tests
src/lib/chat/__tests__/thread-order.test.ts, src/lib/chat/__tests__/thread-viewport-policy.test.ts, src/lib/ai/__tests__/supermemory-workspace-migration.test.ts
Adds test suites for thread ordering behavior, viewport action resolution, and workspace migration routing logic.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

capy

Suggested reviewers

  • cubic-dev-ai

Poem

🐰 A rabbit hops through runtime threads,
No more scattered runtimes spread;
One provider owns them all with care,
Viewport scrolls with grace to spare,
Messages remount, threads reorder,
Chat flows smoother, line by line! 🎉

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 29.41% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'fix: phase 1' is vague and generic, using non-descriptive terms that do not convey meaningful information about the substantial architectural changes in this changeset. Replace with a descriptive title that captures the primary change, such as 'refactor: consolidate chat runtime lifecycle management into ChatProvider' or similar.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/thread-runtime

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 and usage tips.

Made-with: Cursor
@urjitc
urjitc force-pushed the fix/thread-runtime branch from 755eaf3 to eee794f Compare April 28, 2026 06:02

@cubic-dev-ai cubic-dev-ai 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.

No issues found across 13 files

Requires human review: This is a large refactor of core chat state management and scroll logic (1600+ lines). It touches thread lifecycles and UI behavior, requiring human review to ensure no regressions.

@coderabbitai coderabbitai 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.

Actionable comments posted: 3

🧹 Nitpick comments (1)
src/components/chat/ThreadListDropdown.tsx (1)

68-74: Consider memoizing visibleThreads to avoid repeat sort/filter on every render.

This is likely fine at small scale, but wrapping this computation in useMemo avoids unnecessary work during frequent UI re-renders.

Possible refactor
 import {
   type FC,
   useCallback,
   useEffect,
+  useMemo,
   useRef,
   useState,
 } from "react";
@@
-  const visibleThreads = orderThreadsByRuntimeActivity(
-    (threads ?? []).filter((thread) => thread.status !== "archived"),
-    {
-      getThreadStatus: getThreadStatusSnapshot,
-      getThreadLastStartedAt,
-    },
-  );
+  const visibleThreads = useMemo(
+    () =>
+      orderThreadsByRuntimeActivity(
+        (threads ?? []).filter((thread) => thread.status !== "archived"),
+        {
+          getThreadStatus: getThreadStatusSnapshot,
+          getThreadLastStartedAt,
+        },
+      ),
+    [threads, getThreadStatusSnapshot, getThreadLastStartedAt],
+  );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/chat/ThreadListDropdown.tsx` around lines 68 - 74,
visibleThreads is being recomputed on every render via
orderThreadsByRuntimeActivity(...). Wrap that computation in React.useMemo so
the filter/sort only runs when its inputs change: memoize the call that uses
threads, getThreadStatusSnapshot, and getThreadLastStartedAt (and any other
dependent values) so visibleThreads only recalculates when those dependencies
change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/chat/ChatProvider.tsx`:
- Around line 52-69: getStatusSubscription currently relies on the internal
Chat["~registerStatusCallback"] API which is unstable; replace this by wiring
into the documented useChat hook outputs: stop referencing
"~registerStatusCallback" in getStatusSubscription and instead accept/consume
the stable `status` field from useChat (or the hook's returned object) and
register lifecycle callbacks via the documented options such as `onFinish`,
`onError`, etc.; concretely, update getStatusSubscription/getter call sites to
take the hook's `status` (or a documented subscriber API) and return a proper
unsubscribe function created from the documented callbacks rather than binding
to "~registerStatusCallback", and update any places using getStatusSubscription
to provide the useChat `status` or pass through the hook's onFinish/onError
handlers.
- Around line 351-360: The current catch in the opts.hydrate block (inside
ChatProvider, around the initial variable assignment) swallows all errors and
forces initial = [], hiding transient/server failures; since fetchThreadMessages
already returns [] for 404, change the catch to log the error and rethrow (or
propagate it) instead of setting initial = [], so only genuine "not found"
becomes empty while other errors surface to the caller; update the try/catch
around fetchThreadMessages(threadId) (referencing opts.hydrate,
fetchThreadMessages, and initial) accordingly.

In `@src/lib/ai/supermemory-workspace-migration.ts`:
- Around line 44-49: The check for empty customId should trim whitespace first
so whitespace-only values are treated as missing: in the function that uses
customId (the block creating trimmed and calling isThreadUuidCustomId), move or
add the trim earlier so you evaluate trimmed === "" or trimmed == null before
calling isThreadUuidCustomId; return { status: "unresolved", reason:
"no_custom_id" } for null/undefined/blank (trimmed === "") and only call
isThreadUuidCustomId(trimmed) afterwards to decide on { status: "unresolved",
reason: "non_thread_custom_id" }.

---

Nitpick comments:
In `@src/components/chat/ThreadListDropdown.tsx`:
- Around line 68-74: visibleThreads is being recomputed on every render via
orderThreadsByRuntimeActivity(...). Wrap that computation in React.useMemo so
the filter/sort only runs when its inputs change: memoize the call that uses
threads, getThreadStatusSnapshot, and getThreadLastStartedAt (and any other
dependent values) so visibleThreads only recalculates when those dependencies
change.
🪄 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: ee917791-edad-4642-9258-14f6a398298c

📥 Commits

Reviewing files that changed from the base of the PR and between cd37e6f and 755eaf3.

📒 Files selected for processing (13)
  • src/components/chat/ChatProvider.tsx
  • src/components/chat/ChatRuntimes.tsx
  • src/components/chat/Messages.tsx
  • src/components/chat/ThreadBody.tsx
  • src/components/chat/ThreadListDropdown.tsx
  • src/components/chat/use-thread-viewport-controller.ts
  • src/components/layout/DashboardLayout.tsx
  • src/lib/ai/__tests__/supermemory-workspace-migration.test.ts
  • src/lib/ai/supermemory-workspace-migration.ts
  • src/lib/chat/__tests__/thread-order.test.ts
  • src/lib/chat/__tests__/thread-viewport-policy.test.ts
  • src/lib/chat/thread-order.ts
  • src/lib/chat/thread-runtime-state.ts
💤 Files with no reviewable changes (1)
  • src/components/chat/ChatRuntimes.tsx

Comment on lines +52 to +69
function getStatusSubscription(chat: Chat<ChatMessage>) {
const register = (
chat as Chat<ChatMessage> & {
"~registerStatusCallback"?: (onChange: () => void) => () => void;
}
)["~registerStatusCallback"];

if (typeof register !== "function") {
if (process.env.NODE_ENV !== "production") {
console.warn(
"[ChatProvider] Chat['~registerStatusCallback'] is unavailable. Per-thread status subscriptions are disabled.",
);
}
return null;
}

return register.bind(chat);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

For @ai-sdk/reactversion 3.0.170, isChat["~registerStatusCallback"] a documented/stable public API? If not, what is the supported way to subscribe to chat status changes?

💡 Result:

Not stable/public. In @ai-sdk/react’s public documentation for useChat, the documented supported callback/observable surface is the hook’s returned status plus documented callbacks like onToolCall, onData, onFinish, and onError (and related options). The symbol Chat["registerStatusCallback"] is an internal “” method used by the hook to wire updates via React’s useSyncExternalStore, not a documented/stable public API. Supported way to subscribe to chat status changes: - Use the status value returned by useChat and react to it in your component (e.g., in a useEffect). Example: const { status } = useChat(...); useEffect( => { // status changes: e.g. "submitted" | "streaming" | "ready" | "error" // ...handle it }, [status]); Notes: - The useChat hook’s implementation internally calls chatRef.current['~registerStatusCallback'] to support the status state, which indicates that ~registerStatusCallback is intentionally private/internal. - For “push-style” reactions during the lifecycle, prefer documented useChat callbacks such as onFinish and onError.

Citations:


Refactor status subscription to use documented Chat API instead of internal ~registerStatusCallback.

The code relies on Chat["~registerStatusCallback"], which is an internal (non-public) API. While the code handles its absence gracefully, if this internal method is removed or renamed in future @ai-sdk/react versions, per-thread status subscriptions will silently no-op, breaking reactivity and thread-order updates.

Instead, use the documented status field returned by useChat hook and the stable callback options (onFinish, onError, etc.) provided by the hook to subscribe to chat state changes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/chat/ChatProvider.tsx` around lines 52 - 69,
getStatusSubscription currently relies on the internal
Chat["~registerStatusCallback"] API which is unstable; replace this by wiring
into the documented useChat hook outputs: stop referencing
"~registerStatusCallback" in getStatusSubscription and instead accept/consume
the stable `status` field from useChat (or the hook's returned object) and
register lifecycle callbacks via the documented options such as `onFinish`,
`onError`, etc.; concretely, update getStatusSubscription/getter call sites to
take the hook's `status` (or a documented subscriber API) and return a proper
unsubscribe function created from the documented callbacks rather than binding
to "~registerStatusCallback", and update any places using getStatusSubscription
to provide the useChat `status` or pass through the hook's onFinish/onError
handlers.

Comment on lines +351 to +360
if (opts.hydrate) {
try {
initial = await fetchThreadMessages(threadId);
} catch (error) {
console.warn("[ChatProvider] failed to hydrate thread", {
threadId,
error,
});
initial = [];
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don’t silently turn hydration failures into empty thread history.

This catch path converts any load failure into [], so an existing thread can appear blank on transient/server errors. fetchThreadMessages already maps 404 to empty; other errors should be surfaced explicitly.

Suggested fix
-        if (opts.hydrate) {
-          try {
-            initial = await fetchThreadMessages(threadId);
-          } catch (error) {
-            console.warn("[ChatProvider] failed to hydrate thread", {
-              threadId,
-              error,
-            });
-            initial = [];
-          }
-        }
+        if (opts.hydrate) {
+          initial = await fetchThreadMessages(threadId);
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/chat/ChatProvider.tsx` around lines 351 - 360, The current
catch in the opts.hydrate block (inside ChatProvider, around the initial
variable assignment) swallows all errors and forces initial = [], hiding
transient/server failures; since fetchThreadMessages already returns [] for 404,
change the catch to log the error and rethrow (or propagate it) instead of
setting initial = [], so only genuine "not found" becomes empty while other
errors surface to the caller; update the try/catch around
fetchThreadMessages(threadId) (referencing opts.hydrate, fetchThreadMessages,
and initial) accordingly.

Comment on lines +44 to +49
if (customId == null || customId === "") {
return { status: "unresolved", reason: "no_custom_id" };
}
const trimmed = customId.trim();
if (!isThreadUuidCustomId(trimmed)) {
return { status: "unresolved", reason: "non_thread_custom_id" };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Treat blank customId values as missing.

The null/empty check happens before trimming, so whitespace-only values fall through to non_thread_custom_id. If blank IDs can appear from Supermemory, this should return no_custom_id instead.

Suggested fix
 export function routeDocumentToWorkspaceTag(args: {
   customId: string | null | undefined;
   userId: string;
   /** Map chat_threads.id → workspace_id for this user */
   threadToWorkspaceId: Map<string, string>;
 }): RoutingOutcome {
   const { customId, userId, threadToWorkspaceId } = args;
-  if (customId == null || customId === "") {
+  const trimmed = customId?.trim();
+  if (!trimmed) {
     return { status: "unresolved", reason: "no_custom_id" };
   }
-  const trimmed = customId.trim();
   if (!isThreadUuidCustomId(trimmed)) {
     return { status: "unresolved", reason: "non_thread_custom_id" };
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (customId == null || customId === "") {
return { status: "unresolved", reason: "no_custom_id" };
}
const trimmed = customId.trim();
if (!isThreadUuidCustomId(trimmed)) {
return { status: "unresolved", reason: "non_thread_custom_id" };
const trimmed = customId?.trim();
if (!trimmed) {
return { status: "unresolved", reason: "no_custom_id" };
}
if (!isThreadUuidCustomId(trimmed)) {
return { status: "unresolved", reason: "non_thread_custom_id" };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/ai/supermemory-workspace-migration.ts` around lines 44 - 49, The
check for empty customId should trim whitespace first so whitespace-only values
are treated as missing: in the function that uses customId (the block creating
trimmed and calling isThreadUuidCustomId), move or add the trim earlier so you
evaluate trimmed === "" or trimmed == null before calling isThreadUuidCustomId;
return { status: "unresolved", reason: "no_custom_id" } for null/undefined/blank
(trimmed === "") and only call isThreadUuidCustomId(trimmed) afterwards to
decide on { status: "unresolved", reason: "non_thread_custom_id" }.

@greptile-apps

greptile-apps Bot commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR consolidates ChatRuntimesProvider into ChatProvider (eliminating a redundant context layer), extracts scroll-position logic into useThreadViewportController, keys Messages by threadId for correct per-thread hook lifecycle, adds runtime-activity-based thread ordering in the dropdown, and ships a Supermemory workspace migration utility with tests.

Confidence Score: 4/5

Safe to merge; all findings are P2 style/hardening suggestions with no present runtime breakage.

No P0/P1 bugs found. Three P2 issues: a missing character-set guard in the Supermemory tag builder, an O(n) lookup in useThreadStatus that runs per thread row, and a narrow RAF cleanup race in useThreadViewportController. All are low-risk in practice.

src/components/chat/use-thread-viewport-controller.ts (RAF cleanup race), src/lib/ai/supermemory-workspace-migration.ts (character-set validation)

Important Files Changed

Filename Overview
src/components/chat/ChatProvider.tsx Major refactor: internalizes all runtime management from the deleted ChatRuntimesProvider, introduces ChatManagerContext, and exports useThreadStatus/useChatThreadManager. Logic is sound; minor O(n) concern in useThreadStatus.
src/components/chat/ChatRuntimes.tsx Deleted — its responsibilities (transport, runtime registry, error handling, status subscriptions) are absorbed into ChatProvider.
src/components/chat/use-thread-viewport-controller.ts New hook extracting scroll-position logic from Messages.tsx; a narrow RAF cleanup race can allow a stale scroll to fire when the effect re-runs between two consecutive frames.
src/components/chat/Messages.tsx Scroll logic cleanly extracted to useThreadViewportController; local state and RAF management removed correctly.
src/components/chat/ThreadBody.tsx Messages now keyed by threadId, giving each thread its own hook lifecycle and eliminating cross-thread scroll state bleed.
src/components/chat/ThreadListDropdown.tsx Migrated to new context hooks (useChatThreadManager, useThreadStatus from ChatProvider); edit-value sync moved to handleStartEdit; setIsEditing placement is functionally equivalent.
src/lib/ai/supermemory-workspace-migration.ts New migration utility for routing Supermemory docs to workspace-scoped container tags; validates length but omits Supermemory's character-set constraint (alphanumeric, hyphens, underscores).
src/lib/chat/thread-order.ts New stable-sort helper that bubbles active threads to the top while preserving server ordering for idle threads. Correctly uses a readonly copy before sorting.
src/lib/chat/thread-runtime-state.ts New shared type file extracting ActiveChatStatus, ThreadStatus, and isRunningThreadStatus predicate — clean and minimal.
src/components/layout/DashboardLayout.tsx Removes now-unnecessary ChatRuntimesProvider wrapper; ChatProvider is now self-contained.

Comments Outside Diff (1)

  1. src/lib/ai/supermemory-workspace-migration.ts, line 20-28 (link)

    P2 Missing character-set validation in workspaceScopedContainerTag

    The JSDoc explicitly says Supermemory enforces "alphanumeric plus hyphens and underscores only" for container tags. The function only validates length, so a userId containing characters outside that set (e.g. +, ., @ from a non-Clerk auth provider, or email-shaped IDs) would pass here but be rejected by Supermemory at write time — turning migration documents into silent no-ops.

    Consider adding a format guard alongside the length check:

    const SUPERMEMORY_TAG_RE = /^[a-zA-Z0-9_-]+$/;
    if (!SUPERMEMORY_TAG_RE.test(tag)) {
      throw new Error(
        `workspaceScopedContainerTag: tag contains characters not allowed by Supermemory: "${tag}"`,
      );
    }

    Fix in Cursor

Fix All in Cursor

Reviews (1): Last reviewed commit: "fix: phase 1" | Re-trigger Greptile

Comment on lines 185 to 205
@@ -64,76 +195,315 @@ function generateThreadId(): string {
}
return `thread-${Math.random().toString(36).slice(2)}-${Date.now()}`;
}
/**
* Owns the *visible* threadId selection for a workspace and binds the
* current Chat runtime to the rest of the chat surface.
*
* Concurrent generation across threads is achieved by storing one `Chat`
* instance per threadId in `<ChatRuntimesProvider>`'s in-memory registry.
* Switching threads simply rebinds via `useChat({ chat })`; previous
* runtimes keep streaming in the background.
*/

export function ChatProvider({ workspaceId, children }: ChatProviderProps) {
const queryClient = useQueryClient();

const persistedThreadId = useWorkspaceStore(
selectCurrentThreadId(workspaceId),
);
const setCurrentThreadId = useWorkspaceStore(

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.

P2 O(n) includes inside useThreadStatus — called per thread row

aliveThreadIds.includes(threadId) is O(n) and runs on every render for each ThreadListItemRow in ThreadListDropdown. For workspaces with many threads this adds up to O(n²) linear scans per render cycle. Storing alive thread IDs in a Set (or deriving a Set from the array in the same useState) would make this lookup O(1).

// e.g. expose an aliveThreadIdSet derived from the same state:
const chat =
  threadId && aliveThreadIdSet.has(threadId)
    ? getRuntime(threadId)
    : undefined;

Fix in Cursor

Comment on lines +144 to +158
let innerFrameId: number | null = null;
const outerFrameId = requestAnimationFrame(() => {
if (previousStatus == null) {
innerFrameId = requestAnimationFrame(runScroll);
return;
}
runScroll();
});

return () => {
cancelAnimationFrame(outerFrameId);
if (innerFrameId !== null) {
cancelAnimationFrame(innerFrameId);
}
};

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.

P2 Inner RAF not cancelled when outer fires before cleanup runs

innerFrameId is assigned inside the outer RAF callback. If React re-runs this effect (e.g. status or viewportHeight changes) between the two frames, the cleanup runs, cancelAnimationFrame(outerFrameId) is a no-op (the outer RAF already fired), and innerFrameId is still null in the cleanup closure — so the inner RAF is not cancelled. The inner RAF then calls runScroll() against a stale list snapshot from the previous effect invocation.

In practice this race window is tiny (~16 ms), but on slow renders or during stress-testing it can fire spurious scrolls. A reliable fix is a cancelled flag:

let cancelled = false;
const outerFrameId = requestAnimationFrame(() => {
  if (cancelled) return;
  if (previousStatus == null) {
    requestAnimationFrame(() => { if (!cancelled) runScroll(); });
    return;
  }
  runScroll();
});

return () => {
  cancelled = true;
  cancelAnimationFrame(outerFrameId);
};

Fix in Cursor

@urjitc
urjitc merged commit ce9a891 into main Apr 28, 2026
5 of 6 checks passed
@github-project-automation github-project-automation Bot moved this from Backlog to Done in Dev Board Apr 28, 2026

@capy-ai capy-ai 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.

Added 1 comment

Comment thread src/lib/ai/supermemory.ts
? threadId
: `user:${userId}`;

const containerTag = `${userId}_${workspaceId}`;

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.

[🟡 Medium] [🟡 Investigate]

The containerTag passed to withSupermemory changed from userId to ${userId}_${workspaceId}. Since Supermemory indexes stored memories by containerTag, all memories previously stored under the old userId-only tag will no longer be retrievable — the new workspace-scoped tag creates a fresh, empty namespace for every existing user.

// src/lib/ai/supermemory.ts
const containerTag = `${userId}_${workspaceId}`;

If production users already have stored memory content, this change silently orphans it. Verify whether a migration step or backward-compatible fallback (e.g. querying both old and new tags) is needed, or confirm that orphaning old data is acceptable.

@urjitc
urjitc deleted the fix/thread-runtime branch June 29, 2026 17:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant