Skip to content

feat(desktop): enhance browser bookmarks#55

Merged
MocA-Love merged 2 commits intomainfrom
feat/enhance-browser-bm
Apr 3, 2026
Merged

feat(desktop): enhance browser bookmarks#55
MocA-Love merged 2 commits intomainfrom
feat/enhance-browser-bm

Conversation

@MocA-Love
Copy link
Copy Markdown
Owner

@MocA-Love MocA-Love commented Apr 3, 2026

close #52

Summary

  • add bookmark folders with edit support for icon and color
  • add bookmark import/export and bookmark duplication support
  • support moving bookmarks via edit dialog and reordering inside folders
  • harden browser pane bookmark interactions around the webview boundary

Validation

  • bun run lint
  • bun run typecheck

Notes

  • this PR targets MocA-Love/superset main, not upstream

Summary by CodeRabbit

リリースノート

  • New Features

    • ブックマークフォルダの作成・編集・並び替え(フォルダツリー対応)を追加しました
    • ブックマークのインポート・エクスポート(Netscape形式HTML)を実装しました
    • テキストファイルの開く・保存ダイアログを追加しました
    • フォルダ用アイコン選択・カラーピッカーを追加しました
  • Bug Fixes

    • ブックマーク選択とレンダリングのパフォーマンスを最適化しました

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 3, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 245cd5cf-5c0d-4e3b-bd78-519475f00e54

📥 Commits

Reviewing files that changed from the base of the PR and between 75dd6ff and 4739f0c.

📒 Files selected for processing (4)
  • apps/desktop/src/lib/trpc/routers/external/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/BookmarkBar/components/BookmarkFolderItem/BookmarkFolderItem.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/BrowserToolbar/components/BrowserOverflowMenu/BrowserOverflowMenu.tsx
  • apps/desktop/src/renderer/stores/browser-bookmarks.ts

📝 Walkthrough

Walkthrough

デスクトップ版でブックマークを階層ツリー化し、フォルダ編集・DnD・インポート/エクスポートとElectronベースのファイルI/O(openTextFile/saveTextFile)を追加しました。webview相互作用の多重ロック制御も導入されています。

Changes

Cohort / File(s) Summary
TRPC 外部ルーター
apps/desktop/src/lib/trpc/routers/external/index.ts
ElectronのダイアログとNode fs を使うopenTextFile/saveTextFile TRPCミューテーションを追加(ファイル選択・読み書き、キャンセル処理、エラーTRPCError)。
ブックマーク状態管理(ツリー化)
apps/desktop/src/renderer/stores/browser-bookmarks.ts
ブックマークをフラット配列からツリー(bookmark/folderノード)へ移行。folder関連型・ユーティリティ・APIを追加(addFolder/updateFolder/duplicateBookmark/moveNode/removeNode/importBookmarks 等)。永続化バージョンとマイグレーションも更新。
ブックマークHTML入出力
apps/desktop/src/renderer/stores/browser-bookmarks-html.ts
Netscape形式のブックマークHTMLを生成・解析するexportBrowserBookmarksToHtml/importBrowserBookmarksFromHtmlを追加(HTMLパース、タイムスタンプ処理、URL正規化)。
ブックマークUI — BookmarkBar 系
.../BookmarkBar/BookmarkBar.tsx, .../BookmarkBarItem/BookmarkBarItem.tsx
DnDのcollision/axis制約導入、ドラッグ開始時のwebviewインタラクションロック、メニューにコピー操作を追加。BookmarkButton/SortableTriggerへ分割。renderでフォルダとブックマークを分岐。
ブックマークフォルダUI
.../BookmarkFolderItem/*, .../BookmarkFolderDialog/*, .../BookmarkFolderItem/index.ts
フォルダのドロップダウン・ネスト表示・フォルダ編集ダイアログ・フォルダ用DnDロジックとAPI連携を新規追加。
ブラウザツールバー(Import/Export 統合)
.../BrowserOverflowMenu/BrowserOverflowMenu.tsx
メニューに「新規フォルダ」「インポート」「エクスポート」を追加。TRPCのopenTextFile/saveTextFileとブックマークHTMLヘルパーを組み合わせた入出力処理とトースト通知を実装。
永続的 webview インタラクション管理
.../hooks/usePersistentWebview/usePersistentWebview.ts, .../hooks/usePersistentWebview/index.ts
モジュールレベルのロック集合を導入しsetPersistentWebviewInteractionLockを追加。複数ロックの有無でwebviewのpointerEventsを制御。window drag イベントはロックAPIを利用するよう変更。
パフォーマンス最適化(BrowserPane)
apps/desktop/src/renderer/.../BrowserPane/BrowserPane.tsx
currentBookmarkの検索を毎レンダリングからuseMemoでキャッシュするよう変更。
フォルダアイコン定義
apps/desktop/src/renderer/stores/browser-bookmark-folder-icons.tsx
フォルダ用アイコンマップ・型定義・オプション配列と取得ユーティリティを追加。
小さなルート変更
apps/desktop/src/renderer/.../ChangesHeader/ChangesHeader.tsx
Fetch mutation を electronTrpc.changes.fetch に差し替え。

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant UI as BrowserOverflowMenu
    participant TRPC as electronTrpc
    participant Electron
    participant Store as BookmarkStore

    User->>UI: Click "Import Bookmarks"
    UI->>TRPC: openTextFile(request filters)
    TRPC->>Electron: showOpenDialog()
    Electron-->>TRPC: path (or cancel)
    TRPC-->>UI: { path, content } or null
    UI->>Store: importBrowserBookmarksFromHtml(content)
    Store-->>UI: { bookmarksAdded, foldersAdded }
    UI-->>User: show success/error toast
Loading
sequenceDiagram
    actor User
    participant Bar as BookmarkBar
    participant Dnd as DndContext
    participant Lock as usePersistentWebview
    participant Store as BookmarkStore

    User->>Bar: Drag bookmark
    Bar->>Dnd: start drag
    Dnd->>Lock: setPersistentWebviewInteractionLock("drag", true)
    User->>Bar: Drop into folder
    Dnd->>Bar: onDragEnd(active, over)
    Bar->>Store: moveNode(activeId, overId)
    Store-->>Bar: updated tree
    Bar->>Lock: setPersistentWebviewInteractionLock("drag", false)
    Bar-->>User: update UI
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Poem

🐰 フォルダに並べた私の宝庫
クリックで開く、ドラッグで整理
HTMLにのせておでかけもOK
にんじん片手にインポート/エクスポート♪
うさぎのブックマーク、ぴょんと整列!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed タイトルは「feat(desktop): enhance browser bookmarks」で、プルリクエストの主要な変更であるブラウザブックマーク機能の大幅な拡張を正確に反映しており、簡潔で明確である。
Description check ✅ Passed 説明には概要、参照する issue、検証ステップ、追加ノートが含まれており、テンプレートのほぼすべての主要セクションを満たしている。「Type of Change」セクションのみ未記入だが、本質的には「New feature」である。

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/enhance-browser-bm

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@MocA-Love MocA-Love self-assigned this Apr 3, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (12)
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/BookmarkBar/components/BookmarkBarItem/components/EditBookmarkDialog/EditBookmarkDialog.tsx (1)

14-17: FolderOption 型のエクスポートを検討してください

FolderOption インターフェースはこのファイル内でのみ定義されていますが、親コンポーネント(BookmarkBarItem)でも同じ構造を使用しています。型の再利用性を高めるために、この型をエクスポートすることを検討してください。

♻️ 提案する修正
-interface FolderOption {
+export interface FolderOption {
 	id: string;
 	label: string;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/BookmarkBar/components/BookmarkBarItem/components/EditBookmarkDialog/EditBookmarkDialog.tsx`
around lines 14 - 17, Export the FolderOption interface from EditBookmarkDialog
(add "export" to the interface declaration for FolderOption) so it can be
reused; then update the parent component BookmarkBarItem to import and use this
exported FolderOption type instead of duplicating the structure, ensuring both
EditBookmarkDialog and BookmarkBarItem reference the same FolderOption symbol.
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/BrowserToolbar/components/BrowserOverflowMenu/BrowserOverflowMenu.tsx (2)

117-132: エクスポート処理のエラーハンドリングを追加してください

saveTextFileMutation.mutateAsync が失敗した場合のエラーハンドリングがありません。ファイル書き込みエラー時にユーザーに通知できるようにしてください。

♻️ 提案する修正
 const handleExportBookmarks = async () => {
-  const content = exportBrowserBookmarksToHtml(bookmarks);
-  const saved = await saveTextFileMutation.mutateAsync({
-    title: "Export Bookmarks",
-    defaultPath: "bookmarks.html",
-    buttonLabel: "Export",
-    filters: [{ name: "Bookmarks HTML", extensions: ["html"] }],
-    content,
-  });
-
-  if (!saved) {
-    return;
-  }
-
-  toast.success("Bookmarks exported");
+  try {
+    const content = exportBrowserBookmarksToHtml(bookmarks);
+    const saved = await saveTextFileMutation.mutateAsync({
+      title: "Export Bookmarks",
+      defaultPath: "bookmarks.html",
+      buttonLabel: "Export",
+      filters: [{ name: "Bookmarks HTML", extensions: ["html"] }],
+      content,
+    });
+
+    if (!saved) {
+      return;
+    }
+
+    toast.success("Bookmarks exported");
+  } catch (error) {
+    toast.error("Failed to export bookmarks");
+  }
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/BrowserToolbar/components/BrowserOverflowMenu/BrowserOverflowMenu.tsx`
around lines 117 - 132, The export handler handleExportBookmarks should catch
errors from saveTextFileMutation.mutateAsync (and any thrown while generating
content via exportBrowserBookmarksToHtml) and notify the user on failure; wrap
the mutateAsync call in try/catch, call toast.error with a clear message on
failure (and optionally include error.message), and log the error (e.g.,
console.error or processLogger) before returning so the success toast is only
shown on true success.

93-115: インポート処理のエラーハンドリングを追加してください

openTextFileMutation.mutateAsyncimportBrowserBookmarksFromHtml が失敗した場合、例外がキャッチされずコンソールにのみエラーが出力されます。ユーザーに適切なフィードバックを提供するために、try-catch を追加してください。

♻️ 提案する修正
 const handleImportBookmarks = async () => {
+  try {
     const file = await openTextFileMutation.mutateAsync({
       title: "Import Bookmarks",
       buttonLabel: "Import",
       filters: [{ name: "Bookmarks HTML", extensions: ["html", "htm"] }],
     });

     if (!file) {
       return;
     }

     const importedNodes = importBrowserBookmarksFromHtml(file.content);
     const result = importBookmarks(importedNodes);

     if (result.bookmarksAdded === 0 && result.foldersAdded === 0) {
       toast.error("No bookmarks were imported");
       return;
     }

     toast.success(
       `Imported ${result.bookmarksAdded} bookmarks and ${result.foldersAdded} folders`,
     );
+  } catch (error) {
+    toast.error("Failed to import bookmarks");
+  }
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/BrowserToolbar/components/BrowserOverflowMenu/BrowserOverflowMenu.tsx`
around lines 93 - 115, Wrap the entire body of handleImportBookmarks in a
try-catch so any exceptions from openTextFileMutation.mutateAsync,
importBrowserBookmarksFromHtml, or importBookmarks are caught; in the catch
block call toast.error with a user-friendly message (include error.message for
debugging) and optionally log the error to console/diagnostics, then return to
avoid further processing — update references in handleImportBookmarks,
openTextFileMutation.mutateAsync, importBrowserBookmarksFromHtml,
importBookmarks, and toast.error accordingly.
apps/desktop/src/lib/trpc/routers/external/index.ts (2)

263-271: ファイル書き込み時のエラーハンドリングを追加してください

writeFile もディスク容量不足や書き込み権限がない場合に失敗する可能性があります。openTextFile と同様に、明示的なエラーハンドリングを追加することを推奨します。

♻️ 提案する修正
 			if (result.canceled || !result.filePath) {
 				return null;
 			}

-			await writeFile(result.filePath, input.content, "utf-8");
-			return {
-				path: result.filePath,
-			};
+			try {
+				await writeFile(result.filePath, input.content, "utf-8");
+				return {
+					path: result.filePath,
+				};
+			} catch (error) {
+				throw new TRPCError({
+					code: "INTERNAL_SERVER_ERROR",
+					message: error instanceof Error ? error.message : "Failed to write file",
+				});
+			}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/lib/trpc/routers/external/index.ts` around lines 263 - 271,
The resolver currently calls writeFile without error handling (see writeFile and
the surrounding code handling result.canceled/result.filePath); wrap the await
writeFile(...) in a try/catch, and on failure either throw a descriptive
TRPCError (including the original error message) or return a null/structured
error consistent with openTextFile's behavior so callers get meaningful
feedback; ensure the catch includes context (e.g., "failed to write file" + file
path) and preserves the original error for logging/inspection.

229-239: ファイル読み込み時のエラーハンドリングを追加してください

readFile はファイルが存在しない、読み取り権限がない、エンコーディングエラーなど様々な理由で失敗する可能性があります。現在の実装では例外がそのまま伝播しますが、ユーザーフレンドリーなエラーメッセージを返すために明示的なエラーハンドリングを検討してください。

♻️ 提案する修正
 			const filePath = result.filePaths[0];
 			if (!filePath) {
 				return null;
 			}

-			const content = await readFile(filePath, "utf-8");
-			return {
-				path: filePath,
-				content,
-			};
+			try {
+				const content = await readFile(filePath, "utf-8");
+				return {
+					path: filePath,
+					content,
+				};
+			} catch (error) {
+				throw new TRPCError({
+					code: "INTERNAL_SERVER_ERROR",
+					message: error instanceof Error ? error.message : "Failed to read file",
+				});
+			}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/lib/trpc/routers/external/index.ts` around lines 229 - 239,
Wrap the await readFile(...) call in a try/catch and convert filesystem errors
into a user-friendly error response: catch exceptions thrown by readFile (when
reading result.filePaths[0]) and either throw a trpc-compatible error (e.g., new
TRPCError with an appropriate code and a concise message) or return a structured
object containing a clear message and non-sensitive detail; also log or attach
the original error (as cause) for debugging. Ensure you keep the existing null
check for filePath and only perform the try/catch around the readFile and
subsequent return of { path: filePath, content }.
apps/desktop/src/renderer/stores/browser-bookmarks-html.ts (2)

41-45: parseTimestampDate.now()フォールバックが重複呼び出しになる可能性があります。

add_date属性がない場合、毎回Date.now()が呼ばれますが、大量のブックマークをインポートする際に微妙に異なるタイムスタンプが設定されます。意図した動作であれば問題ありませんが、一貫性が必要な場合はインポート開始時に一度だけ取得することを検討してください。

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

In `@apps/desktop/src/renderer/stores/browser-bookmarks-html.ts` around lines 41 -
45, The parseTimestamp function currently calls Date.now() each time it needs a
fallback, causing slightly different timestamps across many imports; change
parseTimestamp to accept an optional fallback timestamp parameter (e.g.,
parseTimestamp(value: string | null, fallbackMs?: number)) and use that when
value is missing or invalid, and ensure the import routine that iterates
bookmarks captures a single fallback timestamp once at start (e.g., const
fallbackMs = Date.now()) and passes it into parseTimestamp for each call so all
fallbacks are identical.

7-13: 手動HTMLエスケープは、このユースケースでは許容範囲です。

静的解析ツールが手動HTML sanitizationを警告していますが、このコンテキストでは以下の理由で問題ありません:

  1. escapeHtmlはエクスポート専用で、内部データをNetscape形式のHTMLファイルに出力するために使用
  2. 出力はファイルに保存されるため、DOMインジェクションのリスクはない
  3. エスケープ順序(&が最初)は正しい

ただし、将来的にファイルがブラウザで直接開かれる可能性を考慮すると、シングルクォートのエスケープ追加を検討してください。

♻️ シングルクォートエスケープの追加(オプション)
 function escapeHtml(value: string): string {
 	return value
 		.replaceAll("&", "&")
 		.replaceAll("<", "&lt;")
 		.replaceAll(">", "&gt;")
-		.replaceAll('"', "&quot;");
+		.replaceAll('"', "&quot;")
+		.replaceAll("'", "&#39;");
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/stores/browser-bookmarks-html.ts` around lines 7 -
13, The static analyzer warning can be addressed by extending the existing
escapeHtml function to also escape single quotes; update the escapeHtml function
to include a .replaceAll("'", "&#39;") step (keeping the existing order that
replaces "&" first) so values output to the Netscape HTML file also have single
quotes escaped, while leaving the rest of the logic in escapeHtml unchanged.
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/BookmarkBar/components/BookmarkFolderItem/BookmarkFolderItem.tsx (1)

192-206: インタラクションロックの管理が2つのuseEffectに分かれています。

Lines 192-199と201-206で同じmenuLockIdを管理していますが、責務が明確に分離されています:

  • 最初のuseEffectはアンマウント時のクリーンアップ(タイマー含む)
  • 2番目はisMenuOpen状態との同期

ただし、201-206のクリーンアップ(203-205)が毎回falseを設定するため、メニューが開いている状態でコンポーネントが再レンダリングされると、一瞬ロックが解除される可能性があります。

♻️ クリーンアップの統合(オプション)
 useEffect(() => {
 	setPersistentWebviewInteractionLock(menuLockId, isMenuOpen);
-	return () => {
-		setPersistentWebviewInteractionLock(menuLockId, false);
-	};
 }, [isMenuOpen, menuLockId]);

アンマウント時のクリーンアップは最初のuseEffectで既に処理されているため、2番目のクリーンアップは不要な可能性があります。

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

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/BookmarkBar/components/BookmarkFolderItem/BookmarkFolderItem.tsx`
around lines 192 - 206, The two useEffect hooks both manage menuLockId causing a
transient unlock during re-renders; remove the cleanup that sets
setPersistentWebviewInteractionLock(menuLockId, false) from the second useEffect
(the one depending on [isMenuOpen, menuLockId]) so it only syncs the lock to
isMenuOpen, and keep the unmount/timer cleanup (pendingOpenTimerRef and the
false reset) in the first useEffect; alternatively merge the logic into a single
useEffect that clears pendingOpenTimerRef on unmount and syncs
setPersistentWebviewInteractionLock(menuLockId, isMenuOpen) during updates to
avoid momentary unlocks.
apps/desktop/src/renderer/stores/browser-bookmarks.ts (3)

700-709: バージョン1から3へのマイグレーションについて。

version: 3に直接ジャンプしていますが、migrate関数は任意の古いデータをsanitizeLegacyNodesで処理するため、バージョン2のデータも適切に処理されます。

ただし、persistedStateisRecordでない場合に空のbookmarks配列を返すのは正しいですが、バージョン情報のロギングがないため、マイグレーション問題のデバッグが困難になる可能性があります。

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

In `@apps/desktop/src/renderer/stores/browser-bookmarks.ts` around lines 700 -
709, The migration currently jumps to version: 3 and sanitizes legacy data via
migrate(persistedState) calling sanitizeLegacyNodes, but it lacks logging to
help debug migration issues; update the migrate function (referencing migrate,
isRecord, sanitizeLegacyNodes, and bookmarks) to log the incoming persistedState
version when present and to emit a warning/error (using the module's logger or
console.warn) when persistedState is not an object and you return { bookmarks:
[] }, so you can trace which version/data caused the fallback and confirm
migrations ran for version 2→3 cases.

173-191: findBookmarkParentFolderIdの戻り値の曖昧さについて。

この関数は以下の2つのケースでnullを返します:

  1. ブックマークがルートレベルにある場合
  2. ブックマークが見つからない場合

呼び出し側でこれらのケースを区別できない可能性があります。ただし、現在の使用箇所(BookmarkBarItem)では、対象ブックマークが必ず存在するコンテキストで呼ばれるため、実用上は問題ありません。

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

In `@apps/desktop/src/renderer/stores/browser-bookmarks.ts` around lines 173 -
191, findBookmarkParentFolderId currently returns null both when a bookmark is
at root and when it's not found, causing ambiguity; change its signature to
return a discriminated result like { found: boolean; parentFolderId: string |
null } (or similar) so callers can distinguish "found-but-root" (found: true,
parentFolderId: null) vs "not found" (found: false). Update the implementation
(function findBookmarkParentFolderId) to return { found: true, parentFolderId:
... } on match and { found: false, parentFolderId: null } at the end, and update
all callers (e.g., BookmarkBarItem) to check the found flag before using
parentFolderId.

560-578: ブックマーク更新時の「削除→再挿入」パターンについて。

updateBookmarkは対象ブックマークを一度ツリーから削除し、指定されたfolderIdに再挿入します。folderIdfalsyの場合はルートに追加されます(line 574)。

この設計は明確ですが、ユーザーが意図せずブックマークをルートに移動してしまう可能性があります。folderIdが明示的にnullの場合のみルート移動とし、undefinedの場合は元の位置を維持する方が安全かもしれません。

♻️ folderId未指定時は元の位置を維持(推奨)
 set((state) => {
+	const currentFolderId = findBookmarkParentFolderId(state.bookmarks, bookmarkId);
 	const removed = removeNodeFromTree(state.bookmarks, bookmarkId);
 	let nextNodes = removed.nodes;

-	if (bookmark.folderId) {
+	const targetFolderId = bookmark.folderId === undefined ? currentFolderId : bookmark.folderId;
+	if (targetFolderId) {
 		const inserted = insertNodeIntoFolder(
 			nextNodes,
 			updatedBookmark,
-			bookmark.folderId,
+			targetFolderId,
 		);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/stores/browser-bookmarks.ts` around lines 560 -
578,
現在の実装はbookmark.folderIdがfalsyだと常にルートに移動してしまうため、bookmark.folderIdがundefinedの場合は元の位置を維持し、nullのみルート移動とするよう修正してください:
removeNodeFromTreeの戻り値(削除したノードの元の親情報)から元のフォルダIDを取得し、bookmark.folderId ===
undefinedならその元フォルダIDを使ってinsertNodeIntoFolderに再挿入、bookmark.folderId ===
nullならルートへ、それ以外はbookmark.folderIdを使って再挿入するロジックに変更してください(参照シンボル:
removeNodeFromTree, insertNodeIntoFolder, bookmark.folderId, updatedBookmark)。
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/BookmarkFolderDialog/BookmarkFolderDialog.tsx (1)

71-77: フォーム送信時のバリデーションが不足しています。

空のタイトルでもそのままonSaveに渡されます。ストア側で"Untitled Folder"にフォールバックしていますが、UIレベルでの最小限のバリデーション(空白のみのタイトル禁止など)を追加することを検討してください。

♻️ 空タイトルの防止(オプション)
 <form
 	className="space-y-4"
 	onSubmit={(event) => {
 		event.preventDefault();
-		onSave({ title, iconKey, color });
+		onSave({ title: title.trim() || "Untitled Folder", iconKey, color });
 	}}
 >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/BookmarkFolderDialog/BookmarkFolderDialog.tsx`
around lines 71 - 77, The form's onSubmit in BookmarkFolderDialog currently
calls onSave with title even if it's empty/whitespace; update the onSubmit
handler in the BookmarkFolderDialog component to trim title and validate it
(e.g., const cleaned = title.trim()) and if cleaned is empty, prevent calling
onSave and set a local validation state (e.g., titleError or isTitleInvalid) so
the UI can show an inline error and keep the dialog open; only call onSave({
title: cleaned, iconKey, color }) when the title passes validation. Ensure the
new validation state is declared in the component and used to render a brief
error message near the title input.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/BookmarkBar/components/BookmarkFolderItem/BookmarkFolderItem.tsx`:
- Around line 121-144: Folder folder nodes are rendered as plain <div> in
BookmarkFolderItem/FolderTreeSection so they aren't draggable; wrap the folder
node rendering with the same sortable wiring used by BookmarkBarItem: call
useSortable (or the sortable HOC) for the folder node, apply the returned
attributes/listeners/style to the folder container and expose a drag handle UI,
and pass the sortable state into FolderTreeSection props so onReorder calls
(which should invoke the store function reorderFolderChildren) receive the
correct source/target indexes and folderId; update
FolderTreeSection/BookmarkFolderItem to accept and forward a sortable prop for
folder nodes and ensure onReorder triggers reorderFolderChildren with the
folderId and new children order.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/BookmarkFolderDialog/BookmarkFolderDialog.tsx`:
- Around line 58-63: The current useEffect in the BookmarkFolderDialog component
resets state whenever initialTitle/initialIconKey/initialColor change while open
is true; change it so the state is initialized only when the dialog opens (open
transitions false→true). Implement this by tracking previous open (e.g., a
useRef or usePrevious) and in the effect run setTitle(initialTitle),
setIconKey(initialIconKey), setColor(initialColor ?? "#64748b") only when open
is true and prevOpen was false; keep the effect's dependency array minimal
(include open and the initial values if you prefer but gate execution on the
rising edge) and reference the existing identifiers: BookmarkFolderDialog, open,
initialTitle, initialIconKey, initialColor, setTitle, setIconKey, setColor.

In `@apps/desktop/src/renderer/stores/browser-bookmarks.ts`:
- Around line 387-439: cloneImportedNodes currently overwrites original
createdAt values with Date.now(); update it to preserve any existing createdAt
from input nodes for both bookmarks and folders: when handling
isBrowserBookmark(node) use node.createdAt if present (fallback to Date.now()),
and when creating folder nodes use nested child node.createdAt if node.createdAt
exists (fallback to Date.now()); keep existing id generation, title
normalization, and children assignment unchanged so imported add_date timestamps
from parseBookmarkList are retained.

---

Nitpick comments:
In `@apps/desktop/src/lib/trpc/routers/external/index.ts`:
- Around line 263-271: The resolver currently calls writeFile without error
handling (see writeFile and the surrounding code handling
result.canceled/result.filePath); wrap the await writeFile(...) in a try/catch,
and on failure either throw a descriptive TRPCError (including the original
error message) or return a null/structured error consistent with openTextFile's
behavior so callers get meaningful feedback; ensure the catch includes context
(e.g., "failed to write file" + file path) and preserves the original error for
logging/inspection.
- Around line 229-239: Wrap the await readFile(...) call in a try/catch and
convert filesystem errors into a user-friendly error response: catch exceptions
thrown by readFile (when reading result.filePaths[0]) and either throw a
trpc-compatible error (e.g., new TRPCError with an appropriate code and a
concise message) or return a structured object containing a clear message and
non-sensitive detail; also log or attach the original error (as cause) for
debugging. Ensure you keep the existing null check for filePath and only perform
the try/catch around the readFile and subsequent return of { path: filePath,
content }.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/BookmarkBar/components/BookmarkBarItem/components/EditBookmarkDialog/EditBookmarkDialog.tsx`:
- Around line 14-17: Export the FolderOption interface from EditBookmarkDialog
(add "export" to the interface declaration for FolderOption) so it can be
reused; then update the parent component BookmarkBarItem to import and use this
exported FolderOption type instead of duplicating the structure, ensuring both
EditBookmarkDialog and BookmarkBarItem reference the same FolderOption symbol.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/BookmarkBar/components/BookmarkFolderItem/BookmarkFolderItem.tsx`:
- Around line 192-206: The two useEffect hooks both manage menuLockId causing a
transient unlock during re-renders; remove the cleanup that sets
setPersistentWebviewInteractionLock(menuLockId, false) from the second useEffect
(the one depending on [isMenuOpen, menuLockId]) so it only syncs the lock to
isMenuOpen, and keep the unmount/timer cleanup (pendingOpenTimerRef and the
false reset) in the first useEffect; alternatively merge the logic into a single
useEffect that clears pendingOpenTimerRef on unmount and syncs
setPersistentWebviewInteractionLock(menuLockId, isMenuOpen) during updates to
avoid momentary unlocks.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/BookmarkFolderDialog/BookmarkFolderDialog.tsx`:
- Around line 71-77: The form's onSubmit in BookmarkFolderDialog currently calls
onSave with title even if it's empty/whitespace; update the onSubmit handler in
the BookmarkFolderDialog component to trim title and validate it (e.g., const
cleaned = title.trim()) and if cleaned is empty, prevent calling onSave and set
a local validation state (e.g., titleError or isTitleInvalid) so the UI can show
an inline error and keep the dialog open; only call onSave({ title: cleaned,
iconKey, color }) when the title passes validation. Ensure the new validation
state is declared in the component and used to render a brief error message near
the title input.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/BrowserToolbar/components/BrowserOverflowMenu/BrowserOverflowMenu.tsx`:
- Around line 117-132: The export handler handleExportBookmarks should catch
errors from saveTextFileMutation.mutateAsync (and any thrown while generating
content via exportBrowserBookmarksToHtml) and notify the user on failure; wrap
the mutateAsync call in try/catch, call toast.error with a clear message on
failure (and optionally include error.message), and log the error (e.g.,
console.error or processLogger) before returning so the success toast is only
shown on true success.
- Around line 93-115: Wrap the entire body of handleImportBookmarks in a
try-catch so any exceptions from openTextFileMutation.mutateAsync,
importBrowserBookmarksFromHtml, or importBookmarks are caught; in the catch
block call toast.error with a user-friendly message (include error.message for
debugging) and optionally log the error to console/diagnostics, then return to
avoid further processing — update references in handleImportBookmarks,
openTextFileMutation.mutateAsync, importBrowserBookmarksFromHtml,
importBookmarks, and toast.error accordingly.

In `@apps/desktop/src/renderer/stores/browser-bookmarks-html.ts`:
- Around line 41-45: The parseTimestamp function currently calls Date.now() each
time it needs a fallback, causing slightly different timestamps across many
imports; change parseTimestamp to accept an optional fallback timestamp
parameter (e.g., parseTimestamp(value: string | null, fallbackMs?: number)) and
use that when value is missing or invalid, and ensure the import routine that
iterates bookmarks captures a single fallback timestamp once at start (e.g.,
const fallbackMs = Date.now()) and passes it into parseTimestamp for each call
so all fallbacks are identical.
- Around line 7-13: The static analyzer warning can be addressed by extending
the existing escapeHtml function to also escape single quotes; update the
escapeHtml function to include a .replaceAll("'", "&#39;") step (keeping the
existing order that replaces "&" first) so values output to the Netscape HTML
file also have single quotes escaped, while leaving the rest of the logic in
escapeHtml unchanged.

In `@apps/desktop/src/renderer/stores/browser-bookmarks.ts`:
- Around line 700-709: The migration currently jumps to version: 3 and sanitizes
legacy data via migrate(persistedState) calling sanitizeLegacyNodes, but it
lacks logging to help debug migration issues; update the migrate function
(referencing migrate, isRecord, sanitizeLegacyNodes, and bookmarks) to log the
incoming persistedState version when present and to emit a warning/error (using
the module's logger or console.warn) when persistedState is not an object and
you return { bookmarks: [] }, so you can trace which version/data caused the
fallback and confirm migrations ran for version 2→3 cases.
- Around line 173-191: findBookmarkParentFolderId currently returns null both
when a bookmark is at root and when it's not found, causing ambiguity; change
its signature to return a discriminated result like { found: boolean;
parentFolderId: string | null } (or similar) so callers can distinguish
"found-but-root" (found: true, parentFolderId: null) vs "not found" (found:
false). Update the implementation (function findBookmarkParentFolderId) to
return { found: true, parentFolderId: ... } on match and { found: false,
parentFolderId: null } at the end, and update all callers (e.g.,
BookmarkBarItem) to check the found flag before using parentFolderId.
- Around line 560-578:
現在の実装はbookmark.folderIdがfalsyだと常にルートに移動してしまうため、bookmark.folderIdがundefinedの場合は元の位置を維持し、nullのみルート移動とするよう修正してください:
removeNodeFromTreeの戻り値(削除したノードの元の親情報)から元のフォルダIDを取得し、bookmark.folderId ===
undefinedならその元フォルダIDを使ってinsertNodeIntoFolderに再挿入、bookmark.folderId ===
nullならルートへ、それ以外はbookmark.folderIdを使って再挿入するロジックに変更してください(参照シンボル:
removeNodeFromTree, insertNodeIntoFolder, bookmark.folderId, updatedBookmark)。
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1b2a6fe8-0fcc-4414-a8a6-d1ec41f1a7ca

📥 Commits

Reviewing files that changed from the base of the PR and between 9e555d9 and 75dd6ff.

📒 Files selected for processing (16)
  • apps/desktop/src/lib/trpc/routers/external/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/BrowserPane.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/BookmarkBar/BookmarkBar.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/BookmarkBar/components/BookmarkBarItem/BookmarkBarItem.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/BookmarkBar/components/BookmarkBarItem/components/EditBookmarkDialog/EditBookmarkDialog.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/BookmarkBar/components/BookmarkFolderItem/BookmarkFolderItem.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/BookmarkBar/components/BookmarkFolderItem/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/BookmarkFolderDialog/BookmarkFolderDialog.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/BookmarkFolderDialog/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/BrowserToolbar/components/BrowserOverflowMenu/BrowserOverflowMenu.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/hooks/usePersistentWebview/index.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/hooks/usePersistentWebview/usePersistentWebview.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/ChangesView/components/ChangesHeader/ChangesHeader.tsx
  • apps/desktop/src/renderer/stores/browser-bookmark-folder-icons.tsx
  • apps/desktop/src/renderer/stores/browser-bookmarks-html.ts
  • apps/desktop/src/renderer/stores/browser-bookmarks.ts

Comment on lines +58 to +63
useEffect(() => {
if (!open) return;
setTitle(initialTitle);
setIconKey(initialIconKey);
setColor(initialColor ?? "#64748b");
}, [initialColor, initialIconKey, initialTitle, open]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 呼び出し元でinitial*プロパティがダイアログ表示中に変更されるかを確認
rg -n "BookmarkFolderDialog" --type=tsx -A 10 | head -100

Repository: MocA-Love/superset

Length of output: 89


🏁 Script executed:

fd "BookmarkFolderDialog.tsx" --type f

Repository: MocA-Love/superset

Length of output: 226


🏁 Script executed:

cat -n apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/BookmarkFolderDialog/BookmarkFolderDialog.tsx | head -80

Repository: MocA-Love/superset

Length of output: 2806


🏁 Script executed:

wc -l apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/BookmarkFolderDialog/BookmarkFolderDialog.tsx

Repository: MocA-Love/superset

Length of output: 230


🏁 Script executed:

cat -n apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/BookmarkFolderDialog/BookmarkFolderDialog.tsx | tail -110

Repository: MocA-Love/superset

Length of output: 4120


🏁 Script executed:

rg "BookmarkFolderDialog" --type ts -A 5 -B 5 | head -150

Repository: MocA-Love/superset

Length of output: 15829


🏁 Script executed:

rg "BookmarkFolderItem" --type ts -A 20 -B 5 | grep -A 20 "isEditOpen\|setIsEditOpen"

Repository: MocA-Love/superset

Length of output: 4455


🏁 Script executed:

cat -n apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/BookmarkBar/components/BookmarkFolderItem/BookmarkFolderItem.tsx | grep -A 30 "BookmarkFolderDialog"

Repository: MocA-Love/superset

Length of output: 1814


🏁 Script executed:

wc -l apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/BookmarkBar/components/BookmarkFolderItem/BookmarkFolderItem.tsx

Repository: MocA-Love/superset

Length of output: 249


useEffectの依存配列の改善:ダイアログ開く時のみ状態を同期するように変更してください。

現在のコードは、initialTitleinitialIconKeyinitialColorが変更されるとダイアログが開いている間でも状態がリセットされます。実際の使用パターンではダイアログがすぐに閉じるため問題になっていませんが、親コンポーネントの再レンダリングでfolder参照が変わると意図しないリセットが起こる可能性があります。

ダイアログを開く際だけ状態を初期化するよう改善することで、より堅牢なコードになります。例えば、openfalseからtrueに変わった時のみリセットする実装が推奨されます。

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

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/BrowserPane/components/BookmarkFolderDialog/BookmarkFolderDialog.tsx`
around lines 58 - 63, The current useEffect in the BookmarkFolderDialog
component resets state whenever initialTitle/initialIconKey/initialColor change
while open is true; change it so the state is initialized only when the dialog
opens (open transitions false→true). Implement this by tracking previous open
(e.g., a useRef or usePrevious) and in the effect run setTitle(initialTitle),
setIconKey(initialIconKey), setColor(initialColor ?? "#64748b") only when open
is true and prevOpen was false; keep the effect's dependency array minimal
(include open and the initial values if you prefer but gate execution on the
rising edge) and reference the existing identifiers: BookmarkFolderDialog, open,
initialTitle, initialIconKey, initialColor, setTitle, setIconKey, setColor.

Comment thread apps/desktop/src/renderer/stores/browser-bookmarks.ts
@MocA-Love MocA-Love merged commit 986978f into main Apr 3, 2026
5 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Apr 3, 2026
MocA-Love added a commit that referenced this pull request Apr 14, 2026
PR #51 以降に追加された機能・改善をまとめて記載。
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.

[feat] 内臓ブラウザのブックマーク関連強化

1 participant