Skip to content

chore(upstream): PR3 v2 workspace delete 統合 (#3443)#178

Merged
MocA-Love merged 1 commit intomainfrom
upstream-merge/pr3-v2-delete-unify
Apr 15, 2026
Merged

chore(upstream): PR3 v2 workspace delete 統合 (#3443)#178
MocA-Love merged 1 commit intomainfrom
upstream-merge/pr3-v2-delete-unify

Conversation

@MocA-Love
Copy link
Copy Markdown
Owner

概要

upstream superset-sh#3443 feat(v2): unify workspace delete through host-service (commit c5f791e9a) を取り込みます。30ファイル +1457/-107 の大型リファクタで、v2 ワークスペース削除フローを host-service 経由の統合パイプラインに置き換えます。

主な変更

  • host-service 新規 API: packages/host-service/src/trpc/router/workspace-cleanup/ + packages/host-service/src/runtime/teardown/ — ワークスペース teardown saga と error taxonomy
  • Dashboard sidebar 側: useDestroyWorkspace hook + 4つの新Pane (ConflictPane, DestroyConfirmPane, TeardownFailedPane, UnknownErrorPane) + useDestroyDialogState hook
  • packages/trpc/src/router/v2-workspace/v2-workspace.ts: delete を protectedProcedurejwtProcedure に変更し host-service から cloud delete を呼べるように。org 外 delete は引き続き FORBIDDEN
  • UI改修: alert-dialog.tsxselect-text 追加、Dashboard sidebar delete dialog の API を onConfirm/title/descriptionworkspaceId/workspaceName/onDeleted
  • Plans/Docs: plans/v1-v2-delete-patterns-audit.md, plans/workspace-delete-unification.md

Auto-merge された3ファイル (fork差分との衝突なし)

  • DashboardSidebarWorkspaceItem.tsx — collapsed/expanded 分岐、context menu、row レイアウトは維持
  • useDashboardSidebarWorkspaceItemActions.ts — direct な cloud delete 呼び出しが新フローに差し替わるだけ。rename/open-in-finder/copy-path 系の fork 既存挙動は温存
  • packages/host-service/src/terminal/terminal.tsdisposeSession export と disposeSessionsByWorkspaceId 追加のみ。relay/tunnel/env 周りの fork 独自実装には未接触

Codex 事前レビュー結果

Yes。この cherry-pick は意味的に受け入れてよい状態です。

  • v1 legacy delete flow (WorkspaceSidebar/WorkspaceListItem/components/DeleteWorkspaceDialog) と v2 新フローは分離されており衝突なし
  • host-service の新 API は relay (apps/relay/src/index.ts:113) の汎用 /hosts/:hostId/trpc/* 転送に allowlist なしで乗るので remote host でも動作
  • useWorkspaceHostUrl が local/remote を正しく分岐するので既存 remote 経路にそのまま乗る
  • v2-workspace tRPC の権限モデル変更は緩めておらず、org 外 delete は引き続き FORBIDDEN

軽微な注意点 (blocker ではない)

Codex が指摘した通り、useDestroyDialogState.ts:72 で unknown error のときに dialog を reopen せず toast のみにしているため、UnknownErrorPane は現状ほぼ到達しません。これはコメントで記載された UX 判断なので、今回は触らず upstream 通りとします。

テスト

  • bun install
  • bun run lint
  • bun run typecheck: /tmp worktree の既知問題 (@types/node 解決) により CI 側に委ねます
  • 実機スモーク: v2 workspace context menu → Delete、conflict/destroy/teardown/unknown の各 Pane 表示、teardown 完了後のサイドバーからの除去と focus 遷移

次のステップ

このPRマージ後、PR4 (v2 paginated branch picker, +3013/-796) に進みます。

* Delete

* plans(workspace-delete): refine design per review

- Drop unsupported claim about v2 branch ephemerality; deleteBranch
  defaults off, no persisted preference
- Add Host ownership section: v2Workspaces.hostId is 1:1 so destroy
  is host-local; FORBIDDEN from other hosts
- Replace "remote-host cleanup" with "abandoned-host cleanup" as the
  real follow-up (cross-device delete isn't a thing under the schema)

* feat(host-service): workspaceCleanup.destroy

Adds a single unified delete path for v2 workspaces. Sequences:
terminals → teardown → worktree → branch → cloud → host sqlite.

- runTeardown silently executes .superset/teardown.sh with a 60s
  timeout, SIGKILL on timeout, 4KB output tail capture; returns
  status + exitCode + tail.
- disposeSessionsByWorkspaceId kills all active PTYs for a workspace
  (releases file locks before worktree remove / teardown).
- Typed errors: CONFLICT for dirty worktrees (prompt force: true),
  INTERNAL_SERVER_ERROR with cause.kind=TEARDOWN_FAILED surfaced to
  the client via errorFormatter so the renderer can show outputTail.
- deleteBranch opts in (default false); force also upgrades
  `git branch -d` to `-D`.
- Cloud failure is swallowed as a warning — disk is already clean,
  cloud self-heals on next sync.

* feat(desktop): wire v2 sidebar delete through workspaceCleanup.destroy

- useDestroyWorkspace hook resolves the workspace's host-service URL
  and calls workspaceCleanup.destroy, normalizing TRPC errors into a
  typed union (conflict / teardown-failed / unknown) for the dialog.
- DashboardSidebarDeleteDialog becomes self-contained: owns the
  mutation, the "delete local branch" checkbox (off by default), and
  renders the force-retry UI for dirty worktrees and teardown
  failures (with outputTail preview).
- useDashboardSidebarWorkspaceItemActions drops the direct
  apiTrpcClient.v2Workspace.delete call; post-success cleanup moves
  to an onDeleted callback (sidebar removal + focus navigation).

Path B (renderer → cloud v2Workspace.delete) is now unused in desktop.

* fix(workspace-delete): review follow-ups

- Run teardown script via createTerminalSessionInternal (same PTY
  primitive v2 uses for interactive terminals) so the script inherits
  the user's shell environment — login rcfiles, PATH, nvm/rbenv, etc.
  Silent: session is transient and never surfaced as a visible pane.
  Output captured via pty.onData; timeout via pty.kill() so behavior
  is cross-platform (no process-group SIGKILL on Windows).
- Guard the timeout callback with `if (settled) return;` to prevent
  the event-loop race where a successful 60s exit is misreported as
  a timeout.
- disposeSession now always marks the DB row disposed even when the
  in-memory session is missing, so zombie `active` rows left from
  crashes get reconciled during bulk workspace cleanup.
- Export TEARDOWN_TIMEOUT_MS from host-service; renderer uses it in
  the dialog copy instead of a literal `60`. Renderer also runs the
  output tail through strip-ansi (host-service returns raw PTY bytes;
  sanitizing is a presentation concern).
- TeardownFailureCause.signal is now `number | null` (Unix signal
  number from node-pty) rather than a bogus NodeJS.Signals cast.
- JSDoc: INTERNAL_ERROR → INTERNAL_SERVER_ERROR on the workspace-
  cleanup error contract.
- Tag the audit doc's ASCII-diagram fence as `text` for markdownlint.
- biome lint:fix.

* Refactor

* fix(workspace-delete): optimistic-close UX (no in-dialog wait)

Mirrors v1's pattern: the destroy runs in the background under a
toast.loading → success/error, the dialog closes immediately on
confirm, and only re-opens when the user has a decision to make.

- Confirm/force-retry close the dialog optimistically; mutation
  continues with a toast for feedback. No more frozen "Deleting..."
  for the up-to-60s teardown duration.
- CONFLICT and TEARDOWN_FAILED reopen the dialog in the matching
  error pane so the user can force-retry with full context. The
  branch opt-in is preserved across the reopen.
- Unknown errors fall through to toast.error — no reopen.
- isPending state and "Deleting..." button copy dropped from all
  panes (never visible since the dialog closes optimistically).

* fix(workspace-delete): move TEARDOWN_TIMEOUT_MS to @superset/shared

The renderer used to value-import TEARDOWN_TIMEOUT_MS from
@superset/host-service, which dragged node-pty (and transitively
@parcel/watcher's native .node binary) into the renderer bundle
— esbuild had no loader for .node and failed the build.

@superset/shared is renderer-safe. Put the constant there so both
host-service and the renderer import the same single source of
truth without crossing the bundler boundary.

* fix(workspace-delete): auth + tolerate missing host-sqlite row

Two bugs unblocking the v2 delete flow:

1. v2Workspace.delete cloud procedure used protectedProcedure
   (session), so host-service's JWT auth returned 401. Switch to
   jwtProcedure (mirrors v2Workspace.create) with an org-membership
   check derived from the workspace's organizationId. Returns
   alreadyGone: true idempotently when the row is missing.

2. workspaceCleanup.destroy threw NOT_FOUND when the host-sqlite
   row was missing, even though the workspace exists in the cloud
   and belongs to this host. That state happens when the workspace
   was created via a flow that didn't register it locally or the
   host DB was reset — the user has no way to delete without this
   fix. Now each disk step is gated on having the local row + a
   resolvable project; missing rows surface as warnings and cloud
   cleanup still proceeds.

Also makes ctx.api absence a warning instead of an upfront
PRECONDITION_FAILED, so local-only cleanup still completes.

* plans(workspace-delete): cloud-as-commit-point redesign

Reshape destroy as a linear preflight → teardown → cloud → local-
cleanup saga. No tombstones, no reconciler, no persistent state.

- Any failure before the cloud step leaves the workspace untouched.
- Any failure after is a warning (local orphans are cheap).
- Force skips preflight + teardown; cleanup is always --force past
  the commit point.
- Three phases cleanly separated so future changes (auto-retry,
  cross-device reconcile) land at a single seam.

Also updates the teardown contract to reflect the PTY-via-
createTerminalSessionInternal approach (env parity w/ v2 setup;
cross-platform timeout via pty.kill) and pins TEARDOWN_TIMEOUT_MS
to @superset/shared so the renderer doesn't drag node-pty into
its bundle.

* fix(workspace-delete): cloud is the commit point

Reorders workspaceCleanup.destroy into three phases so the failure
semantics match the plan doc:

  0. Preflight (dirty worktree) — throws CONFLICT if !force
  1. Teardown script — throws TEARDOWN_FAILED if !force
  2. Cloud delete  ← commit point; throws passthrough on failure
  3. Local cleanup (PTYs, worktree, branch, host sqlite) — warnings only

Any failure in phases 0–2 leaves the workspace fully intact. The
user retries when they've addressed the cause (committed work, fixed
teardown, reconnected cloud, re-authed). Phase 3 is best-effort; local
orphans are cheap and surface as warnings.

No tombstones, no reconciler, no persistent state. Step 3b always
uses --force since we're past the commit point regardless of the
input flag.

* fix(workspace-delete): preserve TeardownFailureCause fields over wire

tRPC's `new TRPCError({ cause })` runs non-Error causes through
getCauseFromUnknown() which wraps them in a synthetic
UnknownCauseError (Error subclass) while copying fields as own
properties. Superjson's transformer recognises it as Error and
serialises only { name, message, stack } — our { kind, exitCode,
signal, timedOut, outputTail } fields were being dropped on the wire,
so the renderer saw an Error with no teardown metadata and
TeardownFailedPane crashed on stripAnsi(undefined).

The errorFormatter now rebuilds a plain object from the cause fields
so superjson serialises it as an object and the renderer gets the
full TeardownFailureCause.

Also defensive outputTail ?? "" in the pane so stripAnsi never
sees undefined.

* feat(ui/alert-dialog): make content text selectable

Desktop globals set user-select: none on html/body for a native feel.
Alert dialogs carry user-facing messages (error details, teardown
output tails, descriptions) that users need to copy — e.g. paste an
error into chat or grep a log snippet.

Applied at the component level so every AlertDialog in the app
benefits, not just the workspace-delete error panes.

* chore(workspace-delete): tighten stale comments + copy

- TeardownFailedPane: drop the defensive-round-trip note now that the
  errorFormatter reliably serializes the cause as a plain object;
  keep just the `?? ""` coalesce and the WHY of stripAnsi.
- ConflictPane: was phrased as "uncommitted or unlocked work" when
  CONFLICT is now only thrown by the preflight dirty check (locked-
  worktree cases fall to warnings). Rewrites copy + JSDoc to match.
- teardown.ts: timeout message said SIGKILL, but `pty.kill()` with no
  args sends SIGHUP on Unix. Drop the false specificity.

* chore(workspace-delete): dev-only hook to simulate cloud failure

SUPERSET_DEBUG_FAIL_CLOUD_DELETE makes workspaceCleanup.destroy throw
at phase 2 (cloud delete) without needing to stop the cloud API dev
server. Used to verify the destroy saga's passthrough-on-cloud-fail
behavior end-to-end.

No-op when unset; guarded solely by env-var presence (truthy).

* Revert "chore(workspace-delete): dev-only hook to simulate cloud failure"

This reverts commit 5c88d88.

* fix(workspace-delete): review sweep

Addresses open PR comments:

- branchDeleted (P1, cubic): track via local flag inside the try
  block instead of computing from preconditions, so the field reports
  whether git branch actually succeeded.

- Teardown timeout can hang (coderabbit major): if pty.kill() doesn't
  cause onExit to fire (zombie PTY), settle the promise directly after
  a 2s grace window so workspaceCleanup.destroy never blocks forever.

- formatTeardownReason signal-terminated (coderabbit minor): handle
  `exitCode === null && signal !== null && !timedOut` with a dedicated
  "terminated by signal N" branch instead of falling through to
  "failed to start".

- Duplicate run calls (coderabbit major): in-flight ref guard in
  useDestroyDialogState.run so a rapid second click (same pane or
  re-opened error pane) can't fire the mutation twice before the
  first resolves.

- Global checkbox id (coderabbit minor): DestroyConfirmPane uses
  useId() so the "Also delete local branch" label targeting doesn't
  collide across dialog instances.

- Audit doc tense (cubic + coderabbit): v1-v2-delete-patterns-audit.md
  now explicitly labeled as a pre-unification snapshot.

- Plan doc delivered-vs-follow-up split (coderabbit): the UI section
  and work order clearly mark what landed in this PR vs. what's
  follow-up (hotkey, EmptyTabView, V2WorkspaceRow affordance).
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 15, 2026

Warning

Rate limit exceeded

@MocA-Love has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 4 minutes and 4 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 4 minutes and 4 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: babcb347-5824-43d6-96f5-f3f8f6831d65

📥 Commits

Reviewing files that changed from the base of the PR and between b412515 and cd8c8c4.

📒 Files selected for processing (30)
  • apps/desktop/src/renderer/hooks/host-service/useDestroyWorkspace/index.ts
  • apps/desktop/src/renderer/hooks/host-service/useDestroyWorkspace/useDestroyWorkspace.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/DashboardSidebarDeleteDialog.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/components/ConflictPane/ConflictPane.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/components/ConflictPane/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/components/DestroyConfirmPane/DestroyConfirmPane.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/components/DestroyConfirmPane/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/components/TeardownFailedPane/TeardownFailedPane.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/components/TeardownFailedPane/formatTeardownReason.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/components/TeardownFailedPane/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/components/UnknownErrorPane/UnknownErrorPane.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/components/UnknownErrorPane/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/hooks/useDestroyDialogState/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/hooks/useDestroyDialogState/useDestroyDialogState.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts
  • packages/host-service/src/index.ts
  • packages/host-service/src/runtime/teardown/index.ts
  • packages/host-service/src/runtime/teardown/teardown.ts
  • packages/host-service/src/terminal/terminal.ts
  • packages/host-service/src/trpc/error-types.ts
  • packages/host-service/src/trpc/index.ts
  • packages/host-service/src/trpc/router/router.ts
  • packages/host-service/src/trpc/router/workspace-cleanup/index.ts
  • packages/host-service/src/trpc/router/workspace-cleanup/workspace-cleanup.ts
  • packages/shared/src/constants.ts
  • packages/trpc/src/router/v2-workspace/v2-workspace.ts
  • packages/ui/src/components/ui/alert-dialog.tsx
  • plans/v1-v2-delete-patterns-audit.md
  • plans/workspace-delete-unification.md
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch upstream-merge/pr3-v2-delete-unify

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

❤️ Share

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

@MocA-Love MocA-Love merged commit 00f0e24 into main Apr 15, 2026
14 checks passed
MocA-Love added a commit that referenced this pull request Apr 15, 2026
-s ours merge to record that upstream commits a3e34bf through
de70163 (13 commits) are semantically already present on origin/main
via the PR1-6 cherry-pick series (PRs #176, #177, #178, #179, #180,
#182), plus fork-adaptation fixes layered on top.

This merge target is de70163 specifically (not upstream/main) so
newer upstream commits (9fff075 and later) remain visible in future
behind counts.

Upstream commits covered by this audit merge:
- a3e34bf  fix(desktop): restore cmd+click requirement for v1 terminal file links (superset-sh#3457)  [PR1/#176]
- 57557f8  fix(desktop): gate v2 workspace children on collection readiness (superset-sh#3464)       [PR1/#176]
- 4ee2e61  fix(desktop): use native clipboard for copy path in v2 sidebar (superset-sh#3462)         [PR1/#176]
- 87d6e93  feat(desktop): close settings with Escape key (superset-sh#3466)                          [PR1/#176]
- 9c7f5f4  chore(desktop): auto-restart host-service on bundle change in dev (superset-sh#3461)      [PR1/#176]
- 93140d9  fix(mcp): accept MCP resource URL as valid OAuth audience (superset-sh#3459)              [PR2/#177]
- be9e000  fix(desktop): drive tray menu off events, fetch real org name (superset-sh#3458)          [PR2/#177]
- c5f791e  feat(v2): unify workspace delete through host-service (superset-sh#3443)                  [PR3/#178]
- 2c24d93  feat(desktop): paginated branch picker with checkout + open actions (superset-sh#3397)    [PR4/#179]
- 2bf1049  feat(desktop/hotkeys): v1 directional pane focus + best-effort v1 override migrator (superset-sh#3460)  [PR5/#180]
- 1294a7d  feat(desktop/hotkeys): restore Cmd+Alt+Arrow for tab/workspace nav (superset-sh#3472)    [PR5/#180]
- de70163  feat(desktop): v2 review tab first pass — PR info, checks, comments (superset-sh#3463)    [PR6/#182]

Intentionally skipped (version bump, fork has independent versioning):
- 1e23353  chore(desktop): bump version to 1.5.5 (superset-sh#3473)

Fork-adaptation fixes layered on top of the cherry-picks:
- PR1: host-service-coordinator alias import fix, settings Escape
       selector narrowing (role-based + popper wrapper), Escape
       close uses replace navigation
- PR2: dual quit mode preservation (requestQuit "release"/"stop"),
       trayUpdateToken guard for stale async fetchHostInfo results
- PR4: ChangesHeader.normalizeBranchName regex rewrite (lint false
       positive), worktree add uses fullRef for remote-tracking
       refs, syncTimedOut reset on pendingId change, GIT_REFS.md
       barrel example fix
- PR5: migrate.ts re-sanitize of existing localStorage overrides
       (v2 marker bump intent), FOCUS_PANE_* enabled:isActive for
       KeepAliveWorkspaces, CATEGORY_ORDER merges Navigation (upstream)
       and Browser (fork)
- PR6: normalizeThreadsToComments flattens all thread.comments (not
       just first), CommentPane overrides <a> (openUrl) and <img>
       (SafeImage), zero-badge suppression, pr-null comments gate

Fork features verified intact (Explore agent audit of combined
36d4de4..35d95f3 range):
- BROWSER_RELOAD / BROWSER_HARD_RELOAD hotkeys
- dual quit mode menu in tray
- v1 terminal cold-restore + retry reconnect (out of range but
  unaffected)
- KeepAliveWorkspaces (FOCUS_PANE_* gated on isActive)
- useCommandPalette + addMemoTab in v2 workspace
- host-service-coordinator rename alias pattern
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants