Skip to content

AI SDK v6 and goodbye RSC#164

Merged
srkirkland merged 4 commits intomainfrom
srk/goodbye-rsc
Feb 25, 2026
Merged

AI SDK v6 and goodbye RSC#164
srkirkland merged 4 commits intomainfrom
srk/goodbye-rsc

Conversation

@srkirkland
Copy link
Copy Markdown
Member

@srkirkland srkirkland commented Feb 25, 2026

Switching to useChat() like Rocky

Summary by CodeRabbit

  • New Features

    • Implemented chat API endpoint with streaming responses and citation handling
    • Added chat history retrieval functionality
    • Development authentication bypass for testing environments
    • Enhanced chat interaction features including improved feedback and sharing workflows
  • Tests

    • Introduced Vitest testing framework with initial configuration

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Feb 25, 2026

📝 Walkthrough

Walkthrough

This PR refactors the chat architecture from an AI state-based model to an explicit props and hook-based approach, adds new API routes for chat and history operations, introduces a development authentication bypass mechanism, and updates AI SDK dependencies from RSC to React while removing related provider wrappers and hooks.

Changes

Cohort / File(s) Summary
Configuration & Dependencies
.gitignore, web/eslint.config.mjs, web/package.json, web/vitest.config.mts
Added Playwright and agents directory ignores. Restructured ESLint config with per-file targeting and explicit import ordering. Updated AI SDK versions (openai ^3.0.33, ai ^6.0.99) and replaced @ai-sdk/rsc with @ai-sdk/react; added vitest test runner with configuration.
Authentication & Dev Bypass
web/src/auth.ts, web/src/lib/devAuthBypass.ts, web/src/app/auth/login/page.tsx
Integrated dev authentication bypass into NextAuth JWT flow with provider-based credentials. Added utilities for reading/validating dev user ID and group. Exposed development-only login UI with form-based bypass submission when enabled.
API Routes
web/src/app/api/chat/route.ts, web/src/app/api/history/route.ts, web/src/app/api/documents/route.ts
Introduced POST /api/chat handler with embedding computation, elastic search, and AI streaming with per-chunk thoughts exposure; added GET /api/history endpoint for fetching group chat history. Removed debug logging from documents endpoint.
Chat Page Refactoring
web/src/app/(home)/[group]/chat/[chatid]/page.tsx, web/src/app/(home)/[group]/share/[shareid]/page.tsx, web/src/app/(home)/[group]/chat/[chatid]/loading.tsx
Removed AI wrapper in favor of direct MainContent rendering with initialChat prop; added auth check in generateMetadata for share page; deleted unused loading component.
Main Chat Component
web/src/components/chat/main.tsx
Migrated from AI state to useChat hook with transport configuration. Added local state for messages, focus, chat metadata. Implemented new event handlers (onFocusSelection, onQuestionSubmit, onFeedbackUpdate, onShareIdUpdate). Added message rendering with loading/error states and auto-navigation to saved chats. Transformed chat history to UI format via helper functions.
Answer Components Refactoring
web/src/components/chat/answer/chatActions.tsx, web/src/components/chat/answer/feedbackButtons.tsx, web/src/components/chat/answer/shareModal.tsx, web/src/components/chat/answer/wonkMessage.tsx
Converted from AI state hooks to props-based interfaces (chat, onReactionUpdate, onShareIdUpdate). Updated event tracking and side effects to use new chat prop. Simplified WonkMessage to accept content directly and optional thought/children props instead of StreamableValue.
Removed ChatActions internal usage in favor of children slot.
Input Components
web/src/components/chat/ask/chatInput.tsx, web/src/components/chat/ask/defaultQuestions.tsx
Converted ChatInput to stateless prop-driven component with group, focus, onFocusSelection, onQuestionSubmit callbacks. Removed AI state and server action dependencies. Deleted DefaultQuestions component entirely.
Chat History Management
web/src/components/chatHistory/chatHistory.tsx, web/src/components/chatHistory/chatHistoryWrapper.tsx
Added status-based handling for getChatHistoryForGroup outcomes. Enhanced ChatHistoryWrapper with group prop and internal state management; added effect to refetch history from /api/history API when route changes, with timestamp parsing and unmount cancellation.
Server Actions & AI Provider Removal
web/src/lib/actions.tsx, web/src/lib/aiProvider.tsx, web/src/lib/hooks/useStreamableText.ts, web/src/lib/gtagProvider.tsx
Removed submitUserMessage action and entire AI provider module. Retained and strongly-typed remaining actions (shareChat, unshareChat, submitFeedback, deleteChatFromSidebar). Deleted useStreamableText hook. Removed console linting directive.
Citation Transform
web/src/lib/chat/citationsTransform.ts, web/src/lib/chat/citationsTransform.test.ts
Introduced CitationPolicy type and createCitationsTransform factory for processing inline <c:id> markers into footnote references, accumulating citations, and appending Markdown citations section on completion with document metadata links.
Layout & Utilities
web/src/components/layout/collapsibleSidebarProvider.tsx, web/src/components/chatHistory/deleteChatButton.tsx, web/src/components/global-error.tsx
Removed ESLint directive in collapsibleSidebarProvider. Made icon variant mutation immutable in deleteChatButton. Reordered imports in global-error (no functional change).

Sequence Diagram

sequenceDiagram
    participant Client as Client
    participant MainContent as MainContent<br/>(useChat)
    participant ChatAPI as POST /api/chat
    participant Embeddings as Embeddings<br/>Service
    participant ElasticSearch as ElasticSearch
    participant AIModel as AI Model
    participant DB as Database
    participant Citations as Citations<br/>Transform

    Client->>MainContent: onQuestionSubmit(question)
    MainContent->>ChatAPI: POST { message, group, focus }
    ChatAPI->>ChatAPI: Validate auth & request
    ChatAPI->>Embeddings: Compute embedding(question)
    Embeddings-->>ChatAPI: embedding vector
    ChatAPI->>ElasticSearch: Search with embedding
    ElasticSearch-->>ChatAPI: Relevant documents
    ChatAPI->>ChatAPI: Construct system message<br/>with context
    ChatAPI->>AIModel: Stream with messages
    AIModel-->>ChatAPI: Token stream + thoughts
    ChatAPI->>Citations: Transform citations
    Citations-->>ChatAPI: [^1] references +<br/>Citations section
    ChatAPI-->>MainContent: Stream response chunks
    MainContent->>MainContent: Update UI messages<br/>& thoughts in real-time
    AIModel-->>ChatAPI: completion
    ChatAPI->>DB: Save system message,<br/>user input, response
    DB-->>ChatAPI: chatId
    ChatAPI-->>MainContent: Final metadata { chatId }
    MainContent->>MainContent: Auto-navigate to<br/>saved chat URL
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • Updates AI SDK and dependencies #150: Modifies AI SDK usage and dependencies across package.json and component imports, directly overlapping with the SDK migration from RSC to React in this PR.
  • Cyd/ucop #131: Updates the chat/group surface with group parameter propagation and ChatHistory modifications, sharing code-level changes to history APIs and chat components with this PR.
  • Rh/share #44: Modifies the chat-sharing surface (SharePage, ShareModal, shareChat/unshareChat actions), with direct overlap in the refactored share functionality in this PR.

Suggested reviewers

  • cydoval

Poem

🐰 From state to props, the rabbit did leap,
No more AI wrapping, just data runs deep,
With hooks and with routes, the new path is clear,
Citations and searches bring focus right here!
A chat that flows cleaner, more honest and true,
Hop on to review—there's much work to do! 🌟

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'AI SDK v6 and goodbye RSC' clearly and accurately summarizes the primary change: upgrading to AI SDK v6 and removing RSC (React Server Components) usage from the codebase.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch srk/goodbye-rsc

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

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 (16)
web/src/auth.ts (1)

118-128: Pre-existing type inconsistency: session.userId is typed as string but holds a number at runtime.

Both the SAML path (line 110) and the new bypass path (line 68) store userId as a number in the JWT token. The session callback casts it with as string (line 127), but this is a TypeScript-only assertion — the runtime value remains a number. Downstream consumers expecting a real string (e.g., string comparison with ===) could be surprised.

This is pre-existing and not introduced by this PR, but worth addressing at some point with an explicit String(token.userId) conversion.

Proposed fix
       if (session) {
-        session.userId = token.userId as string;
+        session.userId = String(token.userId);
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/auth.ts` around lines 118 - 128, The session callback in auth.ts
currently checks token.userId but then assigns it with a TypeScript assertion
(session.userId = token.userId as string), which leaves a numeric runtime value;
change the assignment in the async session({ session, token }) callback to
coerce token.userId to a real string (e.g., use String(token.userId)) after your
existing numeric validation so session.userId is an actual string at runtime and
matches its declared type for downstream consumers.
web/package.json (1)

6-6: Minor: mismatched quoting in predev script.

The closing " after the URL means to view the app falls outside the shell quotes. echo still prints it correctly, but the intent seems to be quoting the full sentence. Consider wrapping the entire message or dropping the inner quotes.

Proposed fix
-    "predev": "echo \"Use http://localhost:3000\" to view the app",
+    "predev": "echo 'Use http://localhost:3000 to view the app'",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/package.json` at line 6, The predev script in package.json has mismatched
quoting around the echo message for "Use http://localhost:3000" causing the
trailing text to sit outside the quotes; update the "predev" entry so the entire
message is consistently quoted (e.g., wrap the whole sentence in double quotes
or single quotes) or remove the inner quotes around the URL so the intent is
clear and the shell string is well-formed.
web/src/lib/devAuthBypass.ts (1)

56-63: Consider wrapping the Prisma call in a try/catch.

If the DB is unreachable during dev, the unhandled rejection from prisma.users.findUnique will surface as a 500 rather than a clean auth failure. For a dev-only utility this is low-priority, but a try/catch returning null would make failures less noisy.

Proposed fix
-      const user = await prisma.users.findUnique({
-        where: { id: userId },
-        select: { id: true, name: true, email: true },
-      });
-
-      if (!user) {
-        return null;
-      }
+      let user;
+      try {
+        user = await prisma.users.findUnique({
+          where: { id: userId },
+          select: { id: true, name: true, email: true },
+        });
+      } catch {
+        console.error('Dev bypass: failed to query user from DB');
+        return null;
+      }
+
+      if (!user) {
+        return null;
+      }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/lib/devAuthBypass.ts` around lines 56 - 63, Wrap the
prisma.users.findUnique(...) call in a try/catch so DB errors return null
instead of bubbling as an unhandled rejection; specifically, around the existing
call that assigns to `user` (the prisma.users.findUnique where { id: userId } is
passed), catch any error and return null (optionally log/debug the error) so the
dev auth bypass function fails cleanly.
web/src/app/auth/login/page.tsx (2)

78-94: Dev login section renders under "Available Campuses" heading — minor UX ordering issue.

The dev section appears between the "Available Campuses" <h4> (line 77) and the actual campus buttons (line 95). Consider moving it below the campus form or above the "Available Campuses" heading so the two aren't visually interleaved.

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

In `@web/src/app/auth/login/page.tsx` around lines 78 - 94, The dev-login JSX
guarded by devAuthBypassEnabled (the fragment containing the form using
devBypassSignInHandler, callbackUrl, and devAuthUserId) is currently rendered
between the "Available Campuses" <h4> and the campus buttons; move that entire
fragment either above the "Available Campuses" heading or after the campus
form/buttons so it no longer interleaves with the campus list—update the JSX
order to place the devAuthBypassEnabled block adjacent to other auth actions for
clearer UX.

42-56: The inline 'use server' directive is redundant.

The file already has 'use server' at line 1, which marks all exported/referenced async functions in this module as server actions. The inner directive on line 44 is harmless but unnecessary.

Also, the as any cast on line 55 is a known workaround for passing custom credential fields through NextAuth's signIn — acceptable here since the types don't account for provider-specific fields.

Remove redundant directive
 async function devBypassSignInHandler(formData: FormData) {
-  'use server';
   if (!devAuthBypassEnabled) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/app/auth/login/page.tsx` around lines 42 - 56, Remove the redundant
inner 'use server' directive from the devBypassSignInHandler function: open the
async function devBypassSignInHandler and delete the "'use server';" line inside
it (leave the module-level 'use server' intact), keeping the rest of the
function (including the signIn call with DEV_AUTH_BYPASS_PROVIDER_ID and the "as
any" cast) unchanged.
web/eslint.config.mjs (1)

30-32: react-refresh plugin is registered but no rules are enabled.

The plugin is added to the plugins map, but there are no react-refresh/* rules in the rules block. Typically you'd enable at least react-refresh/only-export-components to catch HMR-breaking exports.

💡 Suggested addition
     rules: {
+      'react-refresh/only-export-components': [
+        'warn',
+        { allowConstantExport: true },
+      ],
       eqeqeq: 'error',
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/eslint.config.mjs` around lines 30 - 32, The 'react-refresh' plugin is
registered in the plugins map but no plugin rules are enabled; add the
appropriate react-refresh rules (at minimum enable
"react-refresh/only-export-components") to the rules configuration so the plugin
actually enforces HMR-safe exports—update the rules block to include
"react-refresh/only-export-components": "warn" or "error" (and any other desired
react-refresh/* rules) to activate the plugin behavior.
web/src/lib/chat/citationsTransform.test.ts (1)

51-162: Good coverage of the core scenarios.

The three tests cover the happy path (replacement + footnotes), chunk-boundary handling, and the no-citations edge case. Consider adding a test for non-citation < characters in the text (e.g., "Compare <b>bold</b> and <c:0>") to verify they pass through untouched alongside real citations.

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

In `@web/src/lib/chat/citationsTransform.test.ts` around lines 51 - 162, Add a new
unit test in the existing describe('createCitationsTransform') suite to verify
non-citation "<" sequences are preserved while real citations are transformed:
use the existing test helper runTransform with basePolicies and a chunk sequence
containing mixed markup like "Compare <b>bold</b> and <c:0>" (or split across
chunks to exercise boundary handling), then assert the output contains the
unchanged "<b>bold</b>" text and that the citation "[^0]" and the corresponding
footnote from basePolicies are present; place this next to the other it(...)
cases so it uses the same runTransform helper and policies references.
web/src/lib/chat/citationsTransform.ts (2)

18-18: Consider narrowing the any in StreamTextTransform<any>.

If you have a type for the tools/settings used in this project, replacing any would improve type safety. Low priority since this is an internal transform.

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

In `@web/src/lib/chat/citationsTransform.ts` at line 18, The return type uses a
wide generic StreamTextTransform<any>; narrow it to the actual payload/interface
used by this transform (e.g., a tool/settings or message type) by replacing the
any with the specific type (for example ToolSettings, CitationPayload, or the
existing project type that represents transform input/output) and update the
function signature and any downstream usages to use that type; modify or import
the appropriate type and ensure StreamTextTransform<YourType> is used instead of
StreamTextTransform<any> in citationsTransform (referencing StreamTextTransform
and the function that returns it).

81-83: Buffer is silently discarded on finish-step — trailing text after last text-delta could be lost.

If the final text-delta chunk ends with a bare < (buffered as a potential citation start), Line 82 clears it without emitting. In practice this is extremely unlikely since it would require the LLM to end output mid-tag, but you could emit the buffer contents as-is before clearing for correctness:

💡 Optional defensive fix
       if (chunk.type === 'finish-step') {
+        if (buffer) {
+          assistantText += buffer;
+          controller.enqueue({ type: 'text-delta', text: buffer });
+        }
         buffer = '';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/lib/chat/citationsTransform.ts` around lines 81 - 83, In the
finish-step branch where buffer is reset, don't silently discard any remaining
buffered text: before setting buffer = '' (in the block that checks if
chunk.type === 'finish-step'), flush/emit the buffer contents as a final text
output (e.g., treat it like a final text-delta or append to the output array)
when buffer is non-empty so trailing characters (like a lone '<') aren't lost;
locate the buffer variable and the finish-step handling in citationsTransform
(the chunk/type === 'finish-step' branch) and add a guarded emit of buffer prior
to clearing it.
web/src/components/chatHistory/chatHistory.tsx (2)

43-45: Unused error binding in catch block.

The caught error is not logged or inspected. If intentionally ignored, use catch without a binding to avoid lint warnings. If not, consider logging it for observability.

💡 Suggested fix
-  } catch (error) {
+  } catch {
     status = 'error';
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/chatHistory/chatHistory.tsx` around lines 43 - 45, The
catch block in chatHistory.tsx currently declares a wasted error binding ("catch
(error) { status = 'error'; }"); either remove the unused binding by switching
to a bare catch ("catch { ... }") or use the binding to log/inspect the error
(e.g., call console.error or your app logger) before setting status to 'error'
so the exception isn't silently ignored; locate the catch in the function that
sets the status variable and apply one of these fixes.

72-72: !!chats is always truthy — chats defaults to [] (Line 33).

The guard can be simplified or removed since chats is always a (possibly empty) array at this point.

💡 Optional simplification
-          {!!chats && <ChatHistoryWrapper group={group} chats={chats} />}
+          <ChatHistoryWrapper group={group} chats={chats} />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/chatHistory/chatHistory.tsx` at line 72, The
double-negation guard (!!chats) is redundant because chats defaults to an array;
remove the boolean coercion and render ChatHistoryWrapper directly as
<ChatHistoryWrapper group={group} chats={chats} />; if the intent was to render
only when there are items, replace the guard with chats.length > 0 instead —
locate the usage of chats and ChatHistoryWrapper to make the change.
web/src/app/api/chat/route.ts (1)

86-149: No error handling inside the execute callback — stream errors will surface as opaque failures.

If getEmbeddings, getSearchResultsElastic, or convertToModelMessages throw, the error propagates through the stream with no user-friendly message or logging. Consider wrapping the body in a try/catch to write a meaningful error part or close the stream gracefully.

💡 Suggested approach
   const stream = createUIMessageStream({
     execute: async ({ writer }) => {
+      try {
         const writeThought = (thought: string) => {
           // ...
         };
         // ... rest of the execute body ...
+      } catch (err) {
+        // eslint-disable-next-line no-console
+        console.error('Chat stream error', err);
+        writer.write({
+          type: 'error',
+          errorText: 'Something went wrong. Please try again.',
+        });
+      }
     },
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/app/api/chat/route.ts` around lines 86 - 149, Wrap the entire
createUIMessageStream execute callback body (the async function passed to
execute) in a try/catch so any throw from getEmbeddings,
getSearchResultsElastic, convertToModelMessages, streamText, etc. is caught; in
the catch block call writer.write to emit a clear error message part (use same
message shape as writeThought but non-transient or a distinct type like
'data-error' with the error.message), optionally call writer.write a short
user-friendly note, and then close/finish the stream (e.g., ensure
writer.merge/finish is invoked or writer is closed) so the UI receives a
graceful failure instead of an opaque crash; keep references to
createUIMessageStream, execute, writeThought, getEmbeddings,
getSearchResultsElastic, convertToModelMessages, streamText, and
writer.write/merge when applying the change.
web/src/lib/actions.tsx (1)

43-53: deleteChatFromSidebar lacks a return type annotation, unlike its siblings.

Minor inconsistency — the other three actions have explicit Promise<string> or Promise<void> return types.

💡 Suggested fix
 export const deleteChatFromSidebar = async (
   chatId: string,
   isActiveChat: boolean
-) => {
+): Promise<void> => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/lib/actions.tsx` around lines 43 - 53, The deleteChatFromSidebar
function is missing an explicit return type; update its declaration to include
an explicit Promise<void> return type (i.e., change the async arrow signature
for deleteChatFromSidebar to end with : Promise<void>) so it matches the other
action signatures and keeps type consistency; keep the existing logic involving
removeChat, isWonkSuccess, WonkServerError, and redirect unchanged.
web/src/app/api/history/route.ts (1)

10-25: Route delegates auth to service layer; consider explicit auth check for consistency with chat route.

The getChatHistoryForGroup function properly handles authentication internally (calls auth() and checks session.userId), so this route is secure. However, the chat POST route checks session?.userId explicitly at the route level (lines 45-48), while this route defers to the service layer. For consistency and clarity, adding an explicit check at the route level would make the auth boundary self-documenting.

Note: The Number(result.status) on line 23 is correct—WonkStatusCodes values are strings like '401', which convert to numbers properly.

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

In `@web/src/app/api/history/route.ts` around lines 10 - 25, The GET route
delegates auth to getChatHistoryForGroup but should perform an explicit
route-level auth check for consistency with the chat POST route: call auth() at
the top of the GET handler, verify session?.userId exists, and if missing return
NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) before validating
group or invoking getChatHistoryForGroup; keep existing calls to
isValidGroupName, isWonkSuccess and Number(result.status) unchanged.
web/src/app/(home)/[group]/share/[shareid]/page.tsx (1)

32-35: Avoid gating shared-chat metadata behind auth if the page is publicly viewable.

On Line 33, unauthenticated users get a forced 'Chat' title, while Line 65 still renders shared content. This can degrade link previews and social unfurls for shared URLs.

Proposed adjustment
-  const session = (await auth()) as WonkSession;
-  if (!session?.userId) {
-    return { title: 'Chat' };
-  }
-
   const params = await props.params;
   const { shareid } = params;
   const result = await getCachedSharedChat(shareid);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/app/`(home)/[group]/share/[shareid]/page.tsx around lines 32 - 35,
Don't return a default title when auth() yields no session; instead compute page
metadata from the shared chat resource regardless of authentication. Replace the
early return that checks session?.userId in page.tsx with logic that calls the
same shared-content lookup used for rendering the share page (keep references to
auth(), session and the shared chat fetch used later in the file) and set the
title from that shared resource, falling back to 'Chat' only if the shared
metadata is absent. Ensure metadata generation does not depend on WonkSession
being present so public link previews and social unfurls can show the shared
chat info.
web/src/components/chatHistory/chatHistoryWrapper.tsx (1)

74-76: Don’t fully swallow refresh errors.

Ignoring all errors here makes history-refresh failures invisible and harder to debug. At minimum, log at debug/warn level or emit telemetry.

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

In `@web/src/components/chatHistory/chatHistoryWrapper.tsx` around lines 74 - 76,
The catch block in chatHistoryWrapper.tsx currently swallows all errors; modify
the catch to capture the error (e.g., catch (err)) and log it at debug/warn
level or emit telemetry instead of ignoring it. Locate the try/catch around the
history refresh logic (the refresh handling in chatHistoryWrapper) and replace
the empty catch with something like logger.warn("chat history refresh failed", {
err, stack: err?.stack }) or call your telemetry/metrics helper (e.g.,
emitTelemetry("chat.history.refresh.failed", { error: err })) so failures are
visible while preserving existing control flow.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web/src/components/chat/answer/feedbackButtons.tsx`:
- Around line 52-55: The catch block in feedbackButtons.tsx currently swallows
submission errors and only calls onReactionUpdate(undefined); update that catch
to (1) capture the thrown error (e), (2) still roll back via
onReactionUpdate(undefined) but also surface a user-facing notification (either
by invoking your existing toast/notification utility or setting local error
state that renders a small inline message in the FeedbackButtons component), and
(3) log the error (console.error or process logger) including e.message/details
for debugging; ensure the notification text is generic/user-friendly and
reference the same handler where onReactionUpdate is called so the UI rollback
and error display happen together.

In `@web/src/components/chat/answer/shareModal.tsx`:
- Around line 47-52: The share/unshare try/finally blocks (around shareChat and
unshareChat) lack a catch so failures silently reset loading; wrap the await
shareChat(chatId) and await unshareChat(chatId) calls in try/catch, call
onShareIdUpdate only on success, and in the catch set an explicit error state
(e.g., setIsLoading('error') or a new setShareError state) and log the error
(console.error or a logger) so the UI can surface the failure to the user before
the existing finally resets loading. Ensure you update both the shareChat path
(shareChat, onShareIdUpdate, setIsLoading) and the unshare path similarly.

In `@web/src/components/chat/main.tsx`:
- Around line 59-68: The local chat state initialized with React.useState(chat)
isn't synced when the initialChat prop changes, causing stale
group/focus/shareId values; update the component to either add a useEffect that
calls setChat(initialChat) with [initialChat] as dependency (referencing the
chat and setChat state variables and initialChat prop) or ensure the parent
forces remounts by adding a key={initialChat.id} to MainContent where it is
rendered so initialMessages (chatMessagesToUIMessages) and subsequent handlers
(onQuestionSubmit, onFocusSelection) always use the current chat.

---

Nitpick comments:
In `@web/eslint.config.mjs`:
- Around line 30-32: The 'react-refresh' plugin is registered in the plugins map
but no plugin rules are enabled; add the appropriate react-refresh rules (at
minimum enable "react-refresh/only-export-components") to the rules
configuration so the plugin actually enforces HMR-safe exports—update the rules
block to include "react-refresh/only-export-components": "warn" or "error" (and
any other desired react-refresh/* rules) to activate the plugin behavior.

In `@web/package.json`:
- Line 6: The predev script in package.json has mismatched quoting around the
echo message for "Use http://localhost:3000" causing the trailing text to sit
outside the quotes; update the "predev" entry so the entire message is
consistently quoted (e.g., wrap the whole sentence in double quotes or single
quotes) or remove the inner quotes around the URL so the intent is clear and the
shell string is well-formed.

In `@web/src/app/`(home)/[group]/share/[shareid]/page.tsx:
- Around line 32-35: Don't return a default title when auth() yields no session;
instead compute page metadata from the shared chat resource regardless of
authentication. Replace the early return that checks session?.userId in page.tsx
with logic that calls the same shared-content lookup used for rendering the
share page (keep references to auth(), session and the shared chat fetch used
later in the file) and set the title from that shared resource, falling back to
'Chat' only if the shared metadata is absent. Ensure metadata generation does
not depend on WonkSession being present so public link previews and social
unfurls can show the shared chat info.

In `@web/src/app/api/chat/route.ts`:
- Around line 86-149: Wrap the entire createUIMessageStream execute callback
body (the async function passed to execute) in a try/catch so any throw from
getEmbeddings, getSearchResultsElastic, convertToModelMessages, streamText, etc.
is caught; in the catch block call writer.write to emit a clear error message
part (use same message shape as writeThought but non-transient or a distinct
type like 'data-error' with the error.message), optionally call writer.write a
short user-friendly note, and then close/finish the stream (e.g., ensure
writer.merge/finish is invoked or writer is closed) so the UI receives a
graceful failure instead of an opaque crash; keep references to
createUIMessageStream, execute, writeThought, getEmbeddings,
getSearchResultsElastic, convertToModelMessages, streamText, and
writer.write/merge when applying the change.

In `@web/src/app/api/history/route.ts`:
- Around line 10-25: The GET route delegates auth to getChatHistoryForGroup but
should perform an explicit route-level auth check for consistency with the chat
POST route: call auth() at the top of the GET handler, verify session?.userId
exists, and if missing return NextResponse.json({ error: 'Unauthorized' }, {
status: 401 }) before validating group or invoking getChatHistoryForGroup; keep
existing calls to isValidGroupName, isWonkSuccess and Number(result.status)
unchanged.

In `@web/src/app/auth/login/page.tsx`:
- Around line 78-94: The dev-login JSX guarded by devAuthBypassEnabled (the
fragment containing the form using devBypassSignInHandler, callbackUrl, and
devAuthUserId) is currently rendered between the "Available Campuses" <h4> and
the campus buttons; move that entire fragment either above the "Available
Campuses" heading or after the campus form/buttons so it no longer interleaves
with the campus list—update the JSX order to place the devAuthBypassEnabled
block adjacent to other auth actions for clearer UX.
- Around line 42-56: Remove the redundant inner 'use server' directive from the
devBypassSignInHandler function: open the async function devBypassSignInHandler
and delete the "'use server';" line inside it (leave the module-level 'use
server' intact), keeping the rest of the function (including the signIn call
with DEV_AUTH_BYPASS_PROVIDER_ID and the "as any" cast) unchanged.

In `@web/src/auth.ts`:
- Around line 118-128: The session callback in auth.ts currently checks
token.userId but then assigns it with a TypeScript assertion (session.userId =
token.userId as string), which leaves a numeric runtime value; change the
assignment in the async session({ session, token }) callback to coerce
token.userId to a real string (e.g., use String(token.userId)) after your
existing numeric validation so session.userId is an actual string at runtime and
matches its declared type for downstream consumers.

In `@web/src/components/chatHistory/chatHistory.tsx`:
- Around line 43-45: The catch block in chatHistory.tsx currently declares a
wasted error binding ("catch (error) { status = 'error'; }"); either remove the
unused binding by switching to a bare catch ("catch { ... }") or use the binding
to log/inspect the error (e.g., call console.error or your app logger) before
setting status to 'error' so the exception isn't silently ignored; locate the
catch in the function that sets the status variable and apply one of these
fixes.
- Line 72: The double-negation guard (!!chats) is redundant because chats
defaults to an array; remove the boolean coercion and render ChatHistoryWrapper
directly as <ChatHistoryWrapper group={group} chats={chats} />; if the intent
was to render only when there are items, replace the guard with chats.length > 0
instead — locate the usage of chats and ChatHistoryWrapper to make the change.

In `@web/src/components/chatHistory/chatHistoryWrapper.tsx`:
- Around line 74-76: The catch block in chatHistoryWrapper.tsx currently
swallows all errors; modify the catch to capture the error (e.g., catch (err))
and log it at debug/warn level or emit telemetry instead of ignoring it. Locate
the try/catch around the history refresh logic (the refresh handling in
chatHistoryWrapper) and replace the empty catch with something like
logger.warn("chat history refresh failed", { err, stack: err?.stack }) or call
your telemetry/metrics helper (e.g.,
emitTelemetry("chat.history.refresh.failed", { error: err })) so failures are
visible while preserving existing control flow.

In `@web/src/lib/actions.tsx`:
- Around line 43-53: The deleteChatFromSidebar function is missing an explicit
return type; update its declaration to include an explicit Promise<void> return
type (i.e., change the async arrow signature for deleteChatFromSidebar to end
with : Promise<void>) so it matches the other action signatures and keeps type
consistency; keep the existing logic involving removeChat, isWonkSuccess,
WonkServerError, and redirect unchanged.

In `@web/src/lib/chat/citationsTransform.test.ts`:
- Around line 51-162: Add a new unit test in the existing
describe('createCitationsTransform') suite to verify non-citation "<" sequences
are preserved while real citations are transformed: use the existing test helper
runTransform with basePolicies and a chunk sequence containing mixed markup like
"Compare <b>bold</b> and <c:0>" (or split across chunks to exercise boundary
handling), then assert the output contains the unchanged "<b>bold</b>" text and
that the citation "[^0]" and the corresponding footnote from basePolicies are
present; place this next to the other it(...) cases so it uses the same
runTransform helper and policies references.

In `@web/src/lib/chat/citationsTransform.ts`:
- Line 18: The return type uses a wide generic StreamTextTransform<any>; narrow
it to the actual payload/interface used by this transform (e.g., a tool/settings
or message type) by replacing the any with the specific type (for example
ToolSettings, CitationPayload, or the existing project type that represents
transform input/output) and update the function signature and any downstream
usages to use that type; modify or import the appropriate type and ensure
StreamTextTransform<YourType> is used instead of StreamTextTransform<any> in
citationsTransform (referencing StreamTextTransform and the function that
returns it).
- Around line 81-83: In the finish-step branch where buffer is reset, don't
silently discard any remaining buffered text: before setting buffer = '' (in the
block that checks if chunk.type === 'finish-step'), flush/emit the buffer
contents as a final text output (e.g., treat it like a final text-delta or
append to the output array) when buffer is non-empty so trailing characters
(like a lone '<') aren't lost; locate the buffer variable and the finish-step
handling in citationsTransform (the chunk/type === 'finish-step' branch) and add
a guarded emit of buffer prior to clearing it.

In `@web/src/lib/devAuthBypass.ts`:
- Around line 56-63: Wrap the prisma.users.findUnique(...) call in a try/catch
so DB errors return null instead of bubbling as an unhandled rejection;
specifically, around the existing call that assigns to `user` (the
prisma.users.findUnique where { id: userId } is passed), catch any error and
return null (optionally log/debug the error) so the dev auth bypass function
fails cleanly.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dff63b8 and e0c78df.

⛔ Files ignored due to path filters (1)
  • web/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (31)
  • .gitignore
  • web/eslint.config.mjs
  • web/package.json
  • web/src/app/(home)/[group]/chat/[chatid]/loading.tsx
  • web/src/app/(home)/[group]/chat/[chatid]/page.tsx
  • web/src/app/(home)/[group]/share/[shareid]/page.tsx
  • web/src/app/api/chat/route.ts
  • web/src/app/api/documents/route.ts
  • web/src/app/api/history/route.ts
  • web/src/app/auth/login/page.tsx
  • web/src/app/global-error.tsx
  • web/src/auth.ts
  • web/src/components/chat/answer/chatActions.tsx
  • web/src/components/chat/answer/feedbackButtons.tsx
  • web/src/components/chat/answer/shareModal.tsx
  • web/src/components/chat/answer/wonkMessage.tsx
  • web/src/components/chat/ask/chatInput.tsx
  • web/src/components/chat/ask/defaultQuestions.tsx
  • web/src/components/chat/main.tsx
  • web/src/components/chatHistory/chatHistory.tsx
  • web/src/components/chatHistory/chatHistoryWrapper.tsx
  • web/src/components/chatHistory/deleteChatButton.tsx
  • web/src/components/layout/collapsibleSidebarProvider.tsx
  • web/src/lib/actions.tsx
  • web/src/lib/aiProvider.tsx
  • web/src/lib/chat/citationsTransform.test.ts
  • web/src/lib/chat/citationsTransform.ts
  • web/src/lib/devAuthBypass.ts
  • web/src/lib/gtagProvider.tsx
  • web/src/lib/hooks/useStreamableText.ts
  • web/vitest.config.mts
💤 Files with no reviewable changes (7)
  • web/src/app/api/documents/route.ts
  • web/src/components/layout/collapsibleSidebarProvider.tsx
  • web/src/lib/aiProvider.tsx
  • web/src/lib/gtagProvider.tsx
  • web/src/app/(home)/[group]/chat/[chatid]/loading.tsx
  • web/src/components/chat/ask/defaultQuestions.tsx
  • web/src/lib/hooks/useStreamableText.ts

Comment on lines 52 to 55
} catch (e) {
// TODO: handle error
setAIState((currentAIState) => ({
...currentAIState,
reaction: undefined, // unset the reaction on error
}));
onReactionUpdate(undefined);
}
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.

⚠️ Potential issue | 🟡 Minor

Surface submission failures to the user instead of silently reverting.

Current catch path rolls back reaction but gives no feedback. Users can’t tell whether the action failed or was ignored.

Would you like me to open a follow-up issue and draft a small inline error/toast implementation?

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

In `@web/src/components/chat/answer/feedbackButtons.tsx` around lines 52 - 55, The
catch block in feedbackButtons.tsx currently swallows submission errors and only
calls onReactionUpdate(undefined); update that catch to (1) capture the thrown
error (e), (2) still roll back via onReactionUpdate(undefined) but also surface
a user-facing notification (either by invoking your existing toast/notification
utility or setting local error state that renders a small inline message in the
FeedbackButtons component), and (3) log the error (console.error or process
logger) including e.message/details for debugging; ensure the notification text
is generic/user-friendly and reference the same handler where onReactionUpdate
is called so the UI rollback and error display happen together.

Comment on lines +47 to +52
try {
const newShareId = await shareChat(chatId);
onShareIdUpdate(newShareId);
} finally {
setIsLoading('');
}
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.

⚠️ Potential issue | 🟡 Minor

Add explicit failure handling for share/unshare actions.

finally resets loading, but there’s no catch path to show an error state. Users get no explanation when share operations fail.

Proposed pattern
+ const [actionError, setActionError] = React.useState<string>('');

  const handleShare = async (
    type: GTagEvents.SHARE | GTagEvents.REGEN_SHARE
  ) => {
    setIsLoading(type);
+   setActionError('');
    gtagEvent({ event: type, chat });
    try {
      const newShareId = await shareChat(chatId);
      onShareIdUpdate(newShareId);
+   } catch {
+     setActionError('Unable to share this chat right now. Please try again.');
    } finally {
      setIsLoading('');
    }
  };

  const handleUnshare = async () => {
    setIsLoading(GTagEvents.UNSHARE);
+   setActionError('');
    gtagEvent({ event: GTagEvents.UNSHARE, chat });
    try {
      await unshareChat(chatId);
      onShareIdUpdate(undefined);
+   } catch {
+     setActionError('Unable to unshare this chat right now. Please try again.');
    } finally {
      setIsLoading('');
    }
  };

Also applies to: 58-63

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

In `@web/src/components/chat/answer/shareModal.tsx` around lines 47 - 52, The
share/unshare try/finally blocks (around shareChat and unshareChat) lack a catch
so failures silently reset loading; wrap the await shareChat(chatId) and await
unshareChat(chatId) calls in try/catch, call onShareIdUpdate only on success,
and in the catch set an explicit error state (e.g., setIsLoading('error') or a
new setShareError state) and log the error (console.error or a logger) so the UI
can surface the failure to the user before the existing finally resets loading.
Ensure you update both the shareChat path (shareChat, onShareIdUpdate,
setIsLoading) and the unshare path similarly.

Comment on lines +59 to +68
const [chat, setChat] = React.useState<ChatHistory>(initialChat);
const [currentStatusMessage, setCurrentStatusMessage] =
React.useState<string>('');
const lastRedirectedChatIdRef = React.useRef<string | null>(null);
const didLogNewChatEventRef = React.useRef<boolean>(false);

const initialMessages = React.useMemo(
() => chatMessagesToUIMessages(initialChat.messages),
[initialChat.messages]
);
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.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "MainContent call sites (check for key prop):"
rg -nP '<MainContent\b' web/src/app web/src/components

echo
echo "MainContent state init and prop-sync checks:"
rg -nP 'useState<ChatHistory>\(initialChat\)|setChat\(initialChat\)|useEffect\(\(\)\s*=>\s*\{\s*setChat' web/src/components/chat/main.tsx

Repository: ucdavis/PolicyWonk

Length of output: 424


🏁 Script executed:

# Get more context on the MainContent file structure and useEffect patterns
cat -n web/src/components/chat/main.tsx | head -100

Repository: ucdavis/PolicyWonk

Length of output: 3781


🏁 Script executed:

# Check if there's a useEffect that updates chat state based on initialChat prop
rg -A 5 'useEffect.*initialChat|useEffect.*setChat' web/src/components/chat/main.tsx

Repository: ucdavis/PolicyWonk

Length of output: 44


🏁 Script executed:

# Check the full extent of setChat usage in the file
rg -n 'setChat' web/src/components/chat/main.tsx

Repository: ucdavis/PolicyWonk

Length of output: 333


🏁 Script executed:

# Look for useEffect hooks in the file
rg -B 2 -A 5 'useEffect' web/src/components/chat/main.tsx | head -50

Repository: ucdavis/PolicyWonk

Length of output: 426


🏁 Script executed:

# Check the parent page components to see how MainContent is passed
cat -n web/src/app/\(home\)/\[group\]/chat/\[chatid\]/page.tsx | tail -20

Repository: ucdavis/PolicyWonk

Length of output: 604


🏁 Script executed:

# Check the full page component files to see if MainContent has a key prop
cat -n web/src/app/\(home\)/\[group\]/chat/\[chatid\]/page.tsx | grep -B 2 -A 2 'MainContent'

Repository: ucdavis/PolicyWonk

Length of output: 395


🏁 Script executed:

# Check the share page too
cat -n web/src/app/\(home\)/\[group\]/share/\[shareid\]/page.tsx | grep -B 2 -A 2 'MainContent'

Repository: ucdavis/PolicyWonk

Length of output: 443


🏁 Script executed:

# Verify the full extent of useEffect hooks and their dependencies
sed -n '110,220p' web/src/components/chat/main.tsx

Repository: ucdavis/PolicyWonk

Length of output: 3157


🏁 Script executed:

# Get the complete useEffect that handles the redirect and chat id sync
sed -n '110,135p' web/src/components/chat/main.tsx

Repository: ucdavis/PolicyWonk

Length of output: 852


🏁 Script executed:

# Check if initialChat.id or other initialChat properties are ever accessed after mount
rg -n 'initialChat\.' web/src/components/chat/main.tsx

Repository: ucdavis/PolicyWonk

Length of output: 152


Add a useEffect to sync local chat state when initialChat prop changes, or key MainContent to force remounts across route transitions.

useState(initialChat) only seeds on first mount. Since MainContent is rendered without a key prop, navigating between different chats reuses the same component instance. When initialChat prop changes, the local chat state remains stale with previous chat's group, focus, and shareId values. This causes:

  • onQuestionSubmit to send requests with stale group and focus values
  • onFocusSelection to update stale focus state
  • Router redirect checks to use stale chat.group

Add React.useEffect(() => { setChat(initialChat); }, [initialChat]) to sync state, or add a key={initialChat.id} prop to MainContent in both page components to force remounts.

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

In `@web/src/components/chat/main.tsx` around lines 59 - 68, The local chat state
initialized with React.useState(chat) isn't synced when the initialChat prop
changes, causing stale group/focus/shareId values; update the component to
either add a useEffect that calls setChat(initialChat) with [initialChat] as
dependency (referencing the chat and setChat state variables and initialChat
prop) or ensure the parent forces remounts by adding a key={initialChat.id} to
MainContent where it is rendered so initialMessages (chatMessagesToUIMessages)
and subsequent handlers (onQuestionSubmit, onFocusSelection) always use the
current chat.

@srkirkland srkirkland merged commit 7af766d into main Feb 25, 2026
8 checks passed
@srkirkland srkirkland deleted the srk/goodbye-rsc branch February 25, 2026 23:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants