Skip to content

Conversation

@Kitenite
Copy link
Contributor

@Kitenite Kitenite commented Sep 12, 2025

Description

Related Issues

Type of Change

  • Bug fix
  • New feature
  • Documentation update
  • Release
  • Refactor
  • Other (please describe):

Testing

Screenshots (if applicable)

Additional Notes


Important

Enhances chat functionality with incremental message saving, refactors toolset and message handling, and improves Markdown rendering.

  • Chat Functionality:
    • Introduces upsertMessage and loadChat in message.ts for incremental saving and loading of chat messages.
    • Updates streamResponse in route.ts to use new message handling logic.
    • Refactors chat message handling to use ChatMessage type from @onlook/models.
  • Toolset Refactor:
    • Renames ASK_TOOL_SET to askTools and BUILD_TOOL_SET to buildTools in toolset.ts.
    • Updates toolset logic to differentiate between askTools and buildTools based on ChatType.
  • Markdown Rendering:
    • Replaces markdown-renderer.tsx with markdown/index.tsx and markdown/block.tsx for improved Markdown handling.
    • Enhances Markdown rendering performance and code block handling.
  • Database Schema:
    • Updates message.ts schema to include ChatMessage parts and metadata.
    • Refactors conversation.ts and message.ts mappers to align with new chat message structure.
  • Testing:
    • Updates tests in read.test.ts, web-search.test.ts, and web.test.ts to reflect toolset changes and validate new logic.

This description was created by Ellipsis for 20742b6. You can customize this summary. It will automatically update as commits are pushed.


Summary by CodeRabbit

  • New Features

    • Improved chat streaming UI: consistent loading indicator and dedicated "Reasoning" sections.
  • Enhancements

    • Conversations display a human-readable name in history.
    • System messages are hidden from the chat feed.
    • Model/tool behavior simplified for more consistent responses.
  • UX Improvements

    • Error toast shown when sending a message fails.
  • Chores

    • Added Markdown rendering deps.
    • Rewrote agent guide for repo contributors.

@vercel
Copy link

vercel bot commented Sep 12, 2025

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

Project Deployment Preview Comments Updated (UTC)
web Ready Ready Preview Comment Sep 15, 2025 6:41am
1 Skipped Deployment
Project Deployment Preview Comments Updated (UTC)
docs Skipped Skipped Sep 15, 2025 6:41am

@coderabbitai
Copy link

coderabbitai bot commented Sep 12, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Reworks chat domain to use a metadata-driven ChatMessage and ChatConversation.displayName, updates DB schema/mappers, migrates streaming to ChatMessage-first flow (upsert before/after stream, load history), refactors hooks/UI to expose streamingMessage and sendMessageToChat(message, type), renames AI tool exports, and replaces Markdown renderer.

Changes

Cohort / File(s) Summary
Models: Chat types
packages/models/src/chat/message/message.ts, packages/models/src/chat/conversation/index.ts
Replaced role-union messages with UIMessage-style ChatMessage carrying metadata; ChatConversation.titledisplayName, flattened suggestions, added parent IDs.
DB: Schema & mappers
packages/db/src/schema/chat/message.ts, packages/db/src/mappers/chat/message.ts, packages/db/src/mappers/chat/conversation.ts, packages/db/src/mappers/chat/index.ts, packages/db/src/schema/chat/index.ts
Schema: role enum literals, nullable content, optional parts, typed FK. Mappers now metadata-centric; toDbMessage requires conversationId; conversation mapper uses displayName and top-level suggestions.
AI: Stream & toolset
packages/ai/src/stream/index.ts, packages/ai/src/tools/toolset.ts, packages/ai/src/prompt/provider.ts, packages/ai/src/tools/tools/*
Stream logic uses string roles, adds filterReasoningParts/ensureToolResultParts, removes some helpers; toolset exports renamed (askTools/buildTools), added getToolSetFromType and ChatTools; removed getExampleConversation; minor schema descriptions updated.
AI: Tests
packages/ai/test/tools/*.test.ts
Tests updated to new toolset export names (buildTools, askTools).
Web API: Chat helpers & route
apps/web/client/src/app/api/chat/helpers/*, apps/web/client/src/app/api/chat/route.ts
Added upsertMessage and loadChat helpers; route now accepts a single ChatMessage+traceId, upserts incoming message, loads history, streams using history, and upserts final assistant message.
Web Hook/Context
apps/web/client/src/app/project/[id]/_hooks/use-chat.tsx
Context now uses ChatMessage, exposes streamingMessage and sendMessageToChat(message, type); transports/requests include traceId and conversationId.
Editor Store: Chat manager & conversation
apps/web/client/src/components/store/editor/chat/*
Store APIs migrated to ChatMessage; conversation logic uses displayName; adjusted add/resubmit/attach checkpoint flows; upsertMessageInStorage signature changed to include conversationId.
Web UI: Chat components
apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/*, .../history.tsx
Components switched to ChatMessage and string roles; added system handling (not rendered); introduced RenderedChatMessages, ReasoningBlock, replaced Markdown renderer with marked-based MarkdownRenderer; history shows displayName.
Web UI: Send entry points
apps/web/client/src/app/project/[id]/_components/.../chat-input/*, overlay/elements/buttons/chat.tsx, error.tsx, _hooks/use-start-project.tsx
Send flows updated to pass the ChatMessage returned from store into sendMessageToChat(message, chatType); added user-facing error toast in overlay submit.
Tools integration
apps/web/client/src/components/tools/tools.ts
Imports getToolSetFromType from @onlook/ai (moved from local helper).
Server routers
apps/web/client/src/server/api/routers/chat/*.ts, .../usage/index.ts
Message router upserts/updates input.message directly (normalizer removed); suggestion generator validates role enum; usage insert uses onConflictDoNothing().
Markdown & rendering
apps/web/client/src/app/project/[id]/_components/right-panel/.../markdown/*, message-content/*
Removed old MarkdownRenderer file; added MarkdownRenderer (marked lexer + block renderer) and MarkdownBlock; MessageContent now uses ReasoningBlock.
Package config & misc
apps/web/client/package.json, apps/web/client/src/app/project/[id]/providers.tsx, CLAUDE.md
Added marked deps; trivial whitespace change in providers; replaced CLAUDE.md with expanded agents guide.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant UI as Web UI
  participant Store as Editor Store
  participant Hook as ChatProvider/useChat
  participant API as /api/chat (route)
  participant DB as Database

  User->>UI: Submit text
  UI->>Store: addAsk/addEdit/addFix... -> returns ChatMessage
  Store-->>UI: ChatMessage (with metadata)
  UI->>Hook: sendMessageToChat(message, chatType)
  Hook->>API: POST { message, chatType, conversationId, projectId, traceId }
  API->>DB: upsertMessage(id, conversationId, message)
  API->>DB: loadChat(conversationId)
  API->>API: stream using history -> chunks
  API-->>Hook: stream chunks (assistant partial)
  Hook-->>UI: streamingMessage updates
  API->>DB: upsertMessage(final assistant message)
  Hook->>Store: add assistant message to conversation
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Poem

A nibble of bytes, a hop through streams,
I stash your words and stitch up dreams.
DisplayName blooms where titles slept,
Markdown stacked in tidy steps.
Tools renamed, messages flow — thump, thump, my rabbit heart goes so. 🐇✨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title Check ✅ Passed The title "feat: incremental saving chat" concisely and accurately reflects the main change in this PR (adding incremental chat message persistence and related model/schema/helper updates), is specific to the feature, and contains no extraneous noise.
Description Check ✅ Passed The PR description includes the repository template and a detailed auto-generated summary that documents the incremental message persistence (upsertMessage/loadChat), model/schema updates, toolset refactors, and test changes, so the scope and intent are well described for reviewers. However, the template's "Related Issues" and "Testing" sections are left empty and should be populated with issue links and concrete verification steps or commands. Overall the description is mostly complete and informative but would benefit from those additions for clearer review and release notes.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/chat-db

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.

@supabase
Copy link

supabase bot commented Sep 12, 2025

This pull request has been ignored for the connected project wowaemfasoptxrdjhilu because there are no changes detected in apps/backend/supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

Comment on lines 25 to 26
parts: jsonb("parts").$type<AiMessage['parts']>().default([]),
content: text("content"),
Copy link
Contributor

Choose a reason for hiding this comment

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

Data integrity bug: The 'content' field has been moved after 'parts' and is no longer marked as .notNull(), but the original schema had content as required. This breaking change will cause runtime errors when existing code expects content to always be present. Either keep content as .notNull() or update all consuming code to handle null content values.

Suggested change
parts: jsonb("parts").$type<AiMessage['parts']>().default([]),
content: text("content"),
parts: jsonb("parts").$type<AiMessage['parts']>().default([]),
content: text("content").notNull(),

Spotted by Diamond

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

export const conversationInsertSchema = createInsertSchema(conversations);
export const conversationUpdateSchema = createUpdateSchema(conversations, {
id: z.string().uuid(),
id: z.uuid(),
Copy link
Contributor

Choose a reason for hiding this comment

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

Schema validation bug: Changed z.string().uuid() to z.uuid() in conversationUpdateSchema. The z.uuid() method doesn't exist in standard Zod - it should be z.string().uuid(). This will cause runtime errors when the schema is used for validation.

Suggested change
id: z.uuid(),
id: z.string().uuid(),

Spotted by Diamond

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

Copy link

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/history.tsx (1)

1-1: Missing "use client" directive.
This component uses React state and event handlers under app/; mark it as a Client Component.

+'use client';
+
 import { useEditorEngine } from '@/components/store/editor';
🧹 Nitpick comments (5)
packages/db/src/schema/chat/message.ts (2)

24-24: Avoid any type for snapshots.
Switch to unknown or a specific shape to meet the repo guideline of avoiding any.

-    snapshots: jsonb("snapshots").$type<any>(),
+    snapshots: jsonb("snapshots").$type<Record<string, unknown> | unknown[]>(),

8-8: Nit: constant name casing.
CONVERSATION_MESSAGe_RELATION_NAME has a stray lowercase “e”. Consider renaming to CONVERSATION_MESSAGE_RELATION_NAME for consistency, and update imports.

-export const CONVERSATION_MESSAGe_RELATION_NAME = 'conversation_messages';
+export const CONVERSATION_MESSAGE_RELATION_NAME = 'conversation_messages';

And in files importing it, update the symbol name.

apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/history.tsx (1)

91-91: Fallback should handle empty strings too.
Nullish coalescing won’t catch ''. Use logical OR so blank titles render the fallback.

-                                                    {conversation.displayName ?? 'New Conversation'}
+                                                    {conversation.displayName || 'New Conversation'}
packages/db/src/schema/chat/conversation.ts (2)

21-25: Add indexes for new FK columns.
Subchat lookups and cascades will benefit from indexes.

-import { jsonb, pgTable, timestamp, uuid, varchar, type AnyPgColumn } from "drizzle-orm/pg-core";
+import { jsonb, pgTable, timestamp, uuid, varchar, index, type AnyPgColumn } from "drizzle-orm/pg-core";
@@
-export const conversations = pgTable("conversations", {
+export const conversations = pgTable("conversations", {
   /* columns */
-}).enableRLS();
+}, (t) => ({
+  parentConversationIdx: index("conversations_parent_conversation_id_idx").on(t.parentConversationId),
+  parentMessageIdx: index("conversations_parent_message_id_idx").on(t.parentMessageId),
+})).enableRLS();

7-8: Nit: constant name casing (imported).
If you rename the constant to CONVERSATION_MESSAGE_RELATION_NAME in messages, update imports and usages here too.

-import { CONVERSATION_MESSAGe_RELATION_NAME, messages } from "./message";
+import { CONVERSATION_MESSAGE_RELATION_NAME, messages } from "./message";
@@
-    messages: many(messages, {
-        relationName: CONVERSATION_MESSAGe_RELATION_NAME,
+    messages: many(messages, {
+        relationName: CONVERSATION_MESSAGE_RELATION_NAME,
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c82c0ab and 70107a2.

📒 Files selected for processing (7)
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/history.tsx (1 hunks)
  • apps/web/client/src/components/store/editor/chat/conversation.ts (3 hunks)
  • packages/db/src/defaults/conversation.ts (1 hunks)
  • packages/db/src/mappers/chat/conversation.ts (1 hunks)
  • packages/db/src/schema/chat/conversation.ts (2 hunks)
  • packages/db/src/schema/chat/message.ts (2 hunks)
  • packages/models/src/chat/conversation/index.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Do not use the 'any' type unless necessary

Files:

  • packages/db/src/mappers/chat/conversation.ts
  • apps/web/client/src/components/store/editor/chat/conversation.ts
  • packages/db/src/defaults/conversation.ts
  • packages/models/src/chat/conversation/index.ts
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/history.tsx
  • packages/db/src/schema/chat/message.ts
  • packages/db/src/schema/chat/conversation.ts
apps/web/client/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Prefer imports using @/* or ~/* aliases instead of deep relative paths within the client app

Files:

  • apps/web/client/src/components/store/editor/chat/conversation.ts
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/history.tsx
apps/web/client/src/app/**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

Default to Server Components; add 'use client' only when using events, state/effects, browser APIs, or client-only libraries

Files:

  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/history.tsx
apps/web/client/src/app/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Do not use process.env in client code; prefer importing env from @/env

Files:

  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/history.tsx
🧬 Code graph analysis (4)
packages/db/src/mappers/chat/conversation.ts (1)
packages/models/src/chat/conversation/index.ts (1)
  • ChatConversation (3-12)
packages/models/src/chat/conversation/index.ts (1)
packages/models/src/chat/suggestion.ts (1)
  • ChatSuggestion (9-12)
packages/db/src/schema/chat/message.ts (3)
packages/db/src/schema/chat/conversation.ts (1)
  • conversations (11-26)
packages/models/src/chat/message/context.ts (1)
  • MessageContext (53-59)
packages/models/src/chat/message/checkpoint.ts (1)
  • MessageCheckpoints (15-15)
packages/db/src/schema/chat/conversation.ts (1)
packages/db/src/schema/chat/message.ts (1)
  • messages (11-27)
🔇 Additional comments (12)
packages/db/src/schema/chat/message.ts (3)

4-4: Typed FK import looks correct.
This fixes Drizzle typing for cross-table references.


15-15: Good use of typed references to break TS cyclic typing.
This is the recommended pattern with Drizzle when cross-referencing tables.


30-30: Exporting messageUpdateSchema is helpful.
Unblocks partial updates without hand-writing Zod.

packages/db/src/defaults/conversation.ts (1)

12-13: Defaults for subchat IDs look good.
Null-initialized optional parents align with schema and mappers.

packages/db/src/mappers/chat/conversation.ts (2)

5-5: Pass-through fromDbConversation assumes shapes are identical. Verify.
If DB types ever diverge (e.g., optional vs. required), this will leak DB-only fields or nullability mismatches.

Optionally make mapping explicit to lock the contract.

-export const fromDbConversation = (conversation: DbConversation): ChatConversation => {
-    return conversation;
-}
+export const fromDbConversation = (c: DbConversation): ChatConversation => ({
+  id: c.id,
+  displayName: c.displayName ?? null,
+  projectId: c.projectId,
+  createdAt: c.createdAt,
+  updatedAt: c.updatedAt,
+  suggestions: c.suggestions ?? null,
+  parentConversationId: c.parentConversationId ?? null,
+  parentMessageId: c.parentMessageId ?? null,
+});

12-15: Mapping to DB with null coalescing looks right.
Ensures we don’t store empty strings/undefined.

apps/web/client/src/components/store/editor/chat/conversation.ts (2)

53-53: Guard depends on displayName semantics.
With default displayName currently set (see defaults), this won’t block creating redundant empty conversations. Prefer default null in defaults, or adapt the guard accordingly.

-            if (this.current?.messages.length === 0 && !this.current?.conversation.displayName) {
+            if (this.current?.messages.length === 0 && this.current?.conversation.displayName == null) {

146-146: Persist renamed title to storage (unless mutate already does).
If generateTitle doesn’t update the DB, call the update API to keep list and DB in sync.

         listConversation.displayName = title;
+        await this.updateConversationInStorage({ id: conversationId, displayName: title });
packages/models/src/chat/conversation/index.ts (1)

5-11: API shape LGTM.
displayName and top-level suggestions align with UI and DB; optional parent IDs support subchats.

packages/db/src/schema/chat/conversation.ts (3)

3-3: Import of AnyPgColumn is appropriate.
Required for typed self/cross references.


30-31: z.uuid() on update schema is a nice tightening.
Removes reliance on string parsing.


21-25: Cyclic table refs use callbacks — verify bundler/transpiler preserves lazy evaluation.

conversations and messages are both exported (packages/db/src/schema/chat/conversation.ts and packages/db/src/schema/chat/message.ts) and .references(...) uses callback form; confirm your build (ESM→CJS transpile, tree-shaking/module inlining) does not evaluate or remove those callbacks before module initialization.

Comment on lines +125 to 127
if (!this.current.conversation.displayName) {
this.addConversationTitle(this.current.conversation.id, content);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Title generation won’t run if default displayName is a non-null placeholder.
Switch defaults to null (preferred), or treat the placeholder as empty.

Preferred (after changing defaults):

-        if (!this.current.conversation.displayName) {
+        if (this.current.conversation.displayName == null) {

Fallback (if keeping placeholder string):

-        if (!this.current.conversation.displayName) {
+        if (
+          !this.current.conversation.displayName ||
+          this.current.conversation.displayName === 'New Conversation'
+        ) {
📝 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 (!this.current.conversation.displayName) {
this.addConversationTitle(this.current.conversation.id, content);
}
if (this.current.conversation.displayName == null) {
this.addConversationTitle(this.current.conversation.id, content);
}
🤖 Prompt for AI Agents
In apps/web/client/src/components/store/editor/chat/conversation.ts around lines
125-127, the title-generation guard checks only for falsy displayName but a
non-null placeholder string prevents generation; update the default for
conversation.displayName to null (preferred) so the existing check works, or
alter this block to treat the placeholder as empty by changing the condition to
detect the placeholder string (e.g., compare against the placeholder constant or
trim and check length) and then call addConversationTitle when appropriate.

projectId,
createdAt: new Date(),
updatedAt: new Date(),
displayName: 'New Conversation',
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Defaulting displayName breaks title generation flow.
UI/store generate a title only when displayName is falsy; setting a literal 'New Conversation' prevents generation.

-        displayName: 'New Conversation',
+        displayName: null,

UI can still show 'New Conversation' via fallback.

📝 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
displayName: 'New Conversation',
displayName: null,
🤖 Prompt for AI Agents
In packages/db/src/defaults/conversation.ts around line 10, the default sets
displayName to the literal 'New Conversation', which prevents the UI/store from
generating a title (they only generate when displayName is falsy); change the
default so displayName is falsy (e.g., remove the property or set it to
undefined or an empty string) so the title generation flow runs, and let the UI
handle showing 'New Conversation' as a fallback.

Comment on lines 25 to 26
parts: jsonb("parts").$type<AiMessage['parts']>().default([]),
content: text("content"),
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Nullability change can produce empty/invalid messages; harden constraints.

  • parts is now nullable; callers may not expect null. Keep default([]) but add notNull() to avoid NULLs.
  • With content now nullable, ensure at least one of content or parts is present in every row.

Apply:

-    parts: jsonb("parts").$type<AiMessage['parts']>().default([]),
+    parts: jsonb("parts").$type<AiMessage['parts']>().default([]).notNull(),

Optionally add a CHECK so rows always have content or parts (requires table builder block):

// import { sql } from "drizzle-orm";
export const messages = pgTable("messages", { /* cols */ }, (t) => ({
  contentOrParts: /* check name */ {
    // Pseudocode: enforce content IS NOT NULL OR jsonb_array_length(parts) > 0
    // Implement using your preferred check helper and sql template literal.
  }
}));

Run a quick audit to find consumers assuming non-null content:


🏁 Script executed:

#!/bin/bash
rg -n --type=ts --type=tsx -C2 '\bmessage\.content\b|\bcontent:\s*message\.content\b'

Length of output: 27


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "1) Show schema file (provided path in review): packages/db/src/schema/chat/message.ts"
file="packages/db/src/schema/chat/message.ts"
if [ -f "$file" ]; then
  echo "=== $file (lines 1-240) ==="
  sed -n '1,240p' "$file"
else
  echo "File not found at $file"
fi

echo
echo "2) Grep for jsonb('parts') occurrences across the repo"
rg -n --hidden -S 'jsonb\("parts"\)' || true

echo
echo "3) Search for direct consumers referencing message.content or message.parts (repo-wide)"
rg -n --hidden -S '\bmessage\.content\b' || true
rg -n --hidden -S '\bmessage\.parts\b' || true

echo
echo "4) Broader search for `.content` / `.parts` on variables named `message` or destructured patterns (limited to likely code dirs)"
rg -n --hidden -S '\bcontent:\s*message\.content\b' || true
rg -n --hidden -S '\bmessage\s*\.\s*content\b' || true
rg -n --hidden -S '\bmessage\s*\.\s*parts\b' || true

Length of output: 5694


Make messages.parts NOT NULL and enforce content-or-parts invariant

  • parts is nullable but multiple callers use message.parts without null checks (will throw at runtime). Make parts NOT NULL and add a DB CHECK requiring content IS NOT NULL OR jsonb_array_length(parts) > 0.
  • Apply:
-    parts: jsonb("parts").$type<AiMessage['parts']>().default([]),
+    parts: jsonb("parts").$type<AiMessage['parts']>().default([]).notNull(),
  • Add DB CHECK (Postgres): CHECK ((content IS NOT NULL) OR (jsonb_array_length(parts) > 0))
  • Update callers/types (examples found): packages/db/src/mappers/chat/message.ts, packages/ai/src/stream/index.ts, packages/ai/src/prompt/provider.ts, packages/ai/src/apply/client.ts, apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/user-message.tsx
📝 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
parts: jsonb("parts").$type<AiMessage['parts']>().default([]),
content: text("content"),
parts: jsonb("parts").$type<AiMessage['parts']>().default([]).notNull(),
content: text("content"),

Copy link

@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: 12

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/user-message.tsx (1)

209-213: Use stable React keys; avoid random keys.

Using nanoid() re-mounts pills every render, losing local state and hurting perf.

-                            {message.metadata?.context?.map((context) => (
-                                <SentContextPill key={nanoid()} context={context} />
-                            ))}
+                            {message.metadata?.context?.map((context, idx) => (
+                                <SentContextPill key={idx} context={context} />
+                            ))}

If a stable identifier exists on context, prefer it over the index.

♻️ Duplicate comments (2)
packages/db/src/schema/chat/message.ts (1)

20-20: Make parts NOT NULL and keep default [].

Multiple callers assume message.parts is always an array; allowing NULL increases runtime risk.

-    parts: jsonb("parts").$type<ChatMessage['parts']>().default([]),
+    parts: jsonb("parts").$type<ChatMessage['parts']>().default([]).notNull(),
apps/web/client/src/components/store/editor/chat/conversation.ts (1)

125-127: Title generation guard: handle placeholder defaults.

If displayName defaults to a non-empty placeholder (e.g., "New Conversation"), this condition won’t trigger.

-        if (!this.current.conversation.displayName) {
+        if (
+          this.current.conversation.displayName == null ||
+          this.current.conversation.displayName === 'New Conversation'
+        ) {

Prefer making the default displayName null so a simple null-check suffices.

🧹 Nitpick comments (32)
apps/web/client/src/server/api/routers/chat/suggestion.ts (2)

43-43: Prefer explicit typing over satisfies here.

satisfies won’t set suggestions’s variable type. Make it explicit for clearer intent and IDE help.

-            const suggestions = object.suggestions satisfies ChatSuggestion[];
+            const suggestions: ChatSuggestion[] = object.suggestions;

2-2: Avoid deep package internals import.

Import the prompt from the package’s public entry (re-export it if needed) to prevent brittle coupling to internal paths.

-import { SUGGESTION_SYSTEM_PROMPT } from '@onlook/ai/src/prompt/suggest';
+import { SUGGESTION_SYSTEM_PROMPT } from '@onlook/ai';
apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/stream-message.tsx (2)

9-12: Drop useMemo here; simple comparison is cheaper and clearer.

-    const isAssistantStreamMessage = useMemo(() =>
-        streamMessage?.role === 'assistant',
-        [streamMessage?.role]
-    );
+    const isAssistantStreamMessage = streamMessage?.role === 'assistant';

4-4: Consider alias imports for consistency.

Prefer '@/...' to avoid relative paths in client code.

-import { MessageContent } from './message-content';
+import { MessageContent } from '@/app/project/[id]/_components/right-panel/chat-tab/chat-messages/message-content';
packages/ai/test/tools/web.test.ts (1)

3-4: Rename test description to match new export names.

The body uses buildTools/askTools; update the spec name for clarity.

-    it('should be included in both buildToolSet and askToolSet', () => {
+    it('should be included in both buildTools and askTools', () => {

Also applies to: 18-21

packages/ai/test/tools/web-search.test.ts (1)

3-4: Align spec title with renamed exports.

-    it('should be included in both buildToolSet and askToolSet', () => {
+    it('should be included in both buildTools and askTools', () => {

Also applies to: 18-21

packages/ai/test/tools/read.test.ts (2)

3-4: Rename spec title to reflect buildTools/askTools.

-    test('should be included in both toolsets', () => {
+    test('should be included in both tool sets (buildTools, askTools)', () => {

Also applies to: 21-24


87-90: Rename spec title to reflect buildTools/askTools.

-    test('should be included in both toolsets', () => {
+    test('should be included in both tool sets (buildTools, askTools)', () => {
apps/web/client/src/app/api/chat/helpers/stream.ts (1)

28-30: Remove unnecessary async.

Function returns synchronously.

-export async function getToolSetFromType(chatType: ChatType) {
-    return chatType === ChatType.ASK ? askTools : buildTools;
-}
+export function getToolSetFromType(chatType: ChatType) {
+    return chatType === ChatType.ASK ? askTools : buildTools;
+}
apps/web/client/src/app/project/[id]/_hooks/use-chat.tsx (1)

41-41: TODO mismatch: code still saves on finish.

Either remove the TODO or gate the save behind a flag to avoid accidental persistence.

packages/ai/src/tools/toolset.ts (1)

41-52: Optional: tighten typing with satisfies to prevent accidental key/value drift.

Apply:

-export const askTools: ToolSet = {
+export const askTools = {
   [LIST_FILES_TOOL_NAME]: listFilesTool,
   [READ_FILE_TOOL_NAME]: readFileTool,
   [BASH_READ_TOOL_NAME]: bashReadTool,
   [ONLOOK_INSTRUCTIONS_TOOL_NAME]: onlookInstructionsTool,
   [READ_STYLE_GUIDE_TOOL_NAME]: readStyleGuideTool,
   [LIST_BRANCHES_TOOL_NAME]: listBranchesTool,
   [SCRAPE_URL_TOOL_NAME]: scrapeUrlTool,
   [WEB_SEARCH_TOOL_NAME]: webSearchTool,
   [GLOB_TOOL_NAME]: globTool,
   [GREP_TOOL_NAME]: grepTool,
-};
+} as const satisfies ToolSet;

And similarly for buildTools:

-export const buildTools: ToolSet = {
-    ...askTools,
+export const buildTools = {
+    ...askTools,
     [SEARCH_REPLACE_EDIT_FILE_TOOL_NAME]: searchReplaceEditFileTool,
     [SEARCH_REPLACE_MULTI_EDIT_FILE_TOOL_NAME]: searchReplaceMultiEditFileTool,
     [FUZZY_EDIT_FILE_TOOL_NAME]: fuzzyEditFileTool,
     [WRITE_FILE_TOOL_NAME]: writeFileTool,
     [BASH_EDIT_TOOL_NAME]: bashEditTool,
     [SANDBOX_TOOL_NAME]: sandboxTool,
     [TERMINAL_COMMAND_TOOL_NAME]: terminalCommandTool,
     [TYPECHECK_TOOL_NAME]: typecheckTool,
-};
+} as const satisfies ToolSet;
packages/ai/src/stream/index.ts (2)

29-44: Handle 'system' explicitly for clarity (minor).

Currently falls through to default return; being explicit improves readability and future-proofing.

 export const toVercelMessageFromOnlook = (
   message: ChatMessage,
   opt: HydrateMessageOptions,
 ): VercelMessage => {
   if (message.role === 'assistant') {
     return {
       ...message,
       parts: message.parts,
     } satisfies VercelMessage;
-  } else if (message.role === 'user') {
+  } else if (message.role === 'user') {
     const hydratedMessage = getHydratedUserMessage(
       message.id,
       message.parts,
       message.metadata?.context ?? [],
       opt,
     );
     return hydratedMessage;
+  } else if (message.role === 'system') {
+    return message;
   }
   return message;
 };

46-55: Guard against undefined parts in extractTextFromParts.

Avoids a potential runtime error if parts is ever undefined.

-export const extractTextFromParts = (parts: ChatMessage['parts']): string => {
-  return parts
+export const extractTextFromParts = (parts: ChatMessage['parts']): string => {
+  return (parts ?? [])
     ?.map((part) => {
       if (part.type === 'text') {
         return part.text;
       }
       return '';
     })
     .join('');
 };
apps/web/client/src/server/api/routers/chat/message.ts (2)

54-55: upsertMany is an insert; add conflict handling (or rename).

Without onConflict, duplicates will error or create multiple rows. Either add conflict handling or rename to insertMany.

-await ctx.db.insert(messages).values(input.messages);
+await ctx.db
+  .insert(messages)
+  .values(input.messages)
+  .onConflictDoNothing(); // simplest idempotency

If you truly want upsert semantics, update on conflict to EXCLUDED values (requires sql import):

+import { asc, eq, inArray, sql } from 'drizzle-orm';
@@
 await ctx.db
   .insert(messages)
   .values(input.messages)
   .onConflictDoUpdate({
     target: [messages.id],
     set: {
       role: sql`excluded.role`,
       parts: sql`excluded.parts`,
       context: sql`excluded.context`,
       checkpoints: sql`excluded.checkpoints`,
     },
   });

62-63: Optional: restrict update to mutable fields.

Similar to upsert, consider whitelisting fields to avoid accidental updates to immutable columns via messageUpdateSchema.

apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/index.tsx (2)

26-37: Skip rendering wrappers for system messages (minor).

Returning a wrapper div with null child adds empty nodes. Either filter system messages earlier or return null directly from map.

-        return <div key={`message-${message.id}-${index}`}>{messageNode}</div>;
+        if (messageNode == null) return null;
+        return <div key={`message-${message.id}-${index}`}>{messageNode}</div>;

Or pre-filter:

- {messagesToRender.map((message, index) => renderMessage(message, index))}
+ {messagesToRender
+    .filter((m) => m.role !== 'system')
+    .map((message, index) => renderMessage(message, index))}

38-39: Key stability: drop index from key.

Using id alone avoids unnecessary remounts on reordering.

- return <div key={`message-${message.id}-${index}`}>{messageNode}</div>;
+ return <div key={message.id}>{messageNode}</div>;
apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/user-message.tsx (2)

65-70: Handle clipboard write failures.

navigator.clipboard.writeText can reject (permissions/insecure context). Show a toast on failure.

-    const text = getUserMessageContent(message);
-    await navigator.clipboard.writeText(text);
-    setIsCopied(true);
-    setTimeout(() => setIsCopied(false), 2000);
+    try {
+        const text = getUserMessageContent(message);
+        await navigator.clipboard.writeText(text);
+        setIsCopied(true);
+        setTimeout(() => setIsCopied(false), 2000);
+    } catch {
+        toast.error('Failed to copy.');
+    }

12-13: Optional: prefer alias imports for local components.

To follow client import guidelines, consider aliasing ../context-pills/sent-context-pill and ./message-content via @/* if configured.

Also applies to: 147-200

apps/web/client/src/components/store/editor/chat/message.ts (1)

1-1: Import path consistency.

Elsewhere ChatMessage comes from @onlook/models. Consider standardizing import source to reduce churn.

apps/web/client/src/app/api/chat/helpers/message.ts (3)

2-2: Avoid deep package import for db client.

Importing from @onlook/db/src/client reaches into package internals and can break builds. Use the package export.

-import { db } from "@onlook/db/src/client";
+import { db } from "@onlook/db";

27-33: Name consistency: use conversationId.

Parameter is chatId but filters on conversationId. Rename for clarity.

-export const loadChat = async (chatId: string): Promise<ChatMessage[]> => {
+export const loadChat = async (conversationId: string): Promise<ChatMessage[]> => {
     const result = await db.query.messages.findMany({
-        where: eq(messages.conversationId, chatId),
+        where: eq(messages.conversationId, conversationId),
         orderBy: (messages, { asc }) => [asc(messages.createdAt)],
     });
     return result.map((message) => fromDbMessage(message));
 };

27-31: Index for query pattern (conversationId, createdAt).

Since you frequently query by conversationId ordered by createdAt, ensure a composite index exists to avoid full scans.

packages/models/src/chat/message/message.ts (2)

6-26: Remove commented-out legacy types.

Dead code adds noise and confuses type surface.

-// interface BaseChatMessage {
-//     id: string;
-//     createdAt: Date;
-//     role: ChatMessageRole;
-//     threadId: string;
-//     parts: UIMessage['parts'];
-//     metadata: {
-//         vercelId?: string;
-//         context: MessageContext[];
-//         checkpoints: MessageCheckpoints[];
-//     };
-// }
-
-// export interface UserChatMessage extends BaseChatMessage {
-//     role: ChatMessageRole.USER;
-// }
-
-// export interface AssistantChatMessage extends BaseChatMessage {
-//     role: ChatMessageRole.ASSISTANT;
-// }

36-36: Broaden ChatDataPart type.

{} is too restrictive/ambiguous. Prefer a JSON-safe map for tool data.

-export type ChatDataPart = {};
+export type ChatDataPart = Record<string, JSONValue>;
apps/web/client/src/components/store/editor/chat/index.ts (2)

87-89: Guard undefined when removing messages.

Ensure you pass an array.

-        const messagesToRemove = this.conversation.current?.messages.filter((m) => m.metadata?.createdAt && m.metadata.createdAt > oldMessage.metadata?.createdAt);
-        await this.conversation.removeMessages(messagesToRemove);
+        const messagesToRemove = this.conversation.current?.messages.filter(
+            (m) => m.metadata?.createdAt && oldMessage.metadata?.createdAt && m.metadata.createdAt > oldMessage.metadata.createdAt
+        ) ?? [];
+        await this.conversation.removeMessages(messagesToRemove);

2-2: Import path consistency.

Consider aligning with other files (@onlook/models vs @onlook/models/chat) to reduce friction.

packages/db/src/mappers/chat/message.ts (1)

5-15: Optional: retire metadata.vercelId or keep semantics consistent.

New UI messages initialize metadata.vercelId with a fresh UUID, but fromDbMessage sets it to the DB id. If you still need vercelId for dedup, ensure it’s stable end-to-end; otherwise, drop it.

packages/db/src/schema/chat/message.ts (2)

8-8: Typo in relation name constant.

Use a consistent name.

-export const CONVERSATION_MESSAGe_RELATION_NAME = 'conversation_messages';
+export const CONVERSATION_MESSAGES_RELATION_NAME = 'conversation_messages';
@@
-        relationName: CONVERSATION_MESSAGe_RELATION_NAME,
+        relationName: CONVERSATION_MESSAGES_RELATION_NAME,

11-27: Add index for common read path (by conversation, ordered by createdAt).

Improves loadChat performance under growth.

// After pgTable definition
// drizzle does this via `pgIndex` in newer versions; example:
export const messagesByConversationCreatedAtIdx = pgIndex('messages_conversation_created_at_idx')
  .on(messages.conversationId, messages.createdAt);
apps/web/client/src/components/store/editor/chat/conversation.ts (2)

212-214: Avoid mapping to DB shape on the client; push conversion to the server.

Keep the client on public models; let the API accept ChatMessage + conversationId and call toDbMessage server-side.

-    async upsertMessageInStorage(message: ChatMessage, conversationId: string) {
-        await api.chat.message.upsert.mutate({ message: toDbMessage(message, conversationId) });
-    }
+    async upsertMessageInStorage(message: ChatMessage, conversationId: string) {
+        await api.chat.message.upsert.mutate({ message, conversationId });
+    }

53-55: Guard condition readability.

Minor: invert early-return or extract a helper (isCurrentEmptyUntitled) for clarity.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 70107a2 and 2043994.

📒 Files selected for processing (24)
  • apps/web/client/src/app/api/chat/helpers/index.ts (1 hunks)
  • apps/web/client/src/app/api/chat/helpers/message.ts (1 hunks)
  • apps/web/client/src/app/api/chat/helpers/stream.ts (2 hunks)
  • apps/web/client/src/app/api/chat/route.ts (3 hunks)
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/assistant-message.tsx (1 hunks)
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/index.tsx (2 hunks)
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/stream-message.tsx (1 hunks)
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/user-message.tsx (3 hunks)
  • apps/web/client/src/app/project/[id]/_hooks/use-chat.tsx (3 hunks)
  • apps/web/client/src/components/store/editor/chat/conversation.ts (6 hunks)
  • apps/web/client/src/components/store/editor/chat/index.ts (4 hunks)
  • apps/web/client/src/components/store/editor/chat/message.ts (1 hunks)
  • apps/web/client/src/server/api/routers/chat/message.ts (3 hunks)
  • apps/web/client/src/server/api/routers/chat/suggestion.ts (3 hunks)
  • packages/ai/src/stream/index.ts (2 hunks)
  • packages/ai/src/tools/toolset.ts (4 hunks)
  • packages/ai/test/tools/read.test.ts (3 hunks)
  • packages/ai/test/tools/web-search.test.ts (2 hunks)
  • packages/ai/test/tools/web.test.ts (2 hunks)
  • packages/db/src/mappers/chat/index.ts (1 hunks)
  • packages/db/src/mappers/chat/message.ts (1 hunks)
  • packages/db/src/schema/chat/index.ts (1 hunks)
  • packages/db/src/schema/chat/message.ts (1 hunks)
  • packages/models/src/chat/message/message.ts (1 hunks)
✅ Files skipped from review due to trivial changes (2)
  • packages/db/src/mappers/chat/index.ts
  • packages/db/src/schema/chat/index.ts
🧰 Additional context used
📓 Path-based instructions (6)
apps/web/client/src/app/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Do not use process.env in client code; prefer importing env from @/env

Files:

  • apps/web/client/src/app/api/chat/helpers/index.ts
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/assistant-message.tsx
  • apps/web/client/src/app/api/chat/helpers/message.ts
  • apps/web/client/src/app/api/chat/helpers/stream.ts
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/index.tsx
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/stream-message.tsx
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/user-message.tsx
  • apps/web/client/src/app/project/[id]/_hooks/use-chat.tsx
  • apps/web/client/src/app/api/chat/route.ts
apps/web/client/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Prefer imports using @/* or ~/* aliases instead of deep relative paths within the client app

Files:

  • apps/web/client/src/app/api/chat/helpers/index.ts
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/assistant-message.tsx
  • apps/web/client/src/app/api/chat/helpers/message.ts
  • apps/web/client/src/components/store/editor/chat/index.ts
  • apps/web/client/src/app/api/chat/helpers/stream.ts
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/index.tsx
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/stream-message.tsx
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/user-message.tsx
  • apps/web/client/src/server/api/routers/chat/suggestion.ts
  • apps/web/client/src/app/project/[id]/_hooks/use-chat.tsx
  • apps/web/client/src/server/api/routers/chat/message.ts
  • apps/web/client/src/components/store/editor/chat/message.ts
  • apps/web/client/src/app/api/chat/route.ts
  • apps/web/client/src/components/store/editor/chat/conversation.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Do not use the 'any' type unless necessary

Files:

  • apps/web/client/src/app/api/chat/helpers/index.ts
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/assistant-message.tsx
  • packages/ai/test/tools/web.test.ts
  • packages/ai/test/tools/read.test.ts
  • apps/web/client/src/app/api/chat/helpers/message.ts
  • packages/ai/test/tools/web-search.test.ts
  • packages/ai/src/stream/index.ts
  • apps/web/client/src/components/store/editor/chat/index.ts
  • packages/models/src/chat/message/message.ts
  • apps/web/client/src/app/api/chat/helpers/stream.ts
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/index.tsx
  • packages/ai/src/tools/toolset.ts
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/stream-message.tsx
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/user-message.tsx
  • apps/web/client/src/server/api/routers/chat/suggestion.ts
  • packages/db/src/mappers/chat/message.ts
  • apps/web/client/src/app/project/[id]/_hooks/use-chat.tsx
  • apps/web/client/src/server/api/routers/chat/message.ts
  • apps/web/client/src/components/store/editor/chat/message.ts
  • apps/web/client/src/app/api/chat/route.ts
  • apps/web/client/src/components/store/editor/chat/conversation.ts
  • packages/db/src/schema/chat/message.ts
apps/web/client/src/app/**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

Default to Server Components; add 'use client' only when using events, state/effects, browser APIs, or client-only libraries

Files:

  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/assistant-message.tsx
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/index.tsx
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/stream-message.tsx
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/user-message.tsx
  • apps/web/client/src/app/project/[id]/_hooks/use-chat.tsx
apps/web/client/src/server/api/routers/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/client/src/server/api/routers/**/*.ts: Place tRPC routers under apps/web/client/src/server/api/routers and export them from the API root
Use publicProcedure/protectedProcedure from apps/web/client/src/server/api/trpc.ts and validate inputs with Zod
Return SuperJSON-serializable data (plain objects/arrays) from tRPC procedures

Files:

  • apps/web/client/src/server/api/routers/chat/suggestion.ts
  • apps/web/client/src/server/api/routers/chat/message.ts
apps/web/client/src/app/**/{page,layout,route}.ts?(x)

📄 CodeRabbit inference engine (AGENTS.md)

Follow Next.js App Router structure: page.tsx, layout.tsx, and route.ts under apps/web/client/src/app

Files:

  • apps/web/client/src/app/api/chat/route.ts
🧠 Learnings (2)
📚 Learning: 2025-09-12T18:42:26.836Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: AGENTS.md:0-0
Timestamp: 2025-09-12T18:42:26.836Z
Learning: Applies to apps/web/client/messages/**/*.{json,ts} : Add or modify i18n message keys under apps/web/client/messages; avoid breaking renames

Applied to files:

  • apps/web/client/src/app/api/chat/helpers/index.ts
  • apps/web/client/src/app/api/chat/helpers/message.ts
  • apps/web/client/src/components/store/editor/chat/index.ts
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/user-message.tsx
  • apps/web/client/src/server/api/routers/chat/message.ts
📚 Learning: 2025-09-12T18:42:26.836Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: AGENTS.md:0-0
Timestamp: 2025-09-12T18:42:26.836Z
Learning: Run unit tests with 'bun test' and type checking with 'bun run typecheck'

Applied to files:

  • packages/ai/test/tools/read.test.ts
🧬 Code graph analysis (19)
apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/assistant-message.tsx (1)
packages/models/src/chat/message/message.ts (1)
  • ChatMessage (38-38)
packages/ai/test/tools/web.test.ts (1)
packages/ai/src/tools/toolset.ts (2)
  • buildTools (54-64)
  • askTools (41-52)
packages/ai/test/tools/read.test.ts (1)
packages/ai/src/tools/toolset.ts (2)
  • buildTools (54-64)
  • askTools (41-52)
apps/web/client/src/app/api/chat/helpers/message.ts (4)
packages/models/src/chat/message/message.ts (1)
  • ChatMessage (38-38)
packages/db/src/mappers/chat/message.ts (2)
  • toDbMessage (18-34)
  • fromDbMessage (4-16)
packages/db/src/client.ts (1)
  • db (16-16)
packages/db/src/schema/chat/message.ts (1)
  • messages (11-27)
packages/ai/test/tools/web-search.test.ts (1)
packages/ai/src/tools/toolset.ts (2)
  • buildTools (54-64)
  • askTools (41-52)
packages/ai/src/stream/index.ts (2)
packages/db/src/schema/chat/message.ts (1)
  • messages (11-27)
packages/models/src/chat/message/message.ts (1)
  • ChatMessage (38-38)
apps/web/client/src/components/store/editor/chat/index.ts (2)
packages/models/src/chat/message/context.ts (1)
  • MessageContext (53-59)
packages/models/src/chat/message/message.ts (1)
  • ChatMessage (38-38)
packages/models/src/chat/message/message.ts (3)
packages/models/src/chat/message/context.ts (1)
  • MessageContext (53-59)
packages/models/src/chat/message/checkpoint.ts (1)
  • MessageCheckpoints (15-15)
packages/ai/src/tools/toolset.ts (1)
  • ChatTools (66-66)
apps/web/client/src/app/api/chat/helpers/stream.ts (1)
packages/ai/src/tools/toolset.ts (2)
  • askTools (41-52)
  • buildTools (54-64)
apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/index.tsx (2)
apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/assistant-message.tsx (1)
  • AssistantMessage (4-15)
apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/user-message.tsx (1)
  • UserMessage (24-253)
apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/stream-message.tsx (2)
apps/web/client/src/app/project/[id]/_hooks/use-chat.tsx (1)
  • useChatContext (113-119)
packages/db/src/schema/chat/message.ts (1)
  • messages (11-27)
apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/user-message.tsx (1)
packages/models/src/chat/message/message.ts (1)
  • ChatMessage (38-38)
packages/db/src/mappers/chat/message.ts (1)
packages/models/src/chat/message/message.ts (1)
  • ChatMessage (38-38)
apps/web/client/src/app/project/[id]/_hooks/use-chat.tsx (1)
packages/db/src/schema/chat/message.ts (1)
  • messages (11-27)
apps/web/client/src/server/api/routers/chat/message.ts (1)
packages/db/src/schema/chat/message.ts (2)
  • messages (11-27)
  • messageUpdateSchema (30-30)
apps/web/client/src/components/store/editor/chat/message.ts (2)
packages/models/src/chat/message/context.ts (1)
  • MessageContext (53-59)
packages/models/src/chat/message/message.ts (1)
  • ChatMessage (38-38)
apps/web/client/src/app/api/chat/route.ts (3)
packages/models/src/chat/message/message.ts (1)
  • ChatMessage (38-38)
apps/web/client/src/app/api/chat/helpers/message.ts (2)
  • upsertMessage (6-25)
  • loadChat (27-33)
packages/db/src/schema/chat/message.ts (1)
  • messages (11-27)
apps/web/client/src/components/store/editor/chat/conversation.ts (4)
packages/models/src/chat/message/message.ts (1)
  • ChatMessage (38-38)
apps/web/client/src/components/store/editor/chat/message.ts (1)
  • getUserChatMessageFromString (4-21)
packages/git/src/git.ts (1)
  • GitCommit (19-25)
packages/db/src/mappers/chat/message.ts (1)
  • toDbMessage (18-34)
packages/db/src/schema/chat/message.ts (4)
packages/db/src/schema/chat/conversation.ts (1)
  • conversations (11-26)
packages/models/src/chat/message/context.ts (1)
  • MessageContext (53-59)
packages/models/src/chat/message/checkpoint.ts (1)
  • MessageCheckpoints (15-15)
packages/models/src/chat/message/message.ts (1)
  • ChatMessage (38-38)
🪛 Biome (2.1.2)
apps/web/client/src/components/store/editor/chat/index.ts

[error] 83-83: Invalid assignment to oldMessage.metadata?.context

This expression cannot be assigned to

(parse)

apps/web/client/src/components/store/editor/chat/conversation.ts

[error] 165-165: Invalid assignment to userMessage.metadata?.checkpoints

This expression cannot be assigned to

(parse)

🔇 Additional comments (15)
apps/web/client/src/server/api/routers/chat/suggestion.ts (1)

35-39: Keep internal system prompt last and immutable.

Even with validation, ensure no user message with system semantics precedes or follows your system prompt. Current spread is fine if 'system' is disallowed; otherwise, move user messages after the final user prompt or filter as shown above.

apps/web/client/src/app/api/chat/helpers/index.ts (1)

1-1: Barrel re-export looks good.

apps/web/client/src/app/api/chat/helpers/stream.ts (1)

2-2: Import rename LGTM.

apps/web/client/src/app/project/[id]/_hooks/use-chat.tsx (2)

89-91: findLastIndex browser support.

If you target older browsers, consider a fallback for findLastIndex.

Would you like a tiny helper util to compute last index compatible with older environments?


52-56: Ensure useChat picks up updated conversationId on re-renders.

addOrReplaceMessage(toOnlookMessageFromVercel(...)) is invoked with currentConversationId in apps/web/client/src/app/project/[id]/_hooks/use-chat.tsx (lines 52–56 and 69–72 — rg confirms). Sandbox typecheck failed (bun: command not found), so run typecheck/CI locally. Ensure the hook re-reads conversation.current?.conversation.id (or subscribes to conversation changes / avoids stale closures) so messages land in the correct conversation.

apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/assistant-message.tsx (1)

1-1: LGTM: type migration to ChatMessage is consistent.

No client-only hooks here; safe to keep as a Server Component boundary.

Also applies to: 4-4

packages/ai/src/tools/toolset.ts (1)

41-66: Approve rename — verify no leftover references

askTools/buildTools + ChatTools rename looks good and preserves behavior via the spread. Repository search found no matches for ASK_TOOL_SET or BUILD_TOOL_SET; run locally: rg -nP 'ASK_TOOL_SET|BUILD_TOOL_SET' (or confirm CI) to ensure no stragglers remain.

packages/ai/src/stream/index.ts (1)

7-10: String-literal role migration is aligned with the new model.

apps/web/client/src/server/api/routers/chat/message.ts (2)

23-24: LGTM: mapping DB rows via fromDbMessage.

Keeps router output aligned with the unified ChatMessage shape.


31-38: Ownership/authorization check relies on RLS—please verify policies.

You validate existence but not ownership. If RLS enforces tenant scoping on conversations/messages, fine; otherwise add a workspace/user check.

Provide evidence (policy file or tests) that messages/conversations have RLS policies restricting access to the current user/workspace.

apps/web/client/src/components/store/editor/chat/message.ts (1)

9-20: Verify vercelId semantics; mapping currently overwrites it.

fromDbMessage sets metadata.vercelId = message.id when reading from DB, which discards the UUID assigned here. If vercelId is intended to track provider IDs, align creation and mappers, or set it equal to id at creation for consistency.

Possible local alignment:

-    return {
-        id: uuidv4(),
+    const id = uuidv4();
+    return {
+        id,
         role: 'user',
         parts: [{ type: 'text', text: content }],
         metadata: {
             createdAt: new Date(),
             conversationId,
-            vercelId: uuidv4(),
+            vercelId: id,
             context,
             checkpoints: [],
         },
     }
apps/web/client/src/app/api/chat/helpers/message.ts (1)

18-23: Confirm upsert update set.

On conflict you only update conversationId. If messages can change (e.g., parts/role/context edits), you may want to update those fields or enforce immutability explicitly.

packages/models/src/chat/message/message.ts (1)

3-5: Verify TS/ESM resolver for .ts import specifiers

packages/models/src/chat/message/message.ts imports './checkpoint.ts' and './context.ts'. packages/models/tsconfig.json extends '@onlook/typescript/base.json' (no local flags) — confirm the extended/base config or consuming build sets either:

  • moduleResolution: "bundler"
    OR
  • module: "NodeNext" / moduleResolution: "nodeNext"
    and also has allowImportingTsExtensions: true and verbatimModuleSyntax: true.
    If those aren't present, remove the '.ts' extensions or update the tsconfig/build to avoid runtime/build breakage. Check packages/models/tsconfig.json and the referenced @onlook/typescript/base.json (or root/bundler config).
apps/web/client/src/app/api/chat/route.ts (1)

79-81: Trace id choice looks good.

Stable across retries and useful for telemetry correlation.

packages/db/src/schema/chat/message.ts (1)

9-9: Confirm whether 'tool' message role is needed; add 'tool' to the enum if used.

Repo search returned no matches for role:'tool'; confirm whether the AI pipeline emits tool messages — if yes, add 'tool' to packages/db/src/schema/chat/message.ts (message_role enum).

Comment on lines 56 to 61
const { message, chatType, conversationId, projectId } = body as {
message: ChatMessage,
chatType: ChatType,
conversationId: string,
projectId: string,
};
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Validate request body.

Add schema validation (zod) to ensure message.id/role/parts, chatType, conversationId, and projectId are present and well-formed before proceeding.

 import { v4 as uuidv4 } from 'uuid';
+import { z } from 'zod';
@@
-    const { message, chatType, conversationId, projectId } = body as {
-        message: ChatMessage,
-        chatType: ChatType,
-        conversationId: string,
-        projectId: string,
-    };
+    const Body = z.object({
+        message: z.any(), // ideally: refine to your ChatMessage runtime shape
+        chatType: z.nativeEnum(ChatType),
+        conversationId: z.string().uuid(),
+        projectId: z.string().uuid(),
+    });
+    const { message, chatType, conversationId, projectId } = Body.parse(body);
📝 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
const { message, chatType, conversationId, projectId } = body as {
message: ChatMessage,
chatType: ChatType,
conversationId: string,
projectId: string,
};
import { v4 as uuidv4 } from 'uuid';
import { z } from 'zod';
const Body = z.object({
message: z.any(), // ideally: refine to your ChatMessage runtime shape
chatType: z.nativeEnum(ChatType),
conversationId: z.string().uuid(),
projectId: z.string().uuid(),
});
const { message, chatType, conversationId, projectId } = Body.parse(body);
🤖 Prompt for AI Agents
In apps/web/client/src/app/api/chat/route.ts around lines 56-61, the request
body is destructured without validation; add a zod schema to validate that
message has id: string, role: enum/string matching allowed roles, and parts:
non-empty array of strings (or objects matching ChatMessagePart), and that
chatType, conversationId, and projectId are present non-empty strings (chatType
should match the ChatType union). Replace the current unchecked destructuring
with schema.parse(body) (or safeParse and return a 400 response on failure), and
use the parsed values for subsequent logic so malformed requests are rejected
early with a clear 400 error.

Comment on lines 63 to 70
// create or update last message in database
// https://github.com/vercel-labs/ai-sdk-persistence-db/blob/main/lib/db/actions.ts#L50
await upsertMessage({ conversationId, message });

// load the previous messages from the server:
// https://github.com/vercel-labs/ai-sdk-persistence-db/blob/main/lib/db/actions.ts#L50
const messages = await loadChat(conversationId);

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Authorize conversation access and enforce server-side conversationId.

Ensure the provided conversationId belongs to userId/projectId before upserting. Ignore any client-sent metadata.conversationId and trust only the route param.

-    await upsertMessage({ conversationId, message });
+    // Optionally: await assertUserOwnsConversation({ userId, projectId, conversationId });
+    await upsertMessage({ conversationId, message: { ...message, metadata: { ...(message.metadata ?? {}), conversationId } } });

Also, the helper’s onConflictDoUpdate currently updates only conversationId. Consider updating role, parts, context, checkpoints, and createdAt for idempotence:

- .onConflictDoUpdate({
-   target: messages.id,
-   set: { conversationId },
- })
+ .onConflictDoUpdate({
+   target: messages.id,
+   set: {
+     conversationId,
+     role: dbMessage.role,
+     parts: dbMessage.parts,
+     context: dbMessage.context,
+     checkpoints: dbMessage.checkpoints,
+     createdAt: dbMessage.createdAt,
+   },
+ })

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/web/client/src/app/api/chat/route.ts around lines 63-70, you must
enforce server-side conversationId and authorize access before persisting or
loading messages: ignore any client-sent metadata.conversationId and
derive/validate conversationId from the authenticated route param and
user/project context (check that conversationId belongs to the current
userId/projectId and return 403 if not); only then call upsertMessage and
loadChat with the server-validated conversationId. Also update the
upsert/onConflictDoUpdate logic so it updates role, parts, context, checkpoints,
and createdAt (not just conversationId) to make the operation idempotent and
keep message state consistent. Ensure errors are handled and reject requests
when authorization fails.

import { useChatContext } from '@/app/project/[id]/_hooks/use-chat';
import { useEditorEngine } from '@/components/store/editor';
import { ChatType, MessageCheckpointType, type UserChatMessage } from '@onlook/models';
import { ChatType, MessageCheckpointType, type ChatMessage } from '@onlook/models';
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add 'use client' — hooks used in this component will crash without it.

This file uses React state/effects and browser APIs; mark it as a Client Component.

+ 'use client';
+
 import { useChatContext } from '@/app/project/[id]/_hooks/use-chat';
📝 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
import { ChatType, MessageCheckpointType, type ChatMessage } from '@onlook/models';
'use client';
import { useChatContext } from '@/app/project/[id]/_hooks/use-chat';
import { ChatType, MessageCheckpointType, type ChatMessage } from '@onlook/models';
🤖 Prompt for AI Agents
In
apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/user-message.tsx
around line 3, this component uses React hooks/browser APIs but lacks the "use
client" directive; add a "use client" literal as the very first line of the file
(before any imports) so React treats it as a Client Component and hooks/effects
will run correctly.

Comment on lines +41 to 47
.values(input.message)
.onConflictDoUpdate({
target: [messages.id],
set: {
...normalizedMessage,
...input.message,
},
});
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Avoid updating immutable fields during upsert.

Spreading input into set may update id/conversationId/createdAt unintentionally. Restrict to mutable columns.

-            .onConflictDoUpdate({
-              target: [messages.id],
-              set: {
-                ...input.message,
-              },
-            });
+            .onConflictDoUpdate({
+              target: [messages.id],
+              set: {
+                // explicitly allow only mutable fields
+                role: input.message.role,
+                parts: input.message.parts,
+                context: input.message.context,
+                checkpoints: input.message.checkpoints,
+              },
+            });
📝 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
.values(input.message)
.onConflictDoUpdate({
target: [messages.id],
set: {
...normalizedMessage,
...input.message,
},
});
.values(input.message)
.onConflictDoUpdate({
target: [messages.id],
set: {
// explicitly allow only mutable fields
role: input.message.role,
parts: input.message.parts,
context: input.message.context,
checkpoints: input.message.checkpoints,
},
});

Comment on lines +17 to 19
role: z.enum(['user', 'assistant', 'system']),
content: z.string(),
})),
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Disallow client-supplied 'system' role to prevent prompt injection.

Allowing 'system' in input lets users override internal instructions. Restrict to 'user' | 'assistant' or sanitize before passing to the model.

-            messages: z.array(z.object({
-                role: z.enum(['user', 'assistant', 'system']),
-                content: z.string(),
-            })),
+            messages: z.array(z.object({
+                role: z.enum(['user', 'assistant']),
+                content: z.string().min(1),
+            })),

And defensively filter just in case:

-                    ...input.messages,
+                    ...input.messages.filter(m => m.role !== 'system'),
📝 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
role: z.enum(['user', 'assistant', 'system']),
content: z.string(),
})),
role: z.enum(['user', 'assistant']),
content: z.string().min(1),
})),
🤖 Prompt for AI Agents
In apps/web/client/src/server/api/routers/chat/suggestion.ts around lines 17 to
19, the Zod schema currently allows a client-supplied 'system' role which can
enable prompt injection; change the schema to only accept 'user' and 'assistant'
(remove 'system') and/or add defensive filtering after parsing to drop any
messages with role === 'system' before forwarding to the model, ensuring the
server enforces internal system instructions and never trusts client-provided
system roles.

applied: boolean("applied"),
commitOid: text("commit_oid"),
snapshots: jsonb("snapshots").$type<any>(),
content: text("content"),
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Enforce “content OR parts” invariant at DB-level.

Prevent empty messages by adding a CHECK constraint.

// import { sql } from "drizzle-orm";
export const messages = pgTable("messages", {
  // ...columns...
}, (t) => ({
  contentOrPartsChk: {
    // CHECK ((content IS NOT NULL) OR (jsonb_array_length(parts) > 0))
    // If using drizzle-orm/sql:
    constraint: sql`(content IS NOT NULL) OR (jsonb_array_length(${t.parts}) > 0)`,
  },
}));
🤖 Prompt for AI Agents
In packages/db/src/schema/chat/message.ts around line 26, the messages table
allows empty rows because there is no DB-level CHECK enforcing that either
content is present or parts (jsonb array) has length > 0; add a CHECK constraint
to the pgTable options so the table definition includes a constraint like:
constraint: sql`(content IS NOT NULL) OR (jsonb_array_length(${t.parts}) > 0)`,
and import sql from "drizzle-orm" (or drizzle-orm/sql) at the top; update the
table factory call to include this constraint entry (e.g., in the third argument
callback returning contentOrPartsChk) so the database prevents empty messages.

@vercel vercel bot temporarily deployed to Preview – docs September 13, 2025 00:28 Inactive
Copy link

@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: 1

♻️ Duplicate comments (5)
packages/db/src/mappers/chat/message.ts (2)

5-15: Stop spreading DbMessage into ChatMessage (leaks DB-only fields).

Spreading ...message pulls in DB-only/deprecated fields (e.g., content, applied, commitOid, snapshots, possibly conversationId) into the UI object and risks excess-property issues. Map explicitly.

Apply:

 export const fromDbMessage = (message: DbMessage): ChatMessage => {
-    return {
-        ...message,
-        metadata: {
-            conversationId: message.conversationId,
-            createdAt: message.createdAt,
-            vercelId: message.id,
-            context: message.context ?? [],
-            checkpoints: message.checkpoints ?? [],
-        },
-        parts: message.parts ?? [],
-    }
+    return {
+        id: message.id,
+        role: message.role,
+        parts: message.parts ?? [],
+        metadata: {
+            conversationId: message.conversationId,
+            createdAt: message.createdAt,
+            vercelId: message.id,
+            context: message.context ?? [],
+            checkpoints: message.checkpoints ?? [],
+            // include parent links if present (see toDbMessage comment)
+            parentConversationId: (message as any).parentConversationId ?? null,
+            parentMessageId: (message as any).parentMessageId ?? null,
+        },
+    };
 }

24-27: Guarantee parts is an array when persisting.

parts: message.parts can write undefined|null, violating the schema expectation (array).

-        parts: message.parts,
+        parts: Array.isArray(message.parts) ? message.parts : (message.parts ?? []),
apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/user-message.tsx (1)

1-11: Missing 'use client' directive.

This component uses React hooks and browser APIs but lacks the required directive for client components.

+'use client';
+
 import { useChatContext } from '@/app/project/[id]/_hooks/use-chat';
apps/web/client/src/app/api/chat/route.ts (1)

64-69: Add request-body validation (use zod)

Validate req.json() in streamResponse before destructuring; zod is present in apps/web/client/package.json (zod ^4.1.3). File: apps/web/client/src/app/api/chat/route.ts — replace the current req.json() destructure.

+import { z } from 'zod';
+
+const requestBodySchema = z.object({
+    message: z.object({
+        id: z.string(),
+        role: z.string(),
+        parts: z.array(z.any()).min(1),
+        metadata: z.any().optional(),
+    }),
+    chatType: z.nativeEnum(ChatType),
+    conversationId: z.string().uuid(),
+    projectId: z.string().uuid(),
+});
 
 export const streamResponse = async (req: NextRequest, userId: string) => {
     // ... existing code ...
     try {
-        const { message, chatType, conversationId, projectId }: {
-            message: ChatMessage,
-            chatType: ChatType,
-            conversationId: string,
-            projectId: string,
-        } = await req.json()
+        const body = await req.json();
+        const validationResult = requestBodySchema.safeParse(body);
+        if (!validationResult.success) {
+            return new Response(JSON.stringify({
+                error: 'Invalid request body',
+                code: 400,
+                details: validationResult.error.flatten()
+            }), {
+                status: 400,
+                headers: { 'Content-Type': 'application/json' }
+            });
+        }
+        const { message, chatType, conversationId, projectId } = validationResult.data;
apps/web/client/src/components/store/editor/chat/conversation.ts (1)

123-125: Title generation blocked by non-null placeholder displayName.

Same issue as line 51 - if displayName defaults to a placeholder string, automatic title generation won't occur.

-        if (!this.current.conversation.displayName) {
+        if (!this.current.conversation.displayName || this.current.conversation.displayName === 'New Conversation') {
🧹 Nitpick comments (2)
apps/web/client/src/app/project/[id]/_hooks/use-start-project.tsx (1)

101-106: Confirm intent: using addEditMessage for CREATE flow.

If CREATE needs different metadata (e.g., tags, parent links), a dedicated addCreateMessage helper may avoid misclassification.

Proposed tweak:

-            const message = await editorEngine.chat.addEditMessage(
+            const message = await editorEngine.chat.addCreateMessage?.(
                 prompt,
                 context,
             );
-            sendMessageToChat(message, ChatType.CREATE);
+            sendMessageToChat(message, ChatType.CREATE);

If addCreateMessage doesn't exist, verify addEditMessage sets appropriate metadata for CREATE and carries parent IDs when resuming requests.

packages/ai/src/tools/toolset.ts (1)

69-71: Drop unnecessary async from getToolSetFromType.

It’s synchronous; returning a Promise adds overhead without benefit.

-export async function getToolSetFromType(chatType: ChatType) {
-    return chatType === ChatType.ASK ? askTools : buildTools;
-}
+export function getToolSetFromType(chatType: ChatType): ToolSet {
+  return chatType === ChatType.ASK ? askTools : buildTools;
+}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2043994 and f4db51e.

📒 Files selected for processing (14)
  • apps/web/client/src/app/api/chat/helpers/message.ts (1 hunks)
  • apps/web/client/src/app/api/chat/helpers/stream.ts (1 hunks)
  • apps/web/client/src/app/api/chat/route.ts (3 hunks)
  • apps/web/client/src/app/project/[id]/_components/canvas/overlay/elements/buttons/chat.tsx (1 hunks)
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-input/index.tsx (1 hunks)
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/user-message.tsx (4 hunks)
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/error.tsx (1 hunks)
  • apps/web/client/src/app/project/[id]/_hooks/use-chat.tsx (4 hunks)
  • apps/web/client/src/app/project/[id]/_hooks/use-start-project.tsx (1 hunks)
  • apps/web/client/src/components/store/editor/chat/conversation.ts (5 hunks)
  • apps/web/client/src/components/store/editor/chat/index.ts (4 hunks)
  • apps/web/client/src/components/tools/tools.ts (2 hunks)
  • packages/ai/src/tools/toolset.ts (4 hunks)
  • packages/db/src/mappers/chat/message.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/client/src/app/api/chat/helpers/message.ts
🧰 Additional context used
📓 Path-based instructions (5)
apps/web/client/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Prefer imports using @/* or ~/* aliases instead of deep relative paths within the client app

Files:

  • apps/web/client/src/components/tools/tools.ts
  • apps/web/client/src/app/project/[id]/_components/canvas/overlay/elements/buttons/chat.tsx
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/error.tsx
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-input/index.tsx
  • apps/web/client/src/app/api/chat/helpers/stream.ts
  • apps/web/client/src/app/project/[id]/_hooks/use-start-project.tsx
  • apps/web/client/src/components/store/editor/chat/conversation.ts
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/user-message.tsx
  • apps/web/client/src/app/project/[id]/_hooks/use-chat.tsx
  • apps/web/client/src/components/store/editor/chat/index.ts
  • apps/web/client/src/app/api/chat/route.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Do not use the 'any' type unless necessary

Files:

  • apps/web/client/src/components/tools/tools.ts
  • apps/web/client/src/app/project/[id]/_components/canvas/overlay/elements/buttons/chat.tsx
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/error.tsx
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-input/index.tsx
  • apps/web/client/src/app/api/chat/helpers/stream.ts
  • apps/web/client/src/app/project/[id]/_hooks/use-start-project.tsx
  • packages/db/src/mappers/chat/message.ts
  • apps/web/client/src/components/store/editor/chat/conversation.ts
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/user-message.tsx
  • apps/web/client/src/app/project/[id]/_hooks/use-chat.tsx
  • apps/web/client/src/components/store/editor/chat/index.ts
  • packages/ai/src/tools/toolset.ts
  • apps/web/client/src/app/api/chat/route.ts
apps/web/client/src/app/**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

Default to Server Components; add 'use client' only when using events, state/effects, browser APIs, or client-only libraries

Files:

  • apps/web/client/src/app/project/[id]/_components/canvas/overlay/elements/buttons/chat.tsx
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/error.tsx
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-input/index.tsx
  • apps/web/client/src/app/project/[id]/_hooks/use-start-project.tsx
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/user-message.tsx
  • apps/web/client/src/app/project/[id]/_hooks/use-chat.tsx
apps/web/client/src/app/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Do not use process.env in client code; prefer importing env from @/env

Files:

  • apps/web/client/src/app/project/[id]/_components/canvas/overlay/elements/buttons/chat.tsx
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/error.tsx
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-input/index.tsx
  • apps/web/client/src/app/api/chat/helpers/stream.ts
  • apps/web/client/src/app/project/[id]/_hooks/use-start-project.tsx
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/user-message.tsx
  • apps/web/client/src/app/project/[id]/_hooks/use-chat.tsx
  • apps/web/client/src/app/api/chat/route.ts
apps/web/client/src/app/**/{page,layout,route}.ts?(x)

📄 CodeRabbit inference engine (AGENTS.md)

Follow Next.js App Router structure: page.tsx, layout.tsx, and route.ts under apps/web/client/src/app

Files:

  • apps/web/client/src/app/api/chat/route.ts
🧠 Learnings (2)
📚 Learning: 2025-09-12T18:42:26.836Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: AGENTS.md:0-0
Timestamp: 2025-09-12T18:42:26.836Z
Learning: Applies to apps/web/client/messages/**/*.{json,ts} : Add or modify i18n message keys under apps/web/client/messages; avoid breaking renames

Applied to files:

  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/user-message.tsx
📚 Learning: 2025-09-12T18:42:26.836Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: AGENTS.md:0-0
Timestamp: 2025-09-12T18:42:26.836Z
Learning: Applies to apps/web/client/src/app/**/*.tsx : Default to Server Components; add 'use client' only when using events, state/effects, browser APIs, or client-only libraries

Applied to files:

  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/user-message.tsx
🧬 Code graph analysis (6)
packages/db/src/mappers/chat/message.ts (1)
packages/models/src/chat/message/message.ts (1)
  • ChatMessage (38-38)
apps/web/client/src/components/store/editor/chat/conversation.ts (3)
packages/models/src/chat/message/message.ts (1)
  • ChatMessage (38-38)
apps/web/client/src/components/store/editor/chat/message.ts (1)
  • getUserChatMessageFromString (4-21)
packages/db/src/mappers/chat/message.ts (1)
  • toDbMessage (18-35)
apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/user-message.tsx (1)
packages/models/src/chat/message/message.ts (1)
  • ChatMessage (38-38)
apps/web/client/src/app/project/[id]/_hooks/use-chat.tsx (4)
packages/models/src/chat/message/message.ts (1)
  • ChatMessage (38-38)
apps/web/client/src/components/store/editor/chat/context.ts (1)
  • ChatContext (16-291)
apps/web/client/src/components/store/editor/index.tsx (1)
  • useEditorEngine (10-14)
packages/db/src/schema/chat/message.ts (1)
  • messages (11-27)
apps/web/client/src/components/store/editor/chat/index.ts (4)
packages/models/src/chat/message/context.ts (1)
  • MessageContext (53-59)
packages/models/src/chat/message/message.ts (1)
  • ChatMessage (38-38)
packages/git/src/git.ts (1)
  • GitCommit (19-25)
apps/web/client/src/trpc/react.tsx (1)
  • api (23-23)
apps/web/client/src/app/api/chat/route.ts (4)
packages/models/src/chat/message/message.ts (1)
  • ChatMessage (38-38)
apps/web/client/src/app/api/chat/helpers/message.ts (2)
  • upsertMessage (6-26)
  • loadChat (28-34)
packages/db/src/schema/chat/message.ts (1)
  • messages (11-27)
apps/web/client/src/app/api/chat/helpers/usage.ts (1)
  • incrementUsage (43-65)
🔇 Additional comments (17)
packages/db/src/mappers/chat/message.ts (1)

18-24: Persist subchat relationships (parentConversationId / parentMessageId)

  • packages/models/src/chat/message/message.ts — ChatMetadata does NOT include parentConversationId/parentMessageId.
  • packages/db/src/schema/chat/message.ts — messages table has no parentConversationId/parentMessageId columns.
  • If subchat links should live on Message: add optional parentConversationId?: string | null and parentMessageId?: string | null to ChatMetadata; add corresponding columns + migration to messages table; update packages/db/src/mappers/chat/message.ts:
    • toDbMessage: parentConversationId: message.metadata?.parentConversationId ?? null, parentMessageId: message.metadata?.parentMessageId ?? null
    • fromDbMessage: mirror message.parentConversationId/message.parentMessageId into metadata.
  • If subchat links belong on Conversation (conversation.parentConversationId/parentMessageId already implemented), keep message mapper as-is and confirm intended design.
apps/web/client/src/components/tools/tools.ts (1)

12-13: Good move: consume getToolSetFromType from the AI package.

Centralizing toolset resolution in the library reduces drift between UI and backend logic.

apps/web/client/src/app/api/chat/helpers/stream.ts (1)

2-2: LGTM: toolset mapping removed from here and sourced from the AI lib.

This keeps stream helpers focused on model/system-prompt concerns.

apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-input/index.tsx (1)

139-139: API update wired correctly: sending the constructed message to chat.

Passing (message, chatMode) matches the new signature and preserves the built payload.

apps/web/client/src/app/project/[id]/_components/canvas/overlay/elements/buttons/chat.tsx (1)

31-33: Mini-chat path updated to two-arg send — looks good.

Capturing message before sending aligns with the new flow.

apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/error.tsx (1)

23-25: Fix flow is consistent with new API; errors surfaced to users.

Good user feedback on failure.

packages/ai/src/tools/toolset.ts (2)

42-53: askTools composition looks correct and minimal for ASK.

Clean separation of read-only capabilities.


55-66: buildTools correctly supersets askTools.

Good reuse; avoids duplication.

apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/user-message.tsx (1)

87-87: LGTM! Correct update to sendMessageToChat signature.

The function call now correctly passes both the message and chat type, aligning with the updated signature.

apps/web/client/src/components/store/editor/chat/index.ts (3)

49-54: LGTM! Proper separation of concerns for commit attachment.

The new attachCommitToUserMessage method properly handles the commit attachment logic that was previously in the conversation manager.


56-87: Good error handling and metadata management.

The function properly checks for missing conversation and message, builds metadata safely without optional chaining on the left-hand side, and correctly updates checkpoints.


119-134: Proper metadata reconstruction without invalid optional chaining.

The metadata object is correctly rebuilt using spread operator, avoiding the previous invalid optional chaining assignment issue.

apps/web/client/src/app/project/[id]/_hooks/use-chat.tsx (3)

1-2: Add 'use client' directive at the top of the file.

This file uses hooks, state, and browser APIs. According to the coding guidelines, client components need the 'use client' directive as the first line.

The file already has 'use client', which is correct.


28-41: Clean implementation of single message sending.

The prepareSendMessagesRequest correctly sends only the last message with necessary context, avoiding redundant message history transmission.


86-99: Good use of deep cloning for message immutability.

Using jsonClone ensures the original message object isn't mutated, which is important for React state management.

apps/web/client/src/app/api/chat/route.ts (1)

71-72: Add access control check before upserting message

Confirm whether a conversation/project access-check exists; if absent, add one before calling upsertMessage and return 403 on unauthorized.

+        // Verify user has access to this conversation
+        const hasAccess = await verifyUserConversationAccess(userId, conversationId, projectId);
+        if (!hasAccess) {
+            return new Response(JSON.stringify({
+                error: 'Unauthorized access to conversation',
+                code: 403
+            }), {
+                status: 403,
+                headers: { 'Content-Type': 'application/json' }
+            });
+        }
+
         // create or update last message in database
         await upsertMessage({ conversationId, message });
apps/web/client/src/components/store/editor/chat/conversation.ts (1)

185-187: LGTM! Proper update to upsertMessageInStorage signature.

The method correctly passes both the message and conversationId to maintain proper data relationships.

try {
this.creatingConversation = true;
if (this.current?.messages.length === 0 && !this.current?.conversation.title) {
if (this.current?.messages.length === 0 && !this.current?.conversation.displayName) {
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Title generation check needs adjustment for non-null default displayName.

The condition checks for falsy displayName, but if it defaults to a non-null placeholder string like "New Conversation", title generation won't trigger.

-            if (this.current?.messages.length === 0 && !this.current?.conversation.displayName) {
+            if (this.current?.messages.length === 0 && (!this.current?.conversation.displayName || this.current?.conversation.displayName === 'New Conversation')) {

Let me verify what the default displayName value is:

#!/bin/bash
# Check for default displayName values in conversation creation
rg -n "displayName.*=.*['\"]New Conversation['\"]|displayName:.*['\"]New Conversation['\"]" --type ts
🤖 Prompt for AI Agents
In apps/web/client/src/components/store/editor/chat/conversation.ts around line
51, the current condition only checks for a falsy displayName so it won't
trigger title generation when displayName defaults to a non-null placeholder
like "New Conversation"; update the condition to also treat the default
placeholder as "no name" (e.g., check if !this.current?.conversation.displayName
|| this.current.conversation.displayName === 'New Conversation' or compare
against the shared/default constant if one exists) so that when messages length
is 0 and the displayName is the default placeholder the title generation runs.

return updatedMessage;
};

export const debouncedUpsertMessage = debounce(upsertMessage, 500);
Copy link
Contributor

Choose a reason for hiding this comment

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

Using lodash's debounce with an async function may not behave as expected. Debounced functions don’t reliably propagate promise results, which could lead to race conditions or missed DB updates. Consider using an async‐aware debounce or ensuring proper flush (e.g. with { leading: false, trailing: true } and manual flush) if DB persistence is critical.

});
},
onFinish: ({ message }) => {
onFinish: (options: { message: ChatMessage, }) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

The onFinish callback accesses 'options.finishReason', but the parameter type only specifies { message: ChatMessage }. If finishReason is expected, update the type definition to include it to avoid potential undefined behavior.

Suggested change
onFinish: (options: { message: ChatMessage, }) => {
onFinish: (options: { message: ChatMessage, finishReason: string }) => {

Copy link

@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: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
apps/web/client/src/server/api/routers/usage/index.ts (2)

75-83: Idempotency bug: credit is decremented even when insert conflicts (duplicate traceId).

You decrement rate limits before inserting the usage record; with onConflictDoNothing() a duplicate insert returns no row but the credit remains deducted. Add back the credit when no row was inserted, and target the specific unique constraint to avoid masking unrelated conflicts.

-            const usageRecord = await tx.insert(usageRecords).values({
+            const usageRecord = await tx.insert(usageRecords).values({
                 userId: user.id,
                 type: input.type,
                 timestamp: new Date(),
                 traceId: input.traceId,
-            }).onConflictDoNothing().returning({ id: usageRecords.id });
+            })
+            .onConflictDoNothing({ target: [usageRecords.userId, usageRecords.traceId] })
+            .returning({ id: usageRecords.id });
+
+            const usageRecordId = usageRecord?.[0]?.id;
+            if (!usageRecordId && rateLimitId) {
+                // roll back the deducted credit on duplicate traceId
+                await tx.update(rateLimits).set({
+                    left: sql`${rateLimits.left} + 1`,
+                }).where(eq(rateLimits.id, rateLimitId));
+                rateLimitId = undefined;
+            }
-
-            return { rateLimitId, usageRecordId: usageRecord?.[0]?.id };
+            return { rateLimitId, usageRecordId };

4-4: Incorrect date-fns import – likely runtime error.

date-fns/sub uses a default export; named import will be undefined at runtime. Switch to default or top-level named import.

- import { sub } from 'date-fns/sub';
+ import sub from 'date-fns/sub';
+ // or: import { sub } from 'date-fns';
apps/web/client/src/app/api/chat/route.ts (1)

79-87: Authorize conversation access and enforce server-side conversationId in message metadata.

Ensure conversationId belongs to userId/projectId and override any client-sent metadata.conversationId.

-        const modelConfig = await getModelFromType(chatType);
+        // TODO: assert user owns conversation before proceeding (return 403 on failure)
+        // await assertUserOwnsConversation({ userId, projectId, conversationId });
+        const modelConfig = await getModelFromType(chatType);

And keep overriding metadata when upserting (as shown in the previous diff).

♻️ Duplicate comments (6)
packages/db/src/mappers/chat/message.ts (2)

5-15: Don’t spread DB rows into UI messages (leaks deprecated/DB-only fields).

Reintroduces the previously fixed issue; map explicitly to the UI shape.

 export const fromDbMessage = (message: DbMessage): ChatMessage => {
-    return {
-        ...message,
-        metadata: {
-            conversationId: message.conversationId,
-            createdAt: message.createdAt,
-            context: message.context ?? [],
-            checkpoints: message.checkpoints ?? [],
-        },
-        parts: message.parts ?? [],
-    }
+    return {
+        id: message.id,
+        role: message.role,
+        parts: message.parts ?? [],
+        metadata: {
+            conversationId: message.conversationId,
+            createdAt: message.createdAt,
+            context: message.context ?? [],
+            checkpoints: message.checkpoints ?? [],
+        },
+    };
 }

23-26: Guarantee parts is always an array when persisting.

Avoid writing null/undefined; aligns with schema default([]).

-        parts: message.parts,
+        parts: message.parts ?? [],
apps/web/client/src/components/store/editor/chat/index.ts (1)

55-85: Good fix: no optional-chaining on LHS when updating metadata.

You correctly materialize and replace message.metadata instead of assigning to ?.checkpoints/?.context.

Minor: consider using the commit’s timestamp for the checkpoint.

-            {
+            {
                 type: MessageCheckpointType.GIT,
                 oid: commit.oid,
-                createdAt: new Date(),
+                createdAt: new Date(commit.timestamp),
             },
apps/web/client/src/components/store/editor/chat/conversation.ts (2)

112-127: Title generation condition has same placeholder issue.

Same as above: if a non-null placeholder is used, this won’t trigger title generation.

-        if (!this.current.conversation.displayName) {
+        if (!this.current.conversation.displayName ||
+            this.current.conversation.displayName === 'New Conversation') {

51-53: Guard misses “placeholder” displayName; can create duplicate empty conversations.

If displayName defaults to a non-empty placeholder (e.g., "New Conversation"), this check won’t block duplicates. Treat the placeholder as empty or use null default.

-            if (this.current?.messages.length === 0 && !this.current?.conversation.displayName) {
+            if (
+                this.current?.messages.length === 0 &&
+                (!this.current?.conversation.displayName ||
+                 this.current?.conversation.displayName === 'New Conversation')
+            ) {
apps/web/client/src/app/api/chat/route.ts (1)

66-72: Validate request body (schema) before use.

Add Zod (or similar) to enforce shape; reject bad requests with 400.

+import { z } from 'zod';
@@
-        const { message, chatType, conversationId, projectId }: {
-            message: ChatMessage,
-            chatType: ChatType,
-            conversationId: string,
-            projectId: string,
-        } = await req.json()
+        const Body = z.object({
+            message: z.any(), // TODO: refine to ChatMessage runtime schema
+            chatType: z.nativeEnum(ChatType),
+            conversationId: z.string().uuid(),
+            projectId: z.string().uuid(),
+        });
+        const body = await req.json();
+        const { message, chatType, conversationId, projectId } = Body.parse(body);
🧹 Nitpick comments (8)
apps/web/client/src/server/api/routers/usage/index.ts (1)

29-33: Validate traceId length to match DB schema.

DB column is varchar(255); cap the input to prevent insert errors.

-    traceId: z.string().optional(),
+    traceId: z.string().max(255, 'traceId must be <= 255 chars').optional(),
apps/web/client/src/components/store/editor/chat/message.ts (1)

4-20: Guard against empty input and normalize text.

Trim the content and avoid emitting empty text parts; default context to [].

 export const getUserChatMessageFromString = (
     content: string,
-    context: MessageContext[],
+    context: MessageContext[] = [],
     conversationId: string,
 ): ChatMessage => {
+    const text = content.trim();
     return {
         id: uuidv4(),
         role: 'user',
-        parts: [{ type: 'text', text: content }],
+        parts: text ? [{ type: 'text', text }] : [],
         metadata: {
             createdAt: new Date(),
             conversationId,
             context,
             checkpoints: [],
         },
     }
 }
packages/models/src/chat/message/message.ts (1)

14-16: Tighten the “empty data” type.

{} accepts any non-nullish object. Use a no-extra-keys shape to prevent accidental properties.

-export type ChatDataPart = {};
+export type ChatDataPart = Record<string, never>;
apps/web/client/src/app/project/[id]/_hooks/use-chat.tsx (1)

54-54: Drop noisy console log.

Avoid spamming logs in production.

-            console.log('onFinish', options);
+            // onFinish: handled below
apps/web/client/src/components/store/editor/chat/index.ts (1)

41-46: Fire-and-forget commit attach: confirm intent or await it.

this.createAndAttachCommitToUserMessage(...) is not awaited; if the commit/checkpoint must exist before streaming or UI actions, await it or explicitly mark as fire-and-forget with void to signal intent.

Apply:

-        this.createAndAttachCommitToUserMessage(userMessage.id, content);
+        void this.createAndAttachCommitToUserMessage(userMessage.id, content);
apps/web/client/src/components/store/editor/chat/conversation.ts (2)

139-145: Keep current and list entries in sync after title generation.

Also update this.current.conversation.displayName when the selected conversation matches.

         const listConversation = this.conversations.find((c) => c.id === conversationId);
         if (!listConversation) {
             console.error('No conversation found');
             return;
         }
-        listConversation.displayName = title;
+        listConversation.displayName = title;
+        if (this.current?.conversation.id === conversationId) {
+            this.current.conversation.displayName = title;
+        }

147-158: Consider persisting on add/replace or remove unused helper.

addOrReplaceMessage no longer upserts; if persistence is now centralized in the API route, remove upsertMessageInStorage to avoid dead code. If local persistence is still desired in some flows, call it here.

apps/web/client/src/app/api/chat/route.ts (1)

131-137: Assistant message metadata: consider propagating subchat linkage if applicable.

If metadata now supports parentConversationId / parentMessageId, set them here when continuing a subchat.

                 messageMetadata: (_) => ({
                     createdAt: new Date(),
                     conversationId,
                     context: [],
                     checkpoints: [],
+                    // parentConversationId,
+                    // parentMessageId,
                 }),

Would you like me to wire this based on the last user message ID in messages?

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f4db51e and 022389a.

📒 Files selected for processing (9)
  • apps/web/client/src/app/api/chat/helpers/message.ts (1 hunks)
  • apps/web/client/src/app/api/chat/route.ts (4 hunks)
  • apps/web/client/src/app/project/[id]/_hooks/use-chat.tsx (3 hunks)
  • apps/web/client/src/components/store/editor/chat/conversation.ts (5 hunks)
  • apps/web/client/src/components/store/editor/chat/index.ts (4 hunks)
  • apps/web/client/src/components/store/editor/chat/message.ts (1 hunks)
  • apps/web/client/src/server/api/routers/usage/index.ts (1 hunks)
  • packages/db/src/mappers/chat/message.ts (1 hunks)
  • packages/models/src/chat/message/message.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/client/src/app/api/chat/helpers/message.ts
🧰 Additional context used
📓 Path-based instructions (6)
apps/web/client/src/server/api/routers/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/client/src/server/api/routers/**/*.ts: Place tRPC routers under apps/web/client/src/server/api/routers and export them from the API root
Use publicProcedure/protectedProcedure from apps/web/client/src/server/api/trpc.ts and validate inputs with Zod
Return SuperJSON-serializable data (plain objects/arrays) from tRPC procedures

Files:

  • apps/web/client/src/server/api/routers/usage/index.ts
apps/web/client/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Prefer imports using @/* or ~/* aliases instead of deep relative paths within the client app

Files:

  • apps/web/client/src/server/api/routers/usage/index.ts
  • apps/web/client/src/components/store/editor/chat/message.ts
  • apps/web/client/src/components/store/editor/chat/conversation.ts
  • apps/web/client/src/components/store/editor/chat/index.ts
  • apps/web/client/src/app/project/[id]/_hooks/use-chat.tsx
  • apps/web/client/src/app/api/chat/route.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Do not use the 'any' type unless necessary

Files:

  • apps/web/client/src/server/api/routers/usage/index.ts
  • apps/web/client/src/components/store/editor/chat/message.ts
  • packages/db/src/mappers/chat/message.ts
  • apps/web/client/src/components/store/editor/chat/conversation.ts
  • apps/web/client/src/components/store/editor/chat/index.ts
  • apps/web/client/src/app/project/[id]/_hooks/use-chat.tsx
  • packages/models/src/chat/message/message.ts
  • apps/web/client/src/app/api/chat/route.ts
apps/web/client/src/app/**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

Default to Server Components; add 'use client' only when using events, state/effects, browser APIs, or client-only libraries

Files:

  • apps/web/client/src/app/project/[id]/_hooks/use-chat.tsx
apps/web/client/src/app/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Do not use process.env in client code; prefer importing env from @/env

Files:

  • apps/web/client/src/app/project/[id]/_hooks/use-chat.tsx
  • apps/web/client/src/app/api/chat/route.ts
apps/web/client/src/app/**/{page,layout,route}.ts?(x)

📄 CodeRabbit inference engine (AGENTS.md)

Follow Next.js App Router structure: page.tsx, layout.tsx, and route.ts under apps/web/client/src/app

Files:

  • apps/web/client/src/app/api/chat/route.ts
🧬 Code graph analysis (8)
apps/web/client/src/server/api/routers/usage/index.ts (1)
packages/db/src/schema/subscription/usage.ts (1)
  • usageRecords (8-21)
apps/web/client/src/components/store/editor/chat/message.ts (2)
packages/models/src/chat/message/context.ts (1)
  • MessageContext (53-59)
packages/models/src/chat/message/message.ts (1)
  • ChatMessage (16-16)
packages/db/src/mappers/chat/message.ts (1)
packages/models/src/chat/message/message.ts (1)
  • ChatMessage (16-16)
apps/web/client/src/components/store/editor/chat/conversation.ts (3)
packages/models/src/chat/message/message.ts (1)
  • ChatMessage (16-16)
apps/web/client/src/components/store/editor/chat/message.ts (1)
  • getUserChatMessageFromString (4-20)
packages/db/src/mappers/chat/message.ts (1)
  • toDbMessage (17-34)
apps/web/client/src/components/store/editor/chat/index.ts (4)
packages/models/src/chat/message/context.ts (1)
  • MessageContext (53-59)
packages/models/src/chat/message/message.ts (1)
  • ChatMessage (16-16)
packages/git/src/git.ts (1)
  • GitCommit (19-25)
apps/web/client/src/trpc/react.tsx (1)
  • api (23-23)
apps/web/client/src/app/project/[id]/_hooks/use-chat.tsx (4)
packages/models/src/chat/message/message.ts (1)
  • ChatMessage (16-16)
apps/web/client/src/components/store/editor/chat/context.ts (1)
  • ChatContext (16-291)
apps/web/client/src/components/store/editor/index.tsx (1)
  • useEditorEngine (10-14)
packages/db/src/schema/chat/message.ts (1)
  • messages (11-27)
packages/models/src/chat/message/message.ts (3)
packages/models/src/chat/message/context.ts (1)
  • MessageContext (53-59)
packages/models/src/chat/message/checkpoint.ts (1)
  • MessageCheckpoints (15-15)
packages/ai/src/tools/toolset.ts (1)
  • ChatTools (67-67)
apps/web/client/src/app/api/chat/route.ts (5)
packages/models/src/chat/message/message.ts (1)
  • ChatMessage (16-16)
apps/web/client/src/app/api/chat/helpers/message.ts (2)
  • debouncedUpsertMessage (34-34)
  • loadChat (36-42)
packages/db/src/schema/chat/message.ts (1)
  • messages (11-27)
apps/web/client/src/app/api/chat/helpers/usage.ts (1)
  • incrementUsage (43-65)
apps/web/client/src/app/api/chat/helpers/stream.ts (1)
  • errorHandler (89-108)
🔇 Additional comments (2)
apps/web/client/src/components/store/editor/chat/index.ts (1)

93-102: No change needed — getProjectContext is synchronous. getProjectContext(): ProjectMessageContext[] in apps/web/client/src/components/store/editor/chat/context.ts (line 169). Spreading its return is safe; no await required.

apps/web/client/src/app/api/chat/route.ts (1)

66-78: Ensure debouncedUpsertMessage is awaitable (returns a Promise). If the debounce implementation (e.g. lodash/debounce) does not forward/return the wrapped async function’s Promise, awaiting debouncedUpsertMessage(...) is ineffective — use a promise-aware debounce (p-debounce or similar) or call upsertMessage directly.
File: apps/web/client/src/app/api/chat/helpers/message.ts (debouncedUpsertMessage = debounce(upsertMessage, 500)).

Comment on lines 75 to 78
// create or update last message in database
await debouncedUpsertMessage({ id: messageId, conversationId, message });
const messages = await loadChat(conversationId);

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Race: debounced upsert before load can omit the latest user message from context.

Debounced functions often don’t await the inner call; loadChat may run before persistence, so the streamed context can miss the last message. Upsert immediately and/or append the in-memory message to the streamed messages.

-import { debouncedUpsertMessage } from './helpers/message';
+import { upsertMessage, debouncedUpsertMessage } from './helpers/message';
@@
-        // create or update last message in database
-        await debouncedUpsertMessage({ id: messageId, conversationId, message });
-        const messages = await loadChat(conversationId);
+        // upsert immediately to avoid missing context, and include the in-memory message defensively
+        await upsertMessage({ id: messageId, conversationId, message: { ...message, metadata: { ...(message.metadata ?? {}), conversationId } } });
+        const messages = [...await loadChat(conversationId), message];
📝 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
// create or update last message in database
await debouncedUpsertMessage({ id: messageId, conversationId, message });
const messages = await loadChat(conversationId);
import { upsertMessage, debouncedUpsertMessage } from './helpers/message';
// upsert immediately to avoid missing context, and include the in-memory message defensively
await upsertMessage({ id: messageId, conversationId, message: { ...message, metadata: { ...(message.metadata ?? {}), conversationId } } });
const messages = [...await loadChat(conversationId), message];
🤖 Prompt for AI Agents
In apps/web/client/src/app/api/chat/route.ts around lines 75-78, the code uses a
debounced upsert before loading messages which can allow loadChat to run before
persistence and omit the latest user message; replace the debounced call with an
immediate awaited upsert (or call the underlying non-debounced upsert function
and await it) so persistence completes before loadChat, and as a secondary
safety, after loading messages check whether the in-memory message is present
and, if not, append it to the messages array returned to the stream.

import { DefaultChatTransport, lastAssistantMessageIsCompleteWithToolCalls, type UIMessage } from 'ai';
import { ChatType, type ChatMessage } from '@onlook/models';
import { jsonClone } from '@onlook/utility';
import { DefaultChatTransport, lastAssistantMessageIsCompleteWithToolCalls } from 'ai';
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix types for onFinish; import FinishReason.

You read finishReason but the callback param type omits it.

-import { DefaultChatTransport, lastAssistantMessageIsCompleteWithToolCalls } from 'ai';
+import { DefaultChatTransport, lastAssistantMessageIsCompleteWithToolCalls, type FinishReason } from 'ai';
📝 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
import { DefaultChatTransport, lastAssistantMessageIsCompleteWithToolCalls } from 'ai';
import { DefaultChatTransport, lastAssistantMessageIsCompleteWithToolCalls, type FinishReason } from 'ai';
🤖 Prompt for AI Agents
In apps/web/client/src/app/project/[id]/_hooks/use-chat.tsx around line 8, the
onFinish callback type omits the finishReason while the code reads finishReason;
import FinishReason from 'ai' and update the onFinish parameter type to include
finishReason: FinishReason (or FinishReason | undefined if it can be missing).
Ensure the Import list adds FinishReason alongside DefaultChatTransport and
lastAssistantMessageIsCompleteWithToolCalls, and adjust any call sites or type
annotations to accept the new finishReason property.

const chat = useChat({
id: 'user-chat',
const chat = useChat<ChatMessage>({
id: conversationId,
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Verify useChat id handling when conversationId is undefined.

Passing id: conversationId may be undefined on first render; confirm AI SDK behavior and server expectations, or provide a stable fallback id.


🏁 Script executed:

#!/bin/bash
# Inspect server handlers to confirm conversationId is optional and handled.
rg -nC2 -g 'apps/**' -P "api/chat|conversationId|z\.object\(\s*\{[^}]*conversationId" || true

Length of output: 19347


Guard against undefined conversationId before passing it to useChat/send.

editorEngine.chat.conversation.current?.conversation.id can be undefined on first render while server APIs expect a conversationId string (app/api/chat/route.ts; server/api/routers/chat/message.ts, suggestion.ts, conversation.ts). Do not pass id: conversationId directly — either (A) delay creating the chat transport / sending messages until conversationId is defined (apps/web/client/src/app/project/[id]/_hooks/use-chat.tsx:24), or (B) generate a stable temporary id and update server handlers to accept/translate it.

🤖 Prompt for AI Agents
In apps/web/client/src/app/project/[id]/_hooks/use-chat.tsx around line 24, the
code passes id: conversationId directly even though
editorEngine.chat.conversation.current?.conversation.id can be undefined on
first render; change the hook to guard against undefined by delaying creation of
the chat transport and any send calls until conversationId is a non-empty string
(e.g., return early, set up transport inside an effect that runs when
conversationId becomes defined, and expose a loading/ready flag), or
alternatively implement a stable temporary id flow and update server handlers to
accept/translate temporary ids — pick option A (delay until defined) unless you
also update server code.

Comment on lines 52 to 59
onFinish: (options: { message: ChatMessage, }) => {
const { message } = options;
console.log('onFinish', options);
if (!message.metadata) {
return;
}
const finishReason = (message.metadata as { finishReason?: string }).finishReason;
const finishReason = options.finishReason;
lastMessageRef.current = message;
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Type the onFinish options to include finishReason.

Prevents TS error and matches AI SDK.

-        onFinish: (options: { message: ChatMessage, }) => {
+        onFinish: (options: { message: ChatMessage, finishReason: FinishReason }) => {
             const { message } = options;
-            console.log('onFinish', options);
             if (!message.metadata) {
                 return;
             }
             const finishReason = options.finishReason;
📝 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
onFinish: (options: { message: ChatMessage, }) => {
const { message } = options;
console.log('onFinish', options);
if (!message.metadata) {
return;
}
const finishReason = (message.metadata as { finishReason?: string }).finishReason;
const finishReason = options.finishReason;
lastMessageRef.current = message;
onFinish: (options: { message: ChatMessage, finishReason: FinishReason }) => {
const { message } = options;
if (!message.metadata) {
return;
}
const finishReason = options.finishReason;
lastMessageRef.current = message;
🤖 Prompt for AI Agents
In apps/web/client/src/app/project/[id]/_hooks/use-chat.tsx around lines 52 to
59, the onFinish callback options are currently typed as { message: ChatMessage
} but the code reads options.finishReason; update the options type to include
finishReason (e.g. { message: ChatMessage; finishReason?: string } or the
appropriate SDK enum/type) and adjust the function signature and destructuring
accordingly so TypeScript recognizes finishReason and the code compiles without
errors.

Comment on lines +105 to 133
async resubmitMessage(id: string, newMessageContent: string): Promise<ChatMessage | null> {
if (!this.conversation.current?.conversation.id) {
console.error('No conversation found');
return null;
}
const oldMessageIndex = this.conversation.current?.messages.findIndex((m) => m.id === id && m.role === 'user');
if (oldMessageIndex === undefined || oldMessageIndex === -1 || !this.conversation.current?.messages[oldMessageIndex]) {
console.error('No message found with id', id);
return null;
}

const oldMessage = this.conversation.current?.messages[oldMessageIndex] as UserChatMessage;
const oldMessage = this.conversation.current?.messages[oldMessageIndex];

// Update the old message with the new content
const newContext = await this.context.getRefreshedContext(oldMessage.metadata.context);
oldMessage.metadata.context = newContext;
const newContext = await this.context.getRefreshedContext(oldMessage.metadata?.context ?? []);
oldMessage.metadata = {
...oldMessage.metadata,
context: newContext,
createdAt: oldMessage.metadata?.createdAt || new Date(),
conversationId: oldMessage.metadata?.conversationId || this.conversation.current?.conversation.id,
checkpoints: oldMessage.metadata?.checkpoints ?? [],
};
oldMessage.parts = [{ type: 'text', text: newMessageContent }];

// Remove all messages after the old message
const messagesToRemove = this.conversation.current?.messages.filter((m) => m.createdAt > oldMessage.createdAt);
const messagesToRemove = this.conversation.current?.messages.filter((m) => m.metadata?.createdAt && m.metadata.createdAt > (oldMessage.metadata?.createdAt ?? new Date()));
await this.conversation.removeMessages(messagesToRemove);
return oldMessage;
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Use index-based removal to avoid Date serialization pitfalls.

Relying on createdAt comparisons can fail if some entries are strings. Slice by index instead.

-        const messagesToRemove = this.conversation.current?.messages.filter((m) => m.metadata?.createdAt && m.metadata.createdAt > (oldMessage.metadata?.createdAt ?? new Date()));
-        await this.conversation.removeMessages(messagesToRemove);
+        const messagesToRemove = this.conversation.current.messages.slice(oldMessageIndex + 1);
+        await this.conversation.removeMessages(messagesToRemove);
📝 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
async resubmitMessage(id: string, newMessageContent: string): Promise<ChatMessage | null> {
if (!this.conversation.current?.conversation.id) {
console.error('No conversation found');
return null;
}
const oldMessageIndex = this.conversation.current?.messages.findIndex((m) => m.id === id && m.role === 'user');
if (oldMessageIndex === undefined || oldMessageIndex === -1 || !this.conversation.current?.messages[oldMessageIndex]) {
console.error('No message found with id', id);
return null;
}
const oldMessage = this.conversation.current?.messages[oldMessageIndex] as UserChatMessage;
const oldMessage = this.conversation.current?.messages[oldMessageIndex];
// Update the old message with the new content
const newContext = await this.context.getRefreshedContext(oldMessage.metadata.context);
oldMessage.metadata.context = newContext;
const newContext = await this.context.getRefreshedContext(oldMessage.metadata?.context ?? []);
oldMessage.metadata = {
...oldMessage.metadata,
context: newContext,
createdAt: oldMessage.metadata?.createdAt || new Date(),
conversationId: oldMessage.metadata?.conversationId || this.conversation.current?.conversation.id,
checkpoints: oldMessage.metadata?.checkpoints ?? [],
};
oldMessage.parts = [{ type: 'text', text: newMessageContent }];
// Remove all messages after the old message
const messagesToRemove = this.conversation.current?.messages.filter((m) => m.createdAt > oldMessage.createdAt);
const messagesToRemove = this.conversation.current?.messages.filter((m) => m.metadata?.createdAt && m.metadata.createdAt > (oldMessage.metadata?.createdAt ?? new Date()));
await this.conversation.removeMessages(messagesToRemove);
return oldMessage;
}
async resubmitMessage(id: string, newMessageContent: string): Promise<ChatMessage | null> {
if (!this.conversation.current?.conversation.id) {
console.error('No conversation found');
return null;
}
const oldMessageIndex = this.conversation.current?.messages.findIndex((m) => m.id === id && m.role === 'user');
if (oldMessageIndex === undefined || oldMessageIndex === -1 || !this.conversation.current?.messages[oldMessageIndex]) {
console.error('No message found with id', id);
return null;
}
const oldMessage = this.conversation.current?.messages[oldMessageIndex];
// Update the old message with the new content
const newContext = await this.context.getRefreshedContext(oldMessage.metadata?.context ?? []);
oldMessage.metadata = {
...oldMessage.metadata,
context: newContext,
createdAt: oldMessage.metadata?.createdAt || new Date(),
conversationId: oldMessage.metadata?.conversationId || this.conversation.current?.conversation.id,
checkpoints: oldMessage.metadata?.checkpoints ?? [],
};
oldMessage.parts = [{ type: 'text', text: newMessageContent }];
// Remove all messages after the old message
const messagesToRemove = this.conversation.current.messages.slice(oldMessageIndex + 1);
await this.conversation.removeMessages(messagesToRemove);
return oldMessage;
}
🤖 Prompt for AI Agents
In apps/web/client/src/components/store/editor/chat/index.ts around lines
105-133, the removal logic compares createdAt dates (which may be strings)
causing brittle behavior; instead compute the old message index once, validate
it, and remove messages by index: get const oldIndex =
this.conversation.current!.messages.findIndex(...); if oldIndex < 0 return null;
then build messagesToRemove = this.conversation.current!.messages.slice(oldIndex
+ 1) and call await this.conversation.removeMessages(messagesToRemove). Ensure
you stop using createdAt comparisons for slicing and use the existing oldIndex
for deterministic removal.

Copy link

@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: 0

♻️ Duplicate comments (8)
apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/index.tsx (1)

1-1: This component uses client-only hooks; add "use client".

The component imports MobX observer and next-intl useTranslations which require client-side execution.

+'use client';
+
 import { useChatContext } from '@/app/project/[id]/_hooks/use-chat';
apps/web/client/src/app/project/[id]/_hooks/use-chat.tsx (2)

61-66: Type the onFinish options parameter correctly.

The onFinish callback receives additional properties beyond just message. Import and use the proper type from the AI SDK.

+import { type FinishReason } from 'ai';

-        onFinish: (options: { message: ChatMessage; isAbort?: boolean; isDisconnect?: boolean; isError?: boolean }) => {
+        onFinish: (options: { message: ChatMessage; finishReason?: FinishReason; isAbort?: boolean; isDisconnect?: boolean; isError?: boolean }) => {

26-29: Handle undefined conversationId gracefully.

The conversationId can be undefined on first render, which may cause issues with the chat transport.

Consider delaying initialization or providing a fallback:

     const conversationId = editorEngine.chat.conversation.current?.conversation.id;
+    
+    // Skip chat initialization if no conversation
+    if (!conversationId) {
+        return <ChatContext.Provider value={null}>{children}</ChatContext.Provider>;
+    }
apps/web/client/src/app/api/chat/helpers/message.ts (1)

40-40: Debounced async function may cause race conditions.

Using lodash's debounce with an async function doesn't properly handle promise results, which could lead to race conditions or missed DB updates. The debounced function returns immediately without waiting for the promise to resolve.

Consider these alternatives:

  1. Use an async-aware debounce library like p-debounce
  2. Implement manual flush with proper promise handling
  3. Use a queue-based approach for message persistence
-import debounce from "lodash/debounce";
+import pDebounce from "p-debounce";

-export const debouncedUpsertMessage = debounce(upsertMessage, 500);
+export const debouncedUpsertMessage = pDebounce(upsertMessage, 500);
apps/web/client/src/app/api/chat/route.ts (4)

66-72: Validate request body schema.

The request body is destructured without validation, which could cause runtime errors if the structure doesn't match expectations.

+import { z } from 'zod';
+
+const requestBodySchema = z.object({
+    message: z.object({
+        id: z.string(),
+        role: z.enum(['user', 'assistant', 'system']),
+        parts: z.array(z.any()),
+        metadata: z.any().optional(),
+    }),
+    chatType: z.nativeEnum(ChatType),
+    conversationId: z.string().uuid(),
+    projectId: z.string().uuid(),
+    traceId: z.string(),
+});

 try {
-    const { message, chatType, conversationId, projectId, traceId }: {
-        message: ChatMessage,
-        chatType: ChatType,
-        conversationId: string,
-        projectId: string,
-        traceId: string,
-    } = await req.json()
+    const body = await req.json();
+    const { message, chatType, conversationId, projectId, traceId } = requestBodySchema.parse(body);

75-76: Race condition: debounced upsert before loadChat.

The debounced upsert may not complete before loadChat runs, causing the loaded messages to miss the latest user message. This could lead to incomplete context in the AI stream.

+import { upsertMessage } from './helpers/message';
 
-        // create or update last message in database
-        await debouncedUpsertMessage({ id: message.id, conversationId, message });
+        // Use immediate upsert to ensure persistence before loading
+        await upsertMessage({ id: message.id, conversationId, message });
         const messages = await loadChat(conversationId);

139-144: Avoid debounce for final assistant message.

Using debounced persistence for the final message could result in data loss if the process terminates or if multiple requests override each other.

+import { upsertMessage } from './helpers/message';

                 onFinish: async (message) => {
-                    await debouncedUpsertMessage({
+                    await upsertMessage({
                         id: message.responseMessage.id,
                         conversationId,
                         message: message.responseMessage,
                     });
                 },

75-76: Authorize conversation access.

Add authorization checks to ensure the user has access to the specified conversation before performing operations.

#!/bin/bash
# Check if there are existing authorization patterns in the codebase
rg -n "conversationId.*userId|authorize.*conversation" --type ts apps/web

Add authorization before message operations:

// Verify user owns the conversation
const hasAccess = await verifyConversationAccess(userId, conversationId, projectId);
if (!hasAccess) {
    return new Response(JSON.stringify({ error: 'Unauthorized access to conversation' }), {
        status: 403,
        headers: { 'Content-Type': 'application/json' }
    });
}
🧹 Nitpick comments (1)
apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/index.tsx (1)

22-22: Potential undefined dereference in filter operation.

When engineMessages is undefined, the filter will fail. Add a nullish coalescing operator to handle this case safely.

-    const messagesToRender = useMemo(() => isWaiting ? engineMessages?.filter((m) => m.id !== streamingMessage?.id) : engineMessages, [engineMessages, isWaiting]);
+    const messagesToRender = useMemo(() => isWaiting ? (engineMessages ?? []).filter((m) => m.id !== streamingMessage?.id) : engineMessages, [engineMessages, isWaiting, streamingMessage]);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 31d5507 and a946b7a.

📒 Files selected for processing (5)
  • apps/web/client/src/app/api/chat/helpers/message.ts (1 hunks)
  • apps/web/client/src/app/api/chat/route.ts (4 hunks)
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/index.tsx (3 hunks)
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/message-content/reasoning.tsx (1 hunks)
  • apps/web/client/src/app/project/[id]/_hooks/use-chat.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/message-content/reasoning.tsx
🧰 Additional context used
📓 Path-based instructions (4)
apps/web/client/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/client/src/**/*.{ts,tsx}: Use path aliases @/* and ~/* for imports that map to apps/web/client/src/*
Avoid hardcoded user-facing text; use next-intl messages/hooks instead

Files:

  • apps/web/client/src/app/api/chat/helpers/message.ts
  • apps/web/client/src/app/project/[id]/_hooks/use-chat.tsx
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/index.tsx
  • apps/web/client/src/app/api/chat/route.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Do not use the any type unless necessary

Files:

  • apps/web/client/src/app/api/chat/helpers/message.ts
  • apps/web/client/src/app/project/[id]/_hooks/use-chat.tsx
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/index.tsx
  • apps/web/client/src/app/api/chat/route.ts
apps/web/client/src/app/**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/client/src/app/**/*.tsx: Default to Server Components; add 'use client' when using events, state/effects, browser APIs, or client‑only libraries
Do not use process.env in client code; import env from @/env instead

Files:

  • apps/web/client/src/app/project/[id]/_hooks/use-chat.tsx
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/index.tsx
apps/web/client/src/**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/client/src/**/*.tsx: Create MobX store instances with useState(() => new Store()) for stable references across renders
Keep the active MobX store in a useRef and perform async cleanup with setTimeout(() => storeRef.current?.clear(), 0) to avoid route-change races
Avoid useMemo for creating MobX store instances
Avoid putting the MobX store instance in effect dependency arrays if it causes loops; split concerns by domain

Files:

  • apps/web/client/src/app/project/[id]/_hooks/use-chat.tsx
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/index.tsx
🧠 Learnings (2)
📚 Learning: 2025-09-14T01:44:21.199Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: AGENTS.md:0-0
Timestamp: 2025-09-14T01:44:21.199Z
Learning: Applies to apps/web/client/src/**/*.{ts,tsx} : Avoid hardcoded user-facing text; use next-intl messages/hooks instead

Applied to files:

  • apps/web/client/src/app/project/[id]/_hooks/use-chat.tsx
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/index.tsx
📚 Learning: 2025-09-14T01:44:21.199Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: AGENTS.md:0-0
Timestamp: 2025-09-14T01:44:21.199Z
Learning: Applies to apps/web/client/src/app/**/*.tsx : Default to Server Components; add 'use client' when using events, state/effects, browser APIs, or client‑only libraries

Applied to files:

  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/index.tsx
🧬 Code graph analysis (4)
apps/web/client/src/app/api/chat/helpers/message.ts (3)
packages/db/src/mappers/chat/message.ts (2)
  • toDbMessage (17-34)
  • fromDbMessage (4-15)
packages/db/src/client.ts (1)
  • db (16-16)
packages/db/src/schema/chat/message.ts (1)
  • messages (11-27)
apps/web/client/src/app/project/[id]/_hooks/use-chat.tsx (4)
packages/models/src/chat/message/message.ts (1)
  • ChatMessage (16-16)
apps/web/client/src/components/store/editor/chat/context.ts (1)
  • ChatContext (16-291)
apps/web/client/src/components/store/editor/index.tsx (1)
  • useEditorEngine (10-14)
apps/web/client/src/components/tools/tools.ts (1)
  • handleToolCall (179-206)
apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/index.tsx (4)
apps/web/client/src/app/project/[id]/_hooks/use-chat.tsx (1)
  • useChatContext (100-105)
packages/models/src/chat/message/message.ts (1)
  • ChatMessage (16-16)
apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/assistant-message.tsx (1)
  • AssistantMessage (4-15)
apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/user-message.tsx (1)
  • UserMessage (24-253)
apps/web/client/src/app/api/chat/route.ts (4)
packages/models/src/chat/message/message.ts (1)
  • ChatMessage (16-16)
apps/web/client/src/app/api/chat/helpers/message.ts (2)
  • debouncedUpsertMessage (40-40)
  • loadChat (42-48)
packages/db/src/schema/chat/message.ts (1)
  • messages (11-27)
apps/web/client/src/app/api/chat/helpers/stream.ts (1)
  • errorHandler (89-108)
🔇 Additional comments (4)
apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/index.tsx (1)

33-35: System messages filtered correctly.

The implementation properly handles system messages by setting messageNode = null, which prevents rendering as intended per the PR objectives.

apps/web/client/src/app/project/[id]/_hooks/use-chat.tsx (2)

25-26: Eliminate potential race condition with refs.

Using refs instead of state for chatType and traceId is a good approach to avoid stale closures.

Also applies to: 37-37, 71-72


85-95: Efficient streaming message computation.

The useMemo hook correctly computes the streaming message and updates lastMessageRef for error recovery. The implementation properly handles the edge cases.

apps/web/client/src/app/api/chat/helpers/message.ts (1)

18-22: Verify cascade deletion behavior (time-based).

Deleting messages with createdAt > dbMessage?.createdAt can remove legitimate messages when timestamps are very close or system clocks drift — createdAt is set on create (apps/web/client/src/app/api/chat/route.ts:132) and used for ordering (apps/web/client/src/app/api/chat/helpers/message.ts:45).

  • Prefer a monotonic sequence or message-order field (or use message ID as a deterministic tiebreaker) instead of relying solely on timestamps.
  • If timestamps must be used, add a small buffer/tiebreaker (e.g., include id or use >= with explicit tie-break) and add tests for near-simultaneous inserts and clock skew.

return parts.filter((part) => part.type !== 'reasoning');
};

export const ensureToolResultParts = (parts: ChatMessage['parts']): ChatMessage['parts'] => {};
Copy link
Contributor

Choose a reason for hiding this comment

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

The function ensureToolResultParts is currently an empty stub. Either implement its functionality or remove it if not needed.

Suggested change
export const ensureToolResultParts = (parts: ChatMessage['parts']): ChatMessage['parts'] => {};

Copy link

@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: 5

♻️ Duplicate comments (5)
apps/web/client/src/app/api/chat/route.ts (4)

84-87: Good fix: upsert before loading history

Debounce removed; persistence happens before load preventing context gaps.


129-147: Good fix: persist final assistant message immediately

Dropping debounce here prevents lost final writes.


76-83: Validate request body (schema)

Body is parsed without validation; enforce shape (message, chatType, conversationId, projectId, traceId).

+import { z } from 'zod';
@@
-        const { message, chatType, conversationId, projectId, traceId }: {
-            message: ChatMessage,
-            chatType: ChatType,
-            conversationId: string,
-            projectId: string,
-            traceId: string,
-        } = await req.json()
+        const Body = z.object({
+            message: z.any(), // TODO: refine to runtime ChatMessage
+            chatType: z.nativeEnum(ChatType),
+            conversationId: z.string().uuid(),
+            projectId: z.string().uuid(),
+            traceId: z.string().min(1),
+        });
+        const { message, chatType, conversationId, projectId, traceId } = Body.parse(await req.json());

84-87: Authorize conversation and restrict message.role

Verify the conversation belongs to userId/projectId and only accept user‑role inputs on this route.

+        // Enforce ownership/visibility before persistence
+        await assertUserOwnsConversation({ userId, projectId, conversationId });
+
+        // Only users can initiate a streamed turn
+        if (message.role !== 'user') {
+            return new Response(JSON.stringify({ error: 'Invalid message role' }), { status: 400 });
+        }

If helper doesn’t exist, I can provide an implementation against your DB schema.

apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/index.tsx (1)

1-1: Add "use client"

This file uses client‑only hooks (observer, useTranslations, context). It must be a Client Component.

+'use client';
🧹 Nitpick comments (7)
apps/web/client/src/app/api/chat/helpers/message.ts (1)

2-2: Prefer package barrel import over deep import

Import db from @onlook/db to keep package boundaries stable.

-import { db } from "@onlook/db/src/client";
+import { db } from "@onlook/db";
apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/message-content/index.tsx (2)

35-36: lastToolInvocationIdx calculation is wrong

Using parts.length - 1 marks the last part, not the last tool part. This breaks the “loading” state for tool calls.

-                    lastToolInvocationIdx={parts.length - 1}
+                    lastToolInvocationIdx={parts.reduce((acc, p, i) => p.type.startsWith('tool-') ? i : acc, -1)}

23-23: Use a stable, collision‑free key

part.text can duplicate and cause key collisions; prefer messageId+index.

-                    key={part.text}
+                    key={`${messageId}-text-${idx}`}
apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/index.tsx (3)

20-23: Localize user‑facing text

“Loading conversation...” should use next‑intl per guidelines.

-                <p className="text-regularPlus">Loading conversation...</p>
+                <p className="text-regularPlus">{useTranslations()(transKeys.editor.panels.edit.tabs.chat.loading)}</p>

27-33: contentKey should key the list to the conversation

An empty key disables list resets. Use conversation.id.

-        <ChatMessageList contentKey={``}>
+        <ChatMessageList contentKey={conversation.id}>

59-76: Avoid rendering empty wrappers for system messages

Returning a div with null content creates gaps. Skip rendering instead.

-        return <div key={`message-${message.id}-${index}`}>{messageNode}</div>;
+        return messageNode ? <div key={`message-${message.id}-${index}`}>{messageNode}</div> : null;
apps/web/client/src/app/api/chat/route.ts (1)

107-108: Optional: normalize tool parts before converting to model messages

If you plan to use ensureToolResultParts, apply it before convertToStreamMessages.

-                ...convertToStreamMessages(messages),
+                ...convertToStreamMessages(messages /* optionally map( m => ({ ...m, parts: ensureToolResultParts(m.parts) }) */),
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a946b7a and dcece2e.

📒 Files selected for processing (7)
  • apps/web/client/src/app/api/chat/helpers/message.ts (1 hunks)
  • apps/web/client/src/app/api/chat/route.ts (5 hunks)
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/index.tsx (3 hunks)
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/markdown/index.tsx (1 hunks)
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/message-content/index.tsx (1 hunks)
  • packages/ai/src/prompt/provider.ts (0 hunks)
  • packages/ai/src/stream/index.ts (3 hunks)
💤 Files with no reviewable changes (1)
  • packages/ai/src/prompt/provider.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/markdown/index.tsx
🧰 Additional context used
📓 Path-based instructions (4)
apps/web/client/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/client/src/**/*.{ts,tsx}: Use path aliases @/* and ~/* for imports that map to apps/web/client/src/*
Avoid hardcoded user-facing text; use next-intl messages/hooks instead

Files:

  • apps/web/client/src/app/api/chat/helpers/message.ts
  • apps/web/client/src/app/api/chat/route.ts
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/message-content/index.tsx
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/index.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Do not use the any type unless necessary

Files:

  • apps/web/client/src/app/api/chat/helpers/message.ts
  • packages/ai/src/stream/index.ts
  • apps/web/client/src/app/api/chat/route.ts
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/message-content/index.tsx
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/index.tsx
apps/web/client/src/app/**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/client/src/app/**/*.tsx: Default to Server Components; add 'use client' when using events, state/effects, browser APIs, or client‑only libraries
Do not use process.env in client code; import env from @/env instead

Files:

  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/message-content/index.tsx
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/index.tsx
apps/web/client/src/**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/client/src/**/*.tsx: Create MobX store instances with useState(() => new Store()) for stable references across renders
Keep the active MobX store in a useRef and perform async cleanup with setTimeout(() => storeRef.current?.clear(), 0) to avoid route-change races
Avoid useMemo for creating MobX store instances
Avoid putting the MobX store instance in effect dependency arrays if it causes loops; split concerns by domain

Files:

  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/message-content/index.tsx
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/index.tsx
🧠 Learnings (2)
📚 Learning: 2025-09-14T01:44:21.199Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: AGENTS.md:0-0
Timestamp: 2025-09-14T01:44:21.199Z
Learning: Applies to apps/web/client/src/**/*.{ts,tsx} : Avoid hardcoded user-facing text; use next-intl messages/hooks instead

Applied to files:

  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/index.tsx
📚 Learning: 2025-09-14T01:44:21.199Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: AGENTS.md:0-0
Timestamp: 2025-09-14T01:44:21.199Z
Learning: Applies to apps/web/client/src/app/**/*.tsx : Default to Server Components; add 'use client' when using events, state/effects, browser APIs, or client‑only libraries

Applied to files:

  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/index.tsx
🧬 Code graph analysis (5)
apps/web/client/src/app/api/chat/helpers/message.ts (4)
packages/models/src/chat/message/message.ts (1)
  • ChatMessage (16-16)
packages/db/src/mappers/chat/message.ts (2)
  • toDbMessage (17-34)
  • fromDbMessage (4-15)
packages/db/src/client.ts (1)
  • db (16-16)
packages/db/src/schema/chat/message.ts (1)
  • messages (11-27)
packages/ai/src/stream/index.ts (3)
packages/db/src/schema/chat/message.ts (1)
  • messages (11-27)
packages/models/src/chat/message/message.ts (1)
  • ChatMessage (16-16)
packages/ai/src/prompt/provider.ts (1)
  • getHydratedUserMessage (51-116)
apps/web/client/src/app/api/chat/route.ts (5)
packages/models/src/chat/message/message.ts (1)
  • ChatMessage (16-16)
apps/web/client/src/app/api/chat/helpers/message.ts (2)
  • upsertMessage (6-37)
  • loadChat (39-45)
packages/db/src/schema/chat/message.ts (1)
  • messages (11-27)
packages/ai/src/stream/index.ts (1)
  • convertToStreamMessages (5-23)
apps/web/client/src/app/api/chat/helpers/stream.ts (1)
  • errorHandler (89-108)
apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/message-content/index.tsx (3)
apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/markdown/index.tsx (1)
  • MarkdownRenderer (6-45)
apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/message-content/tool-call-display.tsx (1)
  • ToolCallDisplay (26-238)
apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/message-content/reasoning.tsx (1)
  • ReasoningBlock (4-20)
apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/index.tsx (7)
packages/ui/src/components/chat/chat-message-list.tsx (1)
  • ChatMessageList (53-53)
apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/stream-message.tsx (1)
  • StreamMessage (5-27)
apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/error-message.tsx (1)
  • ErrorMessage (8-50)
apps/web/client/src/components/store/editor/index.tsx (1)
  • useEditorEngine (10-14)
apps/web/client/src/app/project/[id]/_hooks/use-chat.tsx (1)
  • useChatContext (100-105)
apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/assistant-message.tsx (1)
  • AssistantMessage (4-15)
apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/user-message.tsx (1)
  • UserMessage (24-253)
🔇 Additional comments (1)
packages/ai/src/stream/index.ts (1)

7-10: Runtime support for findLastIndex

Array.prototype.findLastIndex requires modern runtimes (Node 18+/modern browsers). Confirm your targets or polyfill.

Do we guarantee Node >= 18 and modern browsers across all bundles?

Comment on lines +15 to +36
const dbMessage = toDbMessage(message, conversationId);
return await db.transaction(async (tx) => {
// Remove messages newer than the updated message
await tx.delete(messages).where(and(
eq(messages.conversationId, conversationId),
gt(messages.createdAt, dbMessage?.createdAt ?? new Date()),
));
const [updatedMessage] = await tx
.insert(messages)
.values({
...dbMessage,
id,
})
.onConflictDoUpdate({
target: [messages.id],
set: {
...dbMessage,
id,
},
}).returning();
return updatedMessage;
});
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Return domain type for consistency

upsertMessage returns a raw DB row; loadChat returns ChatMessage. Standardize on ChatMessage.

-export const upsertMessage = async ({
+export const upsertMessage = async ({
@@
-}) => {
+}): Promise<ChatMessage> => {
@@
-        const [updatedMessage] = await tx
+        const [updatedMessage] = await tx
             .insert(messages)
@@
-            }).returning();
-        return updatedMessage;
+            }).returning();
+        return fromDbMessage(updatedMessage);
📝 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
const dbMessage = toDbMessage(message, conversationId);
return await db.transaction(async (tx) => {
// Remove messages newer than the updated message
await tx.delete(messages).where(and(
eq(messages.conversationId, conversationId),
gt(messages.createdAt, dbMessage?.createdAt ?? new Date()),
));
const [updatedMessage] = await tx
.insert(messages)
.values({
...dbMessage,
id,
})
.onConflictDoUpdate({
target: [messages.id],
set: {
...dbMessage,
id,
},
}).returning();
return updatedMessage;
});
export const upsertMessage = async ({
message,
conversationId,
id,
}): Promise<ChatMessage> => {
const dbMessage = toDbMessage(message, conversationId);
return await db.transaction(async (tx) => {
// Remove messages newer than the updated message
await tx.delete(messages).where(and(
eq(messages.conversationId, conversationId),
gt(messages.createdAt, dbMessage?.createdAt ?? new Date()),
));
const [updatedMessage] = await tx
.insert(messages)
.values({
...dbMessage,
id,
})
.onConflictDoUpdate({
target: [messages.id],
set: {
...dbMessage,
id,
},
}).returning();
return fromDbMessage(updatedMessage);
});
};
🤖 Prompt for AI Agents
In apps/web/client/src/app/api/chat/helpers/message.ts around lines 15 to 36,
the function currently returns a raw DB row; change it to return the domain
ChatMessage: after receiving updatedMessage from the transaction, map it to the
domain type (use the existing mapper function such as
fromDbMessage/toChatMessage—or add one if missing) and return that mapped
ChatMessage; update the function signature to Promise<ChatMessage>, add the
required import for the mapper and types, and ensure any callers are updated to
expect the domain type.

Comment on lines +28 to +34
.onConflictDoUpdate({
target: [messages.id],
set: {
...dbMessage,
id,
},
}).returning();
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Avoid updating primary key on conflict

Setting id in the UPDATE clause is redundant and can cause unnecessary write churn.

 .onConflictDoUpdate({
   target: [messages.id],
   set: {
-    ...dbMessage,
-    id,
+    ...dbMessage,
   },
 })
📝 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
.onConflictDoUpdate({
target: [messages.id],
set: {
...dbMessage,
id,
},
}).returning();
.onConflictDoUpdate({
target: [messages.id],
set: {
...dbMessage,
},
}).returning();
🤖 Prompt for AI Agents
In apps/web/client/src/app/api/chat/helpers/message.ts around lines 28 to 34,
the ON CONFLICT DO UPDATE currently includes id in the SET clause which updates
the primary key (redundant and causes write churn); remove id from the update
payload by excluding it from the object passed to set (e.g., spread dbMessage
but omit id or build a new object of fields to update), keep the conflict target
as messages.id and return the updated row as before.

Copy link

@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: 1

♻️ Duplicate comments (4)
packages/ai/src/stream/index.ts (1)

29-39: Normalize tool parts after filtering (apply in both branches).

Wire ensureToolResultParts after filterReasoningParts so the final parts shape is consistent for both assistant and user messages.

-    const parts = filterReasoningParts(message.parts);
+    const filtered = filterReasoningParts(message.parts);
+    const parts = ensureToolResultParts(filtered);

Also applies to: 47-49

apps/web/client/src/app/api/chat/route.ts (2)

76-83: Validate request body (schema).

Parse and reject malformed input early; reduces 500s and abuse surface.

-import {
+import {
     checkMessageLimit,
     decrementUsage,
     errorHandler,
     getModelFromType,
     getSupabaseUser,
     getSystemPromptFromType,
     incrementUsage,
     loadChat,
     repairToolCall,
     upsertMessage
 } from './helpers';
+import { z } from 'zod';
@@
-        const { message, chatType, conversationId, projectId, traceId }: {
-            message: ChatMessage,
-            chatType: ChatType,
-            conversationId: string,
-            projectId: string,
-            traceId: string,
-        } = await req.json()
+        const Body = z.object({
+            message: z.object({
+                id: z.string().uuid(),
+                role: z.enum(['user','assistant','system']),
+                parts: z.array(z.any()),
+                metadata: z.any().optional(),
+            }),
+            chatType: z.nativeEnum(ChatType),
+            conversationId: z.string().uuid(),
+            projectId: z.string().uuid(),
+            traceId: z.string().min(1),
+        });
+        const { message, chatType, conversationId, projectId, traceId } = Body.parse(await req.json());

84-86: Authorize conversation and enforce server-side conversationId.

Verify ownership before upsert; do not trust client-sent IDs.

-        // create or update last message in database
-        await upsertMessage({ id: message.id, conversationId, message });
+        // TODO: assert the user owns this conversationId/projectId pair (403 on failure).
+        // await assertUserOwnsConversation({ userId, projectId, conversationId });
+        // create or update last message in database (force server conversationId)
+        await upsertMessage({
+            id: message.id,
+            conversationId,
+            message: { ...message, metadata: { ...(message.metadata ?? {}), conversationId } as any },
+        });
apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/index.tsx (1)

1-1: Add "use client".

This component uses client-only hooks (MobX, next-intl). Must be a Client Component.

+'use client';
🧹 Nitpick comments (6)
packages/ai/src/tools/tools/read.ts (3)

11-12: Clarify absolute vs. relative path semantics

Explicitly state whether relative paths are supported and what they’re relative to for consistency with list_files’ param docs.

Apply this diff if accurate:

-            'The path to the file to read. Supports fuzzy path matching if exact path is not found.',
+            'The path to the file to read. Can be absolute or relative (relative to the working directory). Supports fuzzy path matching if the exact path is not found.',

58-61: Restore key capabilities in tool description for parity

Mention absolute/relative and fuzzy matching in the top-level description so UIs that only surface this text remain clear.

-        'List files and directories in a specified path. Can filter by type and exclude patterns. Returns file paths with type information (file/directory). Only lists immediate children (non-recursive).',
+        'List files and directories in a specified path. Supports absolute and relative paths with fuzzy matching when exact paths are not found. Can filter by type and exclude patterns. Returns file paths with type information (file/directory). Only lists immediate children (non-recursive).',

13-24: Validate offset/limit and remove ambiguity (1‑based vs 0‑based)

Docs say “cat -n” (1‑based). Enforce integers and bounds to match behavior and prevent large reads.

-    offset: z
-        .number()
+    offset: z
+        .number().int().min(1)
         .optional()
         .describe(
-            'The line number to start reading from. Only provide if the file is too large to read at once',
+            'The 1-based line number to start reading from. Only provide if the file is too large to read at once',
         ),
-    limit: z
-        .number()
+    limit: z
+        .number().int().min(1).max(2000)
         .optional()
         .describe(
-            'The number of lines to read. Only provide if the file is too large to read at once.',
+            'The number of lines to read (max 2000). Only provide if the file is too large to read at once.',
         ),
packages/ai/src/stream/index.ts (1)

51-73: Tighten guard; prefer map for immutability.

Use a map pass with a precise predicate; avoid index mutation and broadened startsWith checks.

-export const ensureToolResultParts = (parts: ChatMessage['parts']): ChatMessage['parts'] => {
-    const processedParts = [...parts];
-
-    for (let i = 0; i < processedParts.length; i++) {
-        const part = processedParts[i];
-        if (!part) continue;
-
-        // Check if this is a tool part that needs completion
-        if (part.type.startsWith('tool-') && 'toolCallId' in part && 'state' in part) {
-            // If tool call is in streaming state, mark it as having input available
-            if (part.state === 'input-streaming') {
-                const updatedPart = {
-                    ...part,
-                    state: 'input-available' as const,
-                };
-                processedParts[i] = updatedPart;
-            }
-        }
-    }
-
-    return processedParts;
-};
+export const ensureToolResultParts = (parts: ChatMessage['parts']): ChatMessage['parts'] => {
+    return parts.map((part) => {
+        if (
+            part &&
+            'type' in part &&
+            (part.type === 'tool-call' || part.type === 'tool-result' || part.type === 'tool') &&
+            'state' in part &&
+            (part as any).state === 'input-streaming'
+        ) {
+            return { ...(part as any), state: 'input-available' as const };
+        }
+        return part;
+    });
+};
apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/index.tsx (2)

21-24: Externalize user-facing text to i18n.

Replace hardcoded “Loading conversation...”.

-                <p className="text-regularPlus">Loading conversation...</p>
+                {/*
+                  TODO: add a key like transKeys.editor.panels.edit.tabs.chat.loading
+                */}
+                <p className="text-regularPlus">
+                    {useTranslations()(transKeys.editor.panels.edit.tabs.chat.loading)}
+                </p>

75-76: Avoid index in React key.

Use stable id-only key to prevent reorder bugs.

-        return <div key={`message-${message.id}-${index}`}>{messageNode}</div>;
+        return <div key={message.id}>{messageNode}</div>;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dcece2e and cd676ec.

📒 Files selected for processing (6)
  • apps/web/client/src/app/api/chat/route.ts (5 hunks)
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/index.tsx (3 hunks)
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/message-content/reasoning.tsx (1 hunks)
  • packages/ai/src/stream/index.ts (3 hunks)
  • packages/ai/src/tools/tools/edit.ts (4 hunks)
  • packages/ai/src/tools/tools/read.ts (2 hunks)
✅ Files skipped from review due to trivial changes (1)
  • packages/ai/src/tools/tools/edit.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/message-content/reasoning.tsx
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Do not use the any type unless necessary

Files:

  • packages/ai/src/tools/tools/read.ts
  • packages/ai/src/stream/index.ts
  • apps/web/client/src/app/api/chat/route.ts
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/index.tsx
apps/web/client/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/client/src/**/*.{ts,tsx}: Use path aliases @/* and ~/* for imports that map to apps/web/client/src/*
Avoid hardcoded user-facing text; use next-intl messages/hooks instead

Files:

  • apps/web/client/src/app/api/chat/route.ts
  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/index.tsx
apps/web/client/src/app/**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/client/src/app/**/*.tsx: Default to Server Components; add 'use client' when using events, state/effects, browser APIs, or client‑only libraries
Do not use process.env in client code; import env from @/env instead

Files:

  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/index.tsx
apps/web/client/src/**/*.tsx

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/client/src/**/*.tsx: Create MobX store instances with useState(() => new Store()) for stable references across renders
Keep the active MobX store in a useRef and perform async cleanup with setTimeout(() => storeRef.current?.clear(), 0) to avoid route-change races
Avoid useMemo for creating MobX store instances
Avoid putting the MobX store instance in effect dependency arrays if it causes loops; split concerns by domain

Files:

  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/index.tsx
🧠 Learnings (2)
📚 Learning: 2025-09-14T01:44:21.199Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: AGENTS.md:0-0
Timestamp: 2025-09-14T01:44:21.199Z
Learning: Applies to apps/web/client/src/**/*.{ts,tsx} : Avoid hardcoded user-facing text; use next-intl messages/hooks instead

Applied to files:

  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/index.tsx
📚 Learning: 2025-09-14T01:44:21.199Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: AGENTS.md:0-0
Timestamp: 2025-09-14T01:44:21.199Z
Learning: Applies to apps/web/client/src/app/**/*.tsx : Default to Server Components; add 'use client' when using events, state/effects, browser APIs, or client‑only libraries

Applied to files:

  • apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/index.tsx
🧬 Code graph analysis (3)
packages/ai/src/stream/index.ts (3)
packages/db/src/schema/chat/message.ts (1)
  • messages (11-27)
packages/models/src/chat/message/message.ts (1)
  • ChatMessage (16-16)
packages/ai/src/prompt/provider.ts (1)
  • getHydratedUserMessage (51-116)
apps/web/client/src/app/api/chat/route.ts (5)
packages/models/src/chat/message/message.ts (1)
  • ChatMessage (16-16)
apps/web/client/src/app/api/chat/helpers/message.ts (2)
  • upsertMessage (6-37)
  • loadChat (39-45)
packages/db/src/schema/chat/message.ts (1)
  • messages (11-27)
packages/ai/src/stream/index.ts (1)
  • convertToStreamMessages (5-23)
apps/web/client/src/app/api/chat/helpers/stream.ts (1)
  • errorHandler (89-108)
apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/index.tsx (6)
apps/web/client/src/components/store/editor/index.tsx (1)
  • useEditorEngine (10-14)
apps/web/client/src/app/project/[id]/_hooks/use-chat.tsx (1)
  • useChatContext (100-105)
apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/stream-message.tsx (1)
  • StreamMessage (5-27)
apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/error-message.tsx (1)
  • ErrorMessage (8-50)
apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/assistant-message.tsx (1)
  • AssistantMessage (4-15)
apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/user-message.tsx (1)
  • UserMessage (24-253)
🔇 Additional comments (1)
apps/web/client/src/app/api/chat/route.ts (1)

141-147: LGTM: final response persisted immediately.

Immediate awaited upsert removes debounce drop risk.

Comment on lines +29 to +33
<ChatMessageList contentKey={streamingMessage?.parts?.join('') ?? ''}>
<RenderedChatMessages />
<StreamMessage />
<ErrorMessage />
</ChatMessageList>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix contentKey: joining objects yields “[object Object]”.

Derive from text parts (or a hash) to ensure meaningful re-renders.

-        <ChatMessageList contentKey={streamingMessage?.parts?.join('') ?? ''}>
+        <ChatMessageList
+            contentKey={
+                streamingMessage
+                    ? streamingMessage.parts
+                        .filter((p) => p.type === 'text')
+                        .map((p: any) => p.text)
+                        .join('')
+                    : ''
+            }
+        >
📝 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
<ChatMessageList contentKey={streamingMessage?.parts?.join('') ?? ''}>
<RenderedChatMessages />
<StreamMessage />
<ErrorMessage />
</ChatMessageList>
<ChatMessageList
contentKey={
(streamingMessage?.parts ?? [])
.filter((p) => p?.type === 'text')
.map((p) => p.text ?? '')
.join('')
}
>
<RenderedChatMessages />
<StreamMessage />
<ErrorMessage />
</ChatMessageList>
🤖 Prompt for AI Agents
In
apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-messages/index.tsx
around lines 29 to 33, the contentKey is built by joining streamingMessage.parts
directly which yields “[object Object]”; instead derive a stable, meaningful key
from the actual text content (e.g., map parts to their text property and join
those) or compute a short hash of JSON.stringify(parts) if parts may contain
nested objects — replace the current join with parts.map(p => p.text ??
'').join('') or a hash of the serialized parts so React re-renders correctly
when textual content changes.

@vercel vercel bot temporarily deployed to Preview – docs September 14, 2025 22:50 Inactive
@Kitenite Kitenite changed the title feat: support subchat in DB feat: incremental saving chat Sep 14, 2025
Copy link

@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: 0

♻️ Duplicate comments (2)
packages/db/src/mappers/chat/message.ts (2)

24-24: Guarantee parts is persisted as an array.

Avoid null/undefined writes; align with schema default.

-        parts: message.parts,
+        parts: message.parts ?? [],

5-14: Don’t spread DbMessage into ChatMessage; map explicitly.

Spreading pulls DB‑only/deprecated columns onto the UI object and risks TS excess‑property errors. Return a minimal, typed ChatMessage.

 export const fromDbMessage = (message: DbMessage): ChatMessage => {
-    return {
-        ...message,
-        metadata: {
-            conversationId: message.conversationId,
-            createdAt: message.createdAt,
-            context: message.context ?? [],
-            checkpoints: message.checkpoints ?? [],
-        },
-        parts: message.parts ?? [],
-    }
+    return {
+        id: message.id,
+        role: message.role,
+        parts: message.parts ?? [],
+        metadata: {
+            conversationId: message.conversationId,
+            createdAt: message.createdAt,
+            context: message.context ?? [],
+            checkpoints: message.checkpoints ?? [],
+        },
+    };
 }
🧹 Nitpick comments (3)
packages/db/src/mappers/chat/message.ts (3)

18-22: createdAt normalization: confirm type contract.

If ChatMetadata.createdAt is Date | string, the current coercion is fine. If it’s always a string (ISO), consider simplifying and asserting early to avoid invalid Date writes.


17-23: Defensive check for conversationId mismatch (optional).

Guard against accidental cross‑conversation writes when callers pass an inconsistent conversationId.

 export const toDbMessage = (message: ChatMessage, conversationId: string): DbMessage => {
-    const createdAt = message.metadata?.createdAt;
+    if (message.metadata?.conversationId && message.metadata.conversationId !== conversationId) {
+        throw new Error("conversationId mismatch between param and message.metadata");
+    }
+    const createdAt = message.metadata?.createdAt;

28-33: Deprecated columns: verify DB constraints; consider omitting writes.

If these columns are nullable or have defaults, prefer not writing them to reduce noise. If non‑null, keep as is.

-        // deprecated
-        applied: null,
-        commitOid: null,
-        snapshots: null,
-        content: '',
+        // deprecated: omit if schema allows (nullable/defaulted)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4d8ca6b and 20742b6.

📒 Files selected for processing (1)
  • packages/db/src/mappers/chat/message.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Do not use the any type unless necessary

Files:

  • packages/db/src/mappers/chat/message.ts
🧬 Code graph analysis (1)
packages/db/src/mappers/chat/message.ts (1)
packages/models/src/chat/message/message.ts (1)
  • ChatMessage (16-16)
🔇 Additional comments (1)
packages/db/src/mappers/chat/message.ts (1)

2-2: Type‑only import LGTM.

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