upstream merge 2026-04-27 PR-A: UI/behavior fixes + hosts settings (12 commits)#445
upstream merge 2026-04-27 PR-A: UI/behavior fixes + hosts settings (12 commits)#445
Conversation
* 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 へ更新する。
📝 Walkthroughウォークスルーこのプルリクエストは、ワークスペース削除後のナビゲーションロジックを統一し、ホスト専用メンバーシップ管理機能を追加し、サイドバーとペインの再レンダリングを最適化するための広範な改善を実装しています。また、設定画面に新しいホスト管理セクションを追加し、通知状態とペーンフォーカスメカニズムを強化しています。 変更内容
推定レビュー工数🎯 4 (複雑) | ⏱️ ~60 分 関連する可能性のあるPR
ウサギの詩
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning Review ran into problems🔥 ProblemsTimed 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. Comment |
🧹 Preview Cleanup CompleteThe following preview resources have been cleaned up:
Thank you for your contribution! 🎉 |
There was a problem hiding this comment.
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.tsxplusindex.tsfor 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の例外はfinallyのinvalidate後に再スローされ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が未変更ならエラー」だけで、changesにroleと一緒に他のフィールド(例:hostId、userIdなど)が含まれている場合は素通りし、サーバーには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)とhandleSetRole、MembersTableProps.onSetRole、MemberRowProps.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.role(MemberRow.tsx行15):V2UsersHostRoleに変更handleSetRoleパラメータ(HostSettings.tsx行149):role: V2UsersHostRoleに変更MembersTableProps.onSetRole(MembersTable.tsx行14):role: V2UsersHostRoleに変更MemberRowProps.onSetRole(MemberRow.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()が一部安定化を相殺します(最適化の改善余地)。
useStableDashboardSidebarProjectsはJSON.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 で明示することを推奨します。
removedWorkspaceIdをcurrentActiveIdとremovedIdの両方に渡しているため、戻り値は実質的に「削除対象がアクティブだった場合に次にフォーカスすべき 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
📒 Files selected for processing (60)
apps/desktop/plans/done/20260425-1430-sidebar-remove-and-rerender.mdapps/desktop/src/renderer/react-query/workspaces/useCloseWorkspace.tsapps/desktop/src/renderer/react-query/workspaces/useDeleteWorkspace.tsapps/desktop/src/renderer/react-query/workspaces/utils/workspace-removal.test.tsapps/desktop/src/renderer/react-query/workspaces/utils/workspace-removal.tsapps/desktop/src/renderer/routes/_authenticated/_dashboard/automations/page.tsxapps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/DashboardSidebar.tsxapps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/DashboardSidebarPortsList.tsxapps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsxapps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceContextMenu/DashboardSidebarWorkspaceContextMenu.tsxapps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.tsapps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/useDashboardSidebarData.tsapps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarShortcuts/useDashboardSidebarShortcuts.tsapps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useNavigateAwayFromWorkspace/useNavigateAwayFromWorkspace.tsapps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/utils/getDeleteFocusTargetWorkspaceId/getDeleteFocusTargetWorkspaceId.tsapps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/utils/getDeleteFocusTargetWorkspaceId/index.tsapps/desktop/src/renderer/routes/_authenticated/_dashboard/pending/$pendingId/page.tsxapps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/DiffPane/components/DiffFileHeader/DiffFileHeader.tsxapps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/TerminalPane.tsxapps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/components/TerminalSessionDropdown/TerminalSessionDropdown.tsxapps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useV2WorkspacePaneLayout/useV2WorkspacePaneLayout.tsapps/desktop/src/renderer/routes/_authenticated/hooks/useOptimisticCollectionActions/useOptimisticCollectionActions.tsapps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/CollectionsProvider.tsxapps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/collections.tsapps/desktop/src/renderer/routes/_authenticated/settings/components/SettingsSidebar/GeneralSettings.tsxapps/desktop/src/renderer/routes/_authenticated/settings/components/SettingsSidebar/ProjectsSettings.tsxapps/desktop/src/renderer/routes/_authenticated/settings/components/SettingsSidebar/SettingsSidebar.tsxapps/desktop/src/renderer/routes/_authenticated/settings/hosts/$hostId/components/HostSettings/HostSettings.tsxapps/desktop/src/renderer/routes/_authenticated/settings/hosts/$hostId/components/HostSettings/components/AddMemberDropdown/AddMemberDropdown.tsxapps/desktop/src/renderer/routes/_authenticated/settings/hosts/$hostId/components/HostSettings/components/AddMemberDropdown/index.tsapps/desktop/src/renderer/routes/_authenticated/settings/hosts/$hostId/components/HostSettings/components/HostHeader/HostHeader.tsxapps/desktop/src/renderer/routes/_authenticated/settings/hosts/$hostId/components/HostSettings/components/HostHeader/index.tsapps/desktop/src/renderer/routes/_authenticated/settings/hosts/$hostId/components/HostSettings/components/MembersTable/MembersTable.tsxapps/desktop/src/renderer/routes/_authenticated/settings/hosts/$hostId/components/HostSettings/components/MembersTable/components/MemberRow/MemberRow.tsxapps/desktop/src/renderer/routes/_authenticated/settings/hosts/$hostId/components/HostSettings/components/MembersTable/components/MemberRow/index.tsapps/desktop/src/renderer/routes/_authenticated/settings/hosts/$hostId/components/HostSettings/components/MembersTable/index.tsapps/desktop/src/renderer/routes/_authenticated/settings/hosts/$hostId/components/HostSettings/index.tsapps/desktop/src/renderer/routes/_authenticated/settings/hosts/$hostId/page.tsxapps/desktop/src/renderer/routes/_authenticated/settings/hosts/components/HostsSettingsSidebar/HostsSettingsSidebar.tsxapps/desktop/src/renderer/routes/_authenticated/settings/hosts/components/HostsSettingsSidebar/index.tsapps/desktop/src/renderer/routes/_authenticated/settings/hosts/layout.tsxapps/desktop/src/renderer/routes/_authenticated/settings/hosts/page.tsxapps/desktop/src/renderer/routes/_authenticated/settings/layout.tsxapps/desktop/src/renderer/routes/_authenticated/settings/projects/components/ProjectsSettingsSidebar/ProjectsSettingsSidebar.tsxapps/desktop/src/renderer/routes/_authenticated/settings/projects/page.tsxapps/desktop/src/renderer/routes/_authenticated/settings/utils/settings-search/settings-search.tsapps/desktop/src/renderer/stores/settings-state.tsapps/desktop/src/renderer/stores/v2-notifications/index.tsapps/desktop/src/renderer/stores/v2-notifications/store.test.tsapps/desktop/src/renderer/stores/v2-notifications/store.tspackages/panes/src/core/store/store.test.tspackages/panes/src/core/store/store.tspackages/panes/src/core/store/utils/index.tspackages/panes/src/core/store/utils/utils.test.tspackages/panes/src/core/store/utils/utils.tspackages/panes/src/index.tspackages/panes/src/react/components/Workspace/components/TabBar/components/TabItem/TabItem.tsxpackages/trpc/src/root.tspackages/trpc/src/router/v2-host/index.tspackages/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
| // 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); |
There was a problem hiding this comment.
🧩 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.tsRepository: 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 5Repository: 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.tsRepository: 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.tsRepository: 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.
| <TooltipContent side="bottom" showArrow={false}> | ||
| {CLICK_HINT_TOOLTIP} | ||
| </TooltipContent> |
There was a problem hiding this comment.
ツールチップからパス表示が消えたことで、長いパスのフル確認手段が失われています。
ボタン本体の <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.
| {isOwner && ( | ||
| <TableCell> | ||
| <Button | ||
| variant="ghost" | ||
| size="sm" | ||
| onClick={() => onRemove(member)} | ||
| aria-label={`Remove ${member.name}`} | ||
| > | ||
| <HiOutlineTrash className="h-4 w-4" /> | ||
| </Button> | ||
| </TableCell> | ||
| )} |
There was a problem hiding this comment.
🧩 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/rendererRepository: MocA-Love/superset
Length of output: 174
🏁 Script executed:
cat -n apps/desktop/src/renderer/routes/_authenticated/settings/hosts/$hostId/components/HostSettings/HostSettings.tsxRepository: 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 -B3Repository: 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.
| 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], | ||
| ); |
There was a problem hiding this comment.
activeOrganizationId が null のときに「No hosts yet.」が誤表示される可能性。
activeOrganizationId が null(セッションのロード中、もしくはユーザーがどの組織にも属していない状態)の場合、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.
| 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> | ||
| ); | ||
| } |
There was a problem hiding this comment.
🧩 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
Citations:
- 1: https://tanstack.com/db/latest/docs/framework/react/reference/functions/useLiveQuery
- 2: https://github.com/TanStack/db/blob/main/packages/react-db/src/useLiveQuery.ts
- 3: https://tanstack.com/db/latest/docs/framework/react/reference/type-aliases/UseLiveQueryStatus
- 4: https://tanstack.com/db/latest/docs/guides/error-handling
初回ロード時に "No hosts yet." がフラッシュ表示される可能性があります。
useLiveQuery のデータが到着するまでの間、デフォルト値 [] により hosts.length === 0 条件が満たされるため、空状態メッセージが一瞬表示されます。useLiveQuery の status フィールド(値: '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".
| 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; |
There was a problem hiding this comment.
🧩 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 -100Repository: 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 -20Repository: 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 -50Repository: 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 -20Repository: 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 -100Repository: 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/nullRepository: 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 -5Repository: 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 -20Repository: 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 -5Repository: 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 -60Repository: 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 -50Repository: 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 -20Repository: 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/nullRepository: 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 -40Repository: 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 -30Repository: 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/nullRepository: 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 -10Repository: MocA-Love/superset
Length of output: 44
初回ロード時に "No projects yet." が一瞬表示される可能性があります。
useLiveQuery と electronTrpc.workspaces.getAllGrouped.useQuery() のいずれもデータ未取得時に [] をデフォルトとしているため、リダイレクト先が確定するまでの間 isEmpty が true となり、空状態のメッセージがフラッシュ表示されます。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).
| 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 }; | ||
| }), |
There was a problem hiding this comment.
🧩 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=tsRepository: MocA-Love/superset
Length of output: 41407
🏁 Script executed:
# Get the full v2UsersHosts table definition
sed -n '487,523p' packages/db/src/schema/schema.tsRepository: 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.tsRepository: 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 を生成する方針に統一するか、あるいは target に id を含める(ただし通常 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.
| 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.", | ||
| }); | ||
| } | ||
| } |
There was a problem hiding this comment.
所有者最終 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.
| 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.", | ||
| }); | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
💡 Codex Review
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".
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)
8afa203f557d450d4160ff70b23fa301c4da270279a514b8a7a84dcb6a9dc059f1fee2c98ca220ad49616010f495818ed2d26fecf141加えて 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.tsxfa301c4da(terminal ドロップアニメ) と9f1fee2c9(terminal session dropdown 簡素化) で fork の以下を維持:apps/desktop/.../DashboardSidebarWorkspaceItem.tsx(fix commit)cb6a9dc05(mark workspace unread) の auto-merge 残りを別 commit で解消。isUnread/onToggleUnreadprops は採用、<div hidden={isDeleting}>ラップとonRemoveFromSidebar={handleRemoveFromSidebar}への切替は 7c14f05 由来のため PR-B 取り込み時に対応。apps/desktop/.../useDashboardSidebarWorkspaceItemActions.ts9616010f4(active item selection 修正) で fork が独自に使っていたgetDeleteFocusTargetWorkspaceIdが削除されたため、新しい@superset/panesのgetActiveIdAfterRemovalに置換。apps/desktop/.../pending/$pendingId/page.tsx26fecf141(sidebar sync hotfix) でensureWorkspaceInSidebarのみ採用。hostUrl/session/activeOrganizationIdの追加行は 9abd936 (machineId 統合) 由来のため PR-B で処理。bun.lock60ff70b23で 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) との比較で全項目健在を確認:cleanupMissingWorktrees/createGitHubIssue/dispatchGitHubWorkflow/getCheckJobSteps/getGitHubRepositoryOverview/getGitHubWorkflowRuns/getJobLogs/getJobStatuses/getMissingWorktrees/getPullRequestIdentityCandidates/getWorkflowRunJobs/replyToPullRequestComment/rerunPullRequestChecks/setActiveSyncWorkspace/setPullRequestDraftState/setPullRequestThreadResolution/updatePullRequestAssignees/updatePullRequestReviewers/uploadGitHubIssueAsset)ansi_up,@vscode/ripgrep,@xyflow/reactTERMINAL_OPTIONS,SUPERSET_WORKSPACE_NAME,moonshot-ai.kimi-code,MainWindowEffects,INCEPTION_AUTH_PROVIDER_ID,v1MigrationState,TiptapPromptEditorapps/desktop/electron-builder.tsのdmg.size = "4g"維持apps/desktop/package.jsonversion 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整合性 OKbun run typecheck全 task green (28 successful)bun run lintbiome greenbun run --filter @superset/desktop build成功確認次のリファレンス
61e7eef05,3efd92763,88e250048,16bcf961d,7c14f05ba,9abd936136c2a7719a,4399b7398,7c168e779,c885eb212Summary by CodeRabbit
リリースノート
新機能
バグ修正
UI/UXの改善