Skip to content

migrate to tiptap v3#1384

Merged
yujonglee merged 2 commits intomainfrom
tiptap-3
Aug 22, 2025
Merged

migrate to tiptap v3#1384
yujonglee merged 2 commits intomainfrom
tiptap-3

Conversation

@yujonglee
Copy link
Contributor

@yujonglee yujonglee commented Aug 22, 2025

Summary by cubic

Migrated the transcript editor to TipTap v3 and replaced the external search-and-replace plugin with an in-repo extension. This trims dependencies and aligns our editor with v3 APIs.

  • Dependencies

    • Upgraded TipTap packages to v3 and bumped related libs (floating-ui, Hono, AI SDK, Tauri plugins).
    • Removed @sereneinserenade/tiptap-search-and-replace; added internal implementation.
    • Updated lockfile and minor dependency versions across apps.
  • Refactors

    • Added a local SearchAndReplace extension and wired it into the transcript editor.
    • Adjusted Mention attributes with explicit typing for parse/render functions.
    • Updated speaker extension import path after folder reorg.
    • Minor locale message updates to match new component references.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 22, 2025

Caution

Review failed

The pull request is closed.

📝 Walkthrough

Walkthrough

Dependency version bumps across several packages (Tauri plugins, AI SDKs, hono, etc.). TipTap migrated from v2 to v3 and related dependencies were replaced. A local SearchAndReplace TipTap extension was added and wired into the transcript editor (replacing an external package and removing BubbleMenu). Minor TypeScript typing and import path tweaks; locale metadata and two listen-button placeholders updated.

Changes

Cohort / File(s) Summary
Hono & workspace deps
admin/server/package.json, apps/pro/package.json, Cargo.toml
Bump hono ^4.9.2 → ^4.9.4; workspace tauri-plugin-opener 2.4 → 2.5 and tauri-plugin-store 2.3 → 2.4; exa-js ^1.8.27 → ^1.9.1 (apps/pro).
Admin web deps
apps/admin/package.json
Bump @ai-sdk/openai ^2.0.16 → ^2.0.19, ai ^5.0.18 → ^5.0.21, posthog-js ^1.260.1 → ^1.260.2.
Desktop (Tauri) deps
apps/desktop/package.json
Bump Tauri plugin packages: @tauri-apps/plugin-dialog, plugin-fs, plugin-http, plugin-opener, plugin-os, plugin-store (minor/patch updates).
Utils deps
packages/utils/package.json
Bump @ai-sdk/openai-compatible ^1.0.10 → ^1.0.11, @tauri-apps/plugin-http ^2.5.1 → ^2.5.2, ai ^5.0.18 → ^5.0.21.
TipTap migration (deps)
packages/tiptap/package.json
Migrate TipTap v2 → v3: add/update many @tiptap/* ^3.2.1, remove tippy.js and @sereneinserenade/tiptap-search-and-replace, bump @floating-ui/dom ^1.7.3 → ^1.7.4.
TipTap local extension
packages/tiptap/src/transcript/extensions/search-and-replace.ts
Add new local SearchAndReplace extension: storage, commands (set terms, navigation, replace/replaceAll), decorations, ProseMirror plugin key, exports and module augmentation.
Transcript editor wiring & imports
packages/tiptap/src/transcript/index.tsx, packages/tiptap/src/transcript/extensions/speaker.ts
Replace external search-and-replace import with local implementation, remove BubbleMenu from extensions, adjust speaker import paths.
Mention typing tweaks
packages/tiptap/src/editor/mention.tsx
Strengthen TypeScript types for parseHTML/renderHTML attribute handlers (Element param and typed attribute shapes).
Node import adjustments
packages/tiptap/src/transcript/nodes.ts
Swap ProseNode import source from prosemirror-model@tiptap/pm/model.
Locales and listen-button placeholders
apps/desktop/src/locales/en/messages.po, apps/desktop/src/locales/ko/messages.po
Update source reference metadata for many entries; change two listen-button placeholders to disabled-state variants ("Wait..." / "Play again" or "Play video"); move/deprecate some entries.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant UI as Transcript UI
  participant Editor as TipTap Editor
  participant SNR as SearchAndReplace Extension
  participant Doc as Document

  User->>UI: Enter search / replace terms, toggle options
  UI->>Editor: commands.search.setSearchTerm / setReplaceTerm / setCaseSensitive
  Editor->>SNR: update storage (searchTerm, replaceTerm, caseSensitive)
  SNR->>Doc: scan document, compute match ranges
  SNR-->>Editor: provide decorations (matches + current highlight)

  User->>UI: Navigate results
  UI->>Editor: commands.search.nextSearchResult / previousSearchResult
  Editor->>SNR: update current index
  SNR-->>Editor: update decorations

  User->>UI: Replace / Replace All
  UI->>Editor: commands.search.replace / replaceAll
  Editor->>SNR: apply transactions to Doc
  SNR-->>Editor: recompute matches and decorations
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

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 b7ec215 and d41e62a.

📒 Files selected for processing (3)
  • Cargo.toml (1 hunks)
  • packages/tiptap/src/transcript/extensions/speaker.ts (1 hunks)
  • packages/tiptap/src/transcript/nodes.ts (1 hunks)
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch tiptap-3

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 @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai 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:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai 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 @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @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.

No issues found across 13 files

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

Caution

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

⚠️ Outside diff range comments (2)
packages/tiptap/src/transcript/extensions/speaker.ts (1)

6-12: Remove or relocate unimplemented serialize command augmentation

It looks like you’ve declared a transcriptCommands.serialize command in your type augmentation, but there is no corresponding implementation via addCommands in this extension (or anywhere else) – the only match is the interface declaration at line 9 in packages/tiptap/src/transcript/extensions/speaker.ts. This leaves a phantom API in your TS surface and will confuse users of the editor.

• File: packages/tiptap/src/transcript/extensions/speaker.ts
Lines 6–12: the declare module "@tiptap/core" block adding serialize: () => ReturnType;

Action items:

  • If you intend to provide a serialize command for transcripts, implement it in this extension’s addCommands() method.
  • Otherwise, remove (or move) this augmentation so that your commands interface accurately reflects only the commands you actually ship.
packages/tiptap/src/transcript/index.tsx (1)

64-81: Remove deprecated shouldRerenderOnTransaction from your useEditor call

In @tiptap/react v3, the React integration was rewritten to avoid per-transaction re-renders by default. The shouldRerenderOnTransaction option no longer exists and should be removed to prevent confusion. If you still need fine-grained control over updates, migrate to the new subscription APIs (useEditorState) or the EditorProvider / useCurrentEditor patterns.

• File: packages/tiptap/src/transcript/index.tsx
• Location: inside your useEditor({ … }) options (lines 64–81)

Suggested diff:

   const editor = useEditor({
     extensions,
     editable,
     immediatelyRender: true,
-    shouldRerenderOnTransaction: false,
     onUpdate: ({ editor }) => {
       if (onUpdate) {
         onUpdate(fromEditorToWords(editor.getJSON() as any));
       }
     },
     …
   });

If you’d like guidance on replicating your previous update-trigger behavior, let me know which part of your UI depended on per-transaction re-renders and I can share the exact useEditorState selector pattern.

🧹 Nitpick comments (17)
apps/pro/package.json (1)

16-18: Side note: zod version inconsistency across apps may increase bundle size.

This file uses zod@^3.25.76, while other apps use zod@^4.0.17. Consider aligning versions repo-wide later to avoid duplicate Zod runtimes.

I can open a follow-up chore PR to align Zod versions if desired.

apps/desktop/package.json (1)

61-69: Remove unused tippy.js dependency from apps/desktop

No references to tippy.js (or @tippyjs) were found anywhere in apps/desktop source files, indicating it’s no longer in use. You can safely remove it to trim your bundle.

• File to update: apps/desktop/package.json
• Remove the following line (approx. line 97):

-    "tippy.js": "^6.3.7",
apps/desktop/src/locales/en/messages.po (1)

321-326: Two variants of “Access granted/Access Granted” exist; align capitalization intentionally.

We now have both "Access granted" (sentence case) and "Access Granted" (title case). If both are intended (label vs heading), keep; else standardize to one form to avoid duplicate strings and translator churn.

Also applies to: 316-320

packages/tiptap/package.json (1)

24-43: Remove BubbleMenu dependency if unused to prevent dragging in tippy.js transitively.

The PR summary says BubbleMenu was removed, but "@tiptap/extension-bubble-menu" remains. If it’s no longer imported anywhere, drop it to reduce bundle size and avoid a soft dependency on tippy.js.

Apply this diff if unused:

-    "@tiptap/extension-bubble-menu": "^3.2.1",
packages/tiptap/src/editor/mention.tsx (1)

305-310: Avoid javascript: URLs for navigation for security and CSP cleanliness.

Prefer a safe href plus click handler rather than href="javascript:void(0)". Not a blocker, but worth swapping.

Example (outside selected lines, for clarity):

href="#"
onClick={(e) => { e.preventDefault(); window[GLOBAL_NAVIGATE_FUNCTION]?.(path); }}
apps/desktop/src/locales/ko/messages.po (2)

267-276: Ensure updated listen-button placeholders are translated (avoid English fallback in primary flows).

The translator note changed, but msgstr is empty. These labels are prominent; consider providing Korean translations for "Wait...", "Play again", and "Play video" variants.

I can propose Korean strings if you share tone/terminology preferences (formal vs casual).


754-757: “Enable Integration” remains untranslated; verify intentional.

This stands out amid otherwise localized settings text. Translate or mark as intentionally English to keep the UI consistent.

packages/tiptap/src/transcript/extensions/search-and-replace.ts (3)

361-371: Minor: debounce heavy recomputation when typing long queries.

Current logic recomputes decorations every transaction. A short debounce (e.g., 50–100ms) on searchTerm changes reduces churn on large docs. Optional, given v3 perf.


91-107: Space-only queries are silently ignored (trim filter); verify UX intent.

Filtering matchText.trim() drops matches for queries like a single space. If you want to support spacing cleanup (e.g., double-space search), don’t trim.

Possible tweak:

-    const matches = Array.from(text.matchAll(searchTerm)).filter(
-      ([matchText]) => matchText.trim(),
-    );
+    const matches = Array.from(text.matchAll(searchTerm));

171-207: replaceAll() correctness looks good; add scrollIntoView for UX.

After replaceAll, consider tr.scrollIntoView() to bring the first change into view. Optional.

-  if (dispatch) {
-    dispatch(tr);
-  }
+  if (dispatch) {
+    dispatch(tr.scrollIntoView());
+  }
packages/tiptap/src/transcript/extensions/speaker.ts (4)

17-67: Guard split operations to avoid rare runtime exceptions.

tr.split(splitPos, 1, ...) can throw if the position isn’t splittable (schema change, invalid cursor pos, or unexpected node). Cheap safeguard: use canSplit before splitting.

Apply this diff:

 import { Extension } from "@tiptap/core";
 import { Plugin, PluginKey, TextSelection } from "prosemirror-state";
+import { canSplit } from "prosemirror-transform";

@@
             if (checkKey("Enter")(event)) {
               const { state, dispatch } = view;
               const { selection } = state;
@@
-              const tr = state.tr.split(splitPos, 1, [
+              if (!canSplit(state.doc, splitPos, 1)) {
+                return false;
+              }
+              const tr = state.tr.split(splitPos, 1, [
                 { type: state.schema.nodes[SPEAKER_NODE_NAME], attrs: speakerAttrs },
               ]);

76-89: Minor typing improvement for readability.

speakerSections currently uses any. Importing the ProseMirror node type clarifies intent and aids tooling.

Apply this diff:

-              const speakerSections: { pos: number; node: any; startPos: number; endPos: number }[] = [];
+              const speakerSections: { pos: number; node: import("prosemirror-model").Node; startPos: number; endPos: number }[] = [];

Alternatively, add a type import at top:

+import type { Node as PMNode } from "prosemirror-model";
...
-              const speakerSections: { pos: number; node: any; startPos: number; endPos: number }[] = [];
+              const speakerSections: { pos: number; node: PMNode; startPos: number; endPos: number }[] = [];

78-88: Comment style nit: prefer “why” over “what”.

The inline comment states what the code does. Consider clarifying intent, e.g., “Stop at speaker nodes so each block navigates as a unit.”


112-121: Edge navigation: confirm behavior with empty speaker blocks.

If a section is empty, startPos === endPos. Selection creation is still valid, but UX might feel “sticky.” If that’s not desired, skip empty sections when building speakerSections.

Proposed tweak inside the doc.descendants loop:

-                if (node.type.name === SPEAKER_NODE_NAME) {
+                if (node.type.name === SPEAKER_NODE_NAME && node.content.size > 0) {

Also applies to: 142-145

packages/tiptap/src/transcript/index.tsx (3)

37-45: Redundant command augmentation may conflict with the local extension’s own types.

If ./extensions/search-and-replace already augments the commands interface (likely), this extra augmentation is unnecessary and risks drift. Prefer a single source of truth.

Apply this diff to remove the duplication:

-declare module "@tiptap/core" {
-  interface Commands<ReturnType> {
-    searchAndReplace: {
-      setSearchTerm: (s: string) => ReturnType;
-      setReplaceTerm: (s: string) => ReturnType;
-      replaceAll: () => ReturnType;
-    };
-  }
-}

82-168: Prefer useImperativeHandle over mutating the ref in an effect.

This avoids timing issues and keeps React conventions. Functionality stays the same.

Apply this diff:

-import { forwardRef, useEffect, useRef } from "react";
+import { forwardRef, useEffect, useRef, useImperativeHandle } from "react";
@@
-    useEffect(() => {
-      if (ref && typeof ref === "object" && editor) {
-        ref.current = {
-          editor,
-          setWords: (words: Word2[]) => {
-            if (!editor) {
-              return;
-            }
-
-            const content = fromWordsToEditor(words);
-            editor.commands.setContent(content);
-          },
-          getWords: () => {
-            if (!editor) {
-              return null;
-            }
-            return fromEditorToWords(editor.getJSON() as any);
-          },
-          scrollToBottom: () => {
-            if (scrollContainerRef.current) {
-              scrollContainerRef.current.scrollTop = scrollContainerRef.current.scrollHeight;
-            }
-          },
-          isNearBottom: () => {
-            if (!scrollContainerRef.current) {
-              return true;
-            }
-
-            const container = scrollContainerRef.current;
-            const threshold = 100;
-            const distanceFromBottom = container.scrollHeight - container.scrollTop - container.clientHeight;
-            return distanceFromBottom <= threshold;
-          },
-          appendWords: (newWords: Word2[]) => {
-            if (!editor || !newWords.length) {
-              return;
-            }
-
-            const jsonFragment = fromWordsToEditor(newWords).content;
-
-            if (!jsonFragment?.length) {
-              return;
-            }
-
-            const endPos = editor.state.doc.content.size;
-
-            editor
-              .chain()
-              .insertContentAt(endPos, jsonFragment)
-              .run();
-          },
-          toText: () => {
-            if (!editor) {
-              return "";
-            }
-
-            const doc = editor.getJSON();
-            if (!doc?.content) {
-              return "";
-            }
-
-            const lines: string[] = [];
-
-            for (const speakerBlock of doc.content) {
-              if (speakerBlock.type !== "speaker" || !speakerBlock.content) {
-                continue;
-              }
-
-              const attrs = speakerBlock.attrs as SpeakerAttributes || {};
-              const speakerLabel = getSpeakerLabel(attrs);
-
-              const textContent = speakerBlock.content
-                .filter((node: any) => node.type === "text")
-                .map((node: any) => node.text || "")
-                .join("")
-                .trim();
-
-              if (textContent) {
-                lines.push(`[${speakerLabel}]\n${textContent}`);
-              }
-            }
-
-            return lines.join("\n\n");
-          },
-        };
-      }
-    }, [editor]);
+    useImperativeHandle(ref, () => ({
+      editor,
+      setWords: (words: Word2[]) => {
+        if (!editor) return;
+        const content = fromWordsToEditor(words);
+        editor.commands.setContent(content);
+      },
+      getWords: () => {
+        if (!editor) return null;
+        return fromEditorToWords(editor.getJSON() as any);
+      },
+      scrollToBottom: () => {
+        if (scrollContainerRef.current) {
+          scrollContainerRef.current.scrollTop = scrollContainerRef.current.scrollHeight;
+        }
+      },
+      isNearBottom: () => {
+        if (!scrollContainerRef.current) return true;
+        const container = scrollContainerRef.current;
+        const threshold = 100;
+        const distanceFromBottom = container.scrollHeight - container.scrollTop - container.clientHeight;
+        return distanceFromBottom <= threshold;
+      },
+      appendWords: (newWords: Word2[]) => {
+        if (!editor || !newWords.length) return;
+        const jsonFragment = fromWordsToEditor(newWords).content;
+        if (!jsonFragment?.length) return;
+        const endPos = editor.state.doc.content.size;
+        editor.chain().insertContentAt(endPos, jsonFragment).run();
+      },
+      toText: () => {
+        if (!editor) return "";
+        const doc = editor.getJSON();
+        if (!doc?.content) return "";
+        const lines: string[] = [];
+        for (const speakerBlock of doc.content) {
+          if (speakerBlock.type !== "speaker" || !speakerBlock.content) continue;
+          const attrs = (speakerBlock.attrs as SpeakerAttributes) || {};
+          const speakerLabel = getSpeakerLabel(attrs);
+          const textContent = speakerBlock.content
+            .filter((node: any) => node.type === "text")
+            .map((node: any) => node.text || "")
+            .join("")
+            .trim();
+          if (textContent) {
+            lines.push(`[${speakerLabel}]\n${textContent}`);
+          }
+        }
+        return lines.join("\n\n");
+      },
+    }), [editor]);

120-133: Appending content: consider avoiding a trailing empty textblock.

If the doc ends with an empty speaker block, insertContentAt(endPos, ...) can leave an empty block in the middle. Optionally, detect and replace the trailing empty node instead of always inserting at endPos.

📜 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 6615e49 and b7ec215.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (12)
  • admin/server/package.json (1 hunks)
  • apps/admin/package.json (3 hunks)
  • apps/desktop/package.json (1 hunks)
  • apps/desktop/src/locales/en/messages.po (19 hunks)
  • apps/desktop/src/locales/ko/messages.po (19 hunks)
  • apps/pro/package.json (1 hunks)
  • packages/tiptap/package.json (1 hunks)
  • packages/tiptap/src/editor/mention.tsx (1 hunks)
  • packages/tiptap/src/transcript/extensions/search-and-replace.ts (1 hunks)
  • packages/tiptap/src/transcript/extensions/speaker.ts (1 hunks)
  • packages/tiptap/src/transcript/index.tsx (1 hunks)
  • packages/utils/package.json (2 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:

  • packages/tiptap/src/transcript/extensions/speaker.ts
  • packages/tiptap/src/transcript/index.tsx
  • packages/tiptap/src/transcript/extensions/search-and-replace.ts
  • packages/tiptap/src/editor/mention.tsx
⏰ 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 (windows, windows-latest)
  • GitHub Check: ci (macos, macos-latest)
🔇 Additional comments (12)
admin/server/package.json (1)

14-14: Hono 4.9.4 patch bump verified

  • Both admin/server/package.json and apps/pro/package.json declare "hono": "^4.9.4", so there’s no version drift across workspaces.
  • In pnpm-lock.yaml,
    @hono/node-server@1.19.0 resolves to hono@4.9.4
    @hono/zod-validator@0.7.2 resolves to hono@4.9.4
  • @hono/node-server specifies a Node engine of >=18.14.1; ensure your runtime meets this (you may add an engines field in your root package.json if desired).
apps/pro/package.json (1)

16-17: Dependency updates verified: exa-js and Hono imports & APIs remain compatible

  • exa-js
    • Only ES-module import found in apps/pro/src/exa.ts (import { Exa } from "exa-js"). No CommonJS require calls detected.
    • No evidence of renamed exports—constructor usage should work as before.
    Suggestion: skim the exa-js 1.9.1 changelog for any subtle API tweaks around HTTP/fetch helpers.

  • Hono
    • ES-module imports of "hono", "hono/cors", and use of new Hono() confirmed in both apps/pro/src/index.ts and admin/server/src/index.ts.
    • Context methods c.text() (and c.json() where applicable), middleware chains (.middleware()), and logging (logger()) usages all present and unchanged.
    • No serveStatic calls found—if you rely on static-file serving, verify that separately.
    • Versions now align (^4.9.4) across admin/server and pro apps, so deduplication will work as expected.

All automated checks passed—no import or API mismatches detected.

packages/utils/package.json (2)

24-25: Dependency versions are consistent across workspaces

  • “@tauri-apps/plugin-http” is pinned to ^2.5.2 in:
    • packages/utils/package.json (line 24)
    • apps/desktop/package.json (line 63)
  • “ai” is pinned to ^5.0.21 in:
    • packages/utils/package.json (line 25)
    • apps/admin/package.json (line 31)

All versions match as intended—please run your package manager to refresh and commit the lockfile.


15-15: Confirm ai peerDependency alignment

I checked packages/utils/package.json and saw that both
"@ai-sdk/openai-compatible": "^1.0.11" and "ai": "^5.0.21" are listed under dependencies, and there are no peerDependencies for either in this package. Please verify that @ai-sdk/openai-compatible@1.0.11 declares a compatible peerDependency on ai (e.g. ^5.0.21) in its own package manifest so that consumers don’t receive version-mismatch warnings at runtime. If its peer range already covers the bumped ai version, no further changes are needed.

apps/admin/package.json (3)

19-19: @ai-sdk/openai bump: usage verified

I searched the entire apps/admin/src directory and found only one invocation of createOpenAI in apps/admin/src/services/ai.api.ts, where it’s called with:

  • baseURL (mapped from data.baseUrl)
  • apiKey (data.apiKey)

Since this is a patch bump to v2.0.19 (no minor/major version change), it should not introduce any breaking changes to the createOpenAI config shape under semver rules. No further API or option-name changes are required.


40-40: posthog-js upgrade verified: no behavioral changes in telemetry flows

I’ve confirmed that the bump to “posthog-js” ^1.260.2 does not alter our initialization or event-capture logic:

  • In apps/admin/src/lib/telemetry.ts at line 10, posthog.init("KEY", { api_host: "https://eu.posthog.com" }, "hypr_admin") remains the only config passed—no new GDPR or consent flags introduced.
  • The capture wrapper (around lines 15–25) still calls posthog.capture(event_name, properties, options) only when telemetry is enabled.
  • There’s no posthog.identify usage or custom consent toggles anywhere in the admin app.

No further changes are needed.


31-31: ai dependency updated consistently—please install deps and verify type compatibility.

  • Both apps/admin/package.json and packages/utils/package.json now pin "ai": "^5.0.21".
  • To confirm there’s no breaking change:
    • Run pnpm install at the repo root.
    • In apps/admin, execute pnpm run typecheck (or tsc --noEmit) to ensure admin code compiles.
    • In packages/utils, add a typecheck script (e.g., "typecheck": "tsc --noEmit") or invoke tsc --noEmit directly to validate shared helpers.
apps/desktop/src/locales/en/messages.po (1)

267-275: Translator note updates for listen-button placeholders look good; confirm string-case consistency across states.

The new notes clarify the disabled-state branch. Double-check that "Play again" vs "Play video" casing matches the rest of the UI verbs for media actions to avoid inconsistent surfaces.

packages/tiptap/package.json (1)

17-17: Good: floating-ui patch bump aligns with custom popper logic replacing tippy.

packages/tiptap/src/transcript/extensions/speaker.ts (1)

4-4: Import path update verified—no further action needed

  • Confirmed SPEAKER_NODE_NAME is exported once in packages/tiptap/src/transcript/nodes.ts:8 with the value "speaker".
  • Found its sole usage in packages/tiptap/src/transcript/extensions/speaker.ts:59, where it correctly indexes state.schema.nodes["speaker"].

The import from ../nodes is correct, and the schema key aligns with the constant value—no runtime mismatch possible.

packages/tiptap/src/transcript/index.tsx (2)

51-63: CSS hook verification complete

The .search-result class is defined in packages/tiptap/src/styles/transcript.css (line 1), so highlights will render as expected.


10-11: All external references have been removed – you’re good to go!

  • No imports of @sereneinserenade/tiptap-search-and-replace remain anywhere in the repo.
  • The dependency isn’t listed in package.json or any lockfile.
  • The only mention is a harmless comment at the top of your local extension file linking back to the original source.

Comment on lines +61 to 69
"@tauri-apps/plugin-dialog": "^2.3.3",
"@tauri-apps/plugin-fs": "^2.4.2",
"@tauri-apps/plugin-http": "^2.5.2",
"@tauri-apps/plugin-opener": "^2.5.0",
"@tauri-apps/plugin-os": "^2.3.1",
"@tauri-apps/plugin-process": "^2.3.0",
"@tauri-apps/plugin-shell": "^2.3.0",
"@tauri-apps/plugin-store": "^2.3.0",
"@tauri-apps/plugin-store": "^2.4.0",
"@tauri-apps/plugin-updater": "^2.9.0",
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

Tauri plugin bumps: verify Rust-side plugin crate versions and capabilities.

JS plugin versions typically need compatible Rust crates in src-tauri/Cargo.toml and correct permissions in tauri.conf.json/capabilities. Validate both.

Run:

If Rust crates lag behind or capability keys changed between minor versions, align them to avoid runtime errors.


🏁 Script executed:

#!/bin/bash
# Inspect Rust plugin crate versions
rg -nP '^(tauri|@tauri)-apps|plugin' src-tauri/Cargo.toml -n -C2 || true
cat src-tauri/Cargo.toml 2>/dev/null | sed -n '1,200p'

# Check for capability/permission config usage for updated plugins
rg -nP '"plugins"\s*:\s*\{' -g 'src-tauri/tauri.conf.json*' -C3 || true

Length of output: 399


🏁 Script executed:

#!/bin/bash
# Locate all Cargo.toml files in the repository
fd --hidden --max-depth 4 Cargo.toml

# Locate all tauri.conf.json files
fd --hidden --max-depth 4 tauri.conf.json

# For each found Cargo.toml, show plugin crate versions
for f in $(fd --hidden --max-depth 4 Cargo.toml); do
  echo "===== $f ====="
  rg -nP '(plugin-)' "$f" -C2 || true
done

# For each found tauri.conf.json, show the plugins configuration block
for f in $(fd --hidden --max-depth 4 tauri.conf.json); do
  echo "===== $f ====="
  rg -nP '"plugins"\s*:\s*\{' -n -C3 "$f" || true
done

Length of output: 34291


Fix Rust plugin crate versions to match JS bumps and verify plugin configurations

  • In the root Cargo.toml (around lines 97–99):
    • Update
      tauri-plugin-opener = "2.4"tauri-plugin-opener = "2.5"
      tauri-plugin-store = "2.3"tauri-plugin-store = "2.4"
  • In apps/desktop/src-tauri/Cargo.toml (currently using { workspace = true } for these crates on lines 45 and 70), either:
    • Keep { workspace = true } and bump the workspace members for tauri-plugin-opener and tauri-plugin-store to the new versions, or
    • Switch to explicit versions:
      tauri-plugin-opener = "2.5"
      tauri-plugin-store   = "2.4"
  • Double-check that every JS plugin in apps/desktop/package.json (e.g. plugin-dialog, plugin-fs, plugin-http, etc.) has a matching Rust crate version in your Cargo.toml.
  • Inspect apps/desktop/src-tauri/tauri.conf.json under the "plugins" block and add configuration or allowlist entries for any newly bumped plugins (for example, FS scopes, HTTP allowlist, opener schemes, etc.) so that permissions align with the plugin versions and capabilities.

These updates will keep your JS and Rust sides in sync and prevent build or runtime errors when loading the updated Tauri plugins.

🤖 Prompt for AI Agents
In apps/desktop/package.json around lines 61–69, the JS Tauri plugin versions
were bumped but the Rust crates are out of sync; update the root Cargo.toml
(around lines 97–99) to set tauri-plugin-opener = "2.5" and tauri-plugin-store =
"2.4", then in apps/desktop/src-tauri/Cargo.toml (lines ~45 and ~70) either keep
{ workspace = true } and bump those workspace members to the matching versions
or replace with explicit entries tauri-plugin-opener = "2.5" and
tauri-plugin-store = "2.4"; confirm every JS plugin listed in
apps/desktop/package.json has a corresponding Rust crate entry in Cargo.toml
with the same major/minor version, and update
apps/desktop/src-tauri/tauri.conf.json plugin allowlist/config blocks to include
any new scopes or settings required by the bumped plugins (FS, HTTP, opener
schemes, etc.).

Comment on lines 46 to 49
"prosemirror-commands": "^1.7.1",
"prosemirror-model": "^1.25.3",
"prosemirror-state": "^1.4.3",
"tippy.js": "^6.3.7",
"turndown": "^7.2.1"
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

Unify ProseMirror imports via @tiptap/pm to avoid version skew.

You’re already using @tiptap/pm in code. Keeping direct prosemirror-* deps can cause duplication/skew. If there are no direct imports, remove these three.

Run to verify direct imports and safe removal:

Also applies to: 40-44


🏁 Script executed:

#!/bin/bash
# Find direct prosemirror imports (should be none if unified via @tiptap/pm)
rg -nP --type=ts --type=tsx -C2 'from\s+["'\'']prosemirror-(commands|model|state)["'\'']'

Length of output: 119


I’ll broaden the search to catch any direct imports or references without relying on ripgrep’s built-in file-type filters.


🏁 Script executed:

#!/bin/bash
# 1. Look for ES imports of prosemirror-* in TS/TSX/JS/JSX files
rg -n -C2 "from ['\"]prosemirror-(commands|model|state)['\"]" -g '*.ts' -g '*.tsx' -g '*.js' -g '*.jsx'

# 2. Catch any string references to prosemirror-commands, -model, or -state anywhere
rg -n -C2 "prosemirror-(commands|model|state)"

Length of output: 9436


Refactor Direct ProseMirror Imports to Use @tiptap/pm

We’ve identified direct imports of prosemirror-model and prosemirror-state in your code. Before removing the corresponding dependencies from packages/tiptap/package.json, update these to import from @tiptap/pm.

• packages/tiptap/src/transcript/nodes.ts

- import { Node as ProseNode } from "prosemirror-model";
+ import { Node as ProseNode } from "@tiptap/pm/model";

• packages/tiptap/src/transcript/extensions/speaker.ts

- import { Plugin, PluginKey, TextSelection } from "prosemirror-state";
+ import { Plugin, PluginKey, TextSelection } from "@tiptap/pm/state";

Once these imports are updated (and any direct prosemirror-commands usages refactored to @tiptap/pm/commands if present), you can safely remove the three dependencies from packages/tiptap/package.json:

// remove:
"prosemirror-commands": "^1.7.1",
"prosemirror-model":    "^1.25.3",
"prosemirror-state":    "^1.4.3",
🤖 Prompt for AI Agents
In packages/tiptap/package.json around lines 46 to 49, you currently list
prosemirror-commands, prosemirror-model and prosemirror-state; update all direct
imports of prosemirror modules in packages/tiptap/src/transcript/nodes.ts and
packages/tiptap/src/transcript/extensions/speaker.ts to import from @tiptap/pm
(and change any direct prosemirror-commands usage to @tiptap/pm/commands),
adjust any type/path names accordingly, run type checks/build to ensure no
breaking API differences, and once those source files no longer reference
prosemirror packages remove the three dependencies from package.json.

Comment on lines +267 to 279
parseHTML: (element: Element) => element.getAttribute("data-id"),
renderHTML: (attributes: { id: string }) => ({ "data-id": attributes.id }),
},
type: {
default: null,
parseHTML: (element) => element.getAttribute("data-type"),
renderHTML: (attributes) => ({ "data-type": attributes.type }),
parseHTML: (element: Element) => element.getAttribute("data-type"),
renderHTML: (attributes: { type: string }) => ({ "data-type": attributes.type }),
},
label: {
default: null,
parseHTML: (element) => element.getAttribute("data-label"),
renderHTML: (attributes) => ({ "data-label": attributes.label }),
parseHTML: (element: Element) => element.getAttribute("data-label"),
renderHTML: (attributes: { label: string }) => ({ "data-label": attributes.label }),
},
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

Attribute handlers’ TS types are too strict; allow nulls to match defaults.

Defaults are null, and getAttribute returns string | null. Narrowing renderHTML inputs to { id: string } etc. is unsound and may force unsafe casts upstream. Accept string | null and coalesce on output.

Apply this diff:

-            parseHTML: (element: Element) => element.getAttribute("data-id"),
-            renderHTML: (attributes: { id: string }) => ({ "data-id": attributes.id }),
+            parseHTML: (element: Element) => element.getAttribute("data-id"),
+            renderHTML: (attributes: { id: string | null }) => ({
+              "data-id": attributes.id ?? "",
+            }),
...
-            parseHTML: (element: Element) => element.getAttribute("data-type"),
-            renderHTML: (attributes: { type: string }) => ({ "data-type": attributes.type }),
+            parseHTML: (element: Element) => element.getAttribute("data-type"),
+            renderHTML: (attributes: { type: string | null }) => ({
+              "data-type": attributes.type ?? "",
+            }),
...
-            parseHTML: (element: Element) => element.getAttribute("data-label"),
-            renderHTML: (attributes: { label: string }) => ({ "data-label": attributes.label }),
+            parseHTML: (element: Element) => element.getAttribute("data-label"),
+            renderHTML: (attributes: { label: string | null }) => ({
+              "data-label": attributes.label ?? "",
+            }),
📝 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
parseHTML: (element: Element) => element.getAttribute("data-id"),
renderHTML: (attributes: { id: string }) => ({ "data-id": attributes.id }),
},
type: {
default: null,
parseHTML: (element) => element.getAttribute("data-type"),
renderHTML: (attributes) => ({ "data-type": attributes.type }),
parseHTML: (element: Element) => element.getAttribute("data-type"),
renderHTML: (attributes: { type: string }) => ({ "data-type": attributes.type }),
},
label: {
default: null,
parseHTML: (element) => element.getAttribute("data-label"),
renderHTML: (attributes) => ({ "data-label": attributes.label }),
parseHTML: (element: Element) => element.getAttribute("data-label"),
renderHTML: (attributes: { label: string }) => ({ "data-label": attributes.label }),
},
parseHTML: (element: Element) => element.getAttribute("data-id"),
renderHTML: (attributes: { id: string | null }) => ({
"data-id": attributes.id ?? "",
}),
},
type: {
default: null,
parseHTML: (element: Element) => element.getAttribute("data-type"),
renderHTML: (attributes: { type: string | null }) => ({
"data-type": attributes.type ?? "",
}),
},
label: {
default: null,
parseHTML: (element: Element) => element.getAttribute("data-label"),
renderHTML: (attributes: { label: string | null }) => ({
"data-label": attributes.label ?? "",
}),
},
🤖 Prompt for AI Agents
In packages/tiptap/src/editor/mention.tsx around lines 267 to 279, the attribute
handler types are too strict: defaults are null and Element.getAttribute returns
string | null, so renderHTML parameter types must accept string | null; update
the renderHTML signatures from { id: string } / { type: string } / { label:
string } to allow string | null and coalesce the output so you return the
data-attribute only when the value is non-null (e.g., use value ?? undefined or
conditionally include the attribute) to avoid forcing unsafe casts upstream.

Comment on lines +255 to +317
addCommands() {
return {
setSearchTerm: (searchTerm: string) => ({ editor }) => {
editor.storage.searchAndReplace.searchTerm = searchTerm;

return false;
},
setReplaceTerm: (replaceTerm: string) => ({ editor }) => {
editor.storage.searchAndReplace.replaceTerm = replaceTerm;

return false;
},
setCaseSensitive: (caseSensitive: boolean) => ({ editor }) => {
editor.storage.searchAndReplace.caseSensitive = caseSensitive;

return false;
},
resetIndex: () => ({ editor }) => {
editor.storage.searchAndReplace.resultIndex = 0;

return false;
},
nextSearchResult: () => ({ editor }) => {
const { results, resultIndex } = editor.storage.searchAndReplace;

const nextIndex = resultIndex + 1;

if (results[nextIndex]) {
editor.storage.searchAndReplace.resultIndex = nextIndex;
} else {
editor.storage.searchAndReplace.resultIndex = 0;
}

return false;
},
previousSearchResult: () => ({ editor }) => {
const { results, resultIndex } = editor.storage.searchAndReplace;

const prevIndex = resultIndex - 1;

if (results[prevIndex]) {
editor.storage.searchAndReplace.resultIndex = prevIndex;
} else {
editor.storage.searchAndReplace.resultIndex = results.length - 1;
}

return false;
},
replace: () => ({ editor, state, dispatch }) => {
const { replaceTerm, results } = editor.storage.searchAndReplace;

replace(replaceTerm, results, { state, dispatch });

return false;
},
replaceAll: () => ({ editor, tr, dispatch }) => {
const { replaceTerm, results } = editor.storage.searchAndReplace;

replaceAll(replaceTerm, results, { tr, dispatch });

return false;
},
};
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

Commands mutate storage without dispatching a transaction; highlights won’t update until another edit happens.

ProseMirror plugins recompute on transactions. Dispatch a no-op/meta tr after changing storage so decorations update immediately.

Apply this diff:

       setSearchTerm: (searchTerm: string) => ({ editor }) => {
         editor.storage.searchAndReplace.searchTerm = searchTerm;
-
-        return false;
+        editor.view.dispatch(editor.state.tr);
+        return true;
       },
       setReplaceTerm: (replaceTerm: string) => ({ editor }) => {
         editor.storage.searchAndReplace.replaceTerm = replaceTerm;
-
-        return false;
+        editor.view.dispatch(editor.state.tr);
+        return true;
       },
       setCaseSensitive: (caseSensitive: boolean) => ({ editor }) => {
         editor.storage.searchAndReplace.caseSensitive = caseSensitive;
-
-        return false;
+        editor.view.dispatch(editor.state.tr);
+        return true;
       },
       resetIndex: () => ({ editor }) => {
         editor.storage.searchAndReplace.resultIndex = 0;
-
-        return false;
+        editor.view.dispatch(editor.state.tr);
+        return true;
       },
       nextSearchResult: () => ({ editor }) => {
         const { results, resultIndex } = editor.storage.searchAndReplace;
@@
-        return false;
+        editor.view.dispatch(editor.state.tr);
+        return true;
       },
       previousSearchResult: () => ({ editor }) => {
         const { results, resultIndex } = editor.storage.searchAndReplace;
@@
-        return false;
+        editor.view.dispatch(editor.state.tr);
+        return true;
       },
📝 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
addCommands() {
return {
setSearchTerm: (searchTerm: string) => ({ editor }) => {
editor.storage.searchAndReplace.searchTerm = searchTerm;
return false;
},
setReplaceTerm: (replaceTerm: string) => ({ editor }) => {
editor.storage.searchAndReplace.replaceTerm = replaceTerm;
return false;
},
setCaseSensitive: (caseSensitive: boolean) => ({ editor }) => {
editor.storage.searchAndReplace.caseSensitive = caseSensitive;
return false;
},
resetIndex: () => ({ editor }) => {
editor.storage.searchAndReplace.resultIndex = 0;
return false;
},
nextSearchResult: () => ({ editor }) => {
const { results, resultIndex } = editor.storage.searchAndReplace;
const nextIndex = resultIndex + 1;
if (results[nextIndex]) {
editor.storage.searchAndReplace.resultIndex = nextIndex;
} else {
editor.storage.searchAndReplace.resultIndex = 0;
}
return false;
},
previousSearchResult: () => ({ editor }) => {
const { results, resultIndex } = editor.storage.searchAndReplace;
const prevIndex = resultIndex - 1;
if (results[prevIndex]) {
editor.storage.searchAndReplace.resultIndex = prevIndex;
} else {
editor.storage.searchAndReplace.resultIndex = results.length - 1;
}
return false;
},
replace: () => ({ editor, state, dispatch }) => {
const { replaceTerm, results } = editor.storage.searchAndReplace;
replace(replaceTerm, results, { state, dispatch });
return false;
},
replaceAll: () => ({ editor, tr, dispatch }) => {
const { replaceTerm, results } = editor.storage.searchAndReplace;
replaceAll(replaceTerm, results, { tr, dispatch });
return false;
},
};
addCommands() {
return {
setSearchTerm: (searchTerm: string) => ({ editor }) => {
editor.storage.searchAndReplace.searchTerm = searchTerm;
editor.view.dispatch(editor.state.tr);
return true;
},
setReplaceTerm: (replaceTerm: string) => ({ editor }) => {
editor.storage.searchAndReplace.replaceTerm = replaceTerm;
editor.view.dispatch(editor.state.tr);
return true;
},
setCaseSensitive: (caseSensitive: boolean) => ({ editor }) => {
editor.storage.searchAndReplace.caseSensitive = caseSensitive;
editor.view.dispatch(editor.state.tr);
return true;
},
resetIndex: () => ({ editor }) => {
editor.storage.searchAndReplace.resultIndex = 0;
editor.view.dispatch(editor.state.tr);
return true;
},
nextSearchResult: () => ({ editor }) => {
const { results, resultIndex } = editor.storage.searchAndReplace;
const nextIndex = resultIndex + 1;
if (results[nextIndex]) {
editor.storage.searchAndReplace.resultIndex = nextIndex;
} else {
editor.storage.searchAndReplace.resultIndex = 0;
}
editor.view.dispatch(editor.state.tr);
return true;
},
previousSearchResult: () => ({ editor }) => {
const { results, resultIndex } = editor.storage.searchAndReplace;
const prevIndex = resultIndex - 1;
if (results[prevIndex]) {
editor.storage.searchAndReplace.resultIndex = prevIndex;
} else {
editor.storage.searchAndReplace.resultIndex = results.length - 1;
}
editor.view.dispatch(editor.state.tr);
return true;
},
replace: () => ({ editor, state, dispatch }) => {
const { replaceTerm, results } = editor.storage.searchAndReplace;
replace(replaceTerm, results, { state, dispatch });
return false;
},
replaceAll: () => ({ editor, tr, dispatch }) => {
const { replaceTerm, results } = editor.storage.searchAndReplace;
replaceAll(replaceTerm, results, { tr, dispatch });
return false;
},
};
}
🤖 Prompt for AI Agents
In packages/tiptap/src/transcript/extensions/search-and-replace.ts around lines
255–317 the commands mutate editor.storage.searchAndReplace directly but never
dispatch a transaction, so the ProseMirror plugin/decorations don’t recompute
until the next edit; after every place you update
editor.storage.searchAndReplace (setSearchTerm, setReplaceTerm,
setCaseSensitive, resetIndex, nextSearchResult, previousSearchResult, replace,
replaceAll) create and dispatch a no-op/meta transaction immediately afterward
(use the provided dispatch when available, otherwise editor.view.dispatch) — for
example build a tr from the current state, set a dedicated meta key (or
timestamp) to signal the update, and dispatch it so the plugin will re-run and
update highlights immediately.

Comment on lines +303 to +309
replace: () => ({ editor, state, dispatch }) => {
const { replaceTerm, results } = editor.storage.searchAndReplace;

replace(replaceTerm, results, { state, dispatch });

return false;
},
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

Bug: replace() always edits the first match, ignoring the current resultIndex.

Users navigating results will expect “Replace” to affect the highlighted one. Wire it to resultIndex instead of results[0].

Apply this diff:

-const replace = (
-  replaceTerm: string,
-  results: Range[],
-  { state, dispatch }: { state: EditorState; dispatch: Dispatch },
-) => {
-  const firstResult = results[0];
-
-  if (!firstResult) {
-    return;
-  }
-
-  const { from, to } = results[0];
-
-  if (dispatch) {
-    dispatch(state.tr.insertText(replaceTerm, from, to));
-  }
-};
+const replaceAtIndex = (
+  replaceTerm: string,
+  results: Range[],
+  index: number,
+  { state, dispatch }: { state: EditorState; dispatch: Dispatch },
+) => {
+  const r = results[index];
+  if (!r) return;
+  if (dispatch) {
+    dispatch(state.tr.insertText(replaceTerm, r.from, r.to).scrollIntoView());
+  }
+};
-      replace: () => ({ editor, state, dispatch }) => {
-        const { replaceTerm, results } = editor.storage.searchAndReplace;
-
-        replace(replaceTerm, results, { state, dispatch });
+      replace: () => ({ editor, state, dispatch }) => {
+        const { replaceTerm, results, resultIndex } = editor.storage.searchAndReplace;
+        replaceAtIndex(replaceTerm, results, resultIndex, { state, dispatch });

Also applies to: 127-144

🤖 Prompt for AI Agents
In packages/tiptap/src/transcript/extensions/search-and-replace.ts around lines
303-309 (and also apply the same fix at lines 127-144), the replace handler
always uses results[0] so the Replace action edits the first match regardless of
which result is currently selected; instead read resultIndex from
editor.storage.searchAndReplace, validate it (ensure results[resultIndex] exists
and is in bounds), then call replace(replaceTerm, results[resultIndex], { state,
dispatch }) so the replacement targets the highlighted/current result; keep
existing return value and error-guarding behavior if no valid result is found.

@yujonglee yujonglee merged commit 659f868 into main Aug 22, 2025
6 of 7 checks passed
@yujonglee yujonglee deleted the tiptap-3 branch August 22, 2025 05:24
@coderabbitai coderabbitai bot mentioned this pull request Nov 18, 2025
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

Comments