Skip to content

upstream merge 2026-04-27 PR-A: UI/behavior fixes + hosts settings (12 commits)#445

Merged
MocA-Love merged 13 commits intomainfrom
upstream/batch-2026-04-27-pr-a
Apr 27, 2026
Merged

upstream merge 2026-04-27 PR-A: UI/behavior fixes + hosts settings (12 commits)#445
MocA-Love merged 13 commits intomainfrom
upstream/batch-2026-04-27-pr-a

Conversation

@MocA-Love
Copy link
Copy Markdown
Owner

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

Summary

upstream (superset-sh/superset) から fork (MocA-Love/superset) への 2026-04-27 バッチ取り込み 第1弾。22 commits を 3 PR に分割した中の PR-A: UI / behavior fixes + hosts settings 基盤 (12 commits)。

DB migration 系・machineId 統合は PR-B、chat / plans / version bump は PR-C で別途対応。

取り込み内容 (chronological)

SHA PR# 概要
8afa203f5 upstream superset-sh#3741 sidebar 削除と再レンダーちらつき修正
57d450d41 upstream superset-sh#3765 workspace クリック時 v2 ready 通知をクリア
60ff70b23 upstream superset-sh#3768 hosts settings ページ追加 (host アクセス管理)
fa301c4da upstream superset-sh#3769 v2 terminal ドロップ時のホバーアニメ改善
270279a51 upstream superset-sh#3771 v2 sidebar の workspace 削除時 nav-away 修正
4b8a7a84d upstream superset-sh#3772 automation list の初期ロード時ちらつき修正
cb6a9dc05 upstream superset-sh#3773 v2 mark workspace as unread 機能追加
9f1fee2c9 upstream superset-sh#3774 session dropdown で他 workspace の terminal を非表示
8ca220ad4 upstream superset-sh#3776 v2 diff ファイルヘッダレイアウトの締め直し
9616010f4 upstream superset-sh#3767 削除後のアクティブアイテム選択修正
95818ed2d upstream superset-sh#3787 v2 ports dropdown から赤色 failed-host インジケータ削除
26fecf141 upstream superset-sh#3746 v2 workspace sidebar sync の hotfix

加えて fork 固有の解消 commit 42a3f0e0e fix(merge): resolve workspace item v2-unread conflicts を含む。

Fork 側のコンフリクト解決

apps/desktop/src/renderer/routes/_authenticated/settings/

60ff70b23 (hosts settings) で fork 既存の Diagnostics / Metrics / ServiceStatus / VSCodeExtensions を維持しつつ hosts セクションを追加。upstream の experimental セクションは fork で削除済みのため取り込まず。

apps/desktop/.../TerminalPane.tsx / usePaneRegistry.tsx / TerminalSessionDropdown.tsx

fa301c4da (terminal ドロップアニメ) と 9f1fee2c9 (terminal session dropdown 簡素化) で fork の以下を維持:

  • TerminalPaneData の legacy 形式ガード (useMemo + crypto.randomUUID fallback)
  • terminal kill 時の確認 alert ダイアログ
  • browser automation highlight / file drag behavior

apps/desktop/.../DashboardSidebarWorkspaceItem.tsx (fix commit)

cb6a9dc05 (mark workspace unread) の auto-merge 残りを別 commit で解消。isUnread / onToggleUnread props は採用、<div hidden={isDeleting}> ラップと onRemoveFromSidebar={handleRemoveFromSidebar} への切替は 7c14f05 由来のため PR-B 取り込み時に対応。

apps/desktop/.../useDashboardSidebarWorkspaceItemActions.ts

9616010f4 (active item selection 修正) で fork が独自に使っていた getDeleteFocusTargetWorkspaceId が削除されたため、新しい @superset/panesgetActiveIdAfterRemoval に置換。

apps/desktop/.../pending/$pendingId/page.tsx

26fecf141 (sidebar sync hotfix) で ensureWorkspaceInSidebar のみ採用。hostUrl / session / activeOrganizationId の追加行は 9abd936 (machineId 統合) 由来のため PR-B で処理。

bun.lock

60ff70b23 で workspace version が 1.6.1 に書き換えられたが fork は v1.5.10-fork.N 運用を継続するため 1.5.10 に戻す。PR-C (c885eb212) 取り込み時に 1.6.2 へ更新する予定。

Fork 固有機能ヘルスチェック

baseline (/tmp/pr-2026-04-27-baseline/fork-features.txt) との比較で全項目健在を確認:

  • 19 個の独自 tRPC procedures (cleanupMissingWorktrees / createGitHubIssue / dispatchGitHubWorkflow / getCheckJobSteps / getGitHubRepositoryOverview / getGitHubWorkflowRuns / getJobLogs / getJobStatuses / getMissingWorktrees / getPullRequestIdentityCandidates / getWorkflowRunJobs / replyToPullRequestComment / rerunPullRequestChecks / setActiveSyncWorkspace / setPullRequestDraftState / setPullRequestThreadResolution / updatePullRequestAssignees / updatePullRequestReviewers / uploadGitHubIssueAsset)
  • fork 専用依存: ansi_up, @vscode/ripgrep, @xyflow/react
  • fork マーカー: TERMINAL_OPTIONS, SUPERSET_WORKSPACE_NAME, moonshot-ai.kimi-code, MainWindowEffects, INCEPTION_AUTH_PROVIDER_ID, v1MigrationState, TiptapPromptEditor
  • apps/desktop/electron-builder.tsdmg.size = "4g" 維持
  • apps/desktop/package.json version 1.5.10 維持
  • packages/db/drizzle/ 最大 idx 0035, packages/local-db/drizzle/ 最大 idx 0072 維持

Codex 事前調査

codex:codex-rescue --model gpt-5.5 --effort high で全 22 commits を解析。3 PR 分割案を採用。conflict 予測通りに解消できた。

Test plan

  • bun install 整合性 OK
  • bun run typecheck 全 task green (28 successful)
  • bun run lint biome green
  • fork baseline 14 項目すべて一致
  • (ローカル) bun run --filter @superset/desktop build 成功確認
  • (手動) hosts settings ページ初回表示
  • (手動) terminal session dropdown 操作 / mark workspace unread / sidebar sync

次のリファレンス

  • PR-B (DB migration / machineId / main workspace, 6 commits): 61e7eef05, 3efd92763, 88e250048, 16bcf961d, 7c14f05ba, 9abd93613
  • PR-C (chat / plans / version bump, 4 commits): 6c2a7719a, 4399b7398, 7c168e779, c885eb212

Summary by CodeRabbit

リリースノート

  • 新機能

    • ホスト管理機能を追加:メンバーの追加・削除とロール管理が可能に
    • ワークスペースの未読状態を切り替える機能を追加
  • バグ修正

    • サイドバーからの削除機能が確実に動作するように改善
    • ワークスペース削除時のナビゲーション動作を修正
  • UI/UXの改善

    • サイドバーの再レンダリングを削減し、パフォーマンスを向上
    • ファイルビューアのヘッダーレイアウトを改善
    • ターミナルセッションドロップダウンに「新規ターミナル」ボタンを追加
    • 不要なエラーメッセージを削除

Kitenite and others added 13 commits April 27, 2026 23:49
* add hosts settings page for managing host access

Adds a Hosts entry to the Organization group in settings, mirroring the
existing Projects two-pane layout: sub-sidebar lists the org's hosts,
detail page shows members with owner-only add/remove/role-change. New
v2Host tRPC router handles the membership mutations with owner-role
enforcement.

* wire host membership through optimistic collection actions

Move addMember/removeMember/setMemberRole to the same Electric pattern
as v2Projects/v2Workspaces: procedures wrap writes in dbWs.transaction
and return txid, the v2UsersHosts collection forwards mutations through
onInsert/onUpdate/onDelete, and HostSettings calls into
useOptimisticCollectionActions.v2UsersHosts. Failures roll the local
state back and surface a toast; success is the optimistic update.

Also breaks HostSettings into co-located subcomponents
(HostHeader, MembersTable, MemberRow, AddMemberDropdown) per AGENTS.md.

* toast on host membership persistence

Optimistic updates make changes appear instantly, but a successful
remove can feel like the click did nothing. Show "Member added /
removed / Role updated" once the server confirms persistence; failures
still rollback and surface the existing error toast (no double-fire).

* untrack runtime scheduled_tasks.lock

* move host membership success toasts to the component

The optimistic-actions hook should expose a transaction primitive,
not couple toast strings to the collection layer. HostSettings now
owns the success messaging via a tiny notifyOnPersist helper.

* remove .claude/scheduled_tasks.lock from .gitignore

* make host membership mutations atomic + protect creator

Pull last-owner guards inside the same dbWs.transaction as the write
and lock the other-owners rows with FOR UPDATE so concurrent removals
or demotions serialize. Switch addMember to a single ON CONFLICT DO
NOTHING insert that returns CONFLICT cleanly when the row already
exists. setMemberRole now errors with NOT_FOUND if the target has no
membership row instead of silently returning success.

Also block removing the host's createdByUser: their host service will
auto re-add them via ensureV2Host on next heartbeat, so the removal is
a confusing transient. Demoting them to member is still allowed
(ensureV2Host does onConflictDoNothing).

aria-label on the host filter input.

* drop self-action guards, protect creator from demotion

The "can't remove yourself" / "can't demote yourself" guards block
valid actions ("step back from admin"). The real invariants are
owner-count (last owner can't leave) and creator (the user whose
machine runs the host service must stay owner) — both already
enforced.

Adds the missing creator-demotion guard in setMemberRole so demoting
the createdByUser to member is refused. Also drops the matching
isSelf disabling in MemberRow so the UI no longer hides actions the
server now allows.
Drop the dashed border, rounded corners, and inner inset so the
overlay touches the pane edges. Keep the overlay mounted and fade
between opacity-0 and opacity-75 so the hover state actually
animates instead of popping in.
The dashboard sidebar's navigate-away check used useParams({ strict:
false }) from a layout-level component, which didn't reliably surface
the leaf route's workspaceId — so deletion silently no-op'd the nav and
left the user on the doomed workspace's URL. Switch to useMatchRoute
(the same pattern the sidebar item uses for isActive).
Wait for the Electric SQL automations collection to hydrate before
rendering the body. Without this gate, the empty-state placeholder
flashes for a frame or two before the real rows arrive.
- size the file badge to its content instead of stretching full-width
- move +/- LOC out of the badge and next to the right-side actions
- drop redundant Open in editor button (badge ⌘ click already does it)
- simplify file badge tooltip to just the click hint
* add v2 mark workspace as unread

Adds a "Mark as Unread"/"Mark as Read" toggle to the v2 sidebar
workspace context menu. Marking unread inserts a manual notification
source ({type:"manual",id:workspaceId}) with status "review", so the
workspace shows the same green dot as a real ready-for-review
notification and is auto-cleared by the existing
clearWorkspaceAttention call when the workspace is clicked.

* broaden mark-as-read to clear any review state

Toggle now reflects any review-status entry (manual or real
ready-for-review notification) and clears them all via
clearWorkspaceAttention. Drops the now-unused clearManualUnread
action.
…#3774)

Filter listSessions to the current workspace and drop the
cross-workspace grouping/navigation UI from the dropdown.
Also collapses the now-unused workspaceId tracking on
TerminalPaneData and the owner/session split inside
useGlobalTerminalLifecycle back to a single workspace
identity per terminal.
…uperset-sh#3787)

* fix(desktop): drop red failed-host indicator from v2 ports dropdown

* feat(desktop): mirror v1 ports docs help icon onto v2 sidebar
Call ensureWorkspaceInSidebar from useFireIntent right after the host
service returns. The existing post-create doNavigate effect also calls
it, but only fires while the user stays on the pending page and after
workspace sync completes; this guarantees the sidebar row appears even
if the user has navigated away or sync is slow.
PR-A の cherry-pick 中に 6ada9bf (v2 mark workspace as unread) が
DashboardSidebarWorkspaceItem.tsx で auto-merge できなかった残りを処理。
isUnread/onToggleUnread props は採用、isDeleting ラップ・
handleRemoveFromSidebar への切替は 7c14f05 由来のため
PR-B 取り込み時へ後送り。

bun.lock の workspace version は 211af80 取り込みで 1.6.1 に
書き換わったが fork は 1.5.10 維持運用なので戻す。
PR-C で c885eb2 取り込み時に 1.6.2 へ更新する。
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 27, 2026

📝 Walkthrough

ウォークスルー

このプルリクエストは、ワークスペース削除後のナビゲーションロジックを統一し、ホスト専用メンバーシップ管理機能を追加し、サイドバーとペインの再レンダリングを最適化するための広範な改善を実装しています。また、設定画面に新しいホスト管理セクションを追加し、通知状態とペーンフォーカスメカニズムを強化しています。

変更内容

コホート / ファイル(s) 概要
ワークスペース削除ユーティリティ
apps/desktop/src/renderer/react-query/workspaces/utils/workspace-removal.*, .../workspace-removal.test.ts
削除後のフォーカスターゲット計算とグループ化されたワークスペースからの削除ロジックを統一する新しいユーティリティモジュールを追加。グループ構造内でのワークスペースIDの順序付きリストを決定し、適切なフォーカス対象を計算します。
ワークスペース削除/クローズミューテーション
apps/desktop/src/renderer/react-query/workspaces/useCloseWorkspace.ts, useDeleteWorkspace.ts
新しいremoveWorkspaceFromGroupsヘルパーとフォーカスターゲット計算を使用し、最適化更新ロジックをリファクタリング。onMutate時のナビゲーション判定を改善。
ワークスペース削除後のナビゲーション
apps/desktop/src/renderer/routes/.../_dashboard/components/DashboardSidebar/utils/getDeleteFocusTargetWorkspaceId/*, .../useNavigateAwayFromWorkspace.ts, .../useDashboardSidebarWorkspaceItemActions.ts
削除されたワークスペース用に非推奨ユーティリティを削除し、新しいgetActiveIdAfterRemovalに統一。ルートマッチングロジックを改善。
ホストメンバーシップ管理
apps/desktop/src/renderer/routes/_authenticated/settings/hosts/$hostId/components/HostSettings/*, .../HostsSettingsSidebar/*, apps/desktop/src/renderer/routes/_authenticated/settings/hosts/page.tsx, layout.tsx
ホスト詳細ページ、メンバー管理UI、メンバー追加ドロップダウン、ホストヘッダーコンポーネント、そしてホスト一覧と詳細ビューをサポートする新しい設定ルートを追加。
ホストメンバーシップTRPC
packages/trpc/src/router/v2-host/*
認可検証と共同トランザクション処理を含むメンバー追加/削除/ロール変更のための新しい保護ミューテーションを追加。
コレクション統合
apps/desktop/src/renderer/routes/_authenticated/hooks/useOptimisticCollectionActions/useOptimisticCollectionActions.ts, apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/collections.ts
ホストメンバーシップ操作用の楽観的更新ハンドラーを追加。v2UsersHosts書き込みハンドラーを実装。
サイドバー再レンダリング最適化
apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/useDashboardSidebarData.ts, useDashboardSidebarShortcuts.ts, .../DashboardSidebar.tsx
ローカルワークスペースIDリスト、PR Map、プロジェクトオブジェクト参照、ショートカットラベルMapの参照安定性を実装。SortableProjectWrapperをmemoで包装。
設定基盤
apps/desktop/src/renderer/routes/_authenticated/settings/components/SettingsSidebar/GeneralSettings.tsx, layout.tsx, apps/desktop/src/renderer/stores/settings-state.ts, apps/desktop/src/renderer/routes/_authenticated/settings/utils/settings-search/settings-search.ts
ホストと新しいプロジェクト設定セクション参照をサポートするように設定ルート、検索項目、セクションタイプを拡張。
プロジェクト設定リファクタリング
apps/desktop/src/renderer/routes/_authenticated/settings/components/SettingsSidebar/ProjectsSettings.tsx, apps/desktop/src/renderer/routes/_authenticated/settings/projects/page.tsx, components/ProjectsSettingsSidebar/ProjectsSettingsSidebar.tsx
廃止されたProjectsSettingsコンポーネントを削除し、プロジェクトインデックスページにナビゲーション機能と検索フィルタリングを追加。
ペーン削除ユーティリティ
packages/panes/src/core/store/utils/utils.ts, utils.test.ts, utils/index.ts
findSiblingPaneIdを削除し、getPaneIdsInLayoutgetActiveIdAfterRemovalを追加して、削除後のペーンフォーカス選択を統一。
ペーンストア
packages/panes/src/core/store/store.ts, store.test.ts
タブとペーンの削除/移動時に新しいgetActiveIdAfterRemovalgetActivePaneIdAfterRemovalを使用するように更新。
ペーン公開API
packages/panes/src/index.ts
getActiveIdAfterRemovalを公開エクスポートに追加。
通知状態
apps/desktop/src/renderer/stores/v2-notifications/store.ts, store.test.ts, index.ts
手動「未読」マーク機能を追加。setManualUnreadメソッド、selectV2WorkspaceIsUnreadセレクタ、useV2WorkspaceIsUnreadフックを実装。
ワークスペースアイテムアクション
apps/desktop/src/renderer/routes/.../_dashboard/components/DashboardSidebar/.../DashboardSidebarWorkspaceItem.tsx, .../DashboardSidebarWorkspaceContextMenu.tsx, hooks/useDashboardSidebarWorkspaceItemActions.ts
未読トグルアクションを追加。通知状態を統合し、コンテキストメニューに「既読/未読としてマーク」オプションを追加。
自動化ページ
apps/desktop/src/renderer/routes/_authenticated/_dashboard/automations/page.tsx
isReadyフラグを使用してからエンプティステートを表示する条件を改善。
ポート表示
apps/desktop/src/renderer/routes/.../_dashboard/components/DashboardSidebar/.../DashboardSidebarPortsList.tsx
ポートロードエラー警告を削除し、ドキュメント参照ボタンを追加。表示を常にtotalPortCountベースに変更。
ペーンコンポーネント
apps/desktop/src/renderer/routes/.../$workspaceId/hooks/usePaneRegistry/components/DiffPane/DiffFileHeader.tsx, TerminalPane.tsx
ファイルビューアボタンレイアウトをリファクタリング。ターミナルペーンのドラッグオーバーレイを常時マウント方式に変更。
ターミナルセッション管理
apps/desktop/src/renderer/routes/.../$workspaceId/.../TerminalSessionDropdown.tsx
セッション削除をダイレクトモード化し、セッション一覧を決定的にソート。UI内に新規ターミナルボタンを追加。
ペーンレイアウト
apps/desktop/src/renderer/routes/.../$workspaceId/hooks/useV2WorkspacePaneLayout/useV2WorkspacePaneLayout.ts
ペーンレイアウト更新時のensureWorkspaceInSidebar呼び出しを削除。
ペンディングワークスペース
apps/desktop/src/renderer/routes/_authenticated/_dashboard/pending/$pendingId/page.tsx
ホストサービスミューテーション後にensureWorkspaceInSidebarを呼び出すように登録フローを追加。
ドキュメント
apps/desktop/plans/done/20260425-1430-sidebar-remove-and-rerender.md
サイドバー削除問題と再レンダリング最適化に関する調査と実装計画を記録。

推定レビュー工数

🎯 4 (複雑) | ⏱️ ~60 分

関連する可能性のあるPR

ウサギの詩

🐰✨ ワークスペースの削除、すっきりリファクタ♪
ホストのメンバー管理も完成したよ〜
再レンダリング減らして、パフォーマンスアップ!
統一されたナビゲーション、もう迷わない
複雑な工事も、無事完了だ 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.17% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR タイトルは「upstream merge 2026-04-27 PR-A: UI/behavior fixes + hosts settings (12 commits)」で、バッチ取り込みの内容(UI/behavior の修正と hosts 設定機能)を的確に説明しており、変更セットの主要な目的を明確に表現しています。
Description check ✅ Passed PR の説明は詳細で構造化されており、取り込み内容、fork 側のコンフリクト解決、fork 固有機能のヘルスチェック、Codex 事前調査、テストプランが包含されており、テンプレートに求められる情報(概要、関連事項、変更タイプ、テスト内容)をすべて網羅しています。
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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 upstream/batch-2026-04-27-pr-a

Warning

Review ran into problems

🔥 Problems

Timed out fetching pipeline failures after 30000ms


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.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 27, 2026

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ⚠️ Neon database branch
  • ⚠️ Electric Fly.io app

Thank you for your contribution! 🎉

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

Caution

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

⚠️ Outside diff range comments (1)
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/TerminalPane.tsx (1)

374-449: ⚠️ Potential issue | 🟡 Minor

切断時にもドロップオーバーレイ/dropEffect = "copy" が表示される点に注意。

handleDrop では connectionState === "closed" の場合に早期 return していますが (Line 392)、handleDragEnter / handleDragOver 側にはガードが無いため、切断中の端末でもドラッグ中にオーバーレイが点灯し、カーソルもコピー可能を示してしまいます。ユーザーから見るとドロップ可能に見えるのにサイレントに無視される挙動になり、UX として若干誤解を招きます。可能であれば、切断時はオーバーレイ/dropEffect を抑止することをご検討ください。

♻️ 提案する修正例
 	const handleDragEnter = (event: React.DragEvent) => {
 		event.preventDefault();
+		if (connectionState === "closed") return;
 		dragCounterRef.current += 1;
 		setIsDropActive(true);
 	};

 	const handleDragOver = (event: React.DragEvent) => {
 		event.preventDefault();
-		event.dataTransfer.dropEffect = "copy";
+		event.dataTransfer.dropEffect =
+			connectionState === "closed" ? "none" : "copy";
 	};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/usePaneRegistry/components/TerminalPane/TerminalPane.tsx
around lines 374 - 449, The drag handlers currently activate the drop overlay
and set dataTransfer.dropEffect even when the terminal is disconnected; add a
guard in handleDragEnter and handleDragOver to check connectionState (same check
used in handleDrop, i.e. connectionState === "closed") and early-return when
closed so you don't call setIsDropActive or set dataTransfer.dropEffect =
"copy"; keep handleDragLeave unchanged so dragCounterRef still decrements, but
ensure handleDragEnter only increments the counter and sets isDropActive when
connectionState is not "closed".
🧹 Nitpick comments (16)
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/components/TerminalSessionDropdown/TerminalSessionDropdown.tsx (2)

261-276: ラベル内「+」ボタンはLGTM(軽微なリファクタ提案あり)。

DropdownMenuLabel 内に置いた "New terminal" アイコンボタンは onSelect を持たないラベルの子要素であり、preventDefault + stopPropagation でメニュー側のクリック処理にも干渉しないため、現状のままで動作します。

一点だけ任意の改善として、同ファイル内の Trash2 ボタン(行317〜329)と本ボタンはサイズ・hover/disabled・stopPropagation のパターンがほぼ同形なので、IconButton 的な小コンポーネントに抽出するとスタイルの drift を防げます。apps/desktop/** のコンポーネント規約上、components/IconButton/IconButton.tsx + index.ts の構成で配置可能です。

As per coding guidelines: "One folder per component with pattern ComponentName/ComponentName.tsx plus index.ts for barrel export".

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

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/usePaneRegistry/components/TerminalPane/components/TerminalSessionDropdown/TerminalSessionDropdown.tsx
around lines 261 - 276, Extract the repeated small icon-button pattern used
inside DropdownMenuLabel (the "+" button that calls handleNewTerminal) and the
similar Trash2 button into a reusable IconButton component (e.g.,
components/IconButton/IconButton.tsx with a barrel export), then replace both
inline <button> usages with <IconButton> to centralize props for size,
hover/disabled styles and an internal stopPropagation/preventDefault handler;
locate the "+" button in TerminalSessionDropdown (inside DropdownMenuLabel) and
the Trash2 usage in the same file and update them to pass
onClick={handleNewTerminal} (or the trash handler) and aria/title props to the
new IconButton.

192-210: 確認モーダル廃止に伴うUX上の留意点。

toast.promise による直接削除はPRの方針通りですが、Trash2 クリック→即 killSession のため、長時間実行中のプロセスを保持しているターミナルを誤クリックで失うリスクがあります。チル運用前提なので必須ではありませんが、以下のいずれかを検討する価値があります:

  • sonner の action ボタンによる Undo(kill 前に短い猶予を置いてからmutate)。
  • attached セッションのみ確認を要求する軽量な分岐。

エラーパスについては、mutateAsync の例外は finallyinvalidate 後に再スローされ toast.promise 側で error 表示されるので、現状の制御フロー自体は問題ありません。

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

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/usePaneRegistry/components/TerminalPane/components/TerminalSessionDropdown/TerminalSessionDropdown.tsx
around lines 192 - 210, The UX risk is that clicking the Trash icon immediately
calls killTerminalSession (via removeTerminalSession) and can inadvertently kill
long-running attached sessions; update
handleRemoveTerminal/removeTerminalSession to either (A) require a lightweight
confirmation when session.attached is true (e.g., show a quick confirm prompt
and only call removeTerminalSession if confirmed) or (B) implement an Undo flow
using sonner action buttons by postponing the kill: show a toast with an "Undo"
action, schedule the killTerminalSession.mutateAsync call after a short delay,
and cancel the scheduled call if the user clicks Undo; reference
removeTerminalSession, handleRemoveTerminal, killTerminalSession, toast.promise,
closePanesForTerminal and VisibleTerminalSession to locate where to add the
confirmation or scheduling/cancel logic.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/DiffPane/components/DiffFileHeader/DiffFileHeader.tsx (1)

88-102: 追加/削除バッジにスクリーンリーダー向けの説明を付与することを推奨します。

+5 / -3 のような短い表記のみだと、スクリーンリーダーでは「プラス5 マイナス3」のように文脈なく読み上げられ、変更の意味が伝わりません。バッジ全体に aria-label(例: "5 additions, 3 deletions")を付け、内部の数値表示を aria-hidden 化するとアクセシビリティが向上します。

♻️ a11y 改善の差分例
-				{(additions > 0 || deletions > 0) && (
-					<span className="font-mono text-[10px] text-muted-foreground">
-						{additions > 0 && (
-							<span className="text-green-700 dark:text-green-400">
-								+{additions}
-							</span>
-						)}
-						{additions > 0 && deletions > 0 && " "}
-						{deletions > 0 && (
-							<span className="text-red-700 dark:text-red-500">
-								-{deletions}
-							</span>
-						)}
-					</span>
-				)}
+				{(additions > 0 || deletions > 0) && (
+					<span
+						className="font-mono text-[10px] text-muted-foreground"
+						aria-label={[
+							additions > 0 ? `${additions} additions` : null,
+							deletions > 0 ? `${deletions} deletions` : null,
+						]
+							.filter(Boolean)
+							.join(", ")}
+					>
+						<span aria-hidden="true">
+							{additions > 0 && (
+								<span className="text-green-700 dark:text-green-400">
+									+{additions}
+								</span>
+							)}
+							{additions > 0 && deletions > 0 && " "}
+							{deletions > 0 && (
+								<span className="text-red-700 dark:text-red-500">
+									-{deletions}
+								</span>
+							)}
+						</span>
+					</span>
+				)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/usePaneRegistry/components/DiffPane/components/DiffFileHeader/DiffFileHeader.tsx
around lines 88 - 102, The additions/deletions badge in DiffFileHeader should
provide screen-reader context: compute an accessibility label (e.g., using the
local values additions and deletions with correct singular/plural like "1
addition" vs "2 additions") and add it as aria-label on the outer span that
currently renders the badge, then mark the inner numeric spans (the ones showing
+{additions} and -{deletions}) as aria-hidden="true" so only the descriptive
aria-label is announced; update the JSX in the DiffFileHeader component to build
the label string and apply aria attributes accordingly.
apps/desktop/src/renderer/routes/_authenticated/settings/hosts/$hostId/components/HostSettings/components/AddMemberDropdown/AddMemberDropdown.tsx (1)

25-32: 候補がない場合に無効化される理由をユーザーに伝える UI を検討してください。

candidates.length === 0 のとき、ボタンが単に disabled になるだけで「なぜ追加できないのか(招待可能な組織メンバーが残っていない 等)」がユーザーに伝わりません。title 属性 / Tooltip / 補助テキストのいずれかで理由を提示することをおすすめします。

♻️ 提案 (Tooltip 例)
-	if (candidates.length === 0) {
-		return (
-			<Button size="sm" variant="outline" disabled>
-				<HiOutlinePlus className="h-4 w-4 mr-1" />
-				Add member
-			</Button>
-		);
-	}
+	if (candidates.length === 0) {
+		return (
+			<Button
+				size="sm"
+				variant="outline"
+				disabled
+				title="All organization members already have access to this host"
+			>
+				<HiOutlinePlus className="h-4 w-4 mr-1" />
+				Add member
+			</Button>
+		);
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/settings/hosts/`$hostId/components/HostSettings/components/AddMemberDropdown/AddMemberDropdown.tsx
around lines 25 - 32, When candidates.length === 0 the AddMemberDropdown renders
a disabled Button with no explanation; update the AddMemberDropdown component to
show why adding is unavailable by adding a tooltip or title and/or helper text
when candidates is empty (e.g., wrap the disabled <Button> in a Tooltip or add a
title attribute explaining "No inviteable organization members remaining" or
similar). Modify the JSX branch that returns the disabled Button (in
AddMemberDropdown) to include the tooltip/aria-describedby for accessibility and
ensure the message references candidates being empty so users understand the
reason.
packages/trpc/src/router/v2-host/v2-host.ts (1)

31-44: requireHostOwner のアクセスチェック範囲を改善してください。

v2UsersHosts の取得時(31-37行目)に organizationId の絞り込みが入っていません。v2Hosts は組織スコープで検証済みですが、後続の v2UsersHosts クエリには組織フィルターがありません。v2UsersHosts テーブルは organizationId フィールドを持つため、防御的に eq(v2UsersHosts.organizationId, organizationId) を追加することをお勧めします。

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

In `@packages/trpc/src/router/v2-host/v2-host.ts` around lines 31 - 44, The access
check in requireHostOwner is missing organization scoping for the v2UsersHosts
lookup; update the db.query.v2UsersHosts.findFirst(...) where clause (the query
that currently uses and(eq(v2UsersHosts.hostId, hostId), eq(v2UsersHosts.userId,
userId))) to also include eq(v2UsersHosts.organizationId, organizationId) so
membership is validated within the same organization as v2Hosts; ensure the
added filter uses the same eq helper and does not change the returned columns or
error handling.
apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/collections.ts (1)

348-359: role 以外の変更がサイレントに破棄される可能性があります。

現状の判定は「role が未変更ならエラー」だけで、changesrole と一緒に他のフィールド(例: hostIduserId など)が含まれている場合は素通りし、サーバーには setMemberRole のみ送信されます。エラーメッセージ "Only role updates are supported" の意図に合わせるなら、role 以外のキーが含まれていても明示的に拒否するほうが安全です。

♻️ 提案: role 以外のキーが含まれる場合も拒否
 onUpdate: async ({ transaction }) => {
   const { original, changes } = transaction.mutations[0];
-  if (changes.role === undefined) {
-    throw new Error("Only role updates are supported on v2_users_hosts");
-  }
+  const changedKeys = Object.keys(changes);
+  if (changes.role === undefined || changedKeys.some((k) => k !== "role")) {
+    throw new Error("Only role updates are supported on v2_users_hosts");
+  }
   const result = await apiClient.v2Host.setMemberRole.mutate({
     hostId: original.hostId,
     userId: original.userId,
     role: changes.role,
   });
   return { txid: result.txid };
 },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/collections.ts`
around lines 348 - 359, The current onUpdate handler for v2_users_hosts only
checks that changes.role is defined but will silently ignore any other changed
fields; update the validation in onUpdate (use transaction.mutations[0],
original, changes) to explicitly reject any change objects that contain keys
other than "role" (e.g., hostId, userId, etc.) by throwing an Error with the
existing message "Only role updates are supported on v2_users_hosts" when extra
keys are present, so that only apiClient.v2Host.setMemberRole.mutate is allowed
to proceed.
apps/desktop/src/renderer/routes/_authenticated/settings/projects/components/ProjectsSettingsSidebar/ProjectsSettingsSidebar.tsx (2)

68-83: noMatches の判定が trimmed ではなく生の filter を見ています。

フィルタリング自体は trimmed(trim + lowercase 後)で行っているのに対し、noMatches の条件は filter !== "" を使っています。空白のみの入力時は trimmed が空でフィルタが無効化されるため、現時点で実害はありませんが、将来 filter の取り扱いを変えた際に整合が崩れやすいので、判定キーを trimmed 側に揃えておくと安全です。

♻️ 提案: trimmed を共有して整合性を確保
-	const { v2Rows, v1Rows, totalUnfiltered } = useMemo(() => {
+	const trimmedFilter = filter.trim().toLowerCase();
+	const { v2Rows, v1Rows, totalUnfiltered } = useMemo(() => {
 		const loadedV2Ids = new Set(v2Projects.map((p) => p.id));
 		// ...
-		const trimmed = filter.trim().toLowerCase();
 		const matches = (rows: ProjectRow[]) =>
-			trimmed
-				? rows.filter((r) => r.name.toLowerCase().includes(trimmed))
+			trimmedFilter
+				? rows.filter((r) => r.name.toLowerCase().includes(trimmedFilter))
 				: rows;
 		// ...
-	}, [groups, v2Projects, filter]);
+	}, [groups, v2Projects, trimmedFilter]);

 	const isEmpty = totalUnfiltered === 0;
 	const noMatches =
-		!isEmpty && v2Rows.length === 0 && v1Rows.length === 0 && filter !== "";
+		!isEmpty &&
+		v2Rows.length === 0 &&
+		v1Rows.length === 0 &&
+		trimmedFilter !== "";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/settings/projects/components/ProjectsSettingsSidebar/ProjectsSettingsSidebar.tsx`
around lines 68 - 83, The noMatches check uses raw filter instead of the trimmed
value used for matching; make them consistent by exposing/using the same trimmed
value: either compute const trimmed = filter.trim().toLowerCase() outside (or
return trimmed from the useMemo that currently computes
v2Rows/v1Rows/totalUnfiltered) and then change the noMatches condition to use
trimmed !== "" (and keep v2Rows.length === 0 && v1Rows.length === 0). Update the
code around the useMemo return and the noMatches declaration so both matching
and emptiness checks reference the same trimmed symbol (trimmed, v2Rows,
v1Rows).

104-108: ユーザー入力値を表示テキストへ直接埋め込んでいる点を確認してください。

No projects match "{filter}" でユーザー入力をそのまま表示しています。React が JSX 経由でエスケープするため XSS にはなりませんが、極端に長い入力や改行・制御文字を含む入力でレイアウトが崩れる可能性があります。表示時に長さ制限や truncate を適用すると堅牢になります(必要であれば任意です)。

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

In
`@apps/desktop/src/renderer/routes/_authenticated/settings/projects/components/ProjectsSettingsSidebar/ProjectsSettingsSidebar.tsx`
around lines 104 - 108, The code directly interpolates user input into the UI in
ProjectsSettingsSidebar (the JSX that renders No projects match "{filter}"),
which can break layout for very long or control-character-filled inputs; fix by
truncating and normalizing the filter before rendering (e.g., create a
sanitizedDisplay = filter.replace(/\s+/g,' ').slice(0, MAX_LEN) + (filter.length
> MAX_LEN ? '…' : '') or use your app's truncate utility) and use that variable
in the <p> instead of raw filter, or apply a CSS text-truncate class to the
container to constrain overflow.
apps/desktop/src/renderer/routes/_authenticated/settings/hosts/layout.tsx (1)

9-9: 型アサーション を避け、TanStack Router の型推論を活用することを推奨します。

useParams({ strict: false }) は子ルート全体の params のユニオン型を返すため、as { hostId?: string } のキャストは型安全性を弱めます。select で必要なフィールドだけを取り出せば、推論済みの型のまま hostId?: string | undefined を取得できます。

♻️ 提案: select を用いた型安全な取り出し
-	const params = useParams({ strict: false }) as { hostId?: string };
+	const hostId = useParams({
+		strict: false,
+		select: (p) => ("hostId" in p ? p.hostId : undefined),
+	});
 	return (
 		<div className="flex h-full w-full">
-			<HostsSettingsSidebar selectedHostId={params.hostId ?? null} />
+			<HostsSettingsSidebar selectedHostId={hostId ?? null} />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/routes/_authenticated/settings/hosts/layout.tsx` at
line 9, Remove the manual type assertion and use useParams' select option to
preserve TanStack Router's inferred types: replace the casted call to useParams
with a select that returns only hostId so you get a properly typed hostId?:
string | undefined (e.g. call useParams({ strict: false, select: p => ({ hostId:
p.hostId }) }) or select: p => p.hostId and destructure the result). Update the
code around the useParams invocation (the useParams call and the local hostId
variable) to remove "as { hostId?: string }" and rely on the selected value
instead.
apps/desktop/src/renderer/routes/_authenticated/settings/hosts/$hostId/components/HostSettings/HostSettings.tsx (2)

17-25: notifyOnPersist のエラー無視について確認。

tx?.isPersisted.promise.then(..., () => {}) で永続化失敗時のハンドラを空にしていますが、useOptimisticMutationRunner 側ですでに void transaction.isPersisted.promise.catch((error) => reportFailure(...)) によってトースト/コンソール通知されているため、ここでの空ハンドラは「成功時にだけトーストを出す」という意図として整合しています。意図どおりであればコメントで残しておくと、後続の保守者が誤って console.error を足す事故を防げます。

📝 任意の補足コメント案
 function notifyOnPersist(
 	tx: PersistableTransaction | null,
 	successMessage: string,
 ) {
+	// Failures are already surfaced via toast.error in useOptimisticMutationRunner;
+	// this handler intentionally only reacts to success.
 	tx?.isPersisted.promise.then(
 		() => toast.success(successMessage),
 		() => {},
 	);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/settings/hosts/`$hostId/components/HostSettings/HostSettings.tsx
around lines 17 - 25, The empty rejection handler in notifyOnPersist
(tx?.isPersisted.promise.then(..., () => {})) is intentional to only show a
success toast because useOptimisticMutationRunner already handles failures via
transaction.isPersisted.promise.catch((error) => reportFailure(...)); add a
short clarifying comment above notifyOnPersist explaining that failures are
handled upstream by useOptimisticMutationRunner and that the empty rejection
handler prevents duplicate error logging/toasts to avoid accidental
console.error additions by future maintainers.

83-99: V2UsersHostRole 型でロール型を統一してください。

useOptimisticCollectionActions.ts では既に V2UsersHostRole@superset/db/enums)を使用していますが、このコンポーネントエコシステムでは "owner" | "member" という文字列リテラルに統一されています。行90の as "owner" | "member" キャストに加えて、MemberRowData.role の型定義(MemberRow.tsx)と handleSetRoleMembersTableProps.onSetRoleMemberRowProps.onSetRole のシグネチャも V2UsersHostRole に揃えることで、enum 側が値を追加された際の型安全性が向上します。

♻️ 修正案
+import type { V2UsersHostRole } from "@superset/db/enums";
 ...
 			.map((row) => {
 				const u = userMap.get(row.userId);
 				return {
 					usersHostsId: row.id,
 					userId: row.userId,
-					role: row.role as "owner" | "member",
+					role: row.role as V2UsersHostRole,
 					name: u?.name ?? "Unknown user",
 					email: u?.email ?? "",
 				};
 			})

合わせて以下も更新してください:

  • MemberRowData.roleMemberRow.tsx 行15):V2UsersHostRole に変更
  • handleSetRole パラメータ(HostSettings.tsx 行149):role: V2UsersHostRole に変更
  • MembersTableProps.onSetRoleMembersTable.tsx 行14):role: V2UsersHostRole に変更
  • MemberRowProps.onSetRoleMemberRow.tsx 行23):role: V2UsersHostRole に変更
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/settings/hosts/`$hostId/components/HostSettings/HostSettings.tsx
around lines 83 - 99, hostUserRows mapping uses a string literal cast "owner" |
"member"; replace that with the shared enum type V2UsersHostRole (imported from
`@superset/db/enums`) and propagate this type across the component API: change the
cast in HostSettings (the members useMemo map) to V2UsersHostRole, update
MemberRowData.role (in MemberRow.tsx) to V2UsersHostRole, update the
handleSetRole parameter in HostSettings to accept role: V2UsersHostRole, and
update MembersTableProps.onSetRole and MemberRowProps.onSetRole signatures to
use V2UsersHostRole so all role types are consistent.
apps/desktop/src/renderer/react-query/workspaces/utils/workspace-removal.test.ts (1)

52-65: removeWorkspaceFromGroups のエッジケースを追加でカバー推奨

AIサマリにある「ワークスペースが残らないグループは除外、空のセクションは保持」という挙動は、現在のテストでは検証されていません。回帰防止のため以下のケースを追加することをお勧めします:

  • セクション内の最後の workspace を削除 → 空のセクションが結果に残ること
  • トップレベルグループの全 workspace を削除 → そのグループが結果から除外されること
♻️ 追加テスト案
 describe("removeWorkspaceFromGroups", () => {
 	it("removes section and top-level workspaces", () => {
 		// ... existing assertions ...
 	});
+
+	it("keeps an empty section when its last workspace is removed", () => {
+		const result = removeWorkspaceFromGroups(groups, "w2");
+		const result2 = removeWorkspaceFromGroups(result, "w3");
+		expect(result2[0]?.sections).toHaveLength(1);
+		expect(result2[0]?.sections[0]?.workspaces).toEqual([]);
+	});
+
+	it("filters out a group that ends up with no visible workspaces", () => {
+		const singleGroup = [
+			{
+				workspaces: [{ id: "w1", tabOrder: 1 }],
+				sections: [],
+				topLevelItems: [
+					{ id: "w1", kind: "workspace" as const, tabOrder: 1 },
+				],
+			},
+		];
+		expect(removeWorkspaceFromGroups(singleGroup, "w1")).toEqual([]);
+	});
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/react-query/workspaces/utils/workspace-removal.test.ts`
around lines 52 - 65, Add tests for the two edge cases described: call
removeWorkspaceFromGroups with a group where removing the last workspace inside
a section should leave that empty section present (verify the section exists and
its workspaces array is empty) and call it with a top-level group where removing
all top-level workspaces should remove the entire group from the returned list
(verify the group is absent). Reference removeWorkspaceFromGroups and assert on
returned groups' sections[].workspaces and topLevelItems to validate these
behaviors.
packages/panes/src/core/store/utils/utils.test.ts (1)

96-103: テスト名の表現がやや分かりにくい(任意)

it("preserves inactive focus, otherwise selects next then previous") の "inactive focus" は、関数仕様(第2引数の現在 active が削除対象でなければそのまま維持)と読み合わせないと意図が掴みにくいです。例えば "preserves current active when it is not the removed id, otherwise selects next then previous" のような表現にすると、引数の意味と挙動が一致して読みやすくなります。

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

In `@packages/panes/src/core/store/utils/utils.test.ts` around lines 96 - 103,
Rename the test title for the describe("getActiveIdAfterRemoval") case to more
clearly describe the behavior: change the it(...) string from "preserves
inactive focus, otherwise selects next then previous" to something explicit like
"preserves current active when it is not the removed id, otherwise selects next
then previous" so the meaning of the second argument (current active) and
expected fallback behavior are unambiguous; update the test case description
surrounding the getActiveIdAfterRemoval assertions to match this clearer
wording.
apps/desktop/src/renderer/stores/v2-notifications/store.test.ts (1)

116-130: manual ソースのクリア動作の検証アサーション追加を推奨

setManualUnread(workspaceId)manual ソースを "review" ステータスで登録し、clearWorkspaceAttention(workspaceId) はそのワークスペースの "review" ステータスを持つ全ソース(manual を含む)を削除します。本テストに manual ソースの登録と削除確認を追加することで、mark-as-read のラウンドトリップが正確に動作することを保証できます。

♻️ 追加アサーション案
 	it("clears only review attention for a workspace", () => {
 		const store = useV2NotificationStore.getState();
 		store.setTerminalStatus("terminal-1", "workspace-1", "review", 100);
 		store.setTerminalStatus("terminal-2", "workspace-1", "working", 101);
 		store.setChatStatus("session-1", "workspace-1", "permission", 102);
 		store.setTerminalStatus("terminal-3", "workspace-2", "review", 103);
+		store.setManualUnread("workspace-1");
+		store.setManualUnread("workspace-2");

 		store.clearWorkspaceAttention("workspace-1");

 		const state = useV2NotificationStore.getState();
 		expect(state.sources["terminal:terminal-1"]).toBeUndefined();
 		expect(state.sources["terminal:terminal-2"]?.status).toBe("working");
 		expect(state.sources["chat:session-1"]?.status).toBe("permission");
 		expect(state.sources["terminal:terminal-3"]?.status).toBe("review");
+		expect(state.sources["manual:workspace-1"]).toBeUndefined();
+		expect(state.sources["manual:workspace-2"]).toBeDefined();
 	});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/stores/v2-notifications/store.test.ts` around lines
116 - 130, Add a `manual` source to the test by calling the store method that
registers manual unread (setManualUnread or equivalent) for "workspace-1" before
calling clearWorkspaceAttention, then after
store.clearWorkspaceAttention("workspace-1") assert that the manual source (e.g.
state.sources["manual:workspace-1"] or whatever key `setManualUnread` uses) is
removed/undefined; update the test around useV2NotificationStore,
setManualUnread, clearWorkspaceAttention and the subsequent expect checks to
include this removal assertion so the round-trip mark-as-read for manual is
validated.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/useDashboardSidebarData.ts (1)

105-142: ペンディングワークスペースの new Date() が一部安定化を相殺します(最適化の改善余地)。

useStableDashboardSidebarProjectsJSON.stringify(project) をフィンガープリントとして使うため Date は ISO 文字列で安定化されますが、computedGroups 内のペンディング項目は再計算ごとに createdAt: new Date() / updatedAt: new Date() を生成しています。そのため [machineId, localPullRequestsByWorkspaceId, sidebarWorkspaces, ...] のどれかが変化して computedGroups が走るたびに、ペンディング行を含むプロジェクトだけはフィンガープリントが毎回変わり、stableProject 再利用の恩恵を受けられません(PR ポーリング時など)。短期的には顕在化しにくい挙動ですが、ペンディング行の Date を pw から導出可能な安定値(例えば pending レコードの createdAt など、無ければ id ベースの固定値)に置き換えるとプロジェクト識別子の安定性がより一貫します。

Also applies to: 408-430

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

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/useDashboardSidebarData.ts`
around lines 105 - 142, The transient use of new Date() in computedGroups
creates non-deterministic fingerprints so useStableDashboardSidebarProjects
cannot reuse stableProject; instead derive pending items' createdAt/updatedAt
from a stable source (e.g., use the pending record's createdAt timestamp or a
deterministic value derived from its id) rather than new Date(), and ensure the
same stable value is used wherever pending rows are built so
getDashboardSidebarProjectFingerprint (and useStableDashboardSidebarProjects)
sees consistent input for pending projects; update computedGroups (the code that
constructs pending project rows) to replace new Date() with the stable derived
value.
apps/desktop/src/renderer/react-query/workspaces/utils/workspace-removal.ts (1)

63-72: getWorkspaceFocusTargetAfterRemoval の意味論を JSDoc で明示することを推奨します。

removedWorkspaceIdcurrentActiveIdremovedId の両方に渡しているため、戻り値は実質的に「削除対象がアクティブだった場合に次にフォーカスすべき ID」を意味します。削除対象がアクティブでないケースでは呼び出し側でナビゲーションしない、という前提が暗黙になっているため、誤用防止のため簡単なドキュメントコメントを追加することを推奨します。

📝 提案する JSDoc 追記
+/**
+ * Computes the workspace ID that should receive focus when the currently
+ * active workspace is removed. Returns null if no suitable target exists.
+ *
+ * NOTE: This assumes the caller is removing the *active* workspace. If the
+ * removed workspace is not active, callers should not navigate based on the
+ * result.
+ */
 export function getWorkspaceFocusTargetAfterRemoval(
 	groups: readonly WorkspaceGroupLike[] | undefined,
 	removedWorkspaceId: string,
 ): string | null {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/react-query/workspaces/utils/workspace-removal.ts`
around lines 63 - 72, Add a concise JSDoc for
getWorkspaceFocusTargetAfterRemoval that explains its semantics: it calls
getActiveIdAfterRemoval with removedWorkspaceId as both currentActiveId and
removedId, so the returned ID is meaningful only when the workspace being
removed was the active one (callers should not treat the return value as a
navigation target if the removed workspace was not active). Mention the
parameters (groups, removedWorkspaceId), describe the return value (next focus
ID or null), and note the implicit caller responsibility to avoid misuse.
🤖 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/react-query/workspaces/utils/workspace-removal.ts`:
- Around line 86-104: The current logic in removeWorkspaceFromGroups produces a
mismatch with getAllGrouped because you remove entire groups via
.filter(hasVisibleWorkspaces) even when user-created sections (group.sections)
should be preserved empty; update the filtering to only drop a group when the
group itself and all its sections have no workspaces (e.g., replace
hasVisibleWorkspaces semantics so it returns true if group.workspaces.length > 0
OR group.topLevelItems contains the item OR any section in group.sections has
workspaces.length > 0), or alternatively remove the final .filter call and
explicitly drop groups only when both group.workspaces and every
section.workspaces are empty; adjust the logic in removeWorkspaceFromGroups
(referencing group.sections, workspaces, topLevelItems, hasVisibleWorkspaces,
and removeWorkspaceFromGroups) accordingly so optimistic cache matches
getAllGrouped.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/usePaneRegistry/components/DiffPane/components/DiffFileHeader/DiffFileHeader.tsx:
- Around line 83-85: The file's TooltipContent was reduced to only
CLICK_HINT_TOOLTIP which removed the ability to view long truncated paths shown
in the <span className="min-w-0 truncate"> inside the DiffFileHeader component;
restore full-path visibility by adding a native tooltip and/or including the
full path in the tooltip: set title={path} on the enclosing <button> element to
provide a native fallback for full paths, and/or change TooltipContent to render
the full path alongside CLICK_HINT_TOOLTIP (or conditionally render the full
path only when the span is truncated) so users can see the complete file path.

In
`@apps/desktop/src/renderer/routes/_authenticated/settings/hosts/`$hostId/components/HostSettings/components/MembersTable/components/MemberRow/MemberRow.tsx:
- Around line 57-68: The trash Button in MemberRow.tsx currently calls
onRemove(member) directly; wrap that action in a confirmation step so
destructive deletes require explicit user consent: replace the direct onClick
handler on the Button (MemberRow -> onClick={() => onRemove(member)}) with logic
that opens a confirmation dialog (e.g., AlertDialog or window.confirm) and only
calls the provided onRemove(member) if the user confirms; coordinate with the
existing handleRemove function in HostSettings.tsx (the delete callback
referenced there) so the same callback is invoked after confirmation and no
other behavior changes.

In
`@apps/desktop/src/renderer/routes/_authenticated/settings/hosts/components/HostsSettingsSidebar/HostsSettingsSidebar.tsx`:
- Around line 30-48: The current query falls back to eq(hosts.organizationId,
"") when activeOrganizationId is null, causing "No hosts yet." to display
incorrectly; modify the logic so useLiveQuery does not run when
activeOrganizationId is null and only queries when a valid ID exists (e.g.,
guard or pass a skip condition into the useLiveQuery call), and update the
render condition that checks hosts/isEmpty to also require a confirmed
organization context (activeOrganizationId != null) so that while the
session/org is loading you show a skeleton/blank state instead of "No hosts
yet."; touch the symbols activeOrganizationId, useLiveQuery (the query selector
for collections.v2Hosts), and the isEmpty rendering branch to implement this
behavior.

In `@apps/desktop/src/renderer/routes/_authenticated/settings/hosts/page.tsx`:
- Around line 23-60: The empty-state flashes because useLiveQuery defaults data
to [] before results arrive; update the hook destructure to also grab the
returned status (e.g., const { data: hosts = [], status } = useLiveQuery(...))
and change the render guard to only show the "No hosts yet." message when status
=== "ready" && hosts.length === 0; keep the firstHostId useMemo and navigation
effect as-is but ensure they handle hosts being empty/null until status becomes
"ready".

In `@apps/desktop/src/renderer/routes/_authenticated/settings/projects/page.tsx`:
- Around line 24-77: The empty-state flashes because both
electronTrpc.workspaces.getAllGrouped.useQuery() and useLiveQuery() default to
[] so isEmpty becomes true before real data loads; fix by waiting for both
queries to finish before showing the empty UI: read the isLoading (or status)
from electronTrpc.workspaces.getAllGrouped.useQuery (e.g., add
isLoading/groupLoad flag) and add a simple v2Loaded flag for useLiveQuery (set
true when v2Projects is first populated), then change the empty check to only
return the "No projects yet." UI when both the workspaces query is not loading
and the v2 query is loaded and both data sets are empty (update references to
groups, v2Projects, firstProjectId and isEmpty accordingly).

In `@packages/trpc/src/router/v2-host/v2-host.ts`:
- Around line 223-241: In setMemberRole there is a TOCTOU race when demoting
owner→member: you must lock the target membership row in the same transaction
(same tx used to query otherOwners) so two concurrent demote/remove requests
can't both pass the "other owners exist" check; update setMemberRole to SELECT
the target v2UsersHosts row (by hostId+userId or the same predicate used to
build target) FOR UPDATE (or otherwise acquire the same advisory lock or run the
transaction at SERIALIZABLE) before querying otherOwners — mirror the locking
approach used in removeMember and ensure tx, v2UsersHosts, target, and the
otherOwners check occur under that lock.
- Around line 149-167: The check for "at least one owner" has a TOCTOU race
because the SELECT ... for("update") only locks other owners and not the row
being deleted (v2UsersHosts row for input.userId), allowing two concurrent
deletes to race; update the transaction logic around tx and v2UsersHosts so the
SELECT ... for("update") locks the target row as well (include
eq(v2UsersHosts.userId, input.userId) in the locked predicate or select by
hostId and role "owner" without excluding input.userId) to ensure deterministic
lock ordering before deleting, or alternatively acquire a host-scoped advisory
lock (pg_advisory_xact_lock) keyed by input.hostId inside the same tx to
serialize deletions.
- Around line 81-111: The onConflictDoNothing target on the insert into
v2UsersHosts (inside the dbWs.transaction lambda) doesn't catch primary-key
collisions for input.id, so a duplicate client-supplied id can still raise a DB
constraint error; fix by generating the row id server-side (remove reliance on
input.id) or ensure conflict handling includes the PK (e.g., include id in the
conflict target) and handle the no-op case consistently; update the insert call
that sets id from input.id and the conflict target used by onConflictDoNothing
(and any callers of this route that expect to supply id) so the server always
produces a stable id or the conflict target covers PK collisions.

---

Outside diff comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/usePaneRegistry/components/TerminalPane/TerminalPane.tsx:
- Around line 374-449: The drag handlers currently activate the drop overlay and
set dataTransfer.dropEffect even when the terminal is disconnected; add a guard
in handleDragEnter and handleDragOver to check connectionState (same check used
in handleDrop, i.e. connectionState === "closed") and early-return when closed
so you don't call setIsDropActive or set dataTransfer.dropEffect = "copy"; keep
handleDragLeave unchanged so dragCounterRef still decrements, but ensure
handleDragEnter only increments the counter and sets isDropActive when
connectionState is not "closed".

---

Nitpick comments:
In
`@apps/desktop/src/renderer/react-query/workspaces/utils/workspace-removal.test.ts`:
- Around line 52-65: Add tests for the two edge cases described: call
removeWorkspaceFromGroups with a group where removing the last workspace inside
a section should leave that empty section present (verify the section exists and
its workspaces array is empty) and call it with a top-level group where removing
all top-level workspaces should remove the entire group from the returned list
(verify the group is absent). Reference removeWorkspaceFromGroups and assert on
returned groups' sections[].workspaces and topLevelItems to validate these
behaviors.

In `@apps/desktop/src/renderer/react-query/workspaces/utils/workspace-removal.ts`:
- Around line 63-72: Add a concise JSDoc for getWorkspaceFocusTargetAfterRemoval
that explains its semantics: it calls getActiveIdAfterRemoval with
removedWorkspaceId as both currentActiveId and removedId, so the returned ID is
meaningful only when the workspace being removed was the active one (callers
should not treat the return value as a navigation target if the removed
workspace was not active). Mention the parameters (groups, removedWorkspaceId),
describe the return value (next focus ID or null), and note the implicit caller
responsibility to avoid misuse.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/useDashboardSidebarData.ts`:
- Around line 105-142: The transient use of new Date() in computedGroups creates
non-deterministic fingerprints so useStableDashboardSidebarProjects cannot reuse
stableProject; instead derive pending items' createdAt/updatedAt from a stable
source (e.g., use the pending record's createdAt timestamp or a deterministic
value derived from its id) rather than new Date(), and ensure the same stable
value is used wherever pending rows are built so
getDashboardSidebarProjectFingerprint (and useStableDashboardSidebarProjects)
sees consistent input for pending projects; update computedGroups (the code that
constructs pending project rows) to replace new Date() with the stable derived
value.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/usePaneRegistry/components/DiffPane/components/DiffFileHeader/DiffFileHeader.tsx:
- Around line 88-102: The additions/deletions badge in DiffFileHeader should
provide screen-reader context: compute an accessibility label (e.g., using the
local values additions and deletions with correct singular/plural like "1
addition" vs "2 additions") and add it as aria-label on the outer span that
currently renders the badge, then mark the inner numeric spans (the ones showing
+{additions} and -{deletions}) as aria-hidden="true" so only the descriptive
aria-label is announced; update the JSX in the DiffFileHeader component to build
the label string and apply aria attributes accordingly.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/usePaneRegistry/components/TerminalPane/components/TerminalSessionDropdown/TerminalSessionDropdown.tsx:
- Around line 261-276: Extract the repeated small icon-button pattern used
inside DropdownMenuLabel (the "+" button that calls handleNewTerminal) and the
similar Trash2 button into a reusable IconButton component (e.g.,
components/IconButton/IconButton.tsx with a barrel export), then replace both
inline <button> usages with <IconButton> to centralize props for size,
hover/disabled styles and an internal stopPropagation/preventDefault handler;
locate the "+" button in TerminalSessionDropdown (inside DropdownMenuLabel) and
the Trash2 usage in the same file and update them to pass
onClick={handleNewTerminal} (or the trash handler) and aria/title props to the
new IconButton.
- Around line 192-210: The UX risk is that clicking the Trash icon immediately
calls killTerminalSession (via removeTerminalSession) and can inadvertently kill
long-running attached sessions; update
handleRemoveTerminal/removeTerminalSession to either (A) require a lightweight
confirmation when session.attached is true (e.g., show a quick confirm prompt
and only call removeTerminalSession if confirmed) or (B) implement an Undo flow
using sonner action buttons by postponing the kill: show a toast with an "Undo"
action, schedule the killTerminalSession.mutateAsync call after a short delay,
and cancel the scheduled call if the user clicks Undo; reference
removeTerminalSession, handleRemoveTerminal, killTerminalSession, toast.promise,
closePanesForTerminal and VisibleTerminalSession to locate where to add the
confirmation or scheduling/cancel logic.

In
`@apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/collections.ts`:
- Around line 348-359: The current onUpdate handler for v2_users_hosts only
checks that changes.role is defined but will silently ignore any other changed
fields; update the validation in onUpdate (use transaction.mutations[0],
original, changes) to explicitly reject any change objects that contain keys
other than "role" (e.g., hostId, userId, etc.) by throwing an Error with the
existing message "Only role updates are supported on v2_users_hosts" when extra
keys are present, so that only apiClient.v2Host.setMemberRole.mutate is allowed
to proceed.

In
`@apps/desktop/src/renderer/routes/_authenticated/settings/hosts/`$hostId/components/HostSettings/components/AddMemberDropdown/AddMemberDropdown.tsx:
- Around line 25-32: When candidates.length === 0 the AddMemberDropdown renders
a disabled Button with no explanation; update the AddMemberDropdown component to
show why adding is unavailable by adding a tooltip or title and/or helper text
when candidates is empty (e.g., wrap the disabled <Button> in a Tooltip or add a
title attribute explaining "No inviteable organization members remaining" or
similar). Modify the JSX branch that returns the disabled Button (in
AddMemberDropdown) to include the tooltip/aria-describedby for accessibility and
ensure the message references candidates being empty so users understand the
reason.

In
`@apps/desktop/src/renderer/routes/_authenticated/settings/hosts/`$hostId/components/HostSettings/HostSettings.tsx:
- Around line 17-25: The empty rejection handler in notifyOnPersist
(tx?.isPersisted.promise.then(..., () => {})) is intentional to only show a
success toast because useOptimisticMutationRunner already handles failures via
transaction.isPersisted.promise.catch((error) => reportFailure(...)); add a
short clarifying comment above notifyOnPersist explaining that failures are
handled upstream by useOptimisticMutationRunner and that the empty rejection
handler prevents duplicate error logging/toasts to avoid accidental
console.error additions by future maintainers.
- Around line 83-99: hostUserRows mapping uses a string literal cast "owner" |
"member"; replace that with the shared enum type V2UsersHostRole (imported from
`@superset/db/enums`) and propagate this type across the component API: change the
cast in HostSettings (the members useMemo map) to V2UsersHostRole, update
MemberRowData.role (in MemberRow.tsx) to V2UsersHostRole, update the
handleSetRole parameter in HostSettings to accept role: V2UsersHostRole, and
update MembersTableProps.onSetRole and MemberRowProps.onSetRole signatures to
use V2UsersHostRole so all role types are consistent.

In `@apps/desktop/src/renderer/routes/_authenticated/settings/hosts/layout.tsx`:
- Line 9: Remove the manual type assertion and use useParams' select option to
preserve TanStack Router's inferred types: replace the casted call to useParams
with a select that returns only hostId so you get a properly typed hostId?:
string | undefined (e.g. call useParams({ strict: false, select: p => ({ hostId:
p.hostId }) }) or select: p => p.hostId and destructure the result). Update the
code around the useParams invocation (the useParams call and the local hostId
variable) to remove "as { hostId?: string }" and rely on the selected value
instead.

In
`@apps/desktop/src/renderer/routes/_authenticated/settings/projects/components/ProjectsSettingsSidebar/ProjectsSettingsSidebar.tsx`:
- Around line 68-83: The noMatches check uses raw filter instead of the trimmed
value used for matching; make them consistent by exposing/using the same trimmed
value: either compute const trimmed = filter.trim().toLowerCase() outside (or
return trimmed from the useMemo that currently computes
v2Rows/v1Rows/totalUnfiltered) and then change the noMatches condition to use
trimmed !== "" (and keep v2Rows.length === 0 && v1Rows.length === 0). Update the
code around the useMemo return and the noMatches declaration so both matching
and emptiness checks reference the same trimmed symbol (trimmed, v2Rows,
v1Rows).
- Around line 104-108: The code directly interpolates user input into the UI in
ProjectsSettingsSidebar (the JSX that renders No projects match "{filter}"),
which can break layout for very long or control-character-filled inputs; fix by
truncating and normalizing the filter before rendering (e.g., create a
sanitizedDisplay = filter.replace(/\s+/g,' ').slice(0, MAX_LEN) + (filter.length
> MAX_LEN ? '…' : '') or use your app's truncate utility) and use that variable
in the <p> instead of raw filter, or apply a CSS text-truncate class to the
container to constrain overflow.

In `@apps/desktop/src/renderer/stores/v2-notifications/store.test.ts`:
- Around line 116-130: Add a `manual` source to the test by calling the store
method that registers manual unread (setManualUnread or equivalent) for
"workspace-1" before calling clearWorkspaceAttention, then after
store.clearWorkspaceAttention("workspace-1") assert that the manual source (e.g.
state.sources["manual:workspace-1"] or whatever key `setManualUnread` uses) is
removed/undefined; update the test around useV2NotificationStore,
setManualUnread, clearWorkspaceAttention and the subsequent expect checks to
include this removal assertion so the round-trip mark-as-read for manual is
validated.

In `@packages/panes/src/core/store/utils/utils.test.ts`:
- Around line 96-103: Rename the test title for the
describe("getActiveIdAfterRemoval") case to more clearly describe the behavior:
change the it(...) string from "preserves inactive focus, otherwise selects next
then previous" to something explicit like "preserves current active when it is
not the removed id, otherwise selects next then previous" so the meaning of the
second argument (current active) and expected fallback behavior are unambiguous;
update the test case description surrounding the getActiveIdAfterRemoval
assertions to match this clearer wording.

In `@packages/trpc/src/router/v2-host/v2-host.ts`:
- Around line 31-44: The access check in requireHostOwner is missing
organization scoping for the v2UsersHosts lookup; update the
db.query.v2UsersHosts.findFirst(...) where clause (the query that currently uses
and(eq(v2UsersHosts.hostId, hostId), eq(v2UsersHosts.userId, userId))) to also
include eq(v2UsersHosts.organizationId, organizationId) so membership is
validated within the same organization as v2Hosts; ensure the added filter uses
the same eq helper and does not change the returned columns or error handling.
🪄 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: 4e003265-9b42-4200-b8f0-e02fe0550041

📥 Commits

Reviewing files that changed from the base of the PR and between 7f96332 and 42a3f0e.

📒 Files selected for processing (60)
  • apps/desktop/plans/done/20260425-1430-sidebar-remove-and-rerender.md
  • apps/desktop/src/renderer/react-query/workspaces/useCloseWorkspace.ts
  • apps/desktop/src/renderer/react-query/workspaces/useDeleteWorkspace.ts
  • apps/desktop/src/renderer/react-query/workspaces/utils/workspace-removal.test.ts
  • apps/desktop/src/renderer/react-query/workspaces/utils/workspace-removal.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/automations/page.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/DashboardSidebar.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/DashboardSidebarPortsList.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceContextMenu/DashboardSidebarWorkspaceContextMenu.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/useDashboardSidebarData.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarShortcuts/useDashboardSidebarShortcuts.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useNavigateAwayFromWorkspace/useNavigateAwayFromWorkspace.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/utils/getDeleteFocusTargetWorkspaceId/getDeleteFocusTargetWorkspaceId.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/utils/getDeleteFocusTargetWorkspaceId/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/pending/$pendingId/page.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/DiffPane/components/DiffFileHeader/DiffFileHeader.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/TerminalPane.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/components/TerminalSessionDropdown/TerminalSessionDropdown.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useV2WorkspacePaneLayout/useV2WorkspacePaneLayout.ts
  • apps/desktop/src/renderer/routes/_authenticated/hooks/useOptimisticCollectionActions/useOptimisticCollectionActions.ts
  • apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/CollectionsProvider.tsx
  • apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/collections.ts
  • apps/desktop/src/renderer/routes/_authenticated/settings/components/SettingsSidebar/GeneralSettings.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/components/SettingsSidebar/ProjectsSettings.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/components/SettingsSidebar/SettingsSidebar.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/hosts/$hostId/components/HostSettings/HostSettings.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/hosts/$hostId/components/HostSettings/components/AddMemberDropdown/AddMemberDropdown.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/hosts/$hostId/components/HostSettings/components/AddMemberDropdown/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/settings/hosts/$hostId/components/HostSettings/components/HostHeader/HostHeader.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/hosts/$hostId/components/HostSettings/components/HostHeader/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/settings/hosts/$hostId/components/HostSettings/components/MembersTable/MembersTable.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/hosts/$hostId/components/HostSettings/components/MembersTable/components/MemberRow/MemberRow.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/hosts/$hostId/components/HostSettings/components/MembersTable/components/MemberRow/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/settings/hosts/$hostId/components/HostSettings/components/MembersTable/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/settings/hosts/$hostId/components/HostSettings/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/settings/hosts/$hostId/page.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/hosts/components/HostsSettingsSidebar/HostsSettingsSidebar.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/hosts/components/HostsSettingsSidebar/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/settings/hosts/layout.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/hosts/page.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/layout.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/projects/components/ProjectsSettingsSidebar/ProjectsSettingsSidebar.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/projects/page.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/utils/settings-search/settings-search.ts
  • apps/desktop/src/renderer/stores/settings-state.ts
  • apps/desktop/src/renderer/stores/v2-notifications/index.ts
  • apps/desktop/src/renderer/stores/v2-notifications/store.test.ts
  • apps/desktop/src/renderer/stores/v2-notifications/store.ts
  • packages/panes/src/core/store/store.test.ts
  • packages/panes/src/core/store/store.ts
  • packages/panes/src/core/store/utils/index.ts
  • packages/panes/src/core/store/utils/utils.test.ts
  • packages/panes/src/core/store/utils/utils.ts
  • packages/panes/src/index.ts
  • packages/panes/src/react/components/Workspace/components/TabBar/components/TabItem/TabItem.tsx
  • packages/trpc/src/root.ts
  • packages/trpc/src/router/v2-host/index.ts
  • packages/trpc/src/router/v2-host/v2-host.ts
💤 Files with no reviewable changes (3)
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/utils/getDeleteFocusTargetWorkspaceId/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/utils/getDeleteFocusTargetWorkspaceId/getDeleteFocusTargetWorkspaceId.ts
  • apps/desktop/src/renderer/routes/_authenticated/settings/components/SettingsSidebar/ProjectsSettings.tsx

Comment on lines +86 to +104
// Keep empty sections: getAllGrouped returns user-created sections even
// when they have no workspaces, so the optimistic cache should match.
const sections = group.sections.map((section) => ({
...section,
workspaces: section.workspaces.filter(
(workspace) => workspace.id !== workspaceId,
),
}));

return {
...group,
workspaces,
sections,
topLevelItems: isTopLevelWorkspace
? group.topLevelItems.filter((item) => item.id !== workspaceId)
: group.topLevelItems,
} as TGroup;
})
.filter(hasVisibleWorkspaces);
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 | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Locate getAllGrouped and inspect whether it can return groups whose
# `workspaces` is empty AND whose `sections` are all empty.
fd -t f -e ts -e tsx | xargs rg -nP -C5 '\bgetAllGrouped\b'

Repository: MocA-Love/superset

Length of output: 50376


🏁 Script executed:

# Get the full getAllGrouped implementation
sed -n '127,200p' apps/desktop/src/lib/trpc/routers/workspaces/procedures/query.ts

Repository: MocA-Love/superset

Length of output: 1859


🏁 Script executed:

# Find and inspect hasVisibleWorkspaces
fd -type f -name "*.ts" -o -name "*.tsx" | xargs rg -n "hasVisibleWorkspaces" -A 5

Repository: MocA-Love/superset

Length of output: 1106


🏁 Script executed:

# Also check the exact implementation of removeWorkspaceFromGroups
cat -n apps/desktop/src/renderer/react-query/workspaces/utils/workspace-removal.ts

Repository: MocA-Love/superset

Length of output: 3452


🏁 Script executed:

# Get the complete getAllGrouped implementation - get more lines
sed -n '127,300p' apps/desktop/src/lib/trpc/routers/workspaces/procedures/query.ts

Repository: MocA-Love/superset

Length of output: 4730


セクションレベルの保持とグループレベルのフィルタの動作不整合を確認してください。

86–87 行目のコメントは「getAllGrouped が空のセクションでもユーザー作成のセクションを返すため、楽観的キャッシュもそれに合わせる」と述べていますが、104 行目の .filter(hasVisibleWorkspaces) は矛盾した動作をしています。

確認した結果、getAllGrouped はワークスペースを持たないグループ(直下に workspaces がなく、全セクションが空)も返しますが、removeWorkspaceFromGroups でそのようなグループが発生した場合、フィルタによって丸ごと削除されます。これにより:

  • 削除後の楽観的キャッシュには存在しないグループが
  • 再フェッチ時には getAllGrouped によって復活

する可視的なちらつきが発生します。

コメントを「セクションは保持 / グループ全体が空になったら除外」に明示するか、フィルタの動作を変更するかのいずれかで整合性を取ってください。

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

In `@apps/desktop/src/renderer/react-query/workspaces/utils/workspace-removal.ts`
around lines 86 - 104, The current logic in removeWorkspaceFromGroups produces a
mismatch with getAllGrouped because you remove entire groups via
.filter(hasVisibleWorkspaces) even when user-created sections (group.sections)
should be preserved empty; update the filtering to only drop a group when the
group itself and all its sections have no workspaces (e.g., replace
hasVisibleWorkspaces semantics so it returns true if group.workspaces.length > 0
OR group.topLevelItems contains the item OR any section in group.sections has
workspaces.length > 0), or alternatively remove the final .filter call and
explicitly drop groups only when both group.workspaces and every
section.workspaces are empty; adjust the logic in removeWorkspaceFromGroups
(referencing group.sections, workspaces, topLevelItems, hasVisibleWorkspaces,
and removeWorkspaceFromGroups) accordingly so optimistic cache matches
getAllGrouped.

Comment on lines +83 to 85
<TooltipContent side="bottom" showArrow={false}>
{CLICK_HINT_TOOLTIP}
</TooltipContent>
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

ツールチップからパス表示が消えたことで、長いパスのフル確認手段が失われています。

ボタン本体の <span>min-w-0 truncate(Line 78)でパスを省略表示する設計のままですが、ツールチップ内容が CLICK_HINT_TOOLTIP のみに簡略化されたため、深い階層のファイルではユーザーがフルパスを確認できなくなります。CLICK_HINT_TOOLTIP はクリック修飾子のヒントを示す目的のメッセージと思われるため、パス表示の代替が必要です。トランケート発生時のみフルパスを併記する、もしくは <button>title={path} を付与してネイティブツールチップで補完するなどの対応をご検討ください。

💡 ネイティブ `title` で補完する案
 					<button
 						type="button"
 						onClick={(event) => {
 							const intent = getSidebarClickIntent(event);
 							if (intent === "openInEditor") {
 								onOpenInExternalEditor?.();
 								return;
 							}
 							onOpenFile?.(intent === "openInNewTab");
 						}}
 						disabled={!onOpenFile && !onOpenInExternalEditor}
 						aria-label="Open in file viewer"
+						title={path}
 						className="flex h-6 min-w-0 items-center gap-1.5 rounded border border-border px-1.5 py-0.5 text-left transition-colors hover:bg-accent disabled:pointer-events-none disabled:opacity-60"
 					>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/usePaneRegistry/components/DiffPane/components/DiffFileHeader/DiffFileHeader.tsx
around lines 83 - 85, The file's TooltipContent was reduced to only
CLICK_HINT_TOOLTIP which removed the ability to view long truncated paths shown
in the <span className="min-w-0 truncate"> inside the DiffFileHeader component;
restore full-path visibility by adding a native tooltip and/or including the
full path in the tooltip: set title={path} on the enclosing <button> element to
provide a native fallback for full paths, and/or change TooltipContent to render
the full path alongside CLICK_HINT_TOOLTIP (or conditionally render the full
path only when the span is truncated) so users can see the complete file path.

Comment on lines +57 to +68
{isOwner && (
<TableCell>
<Button
variant="ghost"
size="sm"
onClick={() => onRemove(member)}
aria-label={`Remove ${member.name}`}
>
<HiOutlineTrash className="h-4 w-4" />
</Button>
</TableCell>
)}
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 | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# HostSettings の onRemove ハンドラに確認 UI(AlertDialog/confirm 等)が含まれているか確認
fd -t f 'HostSettings.tsx' apps/desktop/src/renderer --exec rg -n -C5 'onRemove|AlertDialog|ConfirmDialog|confirm\(' {}

Repository: MocA-Love/superset

Length of output: 363


🏁 Script executed:

fd -t f 'HostSettings.tsx' apps/desktop/src/renderer

Repository: MocA-Love/superset

Length of output: 174


🏁 Script executed:

cat -n apps/desktop/src/renderer/routes/_authenticated/settings/hosts/$hostId/components/HostSettings/HostSettings.tsx

Repository: MocA-Love/superset

Length of output: 199


🏁 Script executed:

fd -t f 'HostSettings.tsx' apps/desktop/src/renderer -x wc -l {}

Repository: MocA-Love/superset

Length of output: 178


🏁 Script executed:

cat -n 'apps/desktop/src/renderer/routes/_authenticated/settings/hosts/$hostId/components/HostSettings/HostSettings.tsx'

Repository: MocA-Love/superset

Length of output: 6535


🏁 Script executed:

rg -n 'AlertDialog|ConfirmDialog|confirm\(' 'apps/desktop/src/renderer/routes/_authenticated/settings/hosts/$hostId/components/HostSettings/' -A3 -B3

Repository: MocA-Love/superset

Length of output: 44


🏁 Script executed:

cat -n 'apps/desktop/src/renderer/routes/_authenticated/settings/hosts/$hostId/components/HostSettings/components/MembersTable/components/MemberRow/MemberRow.tsx'

Repository: MocA-Love/superset

Length of output: 2190


メンバー削除時に確認ダイアログを追加してください。

handleRemove関数(HostSettings.tsx 142-147行)で削除確認が実装されていません。onClick={() => onRemove(member)}がトラッシュボタン押下で即座に削除コールバックを呼び、確認ダイアログなしに削除が実行されます。メンバー削除は破壊的な操作であり、誤操作による削除のリスクがあります。削除前に確認ダイアログ(AlertDialog等)を表示し、ユーザーの明示的な確認を取ってください。

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

In
`@apps/desktop/src/renderer/routes/_authenticated/settings/hosts/`$hostId/components/HostSettings/components/MembersTable/components/MemberRow/MemberRow.tsx
around lines 57 - 68, The trash Button in MemberRow.tsx currently calls
onRemove(member) directly; wrap that action in a confirmation step so
destructive deletes require explicit user consent: replace the direct onClick
handler on the Button (MemberRow -> onClick={() => onRemove(member)}) with logic
that opens a confirmation dialog (e.g., AlertDialog or window.confirm) and only
calls the provided onRemove(member) if the user confirms; coordinate with the
existing handleRemove function in HostSettings.tsx (the delete callback
referenced there) so the same callback is invoked after confirmation and no
other behavior changes.

Comment on lines +30 to +48
const activeOrganizationId = env.SKIP_ENV_VALIDATION
? MOCK_ORG_ID
: (session?.session?.activeOrganizationId ?? null);

const { data: hosts = [] } = useLiveQuery(
(q) =>
q
.from({ hosts: collections.v2Hosts })
.where(({ hosts }) =>
eq(hosts.organizationId, activeOrganizationId ?? ""),
)
.select(({ hosts }) => ({
id: hosts.id,
name: hosts.name,
machineId: hosts.machineId,
isOnline: hosts.isOnline,
})),
[collections, activeOrganizationId],
);
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

activeOrganizationId が null のときに「No hosts yet.」が誤表示される可能性。

activeOrganizationIdnull(セッションのロード中、もしくはユーザーがどの組織にも属していない状態)の場合、eq(hosts.organizationId, "") にフォールバックして常に空配列となり、結果として「組織にホストが無い」と区別がつかないまま isEmpty 経由で "No hosts yet." が表示されます。少なくとも activeOrganizationId が確定するまではクエリ自体をスキップ、または別の状態(例: 何も表示しない/スケルトン)に分岐したほうが UX として正確です。

💡 修正案の一例
-	const { data: hosts = [] } = useLiveQuery(
-		(q) =>
-			q
-				.from({ hosts: collections.v2Hosts })
-				.where(({ hosts }) =>
-					eq(hosts.organizationId, activeOrganizationId ?? ""),
-				)
-				.select(({ hosts }) => ({
-					id: hosts.id,
-					name: hosts.name,
-					machineId: hosts.machineId,
-					isOnline: hosts.isOnline,
-				})),
-		[collections, activeOrganizationId],
-	);
+	const { data: hosts = [] } = useLiveQuery(
+		(q) =>
+			q
+				.from({ hosts: collections.v2Hosts })
+				.where(({ hosts }) =>
+					eq(hosts.organizationId, activeOrganizationId ?? ""),
+				)
+				.select(({ hosts }) => ({
+					id: hosts.id,
+					name: hosts.name,
+					machineId: hosts.machineId,
+					isOnline: hosts.isOnline,
+				})),
+		[collections, activeOrganizationId],
+	);
+	const hasOrgContext = activeOrganizationId !== null;

そのうえで、isEmpty の条件に hasOrgContext を加えるか、ロード中の表示を別途分岐させることをご検討ください。

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

In
`@apps/desktop/src/renderer/routes/_authenticated/settings/hosts/components/HostsSettingsSidebar/HostsSettingsSidebar.tsx`
around lines 30 - 48, The current query falls back to eq(hosts.organizationId,
"") when activeOrganizationId is null, causing "No hosts yet." to display
incorrectly; modify the logic so useLiveQuery does not run when
activeOrganizationId is null and only queries when a valid ID exists (e.g.,
guard or pass a skip condition into the useLiveQuery call), and update the
render condition that checks hosts/isEmpty to also require a confirmed
organization context (activeOrganizationId != null) so that while the
session/org is loading you show a skeleton/blank state instead of "No hosts
yet."; touch the symbols activeOrganizationId, useLiveQuery (the query selector
for collections.v2Hosts), and the isEmpty rendering branch to implement this
behavior.

Comment on lines +23 to +60
const { data: hosts = [] } = useLiveQuery(
(q) =>
q
.from({ hosts: collections.v2Hosts })
.where(({ hosts }) =>
eq(hosts.organizationId, activeOrganizationId ?? ""),
)
.select(({ hosts }) => ({
id: hosts.id,
name: hosts.name,
isOnline: hosts.isOnline,
})),
[collections, activeOrganizationId],
);

const firstHostId = useMemo(() => {
const sorted = [...hosts].sort((a, b) => a.name.localeCompare(b.name));
const online = sorted.find((h) => h.isOnline);
return (online ?? sorted[0])?.id ?? null;
}, [hosts]);

useEffect(() => {
if (firstHostId) {
navigate({
to: "/settings/hosts/$hostId",
params: { hostId: firstHostId },
replace: true,
});
}
}, [firstHostId, navigate]);

if (hosts.length === 0) {
return (
<div className="flex items-center justify-center h-full p-6 text-sm text-muted-foreground">
No hosts yet.
</div>
);
}
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

🌐 Web query:

@tanstack/react-db useLiveQuery loading status return shape

💡 Result:

The useLiveQuery hook from @tanstack/react-db returns an object with reactive data, state, and status information, including loading status indicators. The key properties related to loading status are: - isLoading: boolean - True when the collection status is 'loading' (initial data load) - status: UseLiveQueryStatus - Current status, where UseLiveQueryStatus = CollectionStatus | 'disabled' - CollectionStatus values: 'idle', 'loading', 'initialCommit', 'ready', 'error', 'cleaned-up' Other status booleans: - isReady: boolean - True when status === 'ready' - isIdle: boolean - True when status === 'idle' - isError: boolean - True when status === 'error' - isCleanedUp: boolean - True when status === 'cleaned-up' - isEnabled: boolean - False when query is disabled (callback returns undefined/null) Full return shape (simplified from TypeScript overloads): { data: InferResultType | undefined; state: Map<string | number, GetResult> | undefined; collection: Collection<...> | undefined; status: UseLiveQueryStatus; isLoading: boolean; isReady: boolean; isIdle: boolean; isError: boolean; isCleanedUp: boolean; isEnabled: boolean; } Example usage for loading: const { data, isLoading, isError, status } = useLiveQuery((q) => q.from({ todos: todoCollection }) ) if (isLoading) return

Loading...
if (isError) return
Error: {status}
When the query callback returns undefined/null (disabled): status = 'disabled', all is* flags false except isEnabled = false, data/state/collection = undefined.

Citations:


初回ロード時に "No hosts yet." がフラッシュ表示される可能性があります。

useLiveQuery のデータが到着するまでの間、デフォルト値 [] により hosts.length === 0 条件が満たされるため、空状態メッセージが一瞬表示されます。useLiveQuerystatus フィールド(値: 'idle', 'loading', 'initialCommit', 'ready', 'error', 'cleaned-up')を利用し、status === "ready" になるまで空状態の表示を遅延させることで回避できます。

♻️ 提案: ロード完了後にのみ空状態を表示
-	const { data: hosts = [] } = useLiveQuery(
+	const { data: hosts = [], status } = useLiveQuery(
 		(q) =>
 			q
@@
-	if (hosts.length === 0) {
+	if (status === "ready" && hosts.length === 0) {
 		return (
 			<div className="flex items-center justify-center h-full p-6 text-sm text-muted-foreground">
 				No hosts yet.
 			</div>
 		);
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/routes/_authenticated/settings/hosts/page.tsx`
around lines 23 - 60, The empty-state flashes because useLiveQuery defaults data
to [] before results arrive; update the hook destructure to also grab the
returned status (e.g., const { data: hosts = [], status } = useLiveQuery(...))
and change the render guard to only show the "No hosts yet." message when status
=== "ready" && hosts.length === 0; keep the firstHostId useMemo and navigation
effect as-is but ensure they handle hosts being empty/null until status becomes
"ready".

Comment on lines +24 to +77
const { data: groups = [] } =
electronTrpc.workspaces.getAllGrouped.useQuery();

const { data: v2Projects = [] } = useLiveQuery(
(q) =>
q
.from({ projects: collections.v2Projects })
.where(({ projects }) =>
eq(projects.organizationId, activeOrganizationId ?? ""),
)
.select(({ projects }) => ({
id: projects.id,
name: projects.name,
})),
[collections, activeOrganizationId],
);

const firstProjectId = useMemo(() => {
const v2Sorted = [...v2Projects].sort((a, b) =>
a.name.localeCompare(b.name),
);
if (v2Sorted[0]) return v2Sorted[0].id;

const loadedV2Ids = new Set(v2Projects.map((p) => p.id));
const v1Sorted = groups
.filter(
(g) =>
!g.project.neonProjectId || !loadedV2Ids.has(g.project.neonProjectId),
)
.map((g) => g.project)
.sort((a, b) => a.name.localeCompare(b.name));
return v1Sorted[0]?.id ?? null;
}, [v2Projects, groups]);

useEffect(() => {
if (firstProjectId) {
navigate({
to: "/settings/projects/$projectId",
params: { projectId: firstProjectId },
replace: true,
});
}
}, [firstProjectId, navigate]);

const isEmpty = v2Projects.length === 0 && groups.length === 0;
if (isEmpty) {
return (
<div className="flex items-center justify-center h-full p-6 text-sm text-muted-foreground">
No projects yet.
</div>
);
}

return null;
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
# useLiveQuery の戻り値に loading/pending 系のフラグがあるか確認
rg -nP '\buseLiveQuery\b|isPending|isLoading|status' -C3 -g '!**/node_modules/**' --type=ts --type=tsx | head -100

Repository: MocA-Love/superset

Length of output: 90


🏁 Script executed:

#!/bin/bash
# Find useLiveQuery definition and usage patterns
fd -t f '\.(ts|tsx)$' -x grep -l 'useLiveQuery' {} \; 2>/dev/null | head -20

Repository: MocA-Love/superset

Length of output: 2245


🏁 Script executed:

#!/bin/bash
# Check the file being reviewed
cat -n apps/desktop/src/renderer/routes/_authenticated/settings/projects/page.tsx 2>/dev/null || echo "File not found"

Repository: MocA-Love/superset

Length of output: 2879


🏁 Script executed:

#!/bin/bash
# Search for useLiveQuery definition
rg 'export.*useLiveQuery|function useLiveQuery|const useLiveQuery' -A 5 2>/dev/null | head -50

Repository: MocA-Love/superset

Length of output: 44


🏁 Script executed:

#!/bin/bash
# Check tRPC query hook return values
rg 'useQuery\(\)' -C 3 --type ts 2>/dev/null | grep -E '(isPending|isFetched|isLoading|status)' | head -20

Repository: MocA-Love/superset

Length of output: 3494


🏁 Script executed:

#!/bin/bash
# Find tRPC useQuery hook definition
rg -A 10 'export.*useQuery|function useQuery' apps/desktop --type ts 2>/dev/null | head -100

Repository: MocA-Love/superset

Length of output: 44


🏁 Script executed:

#!/bin/bash
# Search for useLiveQuery hook to see its full return signature
rg 'useLiveQuery' apps/desktop/src/renderer/routes/_authenticated/settings/projects/ -B 2 -A 5 --type ts 2>/dev/null

Repository: MocA-Love/superset

Length of output: 6417


🏁 Script executed:

#!/bin/bash
# Check similar pages/components for how they handle loading states with useLiveQuery
rg -l 'useLiveQuery' apps/desktop/src/renderer/routes/_authenticated/ --type ts 2>/dev/null | xargs -I {} grep -l 'isPending\|isLoading\|status' {} 2>/dev/null | head -5

Repository: MocA-Love/superset

Length of output: 750


🏁 Script executed:

#!/bin/bash
# Check if electronTrpc queries return isPending
rg 'electronTrpc\..*\.useQuery.*isPending|electronTrpc\..*\.useQuery.*isLoading' apps/desktop --type ts 2>/dev/null | head -20

Repository: MocA-Love/superset

Length of output: 44


🏁 Script executed:

#!/bin/bash
# Find electron-trpc setup and useQuery hook definition
fd -t f 'electron-trpc' apps/desktop/src/renderer --type ts 2>/dev/null | head -5

Repository: MocA-Love/superset

Length of output: 44


🏁 Script executed:

#!/bin/bash
# Check electron-trpc lib implementation
cat -n apps/desktop/src/renderer/lib/electron-trpc.ts 2>/dev/null || cat -n apps/desktop/src/renderer/lib/electron-trpc/index.ts 2>/dev/null || echo "File not found"

Repository: MocA-Love/superset

Length of output: 614


🏁 Script executed:

#!/bin/bash
# Look for getAllGrouped definition to understand what it returns
rg 'getAllGrouped' apps/desktop --type ts -B 3 -A 10 2>/dev/null | head -60

Repository: MocA-Love/superset

Length of output: 7335


🏁 Script executed:

#!/antml/bash
# Check if tanstack/react-db useLiveQuery returns any loading/status field
rg '@tanstack/react-db' -l apps/desktop --type ts 2>/dev/null | head -3 | xargs -I {} grep -A 20 'useLiveQuery' {} 2>/dev/null | head -50

Repository: MocA-Love/superset

Length of output: 1674


🏁 Script executed:

#!/bin/bash
# Check how other parts of the codebase handle tRPC query loading states
rg 'electronTrpc\..*\.useQuery\(\)' apps/desktop/src/renderer -A 1 --type ts 2>/dev/null | grep -E '(isPending|isLoading|isFetching|status)' | head -20

Repository: MocA-Love/superset

Length of output: 238


🏁 Script executed:

#!/bin/bash
# Check if isPending is used anywhere with tRPC queries in this codebase
rg 'isPending.*electronTrpc|electronTrpc.*isPending' apps/desktop --type ts 2>/dev/null

Repository: MocA-Love/superset

Length of output: 44


🏁 Script executed:

#!/antml:bash
# Check tanstack/db and react-db documentation/types in node_modules or package.json
cat apps/desktop/package.json 2>/dev/null | grep -A 2 -B 2 'tanstack'

Repository: MocA-Love/superset

Length of output: 680


🏁 Script executed:

#!/bin/bash
# Look at what tRPC React returns - check for actual usage patterns
rg '@trpc/react-query|createTRPCReact' -A 15 apps/desktop/src/renderer/lib --type ts 2>/dev/null | head -40

Repository: MocA-Love/superset

Length of output: 2686


🏁 Script executed:

#!/bin/bash
# Check the actual tRPC React Query hook implementation or type definitions
rg 'isPending|isLoading' apps/desktop/src/renderer/routes/_authenticated/settings --type ts -B 2 | grep -E '(useQuery|isLoading|isPending)' | head -30

Repository: MocA-Love/superset

Length of output: 5591


🏁 Script executed:

#!/bin/bash
# Check a real example where tRPC useQuery is destructured with loading state
cat -n apps/desktop/src/renderer/routes/_authenticated/settings/ringtones/components/RingtonesSettings/RingtonesSettings.tsx 2>/dev/null | grep -A 3 -B 3 'isMutedLoading'

Repository: MocA-Love/superset

Length of output: 680


🏁 Script executed:

#!/bin/bash
# Look for useLiveQuery usage with isLoading
rg 'const.*useLiveQuery' apps/desktop/src/renderer/routes/_authenticated/settings/account -A 1 --type ts 2>/dev/null

Repository: MocA-Love/superset

Length of output: 387


🏁 Script executed:

#!/bin/bash
# Verify the exact destructuring pattern for tRPC useQuery with loading state
rg 'electronTrpc\..*\.useQuery.*{' -A 1 apps/desktop/src/renderer --type ts 2>/dev/null | grep -E '(isLoading|isPending)' | head -10

Repository: MocA-Love/superset

Length of output: 44


初回ロード時に "No projects yet." が一瞬表示される可能性があります。

useLiveQueryelectronTrpc.workspaces.getAllGrouped.useQuery() のいずれもデータ未取得時に [] をデフォルトとしているため、リダイレクト先が確定するまでの間 isEmptytrue となり、空状態のメッセージがフラッシュ表示されます。tRPC useQuery の isLoading フラグを組み合わせて、「ロード完了済み」かつ「データが空」の場合のみ空状態を表示することを推奨します。

♻️ 提案: ロード完了を待ってから空状態を判定
-	const { data: groups = [] } =
-		electronTrpc.workspaces.getAllGrouped.useQuery();
+	const { data: groups = [], isLoading: isGroupsLoading } =
+		electronTrpc.workspaces.getAllGrouped.useQuery();
@@
-	const isEmpty = v2Projects.length === 0 && groups.length === 0;
-	if (isEmpty) {
+	const isLoaded = !isGroupsLoading;
+	const isEmpty = isLoaded && v2Projects.length === 0 && groups.length === 0;
+	if (isEmpty) {
 		return (
 			<div className="flex items-center justify-center h-full p-6 text-sm text-muted-foreground">
 				No projects yet.
 			</div>
 		);
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/routes/_authenticated/settings/projects/page.tsx`
around lines 24 - 77, The empty-state flashes because both
electronTrpc.workspaces.getAllGrouped.useQuery() and useLiveQuery() default to
[] so isEmpty becomes true before real data loads; fix by waiting for both
queries to finish before showing the empty UI: read the isLoading (or status)
from electronTrpc.workspaces.getAllGrouped.useQuery (e.g., add
isLoading/groupLoad flag) and add a simple v2Loaded flag for useLiveQuery (set
true when v2Projects is first populated), then change the empty check to only
return the "No projects yet." UI when both the workspaces query is not loading
and the v2 query is loaded and both data sets are empty (update references to
groups, v2Projects, firstProjectId and isEmpty accordingly).

Comment on lines +81 to +111
const result = await dbWs.transaction(async (tx) => {
const [inserted] = await tx
.insert(v2UsersHosts)
.values({
id: input.id,
organizationId,
userId: input.userId,
hostId: input.hostId,
role: input.role ?? "member",
})
.onConflictDoNothing({
target: [
v2UsersHosts.organizationId,
v2UsersHosts.userId,
v2UsersHosts.hostId,
],
})
.returning();
const txid = await getCurrentTxid(tx);
return { inserted, txid };
});

if (!result.inserted) {
throw new TRPCError({
code: "CONFLICT",
message: "User already has access to this host",
});
}

return { ...result.inserted, txid: result.txid };
}),
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
fd -t f 'v2-users-hosts*' -e ts -x cat {}
fd -t f 'v2_users_hosts*' -x cat {}
rg -nP -C3 '\bv2UsersHosts\b\s*=\s*pgTable|\.unique\(|primaryKey' --type=ts

Repository: MocA-Love/superset

Length of output: 41407


🏁 Script executed:

# Get the full v2UsersHosts table definition
sed -n '487,523p' packages/db/src/schema/schema.ts

Repository: MocA-Love/superset

Length of output: 1215


🏁 Script executed:

# Also check the actual code at lines 81-111 in the file mentioned
sed -n '70,115p' packages/trpc/src/router/v2-host/v2-host.ts

Repository: MocA-Love/superset

Length of output: 1230


onConflictDoNothing の target が PK 衝突をカバーしていません。

target: [organizationId, userId, hostId] は「同じユーザーが同じホストに既に所属」のケースを no-op にしますが、クライアントが渡す input.id が既存行と衝突した場合(たとえば楽観的更新のリトライで同じ UUID を再送),PK 制約違反として未整形のエラーが上がります。onConflictDoNothing では PK 衝突をキャッチできないため、CONFLICT エラーとして処理されず、代わりに database constraint error になります。

設計として、サーバ側で id を生成する方針に統一するか、あるいは targetid を含める(ただし通常 UUID は重複しないため現実的でない)ことをご検討ください。

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

In `@packages/trpc/src/router/v2-host/v2-host.ts` around lines 81 - 111, The
onConflictDoNothing target on the insert into v2UsersHosts (inside the
dbWs.transaction lambda) doesn't catch primary-key collisions for input.id, so a
duplicate client-supplied id can still raise a DB constraint error; fix by
generating the row id server-side (remove reliance on input.id) or ensure
conflict handling includes the PK (e.g., include id in the conflict target) and
handle the no-op case consistently; update the insert call that sets id from
input.id and the conflict target used by onConflictDoNothing (and any callers of
this route that expect to supply id) so the server always produces a stable id
or the conflict target covers PK collisions.

Comment on lines +149 to +167
if (target.role === "owner") {
const otherOwners = await tx
.select({ id: v2UsersHosts.id })
.from(v2UsersHosts)
.where(
and(
eq(v2UsersHosts.hostId, input.hostId),
eq(v2UsersHosts.role, "owner"),
ne(v2UsersHosts.userId, input.userId),
),
)
.for("update");
if (otherOwners.length === 0) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "A host must have at least one owner.",
});
}
}
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 | 🟠 Major

所有者最終 1 名チェックに TOCTOU レースがあります(major)。

FOR UPDATE でロックされるのは「自分以外の owner 行」だけで、削除対象行(input.userId の owner 行)は含まれていません。owner が A,B の 2 名のとき、Tx1: A を削除しようとして B を FOR UPDATE、Tx2: B を削除しようとして A を FOR UPDATE — どちらも otherOwners.length === 1 を満たして delete を実行 → コミット後に owner が 0 名になります。

対策案:

  • 同じトランザクション内で対象行も FOR UPDATE してロック順序を一意にする
  • もしくはトランザクションを SERIALIZABLE 分離レベルで実行する
  • もしくはホスト単位の advisory lock(pg_advisory_xact_lock(hash(hostId)))でクリティカルセクションを直列化する
🔒️ 修正案 (対象行も FOR UPDATE する)
-			const txid = await dbWs.transaction(async (tx) => {
-				const target = await tx.query.v2UsersHosts.findFirst({
-					where: and(
-						eq(v2UsersHosts.hostId, input.hostId),
-						eq(v2UsersHosts.userId, input.userId),
-					),
-					columns: { role: true },
-				});
+			const txid = await dbWs.transaction(async (tx) => {
+				const [target] = await tx
+					.select({ role: v2UsersHosts.role })
+					.from(v2UsersHosts)
+					.where(
+						and(
+							eq(v2UsersHosts.hostId, input.hostId),
+							eq(v2UsersHosts.userId, input.userId),
+						),
+					)
+					.for("update");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/trpc/src/router/v2-host/v2-host.ts` around lines 149 - 167, The
check for "at least one owner" has a TOCTOU race because the SELECT ...
for("update") only locks other owners and not the row being deleted
(v2UsersHosts row for input.userId), allowing two concurrent deletes to race;
update the transaction logic around tx and v2UsersHosts so the SELECT ...
for("update") locks the target row as well (include eq(v2UsersHosts.userId,
input.userId) in the locked predicate or select by hostId and role "owner"
without excluding input.userId) to ensure deterministic lock ordering before
deleting, or alternatively acquire a host-scoped advisory lock
(pg_advisory_xact_lock) keyed by input.hostId inside the same tx to serialize
deletions.

Comment on lines +223 to +241
if (input.role === "member" && target.role === "owner") {
const otherOwners = await tx
.select({ id: v2UsersHosts.id })
.from(v2UsersHosts)
.where(
and(
eq(v2UsersHosts.hostId, input.hostId),
eq(v2UsersHosts.role, "owner"),
ne(v2UsersHosts.userId, input.userId),
),
)
.for("update");
if (otherOwners.length === 0) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "A host must have at least one owner.",
});
}
}
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 | 🟠 Major

setMemberRole でも同様の TOCTOU レースが残っています(major)。

owner → member への降格でも、対象行を FOR UPDATE していないため、removeMember と同じく 2 つの並行リクエストが互いの「他に owner がいる」検査をすり抜け、最終的に owner が 0 名になり得ます。removeMember の修正と合わせて、対象行を同一トランザクションでロックするか、SERIALIZABLE または advisory lock を採用してください。

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

In `@packages/trpc/src/router/v2-host/v2-host.ts` around lines 223 - 241, In
setMemberRole there is a TOCTOU race when demoting owner→member: you must lock
the target membership row in the same transaction (same tx used to query
otherOwners) so two concurrent demote/remove requests can't both pass the "other
owners exist" check; update setMemberRole to SELECT the target v2UsersHosts row
(by hostId+userId or the same predicate used to build target) FOR UPDATE (or
otherwise acquire the same advisory lock or run the transaction at SERIALIZABLE)
before querying otherOwners — mirror the locking approach used in removeMember
and ensure tx, v2UsersHosts, target, and the otherOwners check occur under that
lock.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

<button
className="flex h-full min-w-0 flex-1 items-center gap-1.5 pl-3 pr-1 text-left text-xs transition-colors"
onAuxClick={(event) => {
if (event.button === 1) {

P2 Badge Preserve keyboard activation for tab selection

Selecting a tab now relies only on the parent div’s onMouseDown, but keyboard activation (Enter/Space) on this focused tab button fires click events, not mouse-down events. Because the tab label button no longer calls onSelect, keyboard users can focus a tab but cannot activate it, which regresses tab navigation accessibility and keyboard-driven workflows.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

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.

4 participants