Conversation
|
Caution Review failedThe pull request is closed. 📝 WalkthroughWalkthroughDependency 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
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
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 detailsConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro 💡 Knowledge Base configuration:
You can enable these sources in your CodeRabbit configuration. 📒 Files selected for processing (3)
✨ Finishing Touches
🧪 Generate unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
Status, Documentation and Community
|
There was a problem hiding this comment.
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 unimplementedserializecommand augmentationIt looks like you’ve declared a
transcriptCommands.serializecommand in your type augmentation, but there is no corresponding implementation viaaddCommandsin this extension (or anywhere else) – the only match is the interface declaration at line 9 inpackages/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: thedeclare module "@tiptap/core"block addingserialize: () => ReturnType;Action items:
- If you intend to provide a
serializecommand for transcripts, implement it in this extension’saddCommands()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 deprecatedshouldRerenderOnTransactionfrom youruseEditorcallIn @tiptap/react v3, the React integration was rewritten to avoid per-transaction re-renders by default. The
shouldRerenderOnTransactionoption 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 theEditorProvider/useCurrentEditorpatterns.• File:
packages/tiptap/src/transcript/index.tsx
• Location: inside youruseEditor({ … })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
useEditorStateselector 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 usezod@^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 unusedtippy.jsdependency from apps/desktopNo references to
tippy.js(or@tippyjs) were found anywhere inapps/desktopsource 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: usecanSplitbefore 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.
speakerSectionscurrently usesany. 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 buildingspeakerSections.Proposed tweak inside the
doc.descendantsloop:- 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-replacealready 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: PreferuseImperativeHandleover 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 atendPos.
📜 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.
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis 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.tspackages/tiptap/src/transcript/index.tsxpackages/tiptap/src/transcript/extensions/search-and-replace.tspackages/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.jsonandapps/pro/package.jsondeclare"hono": "^4.9.4", so there’s no version drift across workspaces.- In
pnpm-lock.yaml,
•@hono/node-server@1.19.0resolves tohono@4.9.4
•@hono/zod-validator@0.7.2resolves tohono@4.9.4@hono/node-serverspecifies a Node engine of>=18.14.1; ensure your runtime meets this (you may add anenginesfield in your rootpackage.jsonif 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 inapps/pro/src/exa.ts(import { Exa } from "exa-js"). No CommonJSrequirecalls 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 ofnew Hono()confirmed in bothapps/pro/src/index.tsandadmin/server/src/index.ts.
• Context methodsc.text()(andc.json()where applicable), middleware chains (.middleware()), and logging (logger()) usages all present and unchanged.
• NoserveStaticcalls 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 alignmentI checked packages/utils/package.json and saw that both
"@ai-sdk/openai-compatible": "^1.0.11"and"ai": "^5.0.21"are listed underdependencies, and there are no peerDependencies for either in this package. Please verify that@ai-sdk/openai-compatible@1.0.11declares a compatible peerDependency onai(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 bumpedaiversion, no further changes are needed.apps/admin/package.json (3)
19-19: @ai-sdk/openai bump: usage verifiedI searched the entire
apps/admin/srcdirectory and found only one invocation ofcreateOpenAIinapps/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
createOpenAIconfig shape under semver rules. No further API or option-name changes are required.
40-40: posthog-js upgrade verified: no behavioral changes in telemetry flowsI’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
capturewrapper (around lines 15–25) still callsposthog.capture(event_name, properties, options)only when telemetry is enabled.- There’s no
posthog.identifyusage or custom consent toggles anywhere in the admin app.No further changes are needed.
31-31:aidependency updated consistently—please install deps and verify type compatibility.
- Both
apps/admin/package.jsonandpackages/utils/package.jsonnow pin"ai": "^5.0.21".- To confirm there’s no breaking change:
• Runpnpm installat the repo root.
• Inapps/admin, executepnpm run typecheck(ortsc --noEmit) to ensure admin code compiles.
• Inpackages/utils, add atypecheckscript (e.g.,"typecheck": "tsc --noEmit") or invoketsc --noEmitdirectly 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_NAMEis exported once inpackages/tiptap/src/transcript/nodes.ts:8with the value"speaker".- Found its sole usage in
packages/tiptap/src/transcript/extensions/speaker.ts:59, where it correctly indexesstate.schema.nodes["speaker"].The import from
../nodesis 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 completeThe
.search-resultclass is defined inpackages/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-replaceremain 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.
| "@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", |
There was a problem hiding this comment.
💡 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 || trueLength 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
doneLength 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"
- Update
- 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 fortauri-plugin-openerandtauri-plugin-storeto the new versions, or - Switch to explicit versions:
tauri-plugin-opener = "2.5" tauri-plugin-store = "2.4"
- Keep
- 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.).
| "prosemirror-commands": "^1.7.1", | ||
| "prosemirror-model": "^1.25.3", | ||
| "prosemirror-state": "^1.4.3", | ||
| "tippy.js": "^6.3.7", | ||
| "turndown": "^7.2.1" |
There was a problem hiding this comment.
💡 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.
| 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 }), | ||
| }, |
There was a problem hiding this comment.
🛠️ 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.
| 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.
| 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; | ||
| }, | ||
| }; |
There was a problem hiding this comment.
🛠️ 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.
| 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.
| replace: () => ({ editor, state, dispatch }) => { | ||
| const { replaceTerm, results } = editor.storage.searchAndReplace; | ||
|
|
||
| replace(replaceTerm, results, { state, dispatch }); | ||
|
|
||
| return false; | ||
| }, |
There was a problem hiding this comment.
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.
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
Refactors