Skip to content

Chat fix certain part#1411

Merged
duckduckhero merged 18 commits intomainfrom
chat-fix-certain-part
Aug 28, 2025
Merged

Chat fix certain part#1411
duckduckhero merged 18 commits intomainfrom
chat-fix-certain-part

Conversation

@duckduckhero
Copy link
Contributor

@duckduckhero duckduckhero commented Aug 27, 2025

DON'T MERGE THIS YET


Summary by cubic

Select text in an enhanced note, send it to chat, and apply edits to that exact range. Edits insert HTML, get highlighted inline, and include accept/undo controls.

  • New Features

    • Enabled “Ask AI” in the selection popover; sends the selected HTML and ProseMirror range to chat.
    • Chat input auto-inserts a compact selection reference and passes selectionData on submit.
    • Added edit_enhanced_note tool to replace content at the given range with new HTML.
    • Inserted edits are highlighted via a new aiHighlight TipTap mark and CSS.
    • Floating accept/undo controls on the edited range, with outside-click accept and auto-scroll to the change.
    • Right panel now handles pending selections and opens chat without collapsing.
    • Templates now include selected content and positions to guide edits.
    • Exposed the editor via a global ref for tool-driven edits.
  • Refactors

    • Extracted search_sessions_multi_keywords into a reusable factory.
    • Chat logic accepts selectionData and wires the edit tool only when provided.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 27, 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

Walkthrough

Adds cross-component editor sharing via a global editor ref, threads selectionData/htmlContent through right-panel chat flows, introduces selection→chat tooling (edit_enhanced_note, search tools), adds an AI-highlight TipTap mark with styles, and updates renderer/message rendering to support tool-provided HTML and selection context.

Changes

Cohort / File(s) Summary of Changes
Editor ref integration
apps/desktop/src/components/editor-area/index.tsx, apps/desktop/src/shared/editor-ref.ts
Exposes globalEditorRef, assigns/clears active Tiptap editor in EditorArea, and passes sessionId/editorRef to children.
Selection popover → chat
apps/desktop/src/components/editor-area/text-selection-popover.tsx
Extends popover props with sessionId and editorRef; validates selection belongs to editor, extracts TipTap positions and HTML/text into SelectionData, and sends via sendSelectionToChat; minor UI sizing tweaks.
Right-panel context: selection pipeline
apps/desktop/src/contexts/right-panel.tsx
Adds SelectionData type, pendingSelection state, and public APIs sendSelectionToChat and clearPendingSelection; opens/focuses chat when sending a selection.
Chat input & view integration
apps/desktop/src/components/right-panel/components/chat/chat-input.tsx, apps/desktop/src/components/right-panel/views/chat-view.tsx
ChatInput consumes pendingSelection, deduplicates processing, inserts non-editable selection-ref anchor, updates input value, and forwards selectionData/htmlContent to onSubmit; ChatView updated to forward these args.
Chat logic & message history
apps/desktop/src/components/right-panel/hooks/useChatLogic.ts, apps/desktop/src/components/right-panel/utils/chat-utils.ts
handleSubmit/processUserMessage accept selectionData and htmlContent; prepareMessageHistory accepts selectionData and embeds it into the user message payload for templates; tools wiring conditionally includes edit_enhanced_note when selectionData exists.
Chat tools
apps/desktop/src/components/right-panel/utils/tools/edit_enhanced_note.ts, apps/desktop/src/components/right-panel/utils/tools/search_session_multi_keywords.ts, apps/desktop/src/components/right-panel/utils/tools/search_session_date_range.ts
Adds createEditEnhancedNoteTool (replaces ProseMirror range with new HTML, applies aiHighlight, shows Undo/Accept UI) and two search tools: multi-keyword (createSearchSessionTool) and date-range (createSearchSessionDateRangeTool).
Templates
crates/template/assets/chat.system.jinja, crates/template/assets/chat.user.jinja
Registers edit_enhanced_note and search_sessions_date_range in system template; user template conditionally renders a selectionData block (positions, text, HTML instruction) when provided.
TipTap AI highlight
packages/tiptap/src/shared/ai-highlight.ts, packages/tiptap/src/shared/extensions.ts, packages/tiptap/src/styles/nodes/ai-highlight.css, packages/tiptap/src/styles/tiptap.css
Adds aiHighlight Mark extension (timestamp/sessionId attrs, commands set/unset/toggle), includes it in shared extensions, and adds styling/animation and CSS import.
Renderer mention readonly
packages/tiptap/src/renderer/mention-readonly.ts, packages/tiptap/src/renderer/index.tsx, packages/tiptap/src/styles/mention.css
Adds a read-only mention node (render-only, marks selection-ref labels), includes it in renderer extensions and imports mention styles.
Message rendering & HTML path
apps/desktop/src/components/right-panel/components/chat/message-content.tsx
Message renderer/MarkdownText accepts optional htmlContent (from toolDetails) and prefers provided HTML for the first text part; toolDetails parsing is guarded.
Chat message parsing
apps/desktop/src/components/right-panel/hooks/useChatQueries.ts
Parses tool_details for all messages with robust error handling and attaches parsed toolDetails into mapped message objects.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant U as User
  participant EA as EditorArea (Tiptap)
  participant TP as TextSelectionPopover
  participant RP as RightPanelContext
  participant CI as ChatInput
  participant CL as useChatLogic
  participant TL as Tools
  participant JT as Jinja Template

  U->>EA: Select text
  U->>TP: Click "Ask AI"
  TP->>EA: Read TipTap positions & DOM HTML/text
  TP->>RP: sendSelectionToChat(selectionData)
  RP-->>CI: pendingSelection set (open/focus chat)

  CI->>CI: Insert non-editable selection-ref (dedupe)
  U->>CI: Submit message
  CI->>CL: handleSubmit(mentionedContent, selectionData, htmlContent)
  CL->>JT: prepareMessageHistory(..., selectionData)
  alt selection-driven tool
    CL->>TL: createEditEnhancedNoteTool({ sessionId, selectionData })
    TL->>EA: Use globalEditorRef, replace range, setAIHighlight, show Undo/Accept
  else search tools
    CL->>TL: createSearchSessionTool / createSearchSessionDateRangeTool -> results
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Text popover #1308 — edits EditorArea and TextSelectionPopover; closely related to the global editor ref and selection→chat forwarding.
  • Empty chat redesign #1287 — introduces/searches session tools and overlaps with the multi-keyword tool integration.
  • Minor fixes 0731 2 (chat boosted)  #1265 — updates ChatInput onSubmit and chat flow; related to the expanded onSubmit signature and selectionData/htmlContent plumbing.

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.


📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 1a3bd57 and 0629caa.

📒 Files selected for processing (2)
  • apps/desktop/src/components/right-panel/components/chat/message-content.tsx (4 hunks)
  • apps/desktop/src/components/right-panel/utils/chat-utils.ts (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/desktop/src/components/right-panel/components/chat/message-content.tsx
  • apps/desktop/src/components/right-panel/utils/chat-utils.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: ci (macos, macos-latest)
  • GitHub Check: ci (windows, windows-latest)
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch chat-fix-certain-part

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

‼️ IMPORTANT
Auto-reply has been disabled for this repository in the CodeRabbit settings. The CodeRabbit bot will not respond to your replies unless it is explicitly tagged.

  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbit in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbit in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbit gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbit read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbit help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbit ignore or @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbit summary or @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbit or @coderabbitai anywhere in the PR title to generate the title automatically.

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

18 issues found across 17 files

React with 👍 or 👎 to teach cubic. You can also tag @cubic-dev-ai to give feedback, ask questions, or re-run the review.


if (chunk.type === "tool-result" && !(chunk.toolName === "update_progress" && type === "HyprLocal")) {
didInitializeAiResponse = false;
console.log("tool result", chunk);
Copy link

Choose a reason for hiding this comment

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

Avoid logging full tool-result chunks in production; use structured logging or remove the debug statement.

Prompt for AI agents
Address the following comment on apps/desktop/src/components/right-panel/hooks/useChatLogic.ts at line 455:

<comment>Avoid logging full tool-result chunks in production; use structured logging or remove the debug statement.</comment>

<file context>
@@ -483,6 +452,7 @@ export function useChatLogic({
 
         if (chunk.type === &quot;tool-result&quot; &amp;&amp; !(chunk.toolName === &quot;update_progress&quot; &amp;&amp; type === &quot;HyprLocal&quot;)) {
           didInitializeAiResponse = false;
+          console.log(&quot;tool result&quot;, chunk);
 
           const toolResultMessage: Message = {
</file context>
Suggested change
console.log("tool result", chunk);
// removed debug log

const abortController = new AbortController();
abortControllerRef.current = abortController;

console.log("selectionData exists?", selectionData);
Copy link

Choose a reason for hiding this comment

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

Remove debug logging of selectionData to avoid leaking potentially sensitive user content and reduce noise in production logs.

Prompt for AI agents
Address the following comment on apps/desktop/src/components/right-panel/hooks/useChatLogic.ts at line 303:

<comment>Remove debug logging of selectionData to avoid leaking potentially sensitive user content and reduce noise in production logs.</comment>

<file context>
@@ -283,63 +289,19 @@ export function useChatLogic({
         }
       }
 
-      const searchTool = tool({
-        description:
-          &quot;Search for sessions (meeting notes) with multiple keywords. The keywords should be the most important things that the user is talking about. This could be either topics, people, or company names.&quot;,
-        inputSchema: z.object({
-          keywords: z.array(z.string()).min(3).max(5).describe(
-            &quot;List of 3-5 keywords to search for, each keyword should be concise&quot;,
</file context>
Suggested change
console.log("selectionData exists?", selectionData);
// removed debug log

if (chatInputRef.current) {
chatInputRef.current.focus();
} else {
setTimeout(focusInput, 50);
Copy link

Choose a reason for hiding this comment

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

Unbounded recursive setTimeout retry; add a max retry count or cancel condition to prevent indefinite polling.

Prompt for AI agents
Address the following comment on apps/desktop/src/contexts/right-panel.tsx at line 126:

<comment>Unbounded recursive setTimeout retry; add a max retry count or cancel condition to prevent indefinite polling.</comment>

<file context>
@@ -92,6 +104,51 @@ export function RightPanelProvider({
     [isExpanded, currentView],
   );
 
+  const sendSelectionToChat = useCallback((selectionData: SelectionData) =&gt; {
+    console.log(&quot;🚀 sendSelectionToChat called&quot;);
+    console.log(&quot;Current panel state:&quot;, { isExpanded, currentView });
+    console.log(&quot;Selection data:&quot;, selectionData);
+    
+    setPendingSelection(selectionData);
</file context>

const sendSelectionToChat = useCallback((selectionData: SelectionData) => {
console.log("🚀 sendSelectionToChat called");
console.log("Current panel state:", { isExpanded, currentView });
console.log("Selection data:", selectionData);
Copy link

Choose a reason for hiding this comment

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

Logs potentially sensitive SelectionData (text/sessionId) to console; avoid leaking user content to logs in production.

Prompt for AI agents
Address the following comment on apps/desktop/src/contexts/right-panel.tsx at line 110:

<comment>Logs potentially sensitive SelectionData (text/sessionId) to console; avoid leaking user content to logs in production.</comment>

<file context>
@@ -92,6 +104,51 @@ export function RightPanelProvider({
     [isExpanded, currentView],
   );
 
+  const sendSelectionToChat = useCallback((selectionData: SelectionData) =&gt; {
+    console.log(&quot;🚀 sendSelectionToChat called&quot;);
+    console.log(&quot;Current panel state:&quot;, { isExpanded, currentView });
+    console.log(&quot;Selection data:&quot;, selectionData);
+    
+    setPendingSelection(selectionData);
</file context>

const noteName = noteData?.title || humanData?.full_name || organizationData?.name || "Note";
const selectionRef = `[${noteName} - ${pendingSelection.startOffset}:${pendingSelection.endOffset}]`;
// Use a mention element with data-mention="true" so Tiptap recognizes it
const referenceText = `<a class="mention selection-ref" data-mention="true" data-id="selection-${pendingSelection.startOffset}-${pendingSelection.endOffset}" data-type="selection" data-label="${selectionRef}" contenteditable="false">${selectionRef}</a> `;
Copy link

Choose a reason for hiding this comment

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

Unescaped user-controlled text is interpolated into an HTML string used by setContent; escape or build the node safely to prevent XSS/injection.

Prompt for AI agents
Address the following comment on apps/desktop/src/components/right-panel/components/chat/chat-input.tsx at line 233:

<comment>Unescaped user-controlled text is interpolated into an HTML string used by setContent; escape or build the node safely to prevent XSS/injection.</comment>

<file context>
@@ -197,27 +206,65 @@ export function ChatInput(
 
       onChange(syntheticEvent);
     }
-  }, [onSubmit, onChange, extractMentionedContent]);
+  }, [onSubmit, onChange, extractMentionedContent, pendingSelection, clearPendingSelection]);
 
   useEffect(() =&gt; {
     if (chatInputRef &amp;&amp; typeof chatInputRef === &quot;object&quot; &amp;&amp; editorRef.current?.editor) {
       (chatInputRef as any).current = editorRef.current.editor.view.dom;
</file context>

description: "Edit a specific part of the enhanced note by replacing HTML content at given ProseMirror positions with new HTML content. Use this when the user asks to modify, change, or replace specific selected content. The selected content is provided as HTML, and you should respond with HTML that maintains proper formatting.",
inputSchema: z.object({
startOffset: z.number().describe("The ProseMirror start position of the content to replace"),
endOffset: z.number().describe("The ProseMirror end position of the content to replace"),
Copy link

Choose a reason for hiding this comment

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

Ensure endOffset is an integer by using z.number().int() in the input schema.

Prompt for AI agents
Address the following comment on apps/desktop/src/components/right-panel/utils/tools/edit_enhanced_note.ts at line 21:

<comment>Ensure endOffset is an integer by using z.number().int() in the input schema.</comment>

<file context>
@@ -0,0 +1,284 @@
+import type { SelectionData } from &quot;@/contexts/right-panel&quot;;
+import { globalEditorRef } from &quot;../../../../shared/editor-ref&quot;;
+import { tool } from &quot;@hypr/utils/ai&quot;;
+import { z } from &quot;zod&quot;;
+
+interface EditEnhancedNoteToolDependencies {
+  sessionId: string | null;
+  sessions: Record&lt;string, any&gt;;
+  selectionData?: SelectionData;
</file context>

console.log("🔄 Original content saved for undo:", originalContent);

// Trim and clean the HTML to prevent empty elements
const cleanedHtml = newHtml.trim().replace(/>\s+</g, '><');
Copy link

Choose a reason for hiding this comment

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

Whitespace-collapsing regex removes significant spaces between inline elements, altering content. Avoid collapsing inter-tag whitespace globally.

Prompt for AI agents
Address the following comment on apps/desktop/src/components/right-panel/utils/tools/edit_enhanced_note.ts at line 46:

<comment>Whitespace-collapsing regex removes significant spaces between inline elements, altering content. Avoid collapsing inter-tag whitespace globally.</comment>

<file context>
@@ -0,0 +1,284 @@
+import type { SelectionData } from &quot;@/contexts/right-panel&quot;;
+import { globalEditorRef } from &quot;../../../../shared/editor-ref&quot;;
+import { tool } from &quot;@hypr/utils/ai&quot;;
+import { z } from &quot;zod&quot;;
+
+interface EditEnhancedNoteToolDependencies {
+  sessionId: string | null;
+  sessions: Record&lt;string, any&gt;;
+  selectionData?: SelectionData;
</file context>


const searchResults = await Promise.all(searchPromises);

const combinedResults = new Map();
Copy link

Choose a reason for hiding this comment

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

Use a typed Map with explicit key/value generics to ensure matchedKeywords is type-safe and prevent implicit any.

Prompt for AI agents
Address the following comment on apps/desktop/src/components/right-panel/utils/tools/search_session_multi_keywords.ts at line 26:

<comment>Use a typed Map with explicit key/value generics to ensure matchedKeywords is type-safe and prevent implicit any.</comment>

<file context>
@@ -0,0 +1,59 @@
+import { commands as dbCommands } from &quot;@hypr/plugin-db&quot;;
+import { tool } from &quot;@hypr/utils/ai&quot;;
+import { z } from &quot;zod&quot;;
+
+export const createSearchSessionTool = (userId: string | null) =&gt; {
+  return tool({
+    description:
+      &quot;Search for sessions (meeting notes) with multiple keywords. The keywords should be the most important things that the user is talking about. This could be either topics, people, or company names.&quot;,
+    inputSchema: z.object({
</file context>

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

Caution

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

⚠️ Outside diff range comments (1)
crates/template/assets/chat.system.jinja (1)

1-171: Install and run DPrint to format this file

The CI failure indicates that the dprint formatter isn’t available under ~/.dprint/bin/dprint. Please ensure the DPrint CLI is installed and on your PATH, then reformat this template and commit the changes.

– Install DPrint (choose one):
• Run the official install script
bash curl -fsSL https://dprint.dev/install.sh | bash
• Or follow the “Formatter” section in our README to install via your package manager.

– Once installed, format the file:

dprint fmt --config dprint.json crates/template/assets/chat.system.jinja

– Verify there are no unformatted changes before pushing to unblock CI.

🧹 Nitpick comments (30)
packages/tiptap/src/styles/nodes/ai-highlight.css (2)

10-17: Respect reduced motion preferences

Disable the pulse animation for users with reduced-motion enabled.

 } 
+
+@media (prefers-reduced-motion: reduce) {
+  .tiptap-normal mark[data-ai-highlight="true"],
+  .tiptap mark[data-ai-highlight="true"] {
+    animation: none !important;
+  }
+}
 
 @keyframes ai-highlight-pulse {

2-11: Trim down blanket !important

Prefer selector specificity over !important where possible to reduce CSS override contention, keeping !important only for properties that truly need it (likely just the animation or border in contested contexts).

apps/desktop/src/components/right-panel/utils/tools/search_session_multi_keywords.ts (5)

9-13: Normalize keywords at the schema boundary

Trim inputs to avoid empty/whitespace-only tokens passing validation.

-      keywords: z.array(z.string()).min(3).max(5).describe(
+      keywords: z.array(z.string().trim().min(1)).min(3).max(5).describe(
         "List of 3-5 keywords to search for, each keyword should be concise",
       ),

15-23: Deduplicate and trim keywords before querying

Avoid redundant queries and improve result relevance by normalizing input.

-    execute: async ({ keywords }) => {
-      const searchPromises = keywords.map(keyword =>
+    execute: async ({ keywords }) => {
+      const normalizedKeywords = Array.from(new Set(keywords.map(k => k.trim()).filter(Boolean)));
+      const searchPromises = normalizedKeywords.map(keyword =>
         dbCommands.listSessions({
           type: "search",
           query: keyword,
           user_id: userId || "",
           limit: 3,
         })
       );
 
-      const searchResults = await Promise.all(searchPromises);
+      const searchResults = await Promise.all(searchPromises);

26-40: Add types for combinedResults to avoid implicit anys

Type the Map and derived values for safer access to session.id and matchedKeywords.

-      const combinedResults = new Map();
+      const combinedResults = new Map<string, SessionWithKeywords>();
 
-      searchResults.forEach((sessions, index) => {
-        const keyword = keywords[index];
+      searchResults.forEach((sessions, index) => {
+        const keyword = normalizedKeywords[index];
         sessions.forEach(session => {
-          if (combinedResults.has(session.id)) {
-            combinedResults.get(session.id).matchedKeywords.push(keyword);
-          } else {
-            combinedResults.set(session.id, {
-              ...session,
-              matchedKeywords: [keyword],
-            });
-          }
+          const existing = combinedResults.get(session.id);
+          if (existing) existing.matchedKeywords.push(keyword);
+          else combinedResults.set(session.id, { ...session, matchedKeywords: [keyword] });
         });
       });

Additional code (outside this hunk) to add near the top of the file after imports:

type SessionArray = Awaited<ReturnType<typeof dbCommands.listSessions>>;
type Session = SessionArray extends Array<infer T> ? T : never;
type SessionWithKeywords = Session & { matchedKeywords: string[] };

42-47: Preserve types through finalResults

Type the array so downstream consumers get session+keywords.

-      const finalResults = Array.from(combinedResults.values())
+      const finalResults: SessionWithKeywords[] = Array.from(combinedResults.values())
         .sort((a, b) => b.matchedKeywords.length - a.matchedKeywords.length);

50-55: Align summary map with runtime object keys

Object keys are strings at runtime; use Record<string, number> to reflect that and avoid index type friction.

-          }, {} as Record<number, number>),
+          }, {} as Record<string, number>),
packages/tiptap/src/shared/ai-highlight.ts (2)

67-72: Parse only ai-highlight="true" marks

Tighten the selector to avoid accidentally parsing unrelated marks that merely carry a data-ai-highlight attribute with other values.

-    return [{
-      tag: 'mark[data-ai-highlight]',
+    return [{
+      tag: 'mark[data-ai-highlight="true"]',
       priority: 100, // Higher priority to ensure it's recognized
     }];

31-35: Unused CSS class?

You render class "ai-generated-highlight" but the CSS targets only [data-ai-highlight="true"]. Either add a rule for the class or drop the class to reduce DOM noise.

apps/desktop/src/components/right-panel/views/chat-view.tsx (1)

183-183: Pass handler directly to avoid needless lambda allocation.

Minor cleanup; keeps props stable and helps memoization.

-        onSubmit={(mentionedContent, selectionData) => handleSubmit(mentionedContent, selectionData)}
+        onSubmit={handleSubmit}
crates/template/assets/chat.user.jinja (1)

2-8: Cap selection payload to avoid unbounded token usage

Currently, chat-utils.ts forwards selectionData.text straight into the Jinja template with no length checks, which can lead to prompt bloat (and unpredictable token costs) when users select very large note fragments. To keep token usage predictable, consider truncating or chunking the selection before passing it into the template.

• Location to address:

  • File: apps/desktop/src/components/right-panel/utils/chat-utils.ts (around lines 203–207)
  • File: crates/template/assets/chat.user.jinja (lines 5–8)

• Suggested refactor (optional):

  • In chat-utils.ts, introduce a maximum‐length cap (e.g. 2000 characters) or split large selectionData.text into manageable chunks.
  • Pass the truncated or chunked text into the template so that the Jinja snippet receives only the portion you intend to process:
    selectionData: selectionData
      ? {
    -     text: selectionData.text,
    +     text: truncate(selectionData.text, MAX_SELECTION_LENGTH),
          startOffset: selectionData.startOffset,
          endOffset: selectionData.endOffset,
        }
      : undefined,

This ensures the engine never receives an unexpectedly large payload, keeps token counts predictable, and prevents potential service slowdowns.

apps/desktop/src/shared/editor-ref.ts (1)

8-10: Global mutable ref needs session scoping to avoid cross-tab/editor mixups.

At minimum, attach sessionId and provide attach/detach helpers so tools can validate the target editor.

-export const globalEditorRef = { 
-  current: null as TiptapEditor | null 
-};
+export const globalEditorRef = {
+  current: null as TiptapEditor | null,
+  sessionId: undefined as string | undefined,
+};

Outside this range, consider adding:

export function attachEditor(editor: TiptapEditor, sessionId: string) {
  globalEditorRef.current = editor;
  globalEditorRef.sessionId = sessionId;
}
export function detachEditor(editor: TiptapEditor) {
  if (globalEditorRef.current === editor) {
    globalEditorRef.current = null;
    globalEditorRef.sessionId = undefined;
  }
}
export function getEditorForSession(sessionId: string) {
  return globalEditorRef.sessionId === sessionId ? globalEditorRef.current : null;
}
apps/desktop/src/contexts/ai-edit.tsx (2)

1-1: Memoize provider value to avoid unnecessary rerenders.

Prevents child tree rerender on every state change unrelated to value identity.

-import { createContext, useContext, useState } from "react";
+import { createContext, useContext, useMemo, useState } from "react";

21-25: Memoize context value.

Keeps reference stable for consumers using referential equality.

-  return (
-    <AIEditContext.Provider value={{ currentAIEdit, setCurrentAIEdit }}>
-      {children}
-    </AIEditContext.Provider>
-  );
+  const value = useMemo(() => ({ currentAIEdit, setCurrentAIEdit }), [currentAIEdit]);
+  return <AIEditContext.Provider value={value}>{children}</AIEditContext.Provider>;
apps/desktop/src/components/editor-area/index.tsx (1)

133-145: Prefer callback ref over effect for global editor registration

Avoid effect deps on ref internals and register/unregister atomically via a callback ref.

Apply this diff to remove the effect:

-  // Assign editor to global ref for access by other components (like chat tools)
-  useEffect(() => {
-    if (editorRef.current?.editor) {
-      globalEditorRef.current = editorRef.current.editor;
-    }
-    // Clear on unmount
-    return () => {
-      if (globalEditorRef.current === editorRef.current?.editor) {
-        globalEditorRef.current = null;
-      }
-    };
-  }, [editorRef.current?.editor]);

Then update the Editor ref usage outside this range:

// inside JSX
<Editor
  key={editorKey}
  ref={(inst) => {
    (editorRef as any).current = inst;
    globalEditorRef.current = inst?.editor ?? null;
  }}
  /* ... */
/>

// and for Renderer
<Renderer
  ref={(inst) => {
    (editorRef as any).current = inst;
    globalEditorRef.current = inst?.editor ?? null;
  }}
  /* ... */
/>
apps/desktop/src/components/right-panel/hooks/useChatLogic.ts (4)

292-299: Tool factory usage is clean; minor guard suggestion

Consider constructing edit_enhanced_note only when selectionData exists to avoid carrying unused closures.

- const editEnhancedNoteTool = createEditEnhancedNoteTool({
-   sessionId,
-   sessions,
-   selectionData,
- });
+ const editEnhancedNoteTool = selectionData
+   ? createEditEnhancedNoteTool({ sessionId, sessions, selectionData })
+   : undefined;

303-304: Remove debug logs that can leak PII

selectionData may include user content; avoid logging it in production.

- console.log("selectionData exists?", selectionData);
+ if (import.meta.env.DEV) console.log("selectionData exists?", !!selectionData);

455-456: Drop verbose tool result logging

Avoid logging full tool payloads/results; they may contain sensitive content.

- console.log("tool result", chunk);
+ if (import.meta.env.DEV) console.log("tool finished:", chunk.toolName);

520-571: Close MCP clients on error path, not just onFinish

Currently clients are closed only in onFinish; add cleanup in the catch as well.

-    } catch (error) {
+    } catch (error) {
       console.error(error);
+      try {
+        for (const client of allMcpClients) client.close();
+        hyprMcpClient?.close();
+      } catch {}
apps/desktop/src/components/right-panel/utils/chat-utils.ts (1)

213-213: Remove noisy console log of rendered user message

Rendered prompt can include sensitive notes; don’t log it.

-    console.log("this is user message", userContent);
+    if (import.meta.env.DEV) console.log("user message prepared");
apps/desktop/src/components/editor-area/text-selection-popover.tsx (4)

8-15: Remove unused onAskAI prop

It’s no longer used; drop it from the interface and component props to satisfy “no unused” guideline.

 interface TextSelectionPopoverProps {
   isEnhancedNote: boolean;
   onAnnotate: (selectedText: string, selectedRect: DOMRect) => void;
-  onAskAI?: (selectedText: string) => void;
   isAnnotationBoxOpen: boolean;
   sessionId: string;
-  editorRef: React.RefObject<{ editor: any }>;
+  editorRef: React.RefObject<{ editor: unknown }>;
 }

And in the component signature:

-export function TextSelectionPopover(
-  { isEnhancedNote, onAnnotate, onAskAI, isAnnotationBoxOpen, sessionId, editorRef }: TextSelectionPopoverProps,
+export function TextSelectionPopover(
+  { isEnhancedNote, onAnnotate, isAnnotationBoxOpen, sessionId, editorRef }: TextSelectionPopoverProps,

25-27: Avoid NodeJS.Timeout in browser code

Use ReturnType for portability.

-  const delayTimeoutRef = useRef<NodeJS.Timeout>();
+  const delayTimeoutRef = useRef<ReturnType<typeof setTimeout>>();

110-152: Clarify payload semantics: this is HTML, not plain text

The field is called text but holds HTML; either rename or document clearly to avoid misuse downstream.


1-254: Trim debug logs

Reduce console noise; keep only gated logs.

apps/desktop/src/components/right-panel/utils/tools/edit_enhanced_note.ts (2)

103-110: Avoid innerHTML; build buttons safely

Static SVG/labels are fine, but using DOM APIs avoids future XSS lint noise.

-          const undoBtn = document.createElement('button');
-          undoBtn.innerHTML = `
-            <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
-              <path d="M3 7v6h6"/><path d="M21 17a9 9 0 00-9-9 9 9 0 00-6 2.3L3 13"/>
-            </svg>
-            <span style="margin-left: 4px; font-size: 11px;">Undo</span>
-          `;
+          const undoBtn = document.createElement('button');
+          const undoSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
+          undoSvg.setAttribute('width','12'); undoSvg.setAttribute('height','12');
+          undoSvg.setAttribute('viewBox','0 0 24 24'); undoSvg.setAttribute('fill','none');
+          undoSvg.setAttribute('stroke','currentColor'); undoSvg.setAttribute('stroke-width','2');
+          const p1 = document.createElementNS('http://www.w3.org/2000/svg','path');
+          p1.setAttribute('d','M3 7v6h6');
+          const p2 = document.createElementNS('http://www.w3.org/2000/svg','path');
+          p2.setAttribute('d','M21 17a9 9 0 00-9-9 9 9 0 00-6 2.3L3 13');
+          undoSvg.append(p1,p2);
+          const undoLabel = document.createElement('span');
+          undoLabel.style.marginLeft = '4px';
+          undoLabel.style.fontSize = '11px';
+          undoLabel.textContent = 'Undo';
+          undoBtn.append(undoSvg, undoLabel);

148-155: Same for Accept button: avoid innerHTML

Mirror the approach used for Undo.

-          const acceptBtn = document.createElement('button');
-          acceptBtn.innerHTML = `
-            <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">
-              <polyline points="20 6 9 17 4 12"/>
-            </svg>
-            <span style="margin-left: 4px; font-size: 11px;">Accept</span>
-          `;
+          const acceptBtn = document.createElement('button');
+          const okSvg = document.createElementNS('http://www.w3.org/2000/svg','svg');
+          okSvg.setAttribute('width','12'); okSvg.setAttribute('height','12');
+          okSvg.setAttribute('viewBox','0 0 24 24'); okSvg.setAttribute('fill','none');
+          okSvg.setAttribute('stroke','white'); okSvg.setAttribute('stroke-width','2');
+          const pl = document.createElementNS('http://www.w3.org/2000/svg','polyline');
+          pl.setAttribute('points','20 6 9 17 4 12');
+          okSvg.append(pl);
+          const okLabel = document.createElement('span');
+          okLabel.style.marginLeft = '4px';
+          okLabel.style.fontSize = '11px';
+          okLabel.textContent = 'Accept';
+          acceptBtn.append(okSvg, okLabel);
apps/desktop/src/contexts/right-panel.tsx (2)

121-130: Refactor duplicated focus logic using imported utility

The focus logic is duplicated with the same pattern appearing elsewhere in the file. Consider using the existing focusInput utility from chat-utils.ts.

Import the utility function at the top of the file:

import { focusInput } from "@/components/right-panel/utils/chat-utils";

Then simplify the focus logic:

 setTimeout(() => {
-  const focusInput = () => {
-    if (chatInputRef.current) {
-      chatInputRef.current.focus();
-    } else {
-      setTimeout(focusInput, 50);
-    }
-  };
-  focusInput();
+  focusInput(chatInputRef);
 }, 350);

Note: The existing utility may need to be enhanced to support the retry logic if it doesn't already.


10-16: Consider more descriptive field names for ProseMirror positions

The current field names startOffset and endOffset could be misleading since they represent ProseMirror positions rather than character offsets. Consider renaming them to be more explicit.

 export interface SelectionData {
   text: string;
-  startOffset: number;  // ProseMirror position (not character offset)
-  endOffset: number;    // ProseMirror position (not character offset)
+  startPos: number;  // ProseMirror absolute position
+  endPos: number;    // ProseMirror absolute position
   sessionId: string;
   timestamp: number;
 }
apps/desktop/src/components/right-panel/components/chat/chat-input.tsx (2)

140-141: Consider a more robust deduplication mechanism

Using a string concatenation for deduplication could have edge cases. Consider using a more robust approach.

 const editorRef = useRef<{ editor: TiptapEditor | null }>(null);
-const processedSelectionRef = useRef<string | null>(null);
+const processedSelectionRef = useRef<SelectionData | null>(null);

Then update the comparison logic:

-const selectionId = `${pendingSelection.startOffset}-${pendingSelection.endOffset}-${pendingSelection.timestamp}`;
-
-// Only process if we haven't already processed this exact selection
-if (processedSelectionRef.current !== selectionId) {
+// Only process if we haven't already processed this exact selection
+if (!processedSelectionRef.current || 
+    processedSelectionRef.current.startOffset !== pendingSelection.startOffset ||
+    processedSelectionRef.current.endOffset !== pendingSelection.endOffset ||
+    processedSelectionRef.current.timestamp !== pendingSelection.timestamp) {

And update the marking:

-processedSelectionRef.current = selectionId;
+processedSelectionRef.current = pendingSelection;

346-377: Consider extracting selection-ref styles to a separate CSS file

The inline styles are growing complex. Consider moving them to a dedicated CSS module for better maintainability.

Create a new file chat-input.module.css and move the styles there, then import and use CSS modules for better organization and potential performance benefits.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 873edcf and c65409b.

📒 Files selected for processing (17)
  • apps/desktop/src/components/editor-area/index.tsx (4 hunks)
  • apps/desktop/src/components/editor-area/text-selection-popover.tsx (4 hunks)
  • apps/desktop/src/components/right-panel/components/chat/chat-input.tsx (8 hunks)
  • apps/desktop/src/components/right-panel/hooks/useChatLogic.ts (8 hunks)
  • apps/desktop/src/components/right-panel/utils/chat-utils.ts (3 hunks)
  • apps/desktop/src/components/right-panel/utils/tools/edit_enhanced_note.ts (1 hunks)
  • apps/desktop/src/components/right-panel/utils/tools/search_session_multi_keywords.ts (1 hunks)
  • apps/desktop/src/components/right-panel/views/chat-view.tsx (1 hunks)
  • apps/desktop/src/contexts/ai-edit.tsx (1 hunks)
  • apps/desktop/src/contexts/right-panel.tsx (5 hunks)
  • apps/desktop/src/shared/editor-ref.ts (1 hunks)
  • crates/template/assets/chat.system.jinja (1 hunks)
  • crates/template/assets/chat.user.jinja (1 hunks)
  • packages/tiptap/src/shared/ai-highlight.ts (1 hunks)
  • packages/tiptap/src/shared/extensions.ts (2 hunks)
  • packages/tiptap/src/styles/nodes/ai-highlight.css (1 hunks)
  • packages/tiptap/src/styles/tiptap.css (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{js,ts,tsx,rs}

⚙️ CodeRabbit configuration file

**/*.{js,ts,tsx,rs}: 1. No error handling.
2. No unused imports, variables, or functions.
3. For comments, keep it minimal. It should be about "Why", not "What".

Files:

  • apps/desktop/src/components/right-panel/utils/tools/search_session_multi_keywords.ts
  • packages/tiptap/src/shared/ai-highlight.ts
  • apps/desktop/src/components/right-panel/utils/tools/edit_enhanced_note.ts
  • apps/desktop/src/components/right-panel/views/chat-view.tsx
  • packages/tiptap/src/shared/extensions.ts
  • apps/desktop/src/contexts/right-panel.tsx
  • apps/desktop/src/shared/editor-ref.ts
  • apps/desktop/src/components/right-panel/components/chat/chat-input.tsx
  • apps/desktop/src/contexts/ai-edit.tsx
  • apps/desktop/src/components/editor-area/text-selection-popover.tsx
  • apps/desktop/src/components/right-panel/utils/chat-utils.ts
  • apps/desktop/src/components/right-panel/hooks/useChatLogic.ts
  • apps/desktop/src/components/editor-area/index.tsx
🧬 Code graph analysis (9)
apps/desktop/src/components/right-panel/utils/tools/search_session_multi_keywords.ts (1)
packages/utils/src/ai.ts (1)
  • tool (18-18)
apps/desktop/src/components/right-panel/utils/tools/edit_enhanced_note.ts (3)
apps/desktop/src/contexts/right-panel.tsx (1)
  • SelectionData (10-16)
packages/utils/src/ai.ts (1)
  • tool (18-18)
apps/desktop/src/shared/editor-ref.ts (1)
  • globalEditorRef (8-10)
packages/tiptap/src/shared/extensions.ts (1)
packages/tiptap/src/shared/ai-highlight.ts (1)
  • AIHighlight (26-91)
apps/desktop/src/contexts/right-panel.tsx (1)
apps/desktop/src/components/right-panel/utils/chat-utils.ts (1)
  • focusInput (30-34)
apps/desktop/src/components/right-panel/components/chat/chat-input.tsx (1)
apps/desktop/src/contexts/right-panel.tsx (2)
  • SelectionData (10-16)
  • useRightPanel (237-243)
apps/desktop/src/components/editor-area/text-selection-popover.tsx (2)
apps/desktop/src/contexts/right-panel.tsx (1)
  • useRightPanel (237-243)
packages/ui/src/components/ui/button.tsx (1)
  • Button (37-89)
apps/desktop/src/components/right-panel/utils/chat-utils.ts (1)
apps/desktop/src/contexts/right-panel.tsx (1)
  • SelectionData (10-16)
apps/desktop/src/components/right-panel/hooks/useChatLogic.ts (3)
apps/desktop/src/contexts/right-panel.tsx (1)
  • SelectionData (10-16)
apps/desktop/src/components/right-panel/utils/tools/search_session_multi_keywords.ts (1)
  • createSearchSessionTool (5-59)
apps/desktop/src/components/right-panel/utils/tools/edit_enhanced_note.ts (1)
  • createEditEnhancedNoteTool (12-284)
apps/desktop/src/components/editor-area/index.tsx (1)
apps/desktop/src/shared/editor-ref.ts (1)
  • globalEditorRef (8-10)
🪛 GitHub Actions: .github/workflows/fmt.yaml
packages/tiptap/src/shared/ai-highlight.ts

[error] 1-1: dprint: not formatted according to configuration. Command: ~/.dprint/bin/dprint check --config 'dprint.json'. Run 'dprint --write' to fix formatting.

apps/desktop/src/components/right-panel/utils/tools/edit_enhanced_note.ts

[error] 1-1: dprint: not formatted according to configuration. Command: ~/.dprint/bin/dprint check --config 'dprint.json'. Run 'dprint --write' to fix formatting.

crates/template/assets/chat.system.jinja

[error] 1-1: dprint: not formatted according to configuration. Command: ~/.dprint/bin/dprint check --config 'dprint.json'. Run 'dprint --write' to fix formatting.

packages/tiptap/src/styles/nodes/ai-highlight.css

[error] 1-1: dprint: not formatted according to configuration. Command: ~/.dprint/bin/dprint check --config 'dprint.json'. Run 'dprint --write' to fix formatting.

apps/desktop/src/contexts/right-panel.tsx

[error] 1-1: dprint: not formatted according to configuration. Command: ~/.dprint/bin/dprint check --config 'dprint.json'. Run 'dprint --write' to fix formatting.

apps/desktop/src/shared/editor-ref.ts

[error] 1-1: dprint: not formatted according to configuration. Command: ~/.dprint/bin/dprint check --config 'dprint.json'. Run 'dprint --write' to fix formatting.

apps/desktop/src/components/right-panel/components/chat/chat-input.tsx

[error] 1-1: dprint: not formatted according to configuration. Command: ~/.dprint/bin/dprint check --config 'dprint.json'. Run 'dprint --write' to fix formatting.

apps/desktop/src/contexts/ai-edit.tsx

[error] 1-1: dprint: not formatted according to configuration. Command: ~/.dprint/bin/dprint check --config 'dprint.json'. Run 'dprint --write' to fix formatting.

apps/desktop/src/components/editor-area/text-selection-popover.tsx

[error] 1-1: dprint: not formatted according to configuration. Command: ~/.dprint/bin/dprint check --config 'dprint.json'. Run 'dprint --write' to fix formatting.

apps/desktop/src/components/right-panel/hooks/useChatLogic.ts

[error] 1-1: dprint: not formatted according to configuration. Command: ~/.dprint/bin/dprint check --config 'dprint.json'. Run 'dprint --write' to fix formatting.

apps/desktop/src/components/editor-area/index.tsx

[error] 1-1: dprint: not formatted according to configuration. Command: ~/.dprint/bin/dprint check --config 'dprint.json'. Run 'dprint --write' to fix formatting.

🪛 ast-grep (0.38.6)
apps/desktop/src/components/right-panel/utils/tools/edit_enhanced_note.ts

[warning] 103-108: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: undoBtn.innerHTML = <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M3 7v6h6"/><path d="M21 17a9 9 0 00-9-9 9 9 0 00-6 2.3L3 13"/> </svg> <span style="margin-left: 4px; font-size: 11px;">Undo</span>
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://owasp.org/www-community/xss-filter-evasion-cheatsheet
- https://cwe.mitre.org/data/definitions/79.html

(dom-content-modification)


[warning] 148-153: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: acceptBtn.innerHTML = <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2"> <polyline points="20 6 9 17 4 12"/> </svg> <span style="margin-left: 4px; font-size: 11px;">Accept</span>
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://owasp.org/www-community/xss-filter-evasion-cheatsheet
- https://cwe.mitre.org/data/definitions/79.html

(dom-content-modification)


[warning] 103-108: Direct HTML content assignment detected. Modifying innerHTML, outerHTML, or using document.write with unsanitized content can lead to XSS vulnerabilities. Use secure alternatives like textContent or sanitize HTML with libraries like DOMPurify.
Context: undoBtn.innerHTML = <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M3 7v6h6"/><path d="M21 17a9 9 0 00-9-9 9 9 0 00-6 2.3L3 13"/> </svg> <span style="margin-left: 4px; font-size: 11px;">Undo</span>
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://www.dhairyashah.dev/posts/why-innerhtml-is-a-bad-idea-and-how-to-avoid-it/
- https://cwe.mitre.org/data/definitions/79.html

(unsafe-html-content-assignment)


[warning] 148-153: Direct HTML content assignment detected. Modifying innerHTML, outerHTML, or using document.write with unsanitized content can lead to XSS vulnerabilities. Use secure alternatives like textContent or sanitize HTML with libraries like DOMPurify.
Context: acceptBtn.innerHTML = <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2"> <polyline points="20 6 9 17 4 12"/> </svg> <span style="margin-left: 4px; font-size: 11px;">Accept</span>
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://www.dhairyashah.dev/posts/why-innerhtml-is-a-bad-idea-and-how-to-avoid-it/
- https://cwe.mitre.org/data/definitions/79.html

(unsafe-html-content-assignment)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: cubic · AI code reviewer
  • GitHub Check: ci (macos, macos-latest)
  • GitHub Check: ci (windows, windows-latest)
🔇 Additional comments (22)
packages/tiptap/src/styles/nodes/ai-highlight.css (1)

1-17: dprint formatter failure: CLI not found

It looks like the dprint binary isn’t available at ~/.dprint/bin/dprint, so your formatting step can’t run and CI will continue to fail. Please:

  • Install the dprint CLI on your machine/CI agent. For example:
    npm install -g dprint
    • or pnpm add -D dprint
  • Run the formatter using a local or project‐scoped install instead of a hard-coded path. For example:
    npx dprint fmt packages/tiptap/src/styles/nodes/ai-highlight.css
    pnpm exec dprint fmt packages/tiptap/src/styles/nodes/ai-highlight.css
  • Verify formatting is clean with dprint check (or npx dprint check/pnpm exec dprint check) and commit any changes.

Once the CLI is installed or the invocation is updated, re-run the formatter to unblock CI.

packages/tiptap/src/styles/tiptap.css (1)

13-13: Import placement LGTM

Importing ai-highlight.css right after mark.css is appropriate for mark-level styling precedence.

apps/desktop/src/components/right-panel/utils/tools/search_session_multi_keywords.ts (1)

19-20: No Cross-User Leak; empty user_id simply yields no results
I’ve confirmed that in the generated TypeScript bindings, the listSessions filter always requires a non-optional user_id: string (never undefined or null) — see ListSessionFilter in packages/obsidian/generated/types.gen.ts . On the Rust side, the DB plugin passes this exact string into a SQL WHERE s.user_id = ? query (via crates/db-user/src/sessions_ops.rs), so supplying "" will simply match no sessions rather than falling back to a global/default user . There’s no semantic distinction in the plugin between an empty string and any other string: it always filters by exactly what you pass.

If you prefer to avoid calling listSessions with an empty ID entirely, you can add a guard in the TS code (e.g. if (!userId) return [];), but functionally there’s no risk of leaking another user’s sessions.

packages/tiptap/src/shared/ai-highlight.ts (1)

1-1: Run dprint to format ai-highlight.ts

This file is flagged by dprint. Please ensure you have dprint available (e.g. installed in devDependencies or via your package manager), then run the formatter and verification steps:

# Install dprint if needed
npm install --save-dev dprint

# Format the file and apply quote‐style normalization
npx dprint fmt packages/tiptap/src/shared/ai-highlight.ts

# Verify no other files are out of compliance
npx dprint check

This will enforce the repo’s formatting conventions (including consistent quote style) across your changes.

packages/tiptap/src/shared/extensions.ts (1)

88-88: Extension registration LGTM

AIHighlight is correctly added after Highlight, which is a sensible order for mark precedence.

apps/desktop/src/components/right-panel/views/chat-view.tsx (1)

183-183: onSubmit/handleSubmit signatures match — no changes needed

I’ve verified that:

  • ChatInputProps defines
    onSubmit: (mentionedContent?: Array<{ id: string; type: string; label: string }>, selectionData?: SelectionData) => void
    in
    apps/desktop/src/components/right-panel/components/chat/chat-input.tsx
  • The ChatInput invocation in
    apps/desktop/src/components/right-panel/views/chat-view.tsx
    passes
    (mentionedContent, selectionData) => handleSubmit(mentionedContent, selectionData)
  • handleSubmit from useChatLogic is declared as
    const handleSubmit = async (mentionedContent?: Array<{ id: string; type: string; label: string }>, selectionData?: SelectionData) => { … }
    in
    apps/desktop/src/components/right-panel/hooks/useChatLogic.ts

All parameter types and optionality align across the prop definition, the caller, and the handler implementation.

apps/desktop/src/shared/editor-ref.ts (1)

1-11: Fix dprint formatting to unblock CI

It looks like the CI is failing on styling checks for this file (see .github/workflows/fmt.yaml), but we don’t yet have the dprint CLI installed locally to repro and fix it. To resolve:

  • Install dprint as a dev dependency:
    • npm: npm install --save-dev dprint
    • pnpm: pnpm add -D dprint
  • Add a helper script in your package.json:
    "scripts": {
      "format": "dprint fmt --config dprint.json"
    }
  • Run the formatter against this file:
    npm run format apps/desktop/src/shared/editor-ref.ts
  • Commit the updated file so that the CI dprint/check step passes.

Once formatted, the CI should unblock.

apps/desktop/src/contexts/ai-edit.tsx (1)

1-34: Script requested; awaiting results.

apps/desktop/src/components/editor-area/index.tsx (2)

304-306: Propagating sessionId/editorRef to popover looks good

This aligns the selection-to-chat flow; no issues spotted.


1-341: Fix formatting to unblock CI

dprint is failing for this PR. Run: dprint --write

apps/desktop/src/components/right-panel/hooks/useChatLogic.ts (3)

121-126: Signature extension to carry selectionData is sound

Plumbing selection context into chat is correct and backward-compatible.


322-329: Nice: conditional tool exposure based on selectionData

Exposing edit_enhanced_note only when relevant is the right call.


1-611: Fix formatting to unblock CI

dprint is failing for this PR. Run: dprint --write

apps/desktop/src/components/right-panel/utils/chat-utils.ts (3)

36-47: Extending prepareMessageHistory with selectionData is correct

Type-only import and optional param preserve compatibility.


204-211: Good: selectionData serialized as a plain object

This keeps templates JsonValue-safe.


1-223: Fix formatting to unblock CI

dprint is failing for this PR. Run: dprint --write

apps/desktop/src/components/editor-area/text-selection-popover.tsx (3)

153-196: Great UX: robust selection capture and routing to chat

Selection extraction, ProseMirror positions, and dispatch via sendSelectionToChat look solid.


230-251: Minor: consistent small-button height class already applied

UI tweaks LGTM.


1-254: Fix formatting to unblock CI

dprint is failing for this PR. Run: dprint --write

apps/desktop/src/components/right-panel/utils/tools/edit_enhanced_note.ts (2)

69-76: Good: highlight metadata tracked

Using timestamp/sessionId in the mark aids auditability.


1-285: Fix formatting to unblock CI

dprint is failing for this PR. Run: dprint --write

apps/desktop/src/components/right-panel/components/chat/chat-input.tsx (1)

261-264: Confirmed .mention-container selector is correct

The .mention-container class is defined in packages/tiptap/src/styles/mention.css and applied to the dropdown wrapper in packages/tiptap/src/editor/mention.tsx. It matches the element you’re querying in chat-input.tsx, so no changes are needed.

Comment on lines 225 to 245
console.log("💬 Chat Input Received NEW Selection (ProseMirror):");
console.log("Pending selection:", pendingSelection);
console.log("ProseMirror positions:", pendingSelection.startOffset, "to", pendingSelection.endOffset);

// Create compact reference instead of full quoted text
const noteName = noteData?.title || humanData?.full_name || organizationData?.name || "Note";
const selectionRef = `[${noteName} - ${pendingSelection.startOffset}:${pendingSelection.endOffset}]`;
// Use a mention element with data-mention="true" so Tiptap recognizes it
const referenceText = `<a class="mention selection-ref" data-mention="true" data-id="selection-${pendingSelection.startOffset}-${pendingSelection.endOffset}" data-type="selection" data-label="${selectionRef}" contenteditable="false">${selectionRef}</a> `;
console.log("Generated selection reference:", referenceText);

editorRef.current.editor.commands.setContent(referenceText);
editorRef.current.editor.commands.focus('end');

// Clear the input value to match editor content
const syntheticEvent = {
target: { value: selectionRef },
currentTarget: { value: selectionRef },
} as React.ChangeEvent<HTMLTextAreaElement>;
onChange(syntheticEvent);
console.log("Chat input populated with selection reference");
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Remove console.log statements from selection processing

Multiple console.log statements should be removed before merging to production.

 // Only process if we haven't already processed this exact selection
 if (processedSelectionRef.current !== selectionId) {
-  console.log("💬 Chat Input Received NEW Selection (ProseMirror):");
-  console.log("Pending selection:", pendingSelection);
-  console.log("ProseMirror positions:", pendingSelection.startOffset, "to", pendingSelection.endOffset);
   
   // Create compact reference instead of full quoted text
   const noteName = noteData?.title || humanData?.full_name || organizationData?.name || "Note";
   const selectionRef = `[${noteName} - ${pendingSelection.startOffset}:${pendingSelection.endOffset}]`;
   // Use a mention element with data-mention="true" so Tiptap recognizes it
   const referenceText = `<a class="mention selection-ref" data-mention="true" data-id="selection-${pendingSelection.startOffset}-${pendingSelection.endOffset}" data-type="selection" data-label="${selectionRef}" contenteditable="false">${selectionRef}</a> `;
-  console.log("Generated selection reference:", referenceText);
   
   editorRef.current.editor.commands.setContent(referenceText);
   editorRef.current.editor.commands.focus('end');
   
   // Clear the input value to match editor content
   const syntheticEvent = {
     target: { value: selectionRef },
     currentTarget: { value: selectionRef },
   } as React.ChangeEvent<HTMLTextAreaElement>;
   onChange(syntheticEvent);
-  console.log("Chat input populated with selection reference");
   
   // Mark this selection as processed
   processedSelectionRef.current = selectionId;
 }
🤖 Prompt for AI Agents
In apps/desktop/src/components/right-panel/components/chat/chat-input.tsx around
lines 225 to 245, remove all console.log(...) debugging statements in the
selection processing block (the four logs before generating the selectionRef,
the log after generating selectionRef, and the final log after onChange) so no
console output remains in production; keep the selectionRef creation,
editor.commands.setContent/focus, syntheticEvent creation, and onChange call
intact and unchanged.

const noteName = noteData?.title || humanData?.full_name || organizationData?.name || "Note";
const selectionRef = `[${noteName} - ${pendingSelection.startOffset}:${pendingSelection.endOffset}]`;
// Use a mention element with data-mention="true" so Tiptap recognizes it
const referenceText = `<a class="mention selection-ref" data-mention="true" data-id="selection-${pendingSelection.startOffset}-${pendingSelection.endOffset}" data-type="selection" data-label="${selectionRef}" contenteditable="false">${selectionRef}</a> `;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Potential XSS vulnerability with HTML string concatenation

Constructing HTML strings with user data could lead to XSS vulnerabilities. The selectionRef contains data that could potentially be manipulated.

Consider using TipTap's API to insert nodes programmatically instead of HTML strings:

const mentionNode = {
  type: 'mention',
  attrs: {
    'data-mention': 'true',
    'data-id': `selection-${pendingSelection.startOffset}-${pendingSelection.endOffset}`,
    'data-type': 'selection',
    'data-label': selectionRef,
    contenteditable: 'false'
  },
  content: [{ type: 'text', text: selectionRef }]
};

editorRef.current.editor.commands.insertContent(mentionNode);

Comment on lines 20 to 23
startOffset: z.number().describe("The ProseMirror start position of the content to replace"),
endOffset: z.number().describe("The ProseMirror end position of the content to replace"),
newHtml: z.string().describe("The new HTML content to replace the selected content with. Maintain proper HTML structure and formatting."),
}),
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Validate and constrain tool inputs

Harden start/end offsets and HTML length to prevent bad ranges or huge payloads.

-    inputSchema: z.object({
-      startOffset: z.number().describe("The ProseMirror start position of the content to replace"),
-      endOffset: z.number().describe("The ProseMirror end position of the content to replace"), 
-      newHtml: z.string().describe("The new HTML content to replace the selected content with. Maintain proper HTML structure and formatting."),
-    }),
+    inputSchema: z.object({
+      startOffset: z.number().int().nonnegative().describe("ProseMirror start position (inclusive)"),
+      endOffset: z.number().int().nonnegative().describe("ProseMirror end position (exclusive)"),
+      newHtml: z.string().min(1).max(20000).describe("Replacement HTML; must be well-formed."),
+    }).refine((v) => v.endOffset > v.startOffset, "endOffset must be greater than startOffset"),
📝 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
startOffset: z.number().describe("The ProseMirror start position of the content to replace"),
endOffset: z.number().describe("The ProseMirror end position of the content to replace"),
newHtml: z.string().describe("The new HTML content to replace the selected content with. Maintain proper HTML structure and formatting."),
}),
inputSchema: z.object({
startOffset: z.number().int().nonnegative().describe("ProseMirror start position (inclusive)"),
endOffset: z.number().int().nonnegative().describe("ProseMirror end position (exclusive)"),
newHtml: z.string().min(1).max(20000).describe("Replacement HTML; must be well-formed."),
}).refine((v) => v.endOffset > v.startOffset, "endOffset must be greater than startOffset"),

Comment on lines 41 to 47
// Capture original content from selectionData BEFORE making any changes
const originalContent = selectionData?.text || editor.state.doc.textBetween(startOffset, endOffset);
console.log("🔄 Original content saved for undo:", originalContent);

// Trim and clean the HTML to prevent empty elements
const cleanedHtml = newHtml.trim().replace(/>\s+</g, '><');

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Sanitize LLM-provided HTML before insert

Insertions come from the model; sanitize to prevent malicious markup slipping through.

-        // Trim and clean the HTML to prevent empty elements
-        const cleanedHtml = newHtml.trim().replace(/>\s+</g, '><');
+        // Trim and sanitize HTML
+        const raw = newHtml.trim().replace(/>\s+</g, "><");
+        // Use a sanitizer available in the app (e.g., DOMPurify) if present
+        const cleanedHtml = (window as any).DOMPurify
+          ? (window as any).DOMPurify.sanitize(raw, { USE_PROFILES: { html: true } })
+          : raw;

If DOMPurify isn’t bundled, I can wire it up or switch to an allowlist-based serializer.

📝 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
// Capture original content from selectionData BEFORE making any changes
const originalContent = selectionData?.text || editor.state.doc.textBetween(startOffset, endOffset);
console.log("🔄 Original content saved for undo:", originalContent);
// Trim and clean the HTML to prevent empty elements
const cleanedHtml = newHtml.trim().replace(/>\s+</g, '><');
// Capture original content from selectionData BEFORE making any changes
const originalContent = selectionData?.text || editor.state.doc.textBetween(startOffset, endOffset);
console.log("🔄 Original content saved for undo:", originalContent);
// Trim and sanitize HTML
const raw = newHtml.trim().replace(/>\s+</g, "><");
// Use a sanitizer available in the app (e.g., DOMPurify) if present
const cleanedHtml = (window as any).DOMPurify
? (window as any).DOMPurify.sanitize(raw, { USE_PROFILES: { html: true } })
: raw;
🤖 Prompt for AI Agents
In apps/desktop/src/components/right-panel/utils/tools/edit_enhanced_note.ts
around lines 41 to 47, the code trims and collapses whitespace in newHtml but
does not sanitize model-provided HTML; update the flow to pass cleanedHtml
through a trusted HTML sanitizer (prefer DOMPurify if available: import
DOMPurify, run DOMPurify.sanitize(cleanedHtml, {ALLOWED_TAGS, ALLOWED_ATTR})
with a strict allowlist, and use the sanitized output for insertion), and add a
fallback path that uses an allowlist-based serializer if DOMPurify isn’t bundled
(or throw/abort insertion if no sanitizer is configured). Ensure imports and
types are added, and that sanitized HTML replaces cleanedHtml before any DOM
insertion or editor transaction.

Comment on lines 51 to 67
// Delete old content and insert new content with proper parse options
editor.chain()
.focus()
.setTextSelection({ from: startOffset, to: endOffset })
.deleteSelection()
.insertContent(cleanedHtml, {
parseOptions: {
preserveWhitespace: false
}
})
.run();

// Calculate actual inserted content size
const finalDocSize = editor.state.doc.content.size;
const insertedSize = finalDocSize - initialDocSize + (endOffset - startOffset);
const highlightEnd = startOffset + insertedSize;

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Clamp selection to document bounds

Avoid runtime errors when positions are out of bounds.

-        editor.chain()
+        const docSize = editor.state.doc.content.size;
+        const from = Math.max(0, Math.min(startOffset, docSize));
+        const to = Math.max(from, Math.min(endOffset, docSize));
+        editor.chain()
           .focus()
-          .setTextSelection({ from: startOffset, to: endOffset })
+          .setTextSelection({ from, to })
           .deleteSelection()
           .insertContent(cleanedHtml, {
             parseOptions: {
               preserveWhitespace: false
             }
           })
           .run();
-        
+        // Recompute doc size after changes
         // Calculate actual inserted content size
-        const finalDocSize = editor.state.doc.content.size;
-        const insertedSize = finalDocSize - initialDocSize + (endOffset - startOffset);
-        const highlightEnd = startOffset + insertedSize;
+        const finalDocSize = editor.state.doc.content.size;
+        const insertedSize = finalDocSize - initialDocSize + (to - from);
+        const highlightEnd = from + insertedSize;
📝 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
// Delete old content and insert new content with proper parse options
editor.chain()
.focus()
.setTextSelection({ from: startOffset, to: endOffset })
.deleteSelection()
.insertContent(cleanedHtml, {
parseOptions: {
preserveWhitespace: false
}
})
.run();
// Calculate actual inserted content size
const finalDocSize = editor.state.doc.content.size;
const insertedSize = finalDocSize - initialDocSize + (endOffset - startOffset);
const highlightEnd = startOffset + insertedSize;
// Delete old content and insert new content with proper parse options
const docSize = editor.state.doc.content.size;
const from = Math.max(0, Math.min(startOffset, docSize));
const to = Math.max(from, Math.min(endOffset, docSize));
editor.chain()
.focus()
.setTextSelection({ from, to })
.deleteSelection()
.insertContent(cleanedHtml, {
parseOptions: {
preserveWhitespace: false
}
})
.run();
// Recompute doc size after changes
// Calculate actual inserted content size
const finalDocSize = editor.state.doc.content.size;
const insertedSize = finalDocSize - initialDocSize + (to - from);
const highlightEnd = from + insertedSize;
🤖 Prompt for AI Agents
In apps/desktop/src/components/right-panel/utils/tools/edit_enhanced_note.ts
around lines 51 to 67, clamp startOffset and endOffset to valid document bounds
before calling editor.chain(): ensure startOffset = Math.max(0,
Math.min(startOffset, editor.state.doc.content.size)) and endOffset =
Math.max(startOffset, Math.min(endOffset, editor.state.doc.content.size)) (or
equivalent) so selection positions cannot be out of range; use the clamped
values for setTextSelection, delete/insert operations and for computing
finalDocSize/insertedSize/highlightEnd to avoid runtime out-of-bounds errors.

Comment on lines 163 to 165
- edit_enhanced_note: Only call when you are given a certain Prosemirror range of text that the user wants to edit. Replace that certain range with a new text that is written in a way
that the user wants.

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

Tighten tool contract and forbid raw-HTML chat responses; require selectionData/session gating.

Strengthen the invocation rules so the model edits only via the tool and never emits free-form HTML in chat.

- - edit_enhanced_note: Only call when you are given a certain Prosemirror range of text that the user wants to edit. Replace that certain range with a new text that is written in a way 
-   that the user wants. 
+ - edit_enhanced_note: Call only when selectionData is present (must include startOffset, endOffset, sessionId) and the active editor is available. Provide an HTML fragment that replaces exactly that range, preserving nodes/marks. Do NOT output HTML in normal chat responses—perform edits exclusively via this tool.
📝 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
- edit_enhanced_note: Only call when you are given a certain Prosemirror range of text that the user wants to edit. Replace that certain range with a new text that is written in a way
that the user wants.
- edit_enhanced_note: Call only when selectionData is present (must include startOffset, endOffset, sessionId) and the active editor is available. Provide an HTML fragment that replaces exactly that range, preserving nodes/marks. Do NOT output HTML in normal chat responses—perform edits exclusively via this tool.
🤖 Prompt for AI Agents
In crates/template/assets/chat.system.jinja around lines 163-165, tighten the
tool contract so it is only invoked when a valid selectionData and session are
present and forbid the model from emitting raw HTML in chat; update the
edit_enhanced_note text to state that the tool must only be called when the
caller provides a specific ProseMirror range via selectionData/session gating,
that the tool should return only the replacement text (plain text or structured
JSON) for that range and never free-form or HTML-wrapped chat output, and
include a short note to validate selectionData/session before performing edits.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (6)
apps/desktop/src/components/editor-area/text-selection-popover.tsx (2)

178-183: Avoid console statements in production.

Gate any remaining logs behind process.env.NODE_ENV !== "production" or a logging utility.

-      console.warn("DOM selection doesn't match TipTap selection:");
-      console.warn("DOM:", selection.text);
-      console.warn("TipTap:", tipTapPositions.text);
+      if (process.env.NODE_ENV !== "production") {
+        console.warn("DOM/Tiptap selection mismatch");
+      }

124-165: Derive HTML from ProseMirror slice and scope selection to the editor DOM.

Use ProseMirror DOMSerializer to serialize the slice (from/to) to HTML. Also, if a DOM selection exists, ensure it’s within editor.view.dom. Avoid window-range HTML extraction to prevent mismatches.

-  // Helper to get TipTap/ProseMirror positions from DOM selection
-  const getTipTapPositions = () => {
+  // Why: Ensure HTML is derived from the editor state, not arbitrary DOM.
+  const getTipTapSelection = () => {
     const editor = editorRef.current?.editor;
     if (!editor) {
-      console.warn("No TipTap editor available");
       return null;
     }
 
-    // Get current TipTap selection positions
-    const { from, to } = editor.state.selection;
-
-    // CLEAN HTML APPROACH: Extract selected content as HTML directly
-    let selectedHtml = "";
-    try {
-      // Get the selected DOM range
-      const selection = window.getSelection();
-      if (selection && selection.rangeCount > 0) {
-        const range = selection.getRangeAt(0);
-        const fragment = range.cloneContents();
-
-        // Create a temporary div to get the HTML
-        const tempDiv = document.createElement("div");
-        tempDiv.appendChild(fragment);
-        selectedHtml = tempDiv.innerHTML;
-      }
-
-      // Fallback: if no DOM selection, use plain text
-      if (!selectedHtml) {
-        selectedHtml = editor.state.doc.textBetween(from, to);
-      }
-    } catch (error) {
-      console.warn("Could not extract HTML, falling back to plain text:", error);
-      selectedHtml = editor.state.doc.textBetween(from, to);
-    }
-
-    return {
-      from,
-      to,
-      text: selectedHtml, // Now contains HTML instead of plain text
-    };
+    const { state, view } = editor;
+    const { from, to } = state.selection;
+
+    // If there's a DOM selection, ensure it's inside the editor.
+    const domSel = window.getSelection();
+    if (domSel && domSel.rangeCount > 0) {
+      const range = domSel.getRangeAt(0);
+      if (!view.dom.contains(range.commonAncestorContainer)) {
+        return null;
+      }
+    }
+
+    // Serialize slice to HTML
+    const { DOMSerializer } = require("prosemirror-model");
+    const serializer = DOMSerializer.fromSchema(state.schema);
+    const slice = state.doc.slice(from, to);
+    const tmp = document.createElement("div");
+    tmp.appendChild(serializer.serializeFragment(slice.content));
+    const html = tmp.innerHTML;
+    const text = state.doc.textBetween(from, to);
+
+    return { from, to, html, text };
   };

Note: add the import (see next comment).

apps/desktop/src/components/right-panel/utils/tools/edit_enhanced_note.ts (3)

86-88: Call previous controls’ cleanup() before removing.

Avoid leaked listeners and duplicates.

-          // Remove any existing controls
-          document.querySelector(".ai-edit-controls")?.remove();
+          // Remove any existing controls (call cleanup if available)
+          const prev = document.querySelector(".ai-edit-controls") as any;
+          if (prev) (prev.cleanup?.() ?? prev.remove());

20-26: Strengthen input schema (ints, bounds, and size).

Offsets should be integers with sane bounds; HTML length should be constrained; ensure endOffset > startOffset.

-    inputSchema: z.object({
-      startOffset: z.number().describe("The ProseMirror start position of the content to replace"),
-      endOffset: z.number().describe("The ProseMirror end position of the content to replace"),
-      newHtml: z.string().describe(
-        "The new HTML content to replace the selected content with. Maintain proper HTML structure and formatting.",
-      ),
-    }),
+    inputSchema: z.object({
+      startOffset: z.number().int().nonnegative().describe("ProseMirror start position (inclusive)"),
+      endOffset: z.number().int().nonnegative().describe("ProseMirror end position (exclusive)"),
+      newHtml: z.string().min(1).max(20000).describe("Replacement HTML; must be well‑formed."),
+    }).refine(v => v.endOffset > v.startOffset, { message: "endOffset must be greater than startOffset" }),

45-49: Undo will lose structure/marks; capture original slice, not text.

textBetween (and possibly selectionData?.text) degrades formatting. Capture the ProseMirror slice and reinsert it on undo.

-        // Capture original content from selectionData BEFORE making any changes
-        const originalContent = selectionData?.text || editor.state.doc.textBetween(startOffset, endOffset);
+        // Capture original content as a ProseMirror slice for lossless undo
+        const docSize = editor.state.doc.content.size;
+        const from0 = Math.max(0, Math.min(startOffset, docSize));
+        const to0 = Math.max(from0, Math.min(endOffset, docSize));
+        const originalNode = editor.state.doc.cut(from0, to0);
+        const originalJson = (originalNode.toJSON() as any).content ?? [];

And in the undo handler:

-              .setTextSelection({ from: startOffset, to: highlightEnd })
-              .deleteSelection()
-              .insertContent(originalContent)
+              .setTextSelection({ from: from0, to: highlightEnd })
+              .deleteSelection()
+              .insertContent(originalJson)
               .run();
apps/desktop/src/components/right-panel/components/chat/chat-input.tsx (1)

220-272: Build the mention node programmatically (avoid HTML string + partial escaping).

Concatenating HTML with user-derived labels risks injection; insert a structured node instead.

-        const escapedSelectionRef = selectionRef.replace(/"/g, "&quot;");
-
-        const referenceText =
-          `<a class="mention selection-ref" data-mention="true" data-id="selection-${pendingSelection.startOffset}-${pendingSelection.endOffset}" data-type="selection" data-label="${escapedSelectionRef}" contenteditable="false">${selectionRef}</a> `;
-
-        editorRef.current.editor.commands.setContent(referenceText);
+        const mentionNode = {
+          type: "mention",
+          attrs: {
+            class: "selection-ref",
+            "data-mention": "true",
+            "data-id": `selection-${pendingSelection.startOffset}-${pendingSelection.endOffset}`,
+            "data-type": "selection",
+            "data-label": selectionRef,
+            contenteditable: "false",
+          },
+          content: [{ type: "text", text: selectionRef }],
+        };
+        editorRef.current.editor.commands.setContent({
+          type: "doc",
+          content: [{ type: "paragraph", content: [mentionNode, { type: "text", text: " " }] }],
+        });
🧹 Nitpick comments (6)
apps/desktop/src/components/editor-area/text-selection-popover.tsx (2)

1-8: Add DOMSerializer import required by the serializer usage.

 import { Button } from "@hypr/ui/components/ui/button";
 import { MessageSquare, Sparkles } from "lucide-react";
 import { useEffect, useRef, useState } from "react";
+import { DOMSerializer } from "prosemirror-model";

13-16: Remove unused onAskAI prop from TextSelectionPopoverProps and its destructuring in TextSelectionPopover—it isn’t invoked anywhere after the switch to sendSelectionToChat.

apps/desktop/src/components/right-panel/hooks/useChatLogic.ts (2)

288-295: Only create tools when actually used (avoid unused work).

Creating tools unconditionally does work even when shouldUseTools is false. Inline factories in the tools map so they’re only instantiated when needed.

-      // Create tools using the refactored tool factories
-      const searchTool = createSearchSessionTool(userId);
-      const editEnhancedNoteTool = createEditEnhancedNoteTool({
-        sessionId,
-        sessions,
-        selectionData,
-      });

And below:

         tools: {
           ...(type === "HyprLocal" && { update_progress: tool({ inputSchema: z.any() }) }),
-          ...(shouldUseTools && {
-            ...newMcpTools,
-            search_sessions_multi_keywords: searchTool,
-            ...hyprMcpTools,
-            // Add the edit tool when there's selection data
-            ...(selectionData && { edit_enhanced_note: editEnhancedNoteTool }),
-          }),
+          ...(shouldUseTools && {
+            ...newMcpTools,
+            search_sessions_multi_keywords: createSearchSessionTool(userId),
+            ...hyprMcpTools,
+            ...(selectionData && {
+              edit_enhanced_note: createEditEnhancedNoteTool({ sessionId, sessions, selectionData }),
+            }),
+          }),
         },

574-575: Nit: don’t pass explicit undefined.

Call with three args; avoid the extra undefined.

-    await processUserMessage(prompt, "chat_quickaction_sent", undefined, undefined);
+    await processUserMessage(prompt, "chat_quickaction_sent");
apps/desktop/src/components/right-panel/utils/tools/edit_enhanced_note.ts (2)

107-114: Avoid innerHTML for controls; build DOM nodes.

Static strings still trigger security scanners; construct elements safely.

-          undoBtn.innerHTML = `
-            <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
-              <path d="M3 7v6h6"/><path d="M21 17a9 9 0 00-9-9 9 9 0 00-6 2.3L3 13"/>
-            </svg>
-            <span style="margin-left: 4px; font-size: 11px;">Undo</span>
-          `;
+          {
+            const svg = document.createElementNS("http://www.w3.org/2000/svg","svg");
+            svg.setAttribute("width","12"); svg.setAttribute("height","12");
+            svg.setAttribute("viewBox","0 0 24 24"); svg.setAttribute("fill","none");
+            svg.setAttribute("stroke","currentColor"); svg.setAttribute("stroke-width","2");
+            const p1 = document.createElementNS(svg.namespaceURI,"path");
+            p1.setAttribute("d","M3 7v6h6");
+            const p2 = document.createElementNS(svg.namespaceURI,"path");
+            p2.setAttribute("d","M21 17a9 9 0 00-9-9 9 9 0 00-6 2.3L3 13");
+            const span = document.createElement("span");
+            span.style.marginLeft = "4px"; span.style.fontSize = "11px"; span.textContent = "Undo";
+            svg.appendChild(p1); svg.appendChild(p2); undoBtn.appendChild(svg); undoBtn.appendChild(span);
+          }

Similarly for the Accept button (build SVG + span via DOM).

Also applies to: 155-161


37-43: Guideline violation: remove try/catch and console.error (rule: “No error handling”).

This file adds error handling that conflicts with the project rules for JS/TS files.

If the rule is intentional, remove the try/catch and logging here or centralize handling upstream. I can provide a focused diff once you confirm policy application in tool executors.

Also applies to: 297-303

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between c65409b and f3c5ea5.

📒 Files selected for processing (13)
  • apps/desktop/src/components/editor-area/index.tsx (3 hunks)
  • apps/desktop/src/components/editor-area/text-selection-popover.tsx (4 hunks)
  • apps/desktop/src/components/right-panel/components/chat/chat-input.tsx (8 hunks)
  • apps/desktop/src/components/right-panel/components/chat/message-content.tsx (0 hunks)
  • apps/desktop/src/components/right-panel/hooks/useChatLogic.ts (6 hunks)
  • apps/desktop/src/components/right-panel/utils/chat-utils.ts (3 hunks)
  • apps/desktop/src/components/right-panel/utils/tools/edit_enhanced_note.ts (1 hunks)
  • apps/desktop/src/contexts/ai-edit.tsx (1 hunks)
  • apps/desktop/src/contexts/right-panel.tsx (5 hunks)
  • apps/desktop/src/shared/editor-ref.ts (1 hunks)
  • crates/template/assets/chat.system.jinja (1 hunks)
  • packages/tiptap/src/shared/ai-highlight.ts (1 hunks)
  • packages/tiptap/src/styles/nodes/ai-highlight.css (1 hunks)
💤 Files with no reviewable changes (1)
  • apps/desktop/src/components/right-panel/components/chat/message-content.tsx
🚧 Files skipped from review as they are similar to previous changes (8)
  • apps/desktop/src/components/editor-area/index.tsx
  • apps/desktop/src/contexts/ai-edit.tsx
  • apps/desktop/src/shared/editor-ref.ts
  • crates/template/assets/chat.system.jinja
  • packages/tiptap/src/styles/nodes/ai-highlight.css
  • packages/tiptap/src/shared/ai-highlight.ts
  • apps/desktop/src/contexts/right-panel.tsx
  • apps/desktop/src/components/right-panel/utils/chat-utils.ts
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{js,ts,tsx,rs}

⚙️ CodeRabbit configuration file

**/*.{js,ts,tsx,rs}: 1. No error handling.
2. No unused imports, variables, or functions.
3. For comments, keep it minimal. It should be about "Why", not "What".

Files:

  • apps/desktop/src/components/right-panel/components/chat/chat-input.tsx
  • apps/desktop/src/components/editor-area/text-selection-popover.tsx
  • apps/desktop/src/components/right-panel/hooks/useChatLogic.ts
  • apps/desktop/src/components/right-panel/utils/tools/edit_enhanced_note.ts
🧬 Code graph analysis (1)
apps/desktop/src/components/right-panel/utils/tools/edit_enhanced_note.ts (2)
apps/desktop/src/contexts/right-panel.tsx (1)
  • SelectionData (10-16)
apps/desktop/src/shared/editor-ref.ts (1)
  • globalEditorRef (8-10)
🪛 Biome (2.1.2)
apps/desktop/src/components/editor-area/text-selection-popover.tsx

[error] 32-32: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)

🪛 ast-grep (0.38.6)
apps/desktop/src/components/right-panel/utils/tools/edit_enhanced_note.ts

[warning] 107-112: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: undoBtn.innerHTML = <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M3 7v6h6"/><path d="M21 17a9 9 0 00-9-9 9 9 0 00-6 2.3L3 13"/> </svg> <span style="margin-left: 4px; font-size: 11px;">Undo</span>
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://owasp.org/www-community/xss-filter-evasion-cheatsheet
- https://cwe.mitre.org/data/definitions/79.html

(dom-content-modification)


[warning] 155-160: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: acceptBtn.innerHTML = <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2"> <polyline points="20 6 9 17 4 12"/> </svg> <span style="margin-left: 4px; font-size: 11px;">Accept</span>
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://owasp.org/www-community/xss-filter-evasion-cheatsheet
- https://cwe.mitre.org/data/definitions/79.html

(dom-content-modification)


[warning] 107-112: Direct HTML content assignment detected. Modifying innerHTML, outerHTML, or using document.write with unsanitized content can lead to XSS vulnerabilities. Use secure alternatives like textContent or sanitize HTML with libraries like DOMPurify.
Context: undoBtn.innerHTML = <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M3 7v6h6"/><path d="M21 17a9 9 0 00-9-9 9 9 0 00-6 2.3L3 13"/> </svg> <span style="margin-left: 4px; font-size: 11px;">Undo</span>
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://www.dhairyashah.dev/posts/why-innerhtml-is-a-bad-idea-and-how-to-avoid-it/
- https://cwe.mitre.org/data/definitions/79.html

(unsafe-html-content-assignment)


[warning] 155-160: Direct HTML content assignment detected. Modifying innerHTML, outerHTML, or using document.write with unsanitized content can lead to XSS vulnerabilities. Use secure alternatives like textContent or sanitize HTML with libraries like DOMPurify.
Context: acceptBtn.innerHTML = <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2"> <polyline points="20 6 9 17 4 12"/> </svg> <span style="margin-left: 4px; font-size: 11px;">Accept</span>
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://www.dhairyashah.dev/posts/why-innerhtml-is-a-bad-idea-and-how-to-avoid-it/
- https://cwe.mitre.org/data/definitions/79.html

(unsafe-html-content-assignment)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: ci (macos, macos-latest)
  • GitHub Check: ci (windows, windows-latest)
🔇 Additional comments (4)
apps/desktop/src/components/editor-area/text-selection-popover.tsx (1)

235-251: UI tweaks look good.

Button sizing and divider adjustments are consistent with the surrounding UI.

apps/desktop/src/components/right-panel/hooks/useChatLogic.ts (2)

301-313: Good: selection context is threaded into history.

Passing selectionData to prepareMessageHistory aligns with the new selection-aware flow.


566-571: Good: submit signature extended to carry selection.

This keeps the chat-input and logic in sync.

apps/desktop/src/components/right-panel/components/chat/chat-input.tsx (1)

362-401: CSS override set looks good; scoped to selection-ref exclusions.

The styling keeps the chat editor lean while preserving selection refs.

Comment on lines 167 to 176
if (!selection) {
return;
}

// Get TipTap/ProseMirror positions (much more accurate)
const tipTapPositions = getTipTapPositions();
if (!tipTapPositions) {
console.error("Could not get TipTap positions");
return;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Send both HTML and plain text; gate debug logs; use the new helper.

Use getTipTapSelection(), include html alongside text, and avoid console noise in production.

-  const handleAskAIClick = () => {
+  const handleAskAIClick = () => {
     if (!selection) {
       return;
     }
 
-    // Get TipTap/ProseMirror positions (much more accurate)
-    const tipTapPositions = getTipTapPositions();
-    if (!tipTapPositions) {
-      console.error("Could not get TipTap positions");
-      return;
-    }
+    const tipTap = getTipTapSelection();
+    if (!tipTap) return;
 
-    // Verify DOM selection matches TipTap selection
-    if (selection.text.trim() !== tipTapPositions.text.trim()) {
-      console.warn("DOM selection doesn't match TipTap selection:");
-      console.warn("DOM:", selection.text);
-      console.warn("TipTap:", tipTapPositions.text);
-    }
+    if (process.env.NODE_ENV !== "production") {
+      const plain = tipTap.text?.trim() ?? "";
+      if (selection.text.trim() !== plain) {
+        // Dev-only mismatch signal
+        console.warn("Selection mismatch between DOM and TipTap slice.");
+      }
+    }
 
-    const selectionData = {
-      text: tipTapPositions.text, // Use TipTap's text (more reliable)
-      startOffset: tipTapPositions.from, // ProseMirror position
-      endOffset: tipTapPositions.to, // ProseMirror position
-      sessionId,
-      timestamp: Date.now(),
-    };
+    const selectionData = {
+      text: tipTap.text,          // plain text for prompts
+      html: tipTap.html,          // HTML for precise edits/highlights
+      startOffset: tipTap.from,   // ProseMirror pos
+      endOffset: tipTap.to,       // ProseMirror pos
+      sessionId,
+      timestamp: Date.now(),
+    };
 
     // Send selection to chat
     sendSelectionToChat(selectionData);

Also applies to: 178-183, 185-195

🤖 Prompt for AI Agents
In apps/desktop/src/components/editor-area/text-selection-popover.tsx around
lines 167-176 (and similarly for 178-183 and 185-195), the code uses
getTipTapPositions(), logs directly to console, and only sends plain text;
replace calls to getTipTapPositions() with the new getTipTapSelection() helper,
include both html and text properties when sending the selection, and wrap debug
logs behind a development check or use the app logger instead of console.error
to avoid production noise; ensure the function returns early if
getTipTapSelection() yields no data and propagate both html and text to
downstream callers.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

Caution

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

⚠️ Outside diff range comments (1)
apps/desktop/src/components/right-panel/components/chat/message-content.tsx (1)

75-91: Safe fallback on markdown→HTML failure

On conversion failure you set displayHtml to raw content, which the Renderer will treat as HTML. Escape it or sanitize to avoid injection.

       try {
         let html = await miscCommands.opinionatedMdToHtml(content);
 
         html = html
           .replace(/<p>\s*<\/p>/g, "")
           .replace(/<p>\u00A0<\/p>/g, "")
           .replace(/<p>&nbsp;<\/p>/g, "")
           .replace(/<p>\s+<\/p>/g, "")
           .replace(/<p> <\/p>/g, "")
           .trim();
 
-        setDisplayHtml(html);
+        setDisplayHtml(DOMPurify.sanitize(html));
       } catch (error) {
-        console.error("Failed to convert markdown:", error);
-        setDisplayHtml(content);
+        // Fallback: render as plain text safely
+        const escaped = content
+          .replaceAll("&", "&amp;")
+          .replaceAll("<", "&lt;")
+          .replaceAll(">", "&gt;");
+        setDisplayHtml(`<pre style="white-space:pre-wrap;margin:0">${escaped}</pre>`);
       }
♻️ Duplicate comments (5)
crates/template/assets/chat.system.jinja (1)

163-167: Harden edit_enhanced_note contract; forbid raw-HTML replies; require selectionData/session gating.

Tighten the tool guidance so the model only edits via the tool when valid selectionData is present, and never emits raw HTML in normal chat. Also clarify the date-range tool’s contract.

- - edit_enhanced_note: Only call when you are given a certain Prosemirror range of text that the user wants to edit. Replace that certain range with a new text that is written in a way
-   that the user wants.
+ - edit_enhanced_note: Call only when selectionData is present (must include startOffset, endOffset, sessionId) and the active editor is available. Validate selectionData/session before editing. Provide an HTML fragment that replaces exactly that ProseMirror range, preserving nodes/marks and not altering content outside the range. Do NOT output HTML in normal chat responses—perform edits exclusively via this tool.
 
- - search_sessions_date_range: Search for sessions (meeting notes) within a specific date range. Returns sessions with their enhanced memo content from the specified time period.
+ - search_sessions_date_range: Search for sessions (meeting notes) within a specific date range. Input: ISO 8601 startDate and endDate with timezone (e.g., 2024-01-01T00:00:00Z), and an optional limit (default 50). Returns sessions in that period with their memo content; summarize concisely (no raw dumps in ```).
apps/desktop/src/components/editor-area/text-selection-popover.tsx (2)

29-43: Fix conditional hook usage (Rules of Hooks violation).

useRightPanel() is invoked inside an IIFE/try-catch. Call hooks at top level.

-  // Safe hook usage with fallback
-  const rightPanel = (() => {
-    try {
-      return useRightPanel();
-    } catch {
-      return {
-        sendSelectionToChat: () => {
-          console.warn("RightPanel not available - selection ignored");
-        },
-      };
-    }
-  })();
-
-  const { sendSelectionToChat } = rightPanel;
+  // RightPanel provider is required; call hook at top level.
+  const { sendSelectionToChat } = useRightPanel();

173-204: Use the new helper; send both text and html; dev-only mismatch logs.

Prevents noisy consoles and ambiguous payloads.

   const handleAskAIClick = () => {
     if (!selection) {
       return;
     }
 
-    // Get TipTap/ProseMirror positions (much more accurate)
-    const tipTapPositions = getTipTapPositions();
-    if (!tipTapPositions) {
-      console.error("Could not get TipTap positions");
-      return;
-    }
+    const tipTap = getTipTapSelection();
+    if (!tipTap) return;
 
-    // Verify DOM selection matches TipTap selection
-    if (selection.text.trim() !== tipTapPositions.text.trim()) {
-      console.warn("DOM selection doesn't match TipTap selection:");
-      console.warn("DOM:", selection.text);
-      console.warn("TipTap:", tipTapPositions.text);
-    }
+    if (process.env.NODE_ENV !== "production") {
+      if (selection.text.trim() !== tipTap.text.trim()) {
+        console.warn("Selection mismatch between DOM and TipTap slice.");
+      }
+    }
 
     const selectionData = {
-      text: tipTapPositions.text, // Use TipTap's text (more reliable)
-      startOffset: tipTapPositions.from, // ProseMirror position
-      endOffset: tipTapPositions.to, // ProseMirror position
+      text: tipTap.text,
+      html: tipTap.html,
+      startOffset: tipTap.from,
+      endOffset: tipTap.to,
       sessionId,
       timestamp: Date.now(),
     };
 
-    // Send selection to chat
     sendSelectionToChat(selectionData);
 
     setSelection(null);
   };
apps/desktop/src/components/right-panel/hooks/useChatLogic.ts (1)

460-461: Remove debug log of tool-result chunk (PII risk + noise)

This duplicates a prior review; please drop it.

-          console.log("tool result: ", chunk);
+          // removed debug log
apps/desktop/src/components/right-panel/components/chat/chat-input.tsx (1)

227-280: Build the mention/reference node programmatically (avoid HTML string concat)

Current HTML string assembly is vulnerable to injection via selectionRef inner text. Use TipTap JSON with escaped text.

-        const referenceText =
-          `<a class="mention selection-ref" data-mention="true" data-id="selection-${pendingSelection.startOffset}-${pendingSelection.endOffset}" data-type="selection" data-label="${escapedSelectionRef}" contenteditable="false">${selectionRef}</a> `;
-
-        editorRef.current.editor.commands.setContent(referenceText);
-        editorRef.current.editor.commands.focus("end");
+        const escapeText = (s: string) =>
+          s.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
+        const mentionNode = {
+          type: "mention",
+          attrs: {
+            class: "mention selection-ref",
+            "data-mention": "true",
+            id: `selection-${pendingSelection.startOffset}-${pendingSelection.endOffset}`,
+            type: "selection",
+            label: escapedSelectionRef,
+            contenteditable: "false",
+          },
+          content: [{ type: "text", text: escapeText(selectionRef) }],
+        };
+        editorRef.current.editor.commands.clearContent();
+        editorRef.current.editor.commands.insertContent(mentionNode);
+        editorRef.current.editor.commands.focus("end");

If your mention extension requires specific attrs (id/type/label), align key names accordingly. The point is to avoid innerHTML.

🧹 Nitpick comments (12)
packages/tiptap/src/renderer/mention-readonly.ts (2)

54-90: Trim parsed labels to prevent stray whitespace.

Ensure consistent rendering when labels come from textContent.

Apply:

-          const label = dom.getAttribute("data-label") || dom.textContent || "";
+          const label = ((dom.getAttribute("data-label") ?? dom.textContent) ?? "").trim();

Repeat the same change in the other two getAttrs blocks.


115-118: Mirror the empty-label guard in renderText.

Apply:

-    const label = node.attrs.label || "";
-    return label.startsWith("@") ? label : `@${label}`;
+    const label = (node.attrs.label || "").trim();
+    return label ? (label.startsWith("@") ? label : `@${label}`) : "";
apps/desktop/src/components/right-panel/utils/tools/search_session_date_range.ts (2)

20-31: Pre-validate chronological order at schema or coerce dates once.

Avoid double parsing and centralize validation.

-    execute: async ({ startDate, endDate, limit }) => {
-      // Validate date format
-      const start = new Date(startDate);
-      const end = new Date(endDate);
+    execute: async ({ startDate, endDate, limit }) => {
+      const start = new Date(startDate);
+      const end = new Date(endDate);
       if (isNaN(start.getTime()) || isNaN(end.getTime())) {
         throw new Error("Invalid date format. Please use ISO 8601 format (e.g., '2024-01-01T00:00:00Z')");
       }
       if (start >= end) {
         throw new Error("Start date must be before end date");
       }

(Optional follow-up: lift the chronological check into a zod .refine to fail earlier.)


65-79: Avoid repeated O(n) counts; compute once.

Minor perf cleanup for larger result sets.

-      return {
-        sessions: nonEmptySessions,
-        metadata: {
-          total_found: sessions.length,
-          non_empty_sessions: nonEmptySessions.length,
-          date_range: {
-            start: startDate,
-            end: endDate,
-          },
-          query_limit: limit,
-          sessions_with_enhanced_content: nonEmptySessions.filter(s => s.has_enhanced_content).length,
-          sessions_with_raw_content: nonEmptySessions.filter(s => s.has_raw_content).length,
-          sessions_with_pre_meeting_content: nonEmptySessions.filter(s => s.has_pre_meeting_content).length,
-        },
-      };
+      const counts = nonEmptySessions.reduce(
+        (acc, s) => {
+          if (s.has_enhanced_content) acc.enhanced++;
+          if (s.has_raw_content) acc.raw++;
+          if (s.has_pre_meeting_content) acc.pre++;
+          return acc;
+        },
+        { enhanced: 0, raw: 0, pre: 0 },
+      );
+      return {
+        sessions: nonEmptySessions,
+        metadata: {
+          total_found: sessions.length,
+          non_empty_sessions: nonEmptySessions.length,
+          date_range: { start: startDate, end: endDate },
+          query_limit: limit,
+          sessions_with_enhanced_content: counts.enhanced,
+          sessions_with_raw_content: counts.raw,
+          sessions_with_pre_meeting_content: counts.pre,
+        },
+      };
apps/desktop/src/components/editor-area/text-selection-popover.tsx (1)

13-16: Remove unused onAskAI prop.

Prop is not used; keep API surface minimal.

 interface TextSelectionPopoverProps {
   isEnhancedNote: boolean;
   onAnnotate: (selectedText: string, selectedRect: DOMRect) => void;
-  onAskAI?: (selectedText: string) => void;
   isAnnotationBoxOpen: boolean;
   sessionId: string;
   editorRef: React.RefObject<{ editor: any }>;
 }
@@
-export function TextSelectionPopover(
-  { isEnhancedNote, onAnnotate, onAskAI, isAnnotationBoxOpen, sessionId, editorRef }: TextSelectionPopoverProps,
-) {
+export function TextSelectionPopover(
+  { isEnhancedNote, onAnnotate, isAnnotationBoxOpen, sessionId, editorRef }: TextSelectionPopoverProps,
+) {

Also applies to: 24-25

apps/desktop/src/components/right-panel/hooks/useChatQueries.ts (2)

82-102: Don’t log parse errors in production; avoid any.

Gate logs or use app logger, and avoid any.

-      return dbMessages.map(msg => {
+      return dbMessages.map(msg => {
         // Parse tool_details for all messages
-        let parsedToolDetails: any = undefined;
+        let parsedToolDetails: unknown = undefined;
         if (msg.tool_details) {
           try {
             parsedToolDetails = JSON.parse(msg.tool_details);
           } catch (error) {
-            console.error("Failed to parse tool_details:", msg.id, error);
+            if (process.env.NODE_ENV !== "production") {
+              console.warn("Failed to parse tool_details:", msg.id, error);
+            }
           }
         }
 
         return {

119-120: Avoid noisy sync logs while generating.

Gate behind dev check.

-        console.log("Skipping DB sync - currently generating");
+        if (process.env.NODE_ENV !== "production") {
+          console.log("Skipping DB sync - currently generating");
+        }
apps/desktop/src/components/right-panel/components/chat/message-content.tsx (2)

94-98: Avoid unnecessary work when both inputs are empty

Guard also against both content/htmlContent being empty strings to prevent spawning processContent.

-  }, [content, htmlContent]);
+  }, [content, htmlContent]);

Optional: early-return if neither has length after trim (micro-opt; no behavior change).


171-190: CSS mention styles: scope to component to avoid leakage

The selectors are broad and use !important everywhere. Consider scoping with a unique wrapper class to reduce style collisions.

apps/desktop/src/components/right-panel/components/chat/chat-input.tsx (3)

281-309: Keydown handler: narrow mention dropdown detection

Global querySelector can pick up unrelated elements; if possible, scope to editor’s root or a known container.

-              const mentionDropdown = document.querySelector(".mention-container");
+              const root = editor?.view.dom?.ownerDocument || document;
+              const mentionDropdown = root.querySelector(".mention-container");

365-447: CSS scope nit

The heavy use of !important under .chat-editor could leak. Consider a wrapper class (e.g., .chat-input-root) to tighten specificity instead of !important.


219-220: Comments should focus on “Why,” not “What”

Several comments narrate the code (“Extract HTML content…”, “Clear the selection…”). Trim or rephrase to rationale per coding guideline.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between f3c5ea5 and 1a3bd57.

📒 Files selected for processing (10)
  • apps/desktop/src/components/editor-area/text-selection-popover.tsx (5 hunks)
  • apps/desktop/src/components/right-panel/components/chat/chat-input.tsx (8 hunks)
  • apps/desktop/src/components/right-panel/components/chat/message-content.tsx (4 hunks)
  • apps/desktop/src/components/right-panel/hooks/useChatLogic.ts (9 hunks)
  • apps/desktop/src/components/right-panel/hooks/useChatQueries.ts (1 hunks)
  • apps/desktop/src/components/right-panel/utils/tools/search_session_date_range.ts (1 hunks)
  • apps/desktop/src/components/right-panel/views/chat-view.tsx (1 hunks)
  • crates/template/assets/chat.system.jinja (1 hunks)
  • packages/tiptap/src/renderer/index.tsx (2 hunks)
  • packages/tiptap/src/renderer/mention-readonly.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{js,ts,tsx,rs}

⚙️ CodeRabbit configuration file

**/*.{js,ts,tsx,rs}: 1. No error handling.
2. No unused imports, variables, or functions.
3. For comments, keep it minimal. It should be about "Why", not "What".

Files:

  • apps/desktop/src/components/right-panel/utils/tools/search_session_date_range.ts
  • apps/desktop/src/components/right-panel/hooks/useChatQueries.ts
  • packages/tiptap/src/renderer/index.tsx
  • packages/tiptap/src/renderer/mention-readonly.ts
  • apps/desktop/src/components/editor-area/text-selection-popover.tsx
  • apps/desktop/src/components/right-panel/views/chat-view.tsx
  • apps/desktop/src/components/right-panel/components/chat/chat-input.tsx
  • apps/desktop/src/components/right-panel/components/chat/message-content.tsx
  • apps/desktop/src/components/right-panel/hooks/useChatLogic.ts
🧬 Code graph analysis (5)
apps/desktop/src/components/right-panel/hooks/useChatQueries.ts (1)
apps/desktop/src/components/right-panel/utils/markdown-parser.ts (1)
  • parseMarkdownBlocks (3-62)
packages/tiptap/src/renderer/index.tsx (1)
packages/tiptap/src/renderer/mention-readonly.ts (1)
  • mentionReadonly (8-119)
apps/desktop/src/components/editor-area/text-selection-popover.tsx (3)
apps/desktop/src/contexts/hypr.tsx (1)
  • useHypr (63-69)
apps/desktop/src/contexts/right-panel.tsx (1)
  • useRightPanel (230-236)
packages/ui/src/components/ui/button.tsx (1)
  • Button (37-89)
apps/desktop/src/components/right-panel/components/chat/chat-input.tsx (1)
apps/desktop/src/contexts/right-panel.tsx (2)
  • SelectionData (10-16)
  • useRightPanel (230-236)
apps/desktop/src/components/right-panel/hooks/useChatLogic.ts (5)
apps/desktop/src/contexts/right-panel.tsx (1)
  • SelectionData (10-16)
apps/desktop/src/components/right-panel/components/chat/types.ts (1)
  • Message (7-15)
apps/desktop/src/components/right-panel/utils/tools/search_session_multi_keywords.ts (1)
  • createSearchSessionTool (5-59)
apps/desktop/src/components/right-panel/utils/tools/edit_enhanced_note.ts (1)
  • createEditEnhancedNoteTool (12-306)
apps/desktop/src/components/right-panel/utils/tools/search_session_date_range.ts (1)
  • createSearchSessionDateRangeTool (5-82)
🪛 Biome (2.1.2)
apps/desktop/src/components/editor-area/text-selection-popover.tsx

[error] 32-32: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: ci (macos, macos-latest)
  • GitHub Check: ci (windows, windows-latest)
🔇 Additional comments (14)
packages/tiptap/src/renderer/mention-readonly.ts (1)

21-51: Attributes setup looks good.

packages/tiptap/src/renderer/index.tsx (1)

2-2: CSS import for mentions is appropriate.

apps/desktop/src/components/editor-area/text-selection-popover.tsx (1)

70-74: Good: scope selection to editor DOM.

Ensures we only act on selections within the editor.

Also applies to: 76-76

apps/desktop/src/components/right-panel/views/chat-view.tsx (1)

183-184: Confirm tri-arg onSubmit contract end-to-end.

Ensure ChatInput’s prop type and useChatLogic.handleSubmit both accept (mentionedContent, selectionData, htmlContent) in this order.

apps/desktop/src/components/right-panel/components/chat/message-content.tsx (2)

336-346: Pass htmlContent only when sanitized

You’re passing htmlContent through to MarkdownText. Ensure it’s sanitized upstream (see earlier comment) or pass a sanitizedHtml variable instead.

Would you like me to wire a sanitizedHtml into MessageContent to make the contract explicit?


194-195: Verify Renderer input type
Renderer is imported from @hypr/tiptap/renderer; confirm whether its initialContent prop expects sanitized HTML or TipTap JSON. If it requires JSON, convert displayHtml with the editor’s parser; otherwise keep the HTML sanitization.

apps/desktop/src/components/right-panel/hooks/useChatLogic.ts (5)

297-305: Tool factory wiring LGTM

Factory extraction clarifies responsibilities and enables conditional tool exposure.


320-321: Selection context propagation LGTM

Passing selectionData into prepareMessageHistory is necessary for tool prompting.


325-333: Tool map composition: order and gating look correct

Inclusion of edit_enhanced_note behind selectionData is correct. No duplicate keys detected.


578-584: No action needed: ChatInput callers already updated
The only <ChatInput> instance in chat-view.tsx passes (mentionedContent, selectionData, htmlContent) to handleSubmit in the correct order, so all callers reflect the new signature.


121-127: Call sites updated correctly
Verified that all processUserMessage calls include the new selectionData and htmlContent parameters in the proper order (or fall back to defaults), and all handleSubmit invocations either pass all three args in order or omit them to use undefined defaults.

apps/desktop/src/components/right-panel/components/chat/chat-input.tsx (3)

144-145: Dedup key is good

processedSelectionRef prevents duplicate insertions across rerenders. Nice.


47-48: Context usage LGTM

Using pendingSelection/clearPendingSelection from RightPanel context aligns with the new flow.


191-201: Event metric handling LGTM

Mention extraction and analytics event cadence unaffected by this change.

Comment on lines +131 to +171
// Helper to get TipTap/ProseMirror positions from DOM selection
const getTipTapPositions = () => {
const editor = editorRef.current?.editor;
if (!editor) {
console.warn("No TipTap editor available");
return null;
}

// Get current TipTap selection positions
const { from, to } = editor.state.selection;

// CLEAN HTML APPROACH: Extract selected content as HTML directly
let selectedHtml = "";
try {
// Get the selected DOM range
const selection = window.getSelection();
if (selection && selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
const fragment = range.cloneContents();

// Create a temporary div to get the HTML
const tempDiv = document.createElement("div");
tempDiv.appendChild(fragment);
selectedHtml = tempDiv.innerHTML;
}

// Fallback: if no DOM selection, use plain text
if (!selectedHtml) {
selectedHtml = editor.state.doc.textBetween(from, to);
}
} catch (error) {
console.warn("Could not extract HTML, falling back to plain text:", error);
selectedHtml = editor.state.doc.textBetween(from, to);
}

return {
from,
to,
text: selectedHtml, // Now contains HTML instead of plain text
};
};
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Return both plain text and HTML; gate dev logs; clear naming.

Current helper names the HTML as “text” and logs to console unconditionally. Provide text and html separately, and avoid prod logging.

-  // Helper to get TipTap/ProseMirror positions from DOM selection
-  const getTipTapPositions = () => {
+  // Helper: derive ProseMirror range + text/html slice from current selection
+  const getTipTapSelection = () => {
     const editor = editorRef.current?.editor;
     if (!editor) {
-      console.warn("No TipTap editor available");
+      if (process.env.NODE_ENV !== "production") {
+        console.warn("No TipTap editor available");
+      }
       return null;
     }
 
-    // Get current TipTap selection positions
     const { from, to } = editor.state.selection;
 
-    // CLEAN HTML APPROACH: Extract selected content as HTML directly
-    let selectedHtml = "";
+    let html = "";
+    const text = editor.state.doc.textBetween(from, to);
     try {
-      // Get the selected DOM range
       const selection = window.getSelection();
       if (selection && selection.rangeCount > 0) {
         const range = selection.getRangeAt(0);
         const fragment = range.cloneContents();
 
-        // Create a temporary div to get the HTML
         const tempDiv = document.createElement("div");
         tempDiv.appendChild(fragment);
-        selectedHtml = tempDiv.innerHTML;
+        html = tempDiv.innerHTML;
       }
 
-      // Fallback: if no DOM selection, use plain text
-      if (!selectedHtml) {
-        selectedHtml = editor.state.doc.textBetween(from, to);
-      }
+      if (!html) html = text;
     } catch (error) {
-      console.warn("Could not extract HTML, falling back to plain text:", error);
-      selectedHtml = editor.state.doc.textBetween(from, to);
+      if (process.env.NODE_ENV !== "production") {
+        console.warn("HTML extract failed; falling back to text.", error);
+      }
+      html = text;
     }
 
     return {
       from,
       to,
-      text: selectedHtml, // Now contains HTML instead of plain text
+      text,
+      html,
     };
   };
📝 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
// Helper to get TipTap/ProseMirror positions from DOM selection
const getTipTapPositions = () => {
const editor = editorRef.current?.editor;
if (!editor) {
console.warn("No TipTap editor available");
return null;
}
// Get current TipTap selection positions
const { from, to } = editor.state.selection;
// CLEAN HTML APPROACH: Extract selected content as HTML directly
let selectedHtml = "";
try {
// Get the selected DOM range
const selection = window.getSelection();
if (selection && selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
const fragment = range.cloneContents();
// Create a temporary div to get the HTML
const tempDiv = document.createElement("div");
tempDiv.appendChild(fragment);
selectedHtml = tempDiv.innerHTML;
}
// Fallback: if no DOM selection, use plain text
if (!selectedHtml) {
selectedHtml = editor.state.doc.textBetween(from, to);
}
} catch (error) {
console.warn("Could not extract HTML, falling back to plain text:", error);
selectedHtml = editor.state.doc.textBetween(from, to);
}
return {
from,
to,
text: selectedHtml, // Now contains HTML instead of plain text
};
};
// Helper: derive ProseMirror range + text/html slice from current selection
const getTipTapSelection = () => {
const editor = editorRef.current?.editor;
if (!editor) {
if (process.env.NODE_ENV !== "production") {
console.warn("No TipTap editor available");
}
return null;
}
const { from, to } = editor.state.selection;
let html = "";
const text = editor.state.doc.textBetween(from, to);
try {
const selection = window.getSelection();
if (selection && selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
const fragment = range.cloneContents();
const tempDiv = document.createElement("div");
tempDiv.appendChild(fragment);
html = tempDiv.innerHTML;
}
if (!html) html = text;
} catch (error) {
if (process.env.NODE_ENV !== "production") {
console.warn("HTML extract failed; falling back to text.", error);
}
html = text;
}
return {
from,
to,
text,
html,
};
};
🤖 Prompt for AI Agents
In apps/desktop/src/components/editor-area/text-selection-popover.tsx around
lines 131 to 171, the helper currently stores HTML in a variable named
"selectedHtml" but returns it as "text" and unconditionally uses console.warn;
update the function to produce and return two explicit fields (plainText and
html), rename variables for clarity (e.g., selectedHtml and selectedText),
ensure plainText is derived via editor.state.doc.textBetween(from, to) as
fallback or alongside HTML extraction, and gate any warning logs behind a
dev-only check (e.g., if (process.env.NODE_ENV !== 'production') or use the app
logger) so production builds do not emit console warnings. Ensure the returned
object is { from, to, plainText, html } and adjust call sites accordingly.

Comment on lines +9 to +19
name: "mention",

group: "inline",

inline: true,

selectable: false,

atom: true,

addAttributes() {
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Avoid potential extension name collision ("mention").

If any extension named "mention" is already included via shared.extensions, this will collide. Ensure the Renderer excludes any existing "mention" before adding this one.

Run to confirm duplicates:


🏁 Script executed:

#!/bin/bash
# Find any extensions declaring name: "mention"
rg -nP -C2 'Node\.create\(\s*{\s*name:\s*["\']mention["\']' --type=ts
# Find imports of @tiptap/extension-mention
rg -nP '@tiptap/extension-mention' --type=ts

Length of output: 116


🏁 Script executed:

#!/bin/bash
# search for other occurrences of name: "mention" in the codebase
rg -nP "name\\s*:\\s*['\\\"]mention['\\\"]" --type=ts -C3
# search for usage/import of @tiptap/extension-mention
rg -nP "@tiptap/extension-mention" --type=ts -C3

Length of output: 1150


Exclude existing “mention” extensions before registering the readonly version
Filter out any extensions with name === "mention" (e.g. from @tiptap/extension-mention) in your renderer’s shared.extensions array before adding mentionReadonly to prevent duplicate‐name collisions.

🤖 Prompt for AI Agents
In packages/tiptap/src/renderer/mention-readonly.ts around lines 9 to 19, avoid
duplicate extension names by removing any existing extensions whose name ===
"mention" from shared.extensions before adding the readonly mention extension;
implement this by creating a filtered array (e.g. const filtered =
shared.extensions.filter(e => e.name !== 'mention')) and then register or spread
filtered plus mentionReadonly (or assign shared.extensions =
filtered.concat(mentionReadonly)) so the readonly extension is the only
"mention" entry.

Comment on lines +92 to +113
renderHTML({ node, HTMLAttributes }) {
const label = node.attrs.label || "";
const nodeText = node.textContent || label;
const classes = ["mention"];

// Add selection-ref class if this is a selection reference
if (label.includes("[") && label.includes("]")) {
classes.push("selection-ref");
}

return [
"span",
mergeAttributes(HTMLAttributes, {
class: classes.join(" "),
"data-mention": "true",
"data-id": node.attrs.id,
"data-type": node.attrs.type,
"data-label": node.attrs.label,
}),
nodeText.startsWith("@") ? nodeText : `@${nodeText}`,
];
},
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Don’t re-specify data-id/type/label here; rely on addAttributes to avoid null/undefined attributes.

Also guard against empty text to avoid emitting a lone "@".

Apply:

   renderHTML({ node, HTMLAttributes }) {
     const label = node.attrs.label || "";
     const nodeText = node.textContent || label;
     const classes = ["mention"];

     // Add selection-ref class if this is a selection reference
     if (label.includes("[") && label.includes("]")) {
       classes.push("selection-ref");
     }

-    return [
-      "span",
-      mergeAttributes(HTMLAttributes, {
-        class: classes.join(" "),
-        "data-mention": "true",
-        "data-id": node.attrs.id,
-        "data-type": node.attrs.type,
-        "data-label": node.attrs.label,
-      }),
-      nodeText.startsWith("@") ? nodeText : `@${nodeText}`,
-    ];
+    const displayText =
+      nodeText ? (nodeText.startsWith("@") ? nodeText : `@${nodeText}`) : "";
+    return [
+      "span",
+      mergeAttributes(HTMLAttributes, {
+        class: classes.join(" "),
+        "data-mention": "true",
+      }),
+      displayText,
+    ];
   },
📝 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
renderHTML({ node, HTMLAttributes }) {
const label = node.attrs.label || "";
const nodeText = node.textContent || label;
const classes = ["mention"];
// Add selection-ref class if this is a selection reference
if (label.includes("[") && label.includes("]")) {
classes.push("selection-ref");
}
return [
"span",
mergeAttributes(HTMLAttributes, {
class: classes.join(" "),
"data-mention": "true",
"data-id": node.attrs.id,
"data-type": node.attrs.type,
"data-label": node.attrs.label,
}),
nodeText.startsWith("@") ? nodeText : `@${nodeText}`,
];
},
renderHTML({ node, HTMLAttributes }) {
const label = node.attrs.label || "";
const nodeText = node.textContent || label;
const classes = ["mention"];
// Add selection-ref class if this is a selection reference
if (label.includes("[") && label.includes("]")) {
classes.push("selection-ref");
}
const displayText = nodeText
? (nodeText.startsWith("@") ? nodeText : `@${nodeText}`)
: "";
return [
"span",
mergeAttributes(HTMLAttributes, {
class: classes.join(" "),
"data-mention": "true",
}),
displayText,
];
},
🤖 Prompt for AI Agents
In packages/tiptap/src/renderer/mention-readonly.ts around lines 92 to 113, the
renderHTML implementation is re-specifying data-id/data-type/data-label (which
can produce null/undefined) instead of relying on addAttributes and it can emit
a lone "@" when node text is empty; remove the explicit
"data-id"/"data-type"/"data-label" keys from the merged attributes and only
merge HTMLAttributes (so addAttributes provides those values), and before
returning the displayed text compute nodeText safely (use label fallback) and if
nodeText is empty/whitespace return an empty string instead of "@", otherwise
prefix with "@" only when not already present.

@duckduckhero duckduckhero merged commit a511e18 into main Aug 28, 2025
7 checks passed
@coderabbitai coderabbitai bot mentioned this pull request Oct 6, 2025
@ComputelessComputer ComputelessComputer deleted the chat-fix-certain-part branch December 14, 2025 15:20
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.

1 participant