Skip to content

chore: sync with upstream#2

Open
SilkePilon wants to merge 396 commits intomainfrom
upstream-sync
Open

chore: sync with upstream#2
SilkePilon wants to merge 396 commits intomainfrom
upstream-sync

Conversation

@SilkePilon
Copy link
Copy Markdown
Owner

@SilkePilon SilkePilon commented Apr 1, 2026

Latest Upstream Release: Superset Desktop desktop-v1.7.2

Tag: desktop-v1.7.2
Published: 2026-04-29T01:29:22Z


Release Notes

What's Changed

Full Changelog: superset-sh/superset@desktop-v1.7.1...desktop-v1.7.2


396 new commits from upstream.

This PR is automatically created and updated daily to keep the fork in sync with superset-sh/superset.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 351 files

Note: This PR contains a large number of files. cubic only reviews up to 75 files per PR, so some files may not have been reviewed.

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/desktop/scripts/copy-native-modules.ts">

<violation number="1" location="apps/desktop/scripts/copy-native-modules.ts:185">
P1: `copyExactModuleVersion` is called with a semver range (`dependencyRange`) but it expects an exact version. When the Bun store lookup falls through, `fetchNpmPackage` constructs an invalid npm tarball URL (e.g. `pkg-^4.0.0.tgz`) that always 404s. The `findBunStoreFolderName` fallback may also silently copy a version that doesn't satisfy the range. Consider resolving the range to an exact installed version before passing it here.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

console.log(
` ${dependencyName}: top-level version missing; materializing ${dependencyRange} at the workspace root`,
);
copyExactModuleVersion(
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 1, 2026

Choose a reason for hiding this comment

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

P1: copyExactModuleVersion is called with a semver range (dependencyRange) but it expects an exact version. When the Bun store lookup falls through, fetchNpmPackage constructs an invalid npm tarball URL (e.g. pkg-^4.0.0.tgz) that always 404s. The findBunStoreFolderName fallback may also silently copy a version that doesn't satisfy the range. Consider resolving the range to an exact installed version before passing it here.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/scripts/copy-native-modules.ts, line 185:

<comment>`copyExactModuleVersion` is called with a semver range (`dependencyRange`) but it expects an exact version. When the Bun store lookup falls through, `fetchNpmPackage` constructs an invalid npm tarball URL (e.g. `pkg-^4.0.0.tgz`) that always 404s. The `findBunStoreFolderName` fallback may also silently copy a version that doesn't satisfy the range. Consider resolving the range to an exact installed version before passing it here.</comment>

<file context>
@@ -110,6 +111,119 @@ function copyModuleIfSymlink(
+		console.log(
+			`  ${dependencyName}: top-level version missing; materializing ${dependencyRange} at the workspace root`,
+		);
+		copyExactModuleVersion(
+			nodeModulesDir,
+			dependencyName,
</file context>
Fix with Cubic

Kitenite and others added 29 commits April 12, 2026 20:37
…uperset-sh#3392)

* fix(desktop): enable modal focus trap on v1 + v2 workspace dialogs

Applies the same fix as superset-sh#3276 to both v1 (NewWorkspaceModal) and v2
(DashboardNewWorkspaceModal) Dialogs so alt-tabbing back restores
focus to the modal.

* Lint
superset-sh#3398)

* feat(desktop): modifier+click in file tree opens external editor / new tab

- Meta/Ctrl+click: open in external editor (works on files and folders)
- Shift+click: open file in a new internal tab
- Plain click: unchanged (preview pane / toggle folder)

Adds the behavior to both v1 (FileTreeItem, FileSearchResultItem) and v2
(WorkspaceFilesTreeItem) file trees. v2 guards the external-editor path
with an isLocalWorkspace check so remote hosts toast instead of silently
calling local electron IPC against a path that does not exist.

* fix(desktop): v2 file-tree meta+click reads defaultOpenInApp from v2SidebarProjects

Align with the per-project v2 "Open In" pattern established in superset-sh#3393:
resolve the editor from collections.v2SidebarProjects.defaultOpenInApp
(falling back to finder) and call electronTrpc.external.openInApp —
instead of openFileInEditor, which does v1-era backend resolution off
the projects.defaultApp column that v2 doesn't write to.

* fix(desktop): address PR review on v2 file-tree meta+click

- Gate the isLocal check on the host live query actually resolving, so
  a meta+click before the collections query settles silently no-ops
  instead of toasting a misleading "only supported on local workspaces"
  error (cubic/greptile/coderabbit).
- Use a separator-safe split when deriving the new-tab title from a file
  path so Windows host paths (backslash-separated) don't produce a full
  path as the tab title (cubic).
… tRPC batch (superset-sh#3400)

The workspace tRPC client used httpBatchLink, which returns a batched response
only when every procedure in the batch resolves. On workspace switch, FilesTab's
filesystem.listDirectory (ms) was batched with WorkspaceSidebar's git.getStatus
(5-10s on a large monorepo), so the file tree stalled behind git for every
switch. Swap to httpBatchStreamLink so responses stream as each procedure
completes, allow the trpc-accept header through CORS on the host service, and
drop a now-unnecessary staleTime:0 override on listDirectory.fetch.
…3401)

* feat(desktop): focus neighbor workspace after v2 delete

When the active v2 workspace is deleted, navigate to the previous
workspace in visual order, falling back to the next, then to / if no
neighbors exist. Mirrors v1 behavior (without the wrap-around).

Also switches the delete flow to toast.promise for feedback.

* fix(desktop): address PR feedback on v2 delete focus

- Isolate navigation from toast.promise so a router rejection after a
  successful delete doesn't surface a misleading "Failed to delete"
  toast (greptile).
- Break sort ties in getFlattenedV2WorkspaceIds with section-before-
  workspace to match the sidebar's ordering when tabOrder collides
  (cubic).
…on, terminal override respect (superset-sh#3391)

* attempt fix

* Test and doc

* fix(desktop): terminal & migration respect hotkey overrides

- Rebuild the hotkey reverse index on override store changes so the
  terminal forwards the user's current bindings instead of frozen
  defaults. Fixes swallowed-keystroke on rebound-away defaults and
  dead-binding on new overrides.
- Sanitize migrated overrides: canonicalize and drop malformed strings
  (`ctrl+control`, `ctrl+shift+@`, `meta+[`) that the pre-fix recorder
  could produce.
- Document the meta-on-non-Mac policy (Windows OS intercept, Linux WM
  ownership).

* feat(desktop): allow meta (Win/Super) bindings on non-Mac with OS warning

Drop the blanket reject and extend OS_RESERVED for Windows shell
intercepts (meta+d/e/l/r/tab) so users get a warning instead of a
silent block. Linux WM configs vary too much to predict — trust the
user and let them rebind if a chord doesn't fire.

* refactor(desktop): unify event↔chord matching via shared helpers

Audit against react-hotkeys-hook and internal usages found two more
consumers comparing via event.key, which breaks the same punctuation /
layout / rebind cases fixed in the recorder:

- utils/utils.ts isTerminalReservedEvent had its own event.key-based
  TERMINAL_RESERVED set with ctrl+\\.
- Terminal/helpers.ts matchesKey used event.key to check the
  CLEAR_TERMINAL rebind — silently wrong for any punctuation rebind.

Consolidation:

- Export eventToChord and new matchesChord(event, chord) as the single
  canonical event↔chord matcher.
- Export TERMINAL_RESERVED_CHORDS as the single source of truth.
- Rewrite isTerminalReservedEvent around them.
- Replace matchesKey() with matchesChord().
- Remove duplicated TERMINAL_RESERVED from useRecordHotkeys.ts.

Also adds tests for eventToChord, matchesChord, and
isTerminalReservedEvent parity. Updates plan doc with the cleanup.

* docs(desktop): rewrite hotkey fix plan for concise review

* chore(desktop/hotkeys): deslop — tighten comments, remove dead wrappers

- Merge canonicalizeChord / normalizeChord into one exported function
  (the wrapper was pointless indirection).
- Drop section-banner comments (`// Helpers`, `// Hook`) and comments
  that restated code (`// Must include ctrl or meta…`).
- Tighten JSDocs to convey intent in one line where possible.
- display.ts: drop duplicate short arrow entries (`up: "↑"`) — the
  normalizeToken call aliases them to canonical names already. Extract
  isModifier type guard to kill the cast repetition.

Net -35 LOC, same behavior, same 62 tests passing.

* fix(desktop/hotkeys): canonicalize reserved-chord tables + review cleanups

Addresses PR review feedback:

- Bug: OS_RESERVED[\"windows\"] had \"ctrl+alt+delete\" which never matched
  because canonicalization sorts modifiers alphabetically. Wrap both
  OS_RESERVED and TERMINAL_RESERVED_CHORDS in .map(canonicalizeChord)
  at build time so multi-modifier entries can't silently miss the
  reserved warning. OS_RESERVED values switched from array to Set.
- Extract sanitizeOverride into utils/sanitizeOverride.ts so the test
  imports the real implementation instead of a near-copy.
- Test stub: preserve explicit \`code: undefined\` so the synthetic-event
  guard in captureHotkeyFromEvent is actually exercised (not the
  empty-string branch).
- Resolver tests: resolve a sample hotkey once at describe scope and
  throw if HOTKEYS has no bound defaults, instead of each test silently
  no-op-ing via \`if (!def) return\`.
- Add regression test asserting canonicalizeChord(\"ctrl+alt+delete\")
  sorts to \"alt+ctrl+delete\".

* fix(desktop/hotkeys): re-sanitize localStorage for users who migrated pre-fix

The original guard short-circuited whenever \`hotkey-overrides\` existed in
localStorage, which meant any user who ran the migration before the
Bug 5 sanitizer shipped would keep their corrupt entries
(\`ctrl+control\`, \`ctrl+shift+@\`, \`meta+[\`) forever.

Gate migration/sanitization on a separate \`hotkey-overrides-sanitized-v1\`
marker instead:

- No marker + no store  → tRPC migration with sanitizer.
- No marker + has store → in-place re-sanitization of existing entries.
- Marker present        → done, skip.

The marker is only set once per user; new bindings written by the fixed
recorder can't be corrupt so no further passes are needed.

* fix(desktop/hotkeys): bump migration marker key so pre-sanitizer users re-migrate

Use a dedicated `hotkey-overrides-migrated-v2` marker separate from the
Zustand persist key. Users who ran the migration on the pre-sanitizer
build (~1 day window) will re-run once and get their corrupt entries
(`ctrl+control`, `ctrl+shift+@`, `meta+[`) dropped.

Mark set only on success paths (or when there's nothing to migrate), not
in the catch branch — transient tRPC failures at boot retry next launch
instead of silently giving up on the user's legacy bindings.
…3415)

* Fix code TUI copy

* Handle more keyboard shortcuts

* fix(desktop): match VS Code terminal clipboard handling
* fix v1 split pane startup sizing

* Handle background
…h#3403)

* feat(desktop): directional pane focus via Cmd+Alt+Arrow (v2)

Adds spatial pane navigation to v2 workspaces: Cmd+Alt+Arrow jumps
focus to the visually adjacent pane in that direction (no wrap at
edges). Reclaims Cmd+Alt+Arrow from the retired PREV/NEXT_TAB and
PREV/NEXT_WORKSPACE shortcuts — tabs still cycle via Ctrl+Tab and
both tabs and workspaces keep Cmd+Alt+1..9 jump-to-N.

The spatial neighbor util walks up the LayoutNode path to find the
deepest ancestor split whose axis matches the arrow, then descends
into the sibling subtree picking the near-edge leaf.

* fix(panes): preserve cross-axis alignment during spatial neighbor descent

findEdgePaneId previously fell through to node.first on any
perpendicular split encountered while descending into the sibling
subtree, losing the source pane's row/column position. In a 2x2 grid
this caused the directional focus move to land on the wrong pane
depending on how the grid was grouped in the layout tree (e.g. in
a rows-first 2x2, down from top-right landed on bottom-left).

Track the source pane's path below the pivot ancestor as an alignment
path and consume one entry per perpendicular-split descent, so the
descent mirrors the source's cross-axis choices.

Adds unit tests for getSpatialNeighborPaneId covering single pane,
simple horizontal/vertical splits, edge no-wrap, and both groupings
of the 2x2 grid.

* refactor(desktop): drop linear PREV/NEXT_PANE now that directional nav exists

The 4-way FOCUS_PANE_{LEFT,RIGHT,UP,DOWN} shortcuts supersede linear
pane cycling. Removing PREV/NEXT_PANE also frees ctrl+shift+alt+Arrow
on Windows/Linux, which dodges the Intel HD Graphics screen-rotation
driver shortcut that steals ctrl+alt+Arrow at the OS level.

- Remove PREV_PANE/NEXT_PANE from the hotkey registry and both v1/v2
  handler sites.
- Remap FOCUS_PANE_{LEFT,RIGHT,UP,DOWN} on Windows/Linux from
  ctrl+alt+Arrow to ctrl+shift+alt+Arrow.
- Delete now-unused getNextPaneId/getPreviousPaneId helpers from
  renderer/stores/tabs/utils.ts.

Users who relied on linear cycling can re-add it via settings once
the unbound-default hotkey support lands as a follow-up.
…perset-sh#3421)

The hover icon swap was silently broken because isOpen was read via a
one-shot collections.get() call instead of a live query, so the component
never re-rendered when rightSidebarOpen changed.
…ne title resolution (superset-sh#3420)

* feat(desktop): v2 diff viewer opens in its own tab + pane-derived tab titles

openDiffPane now scans all tabs for an existing diff pane (focus + scroll)
and falls back to addTab, so clicking a file in the Changes sidebar never
hijacks the focused editor tab.

Collapses tab/pane title resolution onto a single canonical field:
PaneDefinition.getTitle is tightened to (pane) => string, file's rich JSX
moves into the existing renderTitle hook, and a new resolveTabTitle helper
powers both the tab bar and the "Move to Tab" context menu. tab.titleOverride
is reserved for user renames; every auto-default caller is stripped and
multi-pane tabs fall back to "Tab N" instead of "tab-<uuid>".

* feat(desktop): pane-derived tab titles reserve tab.titleOverride for user renames

Preset execution and workspace bootstrap were baking preset.name /
terminal.label onto tab.titleOverride, which meant those names persisted
misleadingly after a tab was split and couldn't be distinguished from a real
user rename. Move both to the pane's titleOverride instead, and teach
resolveTabTitle to read pane.titleOverride before falling through to
getTitle() for single-pane tabs. tab.titleOverride is now written only by
the tab-bar rename action; splitting a named tab flips the label to "Tab N"
while the pane keeps its name in its header, and user renames still win
over everything.

* fix(desktop): browser.getTitle falls back to "Browser" for about:blank

Unnavigated browser panes had their pane header fall through to pane.id
(a raw UUID) because getTitle returned undefined for about:blank and the
old titleOverride: "Browser" default was removed along with the other
auto-default titleOverride writes.

* fix(desktop): browser tab title uses URL.host to preserve port

URL.hostname drops the port, so localhost:3000 and localhost:4000 both
rendered as "localhost" in the tab bar. URL.host keeps the port when one
is explicitly set.
…ditor) (superset-sh#3418)

react-hotkeys-hook skips events whose target is contentEditable unless
enableOnContentEditable is set. CodeMirror 6 renders its editor as a
contenteditable element, so app chords like cmd+w stopped firing when
focus was in the v1 file editor — meta+w never reached the CLOSE_TERMINAL
handler that closes the focused pane.

Default enableOnContentEditable: true in useHotkey, mirroring the existing
enableOnFormTags default. Callers can still opt out via options.
…kspace (superset-sh#3422)

* feat(desktop/hotkeys): allow unbound defaults; restore PREV/NEXT tab+workspace

Widen PlatformKey and HotkeyDefinition so hotkey entries can register
with a null chord per platform. Downstream consumers were already
null-safe from superset-sh#3391 (useBinding, buildRegisteredAppChords,
formatHotkeyDisplay, sanitizeOverride, HotkeyMenuShortcut), so the
schema widening is the only structural change needed.

Re-introduce PREV_TAB, NEXT_TAB, PREV_WORKSPACE, NEXT_WORKSPACE as
registered-but-unbound entries so users who want tab/workspace neighbor
navigation can rebind them in Settings → Keyboard. PR superset-sh#3403 removed
these to free Cmd+Alt+Arrow for directional pane focus; this restores
the hotkey IDs (and their v1/v2 handlers) without claiming any default
chord. Users with pre-superset-sh#3403 overrides for these IDs will transparently
get their bindings back since the override is preserved in localStorage.

- Null-guard canonicalizeChord(defaultKey) in useRecordHotkeys so
  recording a new chord for an unbound hotkey no longer throws.
- Replace the force-cast in resolveHotkeyFromEvent.test.ts sample
  picker with a type predicate so sampleDef.key narrows to string
  honestly instead of lying about the widened schema.

* fix(desktop/hotkeys): restore prevIndex in v2 PREV_WORKSPACE handler
…rs selectable (superset-sh#3432)

Global `user-select: none` on body blocks copying error text on full-page error states. Matches the pattern already used by v1 WorkspaceInitializingView.
superset-sh#3457)

Gate `onFileLinkClick` behind metaKey/ctrlKey in v1 helpers so file-path
links once again require cmd/ctrl+click, matching pre-superset-sh#3382 behavior.
The shared `LinkDetectorAdapter` (used by v2) still activates on plain
click and is untouched.
…et-sh#3458)

Replace the 5s tray polling interval with event-driven refresh:
`mouse-enter` + host-service status-changed trigger an async rebuild.
Drop the hostInfoCache Map — updateTrayMenu is now async and fetches
host.info inline with a 2s AbortController timeout, so there is no
stale state to manage. Unwrap the superjson envelope (`result.data.json`)
that the original polling code missed, which was causing every org to
render as a UUID slice (previously) or "Loading…" (after the first
pass of this fix).

Closes superset-sh#3454
…uperset-sh#3397)

* Branch discovery

* UI

* branch discovery

* feat(desktop): paginated branch picker with tabs, checkout, and open actions

Rework the v2 new-workspace modal's branch picker to handle thousands of
branches and surface existing workspaces directly.

- Host-service searchBranches: cursor pagination, refresh flag gated by
  30s TTL, server-side filter (branch/worktree), reflog-based recency,
  git-worktree-derived worktreePath, isCheckedOut flag.
- New checkout procedure: git worktree add <path> <existing-branch>
  (no -b, no dedup). Auto-resolves local vs origin/<branch>.
- Renderer: useInfiniteQuery + IntersectionObserver for infinite scroll;
  tabs for Branch vs Worktree filters; hover-reveal action buttons —
  Check out on Branch tab (disabled when isCheckedOut), Open on Worktree
  tab (navigates to existing workspace). Click row body still sets base
  branch for the prompt-driven fork flow.

* Concise doc

* fix(desktop): remote-only checkout tracking, host-scoped open, a11y

- Remote-only Check out: use `git worktree add --track -b <branch>` so the
  worktree lands on a proper local tracking branch instead of detached HEAD.
  Auto-track only fires for the short name; passing `origin/<branch>` is
  treated as a commit-ish.
- Open existing: scope the branch→workspace map by host id so the Worktree
  tab doesn't navigate to a workspace on the wrong host when the same
  branch exists on multiple hosts.
- Picker a11y: reveal row action buttons on focus-within, not just hover.
  Swap the disabled span for a focusable button with aria-disabled so
  keyboard users can reach the "already checked out" tooltip.

* fix(desktop): fall back to branch-only workspace lookup when host id unresolved

The previous host-scoped filter returned an empty map when targetHostId was
null (collections still loading, or no v2_hosts row matched the renderer's
machineId). That broke Open on the Worktree tab in the common single-host
case. Keep the host filter as a soft constraint — apply it only when a host
id resolves; otherwise match by (projectId, branch).

* feat(desktop): adopt orphan worktrees on Worktree-tab Create

A `.worktrees/<branch>` directory can exist on disk without a matching
`workspaces` row (older flows, partial failures, manual creation). The
Worktree tab now surfaces these explicitly: rows with a workspace row
show "Open" and navigate; rows without one show "Create" and adopt the
worktree by registering cloud + local workspace rows, then navigate.

- Host-service: `searchBranches` emits `hasWorkspace` per row (joined
  against workspaces table for this project on this host).
- Host-service: new `adopt` procedure — no git ops, just cloud + local
  DB registration over an existing worktree path. Idempotent.
- Renderer: Worktree-tab button label + handler switch on `hasWorkspace`.
  New `useAdoptWorktree` hook wraps the mutation.
- Docs: Worktree-tab row-state table updated, procedure list updated.

* Style

* Handle git ref

* doc change

* refactor(host-service): discriminated ResolvedRef for git refs

Closes a class of bugs where ref kind was inferred from short-name
prefix (`startsWith("origin/")`) — fragile because a local branch can
legitimately be named `origin/foo` and would be misclassified as a
remote-tracking ref.

- New `runtime/git/refs.ts`: `ResolvedRef` discriminated union (local /
  remote-tracking / tag / head) with template-literal `fullRef` types.
  `resolveRef` probes against full refnames, classification is
  unambiguous. Helpers: `asLocalRef`, `asRemoteRef`,
  `resolveDefaultBranchName`.
- `resolveStartPoint` now returns `ResolvedRef`. `create` and `checkout`
  switch on `kind` instead of inspecting ref strings. Fixes the
  `origin/foo` local-branch misclassification in `checkout` and the
  spurious-fetch case in `create`.
- `searchBranches` and `listBranchNames` parsing: derive isLocal/isRemote
  and the user-facing name from the FULL refname's structural prefix
  (`refs/heads/`, `refs/remotes/origin/`) — not from the short form.
- New regression test: local branch named `origin/foo` resolves as
  `local`, not `remote-tracking`.
- New lint script `scripts/check-git-ref-strings.sh` blocks
  `.startsWith("origin/")` and `.replace("origin/", ...)` outside of
  `runtime/git/refs.ts`. V1 desktop tRPC routers excluded with a doc'd
  follow-up — they have pre-existing instances of the bug class.
- Pattern documented in `packages/host-service/GIT_REFS.md` along with
  prior art (GitHub Desktop, VSCode).

* fix(host-service, desktop): resolveRef remote-prefix handling + a11y/UX cleanups

Addresses review carryovers and a real-bug new finding in the ref helper.

- resolveRef: accept `<remote>/<branch>` shortform input. Previously
  `resolveRef("origin/foo")` probed `refs/remotes/origin/origin/foo`
  (double-prefix) and returned null. Strip the remote prefix on the
  remote-probe path only — local probe is unchanged so a local branch
  literally named `origin/foo` still wins. New refs.test.ts covers all
  documented input shapes (12 tests).
- listWorktreeBranches: drop dead `currentPath === worktreesRoot` branch
  (a Superset worktree is always a child, never the root). Replace silent
  catch with console.warn so a failed `git worktree list` is observable.
- Picker action buttons: drop native `disabled` attribute on the
  "Check out" disabled state; keep aria-disabled. Native disabled buttons
  block pointer events so the Tooltip explaining "already checked out
  elsewhere" never opens.
- IntersectionObserver guard: track an in-flight flag inside the callback
  so a re-mounted observer can't cascade-load all remaining pages when
  the sentinel stays in the root margin after a page loads.
- BranchRow type: import from useBranchContext barrel instead of
  re-declaring locally — keeps single source of truth from inferRouterOutputs.
- Picker actions (Create, Check out): respect `draft.workspaceName` when
  set instead of hardcoding `branchName`. Falls back to branchName.

* fix(host-service): resolveStartPoint prefers local over remote-tracking

A workspace branch like `agreeable-ermine` is local-only. If a stale
`refs/remotes/origin/agreeable-ermine` cached ref exists (one-off push,
missed prune), the previous remote-first logic silently picked it for
the worktree start point — and `git worktree add ... origin/<branch>`
failed with "fatal: invalid reference" when the cached ref no longer
resolved.

Local-first matches user intent: they pick branches from a list of refs
they can see locally — they expect to fork from that exact local state.
Remote freshness is bounded by the picker's refresh-on-modal-open
already triggering `git fetch --prune`.

Remote-tracking is still the fallback when local doesn't exist (the
remote-only branch case that `Check out` handles correctly).

* feat(workspace-creation): plumb baseBranchSource hint from picker to server

The picker already knows whether the row the user clicked was local or
remote-only (`isLocal` / `isRemote`). Carry that fact through the create
flow as `baseBranchSource: "local" | "remote-tracking"` instead of
re-resolving server-side. The local-first fix in resolveStartPoint stays
as a safer default, but the hint removes the guesswork entirely:

- The server can't be misled by a stale cached `refs/remotes/origin/<x>`
  because it doesn't probe — it uses what the picker said.
- The user's intent (the row they actually saw + clicked) is what gets
  forked from. No silent priority inversion between client and server.

Plumbing:
- Server: `composer.baseBranchSource?: "local" | "remote-tracking"`.
  When set, `buildStartPointFromHint` constructs a ResolvedRef directly,
  skipping `resolveStartPoint`. Without it, falls back to probing
  (preserves behavior for non-picker callers).
- Picker `onSelectCompareBaseBranch` now takes `(name, source)`.
  Source is `branch.isLocal ? "local" : "remote-tracking"`.
- Draft + pending-workspace schema gain `baseBranchSource` (nullable for
  legacy rows). Pending page retry path passes the field through.

* feat(workspace-creation): unify fork/checkout/adopt under pending-page flow

Three buttons (Submit / Check out / Create) now share one path: the
modal inserts a pendingWorkspaces row tagged with `intent` and navigates
to /pending/<id>; the page owns the host-service mutation, the
loading/error UX, and retry. Previously only fork went through this
path — checkout and adopt were fire-and-forget and silently failed when
the slow paths (clone, fetch, setup script) errored.

- Schema: pendingWorkspaceSchema gains `intent: "fork" | "checkout" |
  "adopt"` discriminator and `warnings: string[]`. Fork-only fields
  (prompt, baseBranch, linkedIssues, linkedPR, attachmentCount) are
  default-empty for checkout/adopt rows. v2 isn't released — no
  migration concerns.
- Pending page: `useFireIntent` switches on intent and calls the right
  mutation (createWorkspace / checkoutWorkspace / adoptWorktree). Fires
  once on first mount via a ref guard; same hook drives Retry. Adopt
  has no host-side progress steps so the page shows a generic spinner
  for that intent. Warnings array surfaced on success (especially
  useful for checkout's "setup terminal failed" case).
- PromptGroup: handleCheckout / handleAdoptWorktree now insert a
  pending row + navigate via shared `insertPendingAndNavigate`. The
  fire-and-forget mutation calls in this component are gone; so are
  the local imports of useCheckoutDashboardWorkspace / useAdoptWorktree.
- useSubmitWorkspace: drops the post-navigate fire path. It's pure
  "insert + navigate" now, matching checkout/adopt. mapLinkedContext is
  inlined into the page where the linkedContext is built per intent.
- Doc: PENDING_FLOW.md captures the design — three intents, one path,
  the schema rationale, retry semantics, and the explicit decisions on
  adopt UX and warnings display.

* fix(desktop): use client collection as source of truth for workspace existence

After deleting a workspace, reopening the modal and clicking the branch on
the Worktree tab surfaced "Could not find existing workspace for this
branch." Root cause: two disagreeing sources:

- Server-side `hasWorkspace` (host-service local `workspaces` table): still
  true, because the cloud delete doesn't cascade to the host DB.
- Client `workspaceByBranch` (v2Workspaces collection, cloud-synced): false,
  because the cloud row was soft-deleted.

Picker rendered "Open" from the server snapshot → click hit the client
lookup → miss → error toast.

Fix: pass `hasWorkspaceForBranch(name)` into the picker from PromptGroup,
computed against the cloud-synced collection. The client is authoritative
for "does a workspace row currently exist?" — the server field is a
host-side cache that can be stale after deletion.

Side benefit: an orphan worktree (disk dir without a cloud row) now
correctly gets the "Create" button, and clicking it adopts the existing
worktree into a fresh workspace. Deleted-but-left-on-disk workspaces
become resurrectable with one click.

Host-side cleanup (removing the local `workspaces` row + the worktree
directory when a workspace is deleted) is a separate concern — flagged
for a follow-up.

* docs: add workspace delete design (host-service orchestrates cleanup)

Captures the design for full-resource workspace deletion — worktree on
disk, optional branch, cloud row, local host-DB row. Host-service
orchestrates in that order (hard-to-reverse side first).

Covers failure modes, UX decisions (delete-branch opt-in, confirm only
when dirty), and what replaces the current cloud-only v2Workspace.delete.

Not implemented here — separate PR picks this up.

* fix(host-service): adopt always creates a fresh cloud row

The old short-circuit returned `existingLocal.id` whenever a local
`workspaces` row matched the (project, branch) pair, without calling
cloud. After a prior workspace was hard-deleted in the cloud, the host
sqlite row leaked — so re-adopting the worktree echoed a phantom id
whose cloud row no longer existed. Renderer navigated to it, Electric
never synced anything back, sidebar/target page showed "not found"
indefinitely.

Fix: drop the local-idempotency shortcut. Always call `v2Workspace.create`;
the new cloudRow.id is authoritative. If a stale local row exists for
this (project, branch), replace it with the fresh mapping.

Also: pending page now waits for the newly-created cloud row to sync
into the Electric-backed `v2Workspaces` collection before navigating,
with a 3s fallback. Fast intents (adopt) were beating the sync to the
punch and landing on /v2-workspace/<id> before the row was visible.

Docs: replaced the draft delete design with the canonical version —
v2 delete unifies through `workspaceCleanup.destroy`; implementation
owned by a follow-up PR.

* fix: reset pending-page refs on navigation, clear progress on early throws, harden lint script

Three PR-review follow-ups:

- Pending page: reset `firedRef` / `navigatedRef` when `pendingId` changes
  under a mounted component. Previously, navigating from one pending
  page to another kept the flags stuck at true → the second page never
  fired its mutation and sat in "creating" forever.
- Checkout procedure: `clearProgress(pendingId)` on the empty-branch
  and `safeResolveWorktreePath` error paths. Without this, a stale
  pending row lingered until the 5-minute sweeper removed it.
- check-git-ref-strings.sh: distinguish ripgrep exit codes. Exit 1
  (no matches) still passes; exit 2+ (unreadable file, bad regex) now
  fails loudly. `2>/dev/null` previously masked real scan failures as
  clean passes.

* refactor(desktop): tighten pending-row schema, extract + test intent payload builders

Addresses two code-smell findings from the PR review:

- pendingWorkspaceSchema had `hostTarget`, `linkedIssues`, `linkedPR` as
  `z.unknown()`, forcing `as`-casts at every read site and hiding
  malformed rows until a consumer crashed. Replaced with structured zod
  shapes (discriminatedUnion for hostTarget, typed object schemas for
  linkedIssues/linkedPR). Drops the casts in the pending page and the
  submit hook, and any malformed row now fails zod parsing instead of
  reaching the dispatch.

- Intent dispatch logic in useFireIntent (the switch that translates
  pending-row state into mutation inputs per intent) was untested and
  tangled up with IO. Extracted the payload-building into pure helpers
  in buildIntentPayload.ts and added a contract suite (11 tests, all
  passing) covering:
    - mapLinkedContextFromPending: internal filtering, github filtering,
      skip-missing-fields, linkedPR passthrough, empty-input
    - buildForkPayload: full fork shape, empty-prompt → undefined,
      attachments plumbing, host-tracking hostTarget survives
    - buildCheckoutPayload: branch + runSetupScript only
    - buildAdoptPayload: minimal shape

Pending page's useFireIntent becomes a thin wrapper: build payload,
call mutation, update collection. Bonus cleanup: the `hostUrl`
derivation block collapses from a nested ternary with 3 string casts
to a plain discriminant switch now that the type is known.

* docs: consolidate branch discovery, pending flow, and delete designs

Merges three separate design docs in the modal directory into one
`DESIGN.md` covering the full v2 workspace-creation architecture:

- Branch discovery (data shape, server flow, client flow) — was
  BRANCH_DISCOVERY_DESIGN.md
- Actions per row (Fork / Check out / Open / Create, authority for
  hasWorkspace)
- Workspace creation flow (three intents, pending-row schema,
  pending-page dispatch, baseBranchSource plumbing, per-mutation
  details) — was PENDING_FLOW.md
- Workspace delete (follow-up PR spec with host-service ownership
  principle) — was WORKSPACE_DELETE_DESIGN.md
- Invariants + enforcement (authority decisions, tests, lint, type
  guarantees)

Cross-links to `packages/host-service/GIT_REFS.md` which stays
separate — it's a cross-cutting pattern doc, not modal-specific.

Code-comment pointers updated from the old filenames to the new
consolidated DESIGN.md with section references.

* docs: rename DESIGN.md → V2_WORKSPACE_CREATION.md, move to apps/desktop root

The doc covers a multi-module subsystem (modal + pending page +
host-service procedures) — it doesn't belong nested deep inside the
modal directory where 'DESIGN.md' is also too generic to be findable.

Moved to apps/desktop/V2_WORKSPACE_CREATION.md, parallel to AGENTS.md
and CLAUDE.md, where system-level architecture docs live in this repo.
Name says what it covers. Code-comment pointers updated to use the new
path. Internal cross-link to GIT_REFS.md fixed for the new depth.

* docs: fold WORKSPACE_CREATION_FALLBACK research into V2_WORKSPACE_CREATION

The fallback doc was the original design proposal for resolveStartPoint
+ --no-track + targeted single-ref fetch. All shipped in this PR. The
spec/diff sections are now stale — implementation lives in code.

Kept the prior-art comparison (VS Code, T3Code, GitHub Desktop, v1) as
an appendix in V2_WORKSPACE_CREATION.md — that's the part future
contributors might re-derive without reference. Updated the comparison
table to reflect what we actually shipped (local-first instead of
remote-first; targeted single-ref fetch). Added the rationale for
each rejection (why not VS Code's upstream lookup, why not T3Code's
gh-merge-base, etc.).

Removed packages/host-service/WORKSPACE_CREATION_FALLBACK.md.

* refactor(desktop): extract picker controller + linked-context hooks from PromptGroup

PromptGroup was 592 lines mixing form state, agent prefs, branch picker
state + handlers, link handlers, hotkey wiring, and JSX. Split the
picker controller and link handlers into co-located hooks; PromptGroup
is now 360 lines focused on form composition.

- useBranchPickerController: owns picker state (search, filter), the
  branch-context query, host-id resolution + workspace lookup, and the
  three per-row action handlers (Open / Check out / Adopt). Returns a
  `pickerProps` bag the caller spreads into <CompareBaseBranchPicker>.
  Hides ~70 lines of plumbing behind one hook call.
- useLinkedContext: bundles addLinkedIssue / addLinkedGitHubIssue /
  removeLinkedIssue / setLinkedPR / removeLinkedPR. Pure delegation
  over updateDraft.
- Collapse the PromptGroup / PromptGroupInner indirection. There was
  no error boundary, provider, or conditional in the wrapper — just a
  pass-through. Removed.

Behavior unchanged. Typecheck + lint clean; 11 pending-page tests pass.
…h#3459)

MCP clients on spec 2025-06-18 send resource=<mcp-url> in authorize/token
requests. better-auth 1.5.6 validates that against validAudiences and our
allowlist only contained the bare API origin, so tokens were rejected with
"requested resource invalid". Add the MCP endpoint to validAudiences and to
the JWT verifier's audience list.
…rride migrator (superset-sh#3460)

* feat(desktop/hotkeys): directional pane focus for v1 + Mac alt modifier

Restores keyboard pane navigation to v1 workspaces with the same
Cmd+Alt+Arrow chords v2 uses, via a standalone spatial neighbor walker
over v1's MosaicNode<string> tree (no cross-package coupling to v2).
Also lets the recorder accept alt-only chords on Mac and warns when
bound alt-only chords could mask typing special characters.

* feat(desktop/hotkeys): best-effort migrator for v1 event.key overrides

The v1 recorder stored chords from event.key (layout-dependent raw
glyphs like "meta+,", "ctrl+shift+@", "meta+alt+ª"), while the new
store matches on event.code names. Extends sanitizeOverride with a
token rewrite pre-pass covering US-ANSI punctuation, shifted glyphs,
and macOS Option dead-keys; gates the Option dead-key table behind a
navigator.keyboard-based US-layout probe so non-US Mac users aren't
silently rebound to the wrong physical key.
…perset-sh#3461)

* chore(desktop): auto-restart host-service on bundle change in dev

Watches the built host-service.js in NODE_ENV=development and restarts
running instances via the coordinator when electron-vite rewrites the
bundle. Fast edit→reload loop for packages/host-service and
src/main/host-service without restarting Electron. Not true HMR —
in-memory host-service state (PTYs, watchers, chat streams) is torn
down on each reload.

* fix(desktop): wait for host-service bundle to stabilize before reload

Rollup rewrites host-service.js via unlink+rewrite, so the fs.watch
fires while the file is missing or partially written. The coordinator
now polls statSync until size is non-zero and stable for 150ms (5s
deadline) before calling restartAll, avoiding MODULE_NOT_FOUND on
respawn.
…erset-sh#3464)

The v2-workspace layout rendered child routes without WorkspaceTrpcProvider
during the tick between workspaceId changing and the useLiveQuery join
resolving. If the outgoing page hadn't finished unmounting, its hooks
(TerminalPane, useGitChangeEvents, etc.) crashed with "useWorkspaceClient
must be used within WorkspaceClientProvider".

Hold the render until useLiveQuery reports isReady so the new workspace
mounts into a valid provider on the same tick, and show an explicit
"Workspace not found" state when the collection has hydrated but the id
doesn't resolve.
* 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).
…set-sh#3462)

* fix(desktop): use native clipboard for copy path in v2 sidebar

navigator.clipboard.writeText silently fails when focus is elsewhere
(e.g., terminal), so the toast showed but nothing was actually copied.
Switch to useCopyToClipboard which routes through Electron's native
clipboard via tRPC, matching the v1 pathway.

* feat(desktop): wire up Copy Path in v2 dashboard sidebar

The workspace item context menu's Copy Path just toasted "coming soon".
Fetch the worktreePath from the local host service for local workspaces,
copy via electronTrpc.external.copyPath (native clipboard), and show a
proper success toast. Non-local workspaces fall back to an explanatory
error since the path only exists on the owning machine.

* feat(desktop): wire up Open in Finder in v2 dashboard sidebar

Share the local worktreePath resolver between Copy Path and Open in
Finder, and route Open in Finder through electronTrpc.external.openInFinder
instead of toasting "coming soon".

* refactor(desktop): extract shared PathActionsMenuItems, wrap copy in try/catch

Addresses review feedback on PR superset-sh#3462:
- Pulled the Reveal in Finder / Copy Path / Copy Relative Path items
  shared between FileContextMenu and FolderContextMenu into a single
  PathActionsMenuItems component.
- Wrap useCopyToClipboard calls in try/catch so IPC failures surface a
  proper error toast instead of failing silently.

* refactor(desktop): hide Open in Finder & Copy Path for non-local workspaces

- Gate the Open in Finder / Copy Path items in the dashboard sidebar
  workspace context menu behind isLocalWorkspace so they only appear
  for workspaces on the active machine.
- Drop the now-redundant hostType guard from the actions hook.
- Wrap openInFinder in try/catch in PathActionsMenuItems so native
  action failures surface a toast (addresses coderabbit/cubic P2 on superset-sh#3462).
Press Escape on any settings page to navigate back to where settings
was opened from. Skips when focus is in a form field, contenteditable,
or any open Radix layer (dialog, popover, dropdown).
…uperset-sh#3472)

Flip Cmd+Alt+Arrow (mac) / Ctrl+Shift+Alt+Arrow (win/linux) back to
the pre-superset-sh#3403 behavior:

- Cmd+Alt+Left / Right → Previous / Next Tab
- Cmd+Alt+Up / Down   → Previous / Next Workspace

FOCUS_PANE_{LEFT,RIGHT,UP,DOWN} remain registered but are now
unbound by default — users who want directional pane focus can
rebind them in Settings → Keyboard. PREV_TAB_ALT / NEXT_TAB_ALT
(Ctrl+Tab / Ctrl+Shift+Tab) are unchanged.

Also move SCROLL_TO_BOTTOM on Windows/Linux from ctrl+shift+alt+down
to ctrl+end to avoid the silent collision with NEXT_WORKSPACE that
this restoration would otherwise introduce. buildRegisteredAppChords
uses a Map keyed by canonical chord, so duplicate chords silently
clobber each other in the reverse lookup. mac SCROLL_TO_BOTTOM
(meta+shift+down) is unchanged.

Registered users who previously overrode any of these IDs keep their
overrides — defaults are just fallbacks.
…uperset-sh#3463)

* feat(desktop): add Review tab to v2 workspace sidebar

Port the v1 Review tab functionality into the v2 workspace sidebar,
replacing the "Checks — Coming soon" stub. Uses v2-native host-service
endpoints (git.getPullRequest, git.getPullRequestThreads) instead of
v1 electron endpoints.

Features:
- PR header with title, state icon, review decision badge
- Collapsible CI checks section with status icons and links
- PR comments with avatars, timestamps, and preview text
- Copy individual comments or copy all to clipboard
- Comment pane: click a comment to view rendered markdown in a tab
- GitHub link in pane header to open comment on GitHub
- Copyable tables in rendered markdown

* fix(desktop): address review tab PR feedback

- Fix memory leaks: add timeout cleanup refs in CommentPane and
  CopyableTable, clear previous timeout on rapid clicks
- Add error state: show "Unable to load review status" when tRPC
  query fails instead of infinite loading spinner
- Fix getIcon fallback: show MessageSquare icon when avatarUrl is
  missing instead of rendering <img src={undefined}>
- Add aria-label to comment open button for screen readers
- Add aria-hidden to decorative arrow icon in PRHeader
- Add group-focus-visible:opacity-100 to PRHeader arrow for keyboard nav

* fix(desktop): add .catch() to all clipboard promise chains

Prevents unhandled promise rejections in the renderer if
electronTrpcClient.external.copyText.mutate() fails.

* fix(desktop): guard state updates after unmount in CommentPane

Add isMountedRef to both CommentPane and CopyableTable to prevent
setCopied calls after unmount when the async clipboard IPC resolves
after the component is gone.

* fix(desktop): fix comments not loading until refresh

The threads query had staleTime: 30_000 which cached empty results
when the query fired before the host-service had PR data synced.
Remove staleTime so every mount/refocus fetches fresh data (background
polling at 30s still runs via refetchInterval).

Also tighten the enabled condition to use prQuery.isSuccess instead
of !!prQuery.data for clearer intent, and use composite key for
CheckRow to handle duplicate check names.

* fix(desktop): unmount guard in CommentsSection + single-pass check status

- Add isMountedRef to CommentsSection so markCopied doesn't set state
  after unmount (same pattern as CommentPane/CopyableTable)
- Rewrite computeChecksStatus as single loop instead of 3 passes
  (filter + some(failure) + some(pending) each called coerceCheckStatus)

* feat(desktop): render mermaid diagrams + syntax highlighting in comment pane

Use the existing CodeBlock component which handles mermaid via
@streamdown/mermaid and syntax highlighting via react-syntax-highlighter.

* fix(desktop): lighten mermaid diagram background in comment pane

* fix(desktop): hide mermaid label + lighten actual diagram background

- Hide the "mermaid" label and action buttons on the block wrapper
- Strip the outer border/padding so it blends with the page
- Target the SVG element itself to lighten its background color

* fix(desktop): revert hacky mermaid overrides, use CSS text color instead

Revert the custom Streamdown/SyntaxHighlighter approach. Go back to
using the shared CodeBlock component. Instead of changing the mermaid
canvas background, override the SVG text fill and node/edge label
colors via CSS to use --foreground, making them readable on both
light and dark themes.

* fix(desktop): use mermaid themeVariables for light text in dark mode

Use Streamdown directly with custom themeVariables to set light text
colors (primaryTextColor, nodeTextColor, labelTextColor, etc.) on the
dark mermaid theme. Removes the CSS hacks that couldn't override
mermaid's inline SVG styles.

* fix(desktop): mermaid dark theme contrast + hide label/wrapper chrome

- Use theme: "base" with full dark color palette so themeVariables
  actually control node fills (dark gray nodes, light text)
- Hide "mermaid" label and action buttons via CSS
- Strip background/border from outer block and inner wrapper so
  diagram sits directly on the page background

* fix(desktop): extract CommentCodeBlock as standalone component

Move code block rendering out of useMemo into a proper React component
so the component identity is stable across renders. Prevents unmount/
remount of Streamdown/SyntaxHighlighter when the theme changes.

Mermaid theme variable objects extracted as module-level constants.
Kitenite and others added 30 commits April 28, 2026 17:10
… modal (superset-sh#3844)

* feat(desktop): persist last project + base branch in v2 workspace modal

Remember the user's most recent project and per-project base branch in
localStorage so the new-workspace modal pre-fills them on next open. Uses
the same plain localStorage pattern as useAgentLaunchPreferences.

* feat(desktop): also persist last selected device in v2 workspace modal

* refactor(desktop): use zustand persist store for v2 workspace defaults

Switches the persistence layer to a zustand persist store, matching the
pattern used by v2-project-local-meta and other stores in
renderer/stores. Also clears the per-project base-branch default when
the user explicitly clears the picker, so a stale default doesn't
re-appear on next open.

* fix(desktop): shape-validate persisted lastHostTarget before applying
…et-sh#3811)

* fix(host-service): isolate subsystem crashes from main thread

Subsystem throws (timer callbacks, EventEmitter listeners, pty
data/exit handlers, harness subscribe callback) used to escape into
the process and could take the whole host-service down. Now they're
contained per-subsystem and a process-level safety net catches
anything that still slips through.

- Add safety.ts with installProcessSafetyNet (uncaughtException +
  unhandledRejection log without exiting) and safeSync/safeAsync
  helpers for wrapping callbacks.
- Wrap hot async entry points in EventBus, GitWatcher,
  PullRequestRuntimeManager, ChatRuntimeManager, and terminal PTY
  callbacks.
- Surface the previously-silent malformed-message swallow in
  EventBus.parseClientMessage.

Startup exit on init failure is intentionally preserved so the
coordinator / external supervisor can restart with a clean process.

* refactor(host-service): address review feedback on safety wrappers

- Install process safety net only after server is listening so startup
  throws still reach main().catch and exit non-zero.
- Rename misleading `safeSync` local in pull-requests.ts to
  `runBranchSync`/`runProjectRefresh` — it wraps an async fn.
- Hoist `safeSync(event-bus:send, sendMessage)` to module scope to
  avoid per-broadcast-per-socket closure allocation.
- Use `safeAsync` in git-watcher rescan for consistency with the
  pull-requests interval pattern.
- Drop low-value `promise` field from unhandledRejection log.

* refactor(host-service): simplify crash isolation to process-net + fan-out guards

Drop safeSync/safeAsync wrappers entirely. The Node process-level handlers
already catch throws from setInterval/setTimeout bodies, EventEmitter
listeners, native pty callbacks, and orphaned promise continuations — so
wrapping each one individually was redundant.

The two cases that genuinely need inline error handling are loops over
multiple subscribers, where a throw skips the rest of the iteration:

- EventBus.broadcast — wrap per-socket send and drop dead sockets, matching
  the opencode pty fan-out pattern. Converts "log forever per broadcast"
  into "one log + clean state".
- GitWatcher.scheduleFlush listener loop — inline try/catch so one bad
  subscriber can't skip siblings.

Aligns with the field norm (opencode, tabby, hyper, mastra-code-ui all rely
on process-level handlers + targeted inline guards rather than per-seam
wrappers). Net -68 lines.

* chore(host-service): drop redundant label arg, restore one-liner setInterval

- installProcessSafetyNet had a default label arg that both call sites
  passed identically; drop the param and inline "host-service" in the logs.
- git-watcher.ts start() reverts to its original setInterval one-liner
  shape (the multi-line form was leftover from an earlier refactor).
…review (superset-sh#3850)

* feat(paywall): gate relay + automations + remote workspaces, dither preview

Tightens the Pro paywall around three free-leaking surfaces and refreshes
the paywall preview shader.

UI gates
- Relay (Settings → Security): turning on "Allow remote workspaces to
  access this device" now goes through gateFeature(REMOTE_WORKSPACES).
  Off-toggling stays ungated and keeps the new on/off confirm dialog.
- Automations: sidebar entry is visible to free users (was hidden); click
  routes through gateFeature(AUTOMATIONS). Direct URLs to /automations
  redirect via a new layout-level guard.
- Remote / cloud workspaces in the v2-workspaces list: open + add-to-sidebar
  on remote-device / cloud rows are gated. Local rows are unaffected. The
  workspace page itself is intentionally not gated — server-side
  host.checkAccess already returns paidPlan: false, so unpaid users hit a
  data-layer block instead of an accidental modal lock-out.

Server-side defense (zero new round-trips)
- findOrgMembership now LEFT JOINs subscriptions in the same statement, so
  callers gating on plan don't pay an extra query. Adds
  requireActiveOrgMembershipWithSubscription sibling.
- automation.create / setEnabled / runNow refuse non-paid orgs.
- host.checkAccess returns { allowed, paidPlan }; relay's existing
  5-min LRU caches the combined boolean — no per-request API spam.
- dispatchAutomation joins subscriptions into resolveTargetHost and
  short-circuits unpaid runs with a recorded skip.

Paywall preview
- Replaces MeshGradient with a DitheredBackground (shaders-react Dithering
  warp/4x4) ported from marketing, matching the new visual direction.
- Adds AutomationsDemo + RemoteWorkspacesDemo; drops obsolete
  CloudWorkspacesDemo / IntegrationsDemo.
- Reorders PRO_FEATURES to remote-workspaces → automations →
  team-collaboration → tasks → mobile-app and tightens the
  remote-workspaces description so feature-switching no longer shifts
  the modal height.

* refactor(trpc): split membership-with-subscription into its own helper

Restores the original `findOrgMembership` / `verifyOrgMembership` shapes so
non-gating callers (organization, integration utils) keep their old return
types, and moves the subscription join into dedicated siblings used only by
plan-gating procedures.

- findOrgMembershipWithSubscription / verifyOrgMembershipWithSubscription
  are the new joined helpers.
- requireActiveOrgMembershipWithSubscription routes through the new
  verify*WithSubscription helper.
- Reverts incidental rename churn in organization.ts.

* fix(automations,paywall): address PR review

- automation_runs: add `skipped_unpaid` status so paywall blocks don't
  inflate the `skipped_offline` metric. `recordSkipped` now takes the
  status as a parameter; dispatchAutomation uses it for the unpaid path
  and returns a matching `skipped_unpaid` outcome (was: persisting
  `skipped_offline` while returning `dispatch_failed` — both wrong).
  Includes drizzle migration 0040.
- findOrgMembershipWithSubscription: deterministic ORDER BY (active
  before trialing, then most recent) so a brief active+trialing overlap
  doesn't return a non-deterministic row.
- automations layout: useRef one-shot guard + replace navigation so the
  paywall + redirect don't re-fire on rerender (gateFeature isn't
  memoized) and don't stack history entries.

* perf(automations): drop unpaid orgs at the cron, not in dispatch

Filters automation evaluate-cron to skip rows whose org isn't on a paid
plan, so unpaid automations never enqueue, never enter dispatch, and
never write a run row. This is the primary fix; the dispatch-side
`skipped_unpaid` path stays as defense-in-depth (the subscription join
is already paid for by host resolution, so it's free).

Also simplifies the deterministic ORDER BY across the three
subscription joins (membership / dispatch / host.checkAccess) to plain
`desc(subscriptions.createdAt)` — pick the newest matching row, drop
the contrived case-expression preference.

* refactor(automations): drop skipped_unpaid enum, bail silently in dispatch

Now that the cron evaluate-step filters unpaid orgs out of the enqueue
batch, the dispatch-side unpaid check only fires in a tiny race window
between SELECT and dispatch. That doesn't justify a new DB enum +
migration: defense-in-depth bails before any work, but no longer writes
a run row.

- Reverts enum + 0040 migration.
- Reverts recordSkipped to its single-status form.
- Unpaid path returns { status: "skipped_unpaid", runId: null } without
  a DB write (status mirrors the runtime-only "conflict" outcome).

* refactor(billing): use ACTIVE_SUBSCRIPTION_STATUSES constant in subscription joins

Replaces 5 inline ["active", "trialing"] arrays with the shared
constant from @superset/shared/billing so adding a new active-equivalent
status (e.g. past_due_grace) only needs one edit instead of grepping
across queries.
…erset-sh#3856)

`disposeSessionsByWorkspaceId` filtered by `status = "active"`, so any
session whose row had drifted to `exited` (e.g. via `pty.onExit`) was
skipped on workspace deletion — leaving the in-memory `sessions` Map
entry behind. Widen to `status != "disposed"` so zombie rows get
disposed too. `disposeSession` is already idempotent against a dead PTY.
The v2 ChatPaneInterface root sat inside PaneContent's row-direction
flex with no width directive, so it sized to its intrinsic content and
the chat appeared smushed. Add w-full to match every other v2 pane;
also restore w-full on the SessionSelector trigger button.
…set-sh#3858)

The v2 diff file header used `justify-between` with `flex-wrap`, so when
the action cluster wrapped to a second row at narrow widths, the
remaining chevron, status indicator, and file badge spread out across
the first row with large gaps between them. Removing `justify-between`
keeps those items tight on the left while `ml-auto` on the action div
still anchors it to the right when it wraps.
…S failure (superset-sh#3861)

When the laptop wakes from sleep and DNS hasn't recovered, the tunnel's
auto-reconnect path calls getAuthToken() which fetches api.superset.sh.
The fetch throws ENOTFOUND, the rejection escapes via `void this.connect()`,
and the host-service process crashes — orphaning every PTY. The coordinator
respawns on a new port, but the renderer keeps reconnecting to the dead
port until it gives up.

Wrap connect()'s body in try/catch and route any throw back through
scheduleReconnect, so transient network failures behave the same as
WebSocket socket-level errors.
…uperset-sh#3852)

* feat(desktop): v2 workspaces list filters, project icons, PR hover

- Replace device tab toggle with a Select. Drop the never-populated "cloud"
  option, default to "This device", and list each remote host (with online
  dot + per-host count) below a separator.
- Add a project filter Select, keyed by project, using actual GitHub org
  avatars (https://github.com/{owner}.png) by leftJoining v2Projects to
  githubRepositories. Falls back to first letter on missing-owner / load
  error.
- Group rows by project under sticky section headers (h-8 column header
  flush with top-8 project header to avoid sub-pixel gap). Section header
  shows the project icon, name, and workspace count.
- Surface PR data per row as a small pill (PR icon + #NNN + checks dot)
  that opens a HoverCard mirroring v1's PR detail layout (status badge,
  review status, additions/deletions, title, checks summary + collapsible
  list, "View on GitHub"). Presentational helpers are duplicated locally
  rather than imported from the v1 hover-card tree.
- Replace the WebKit native search clear "x" with InputGroupButton + LuX
  via type="text" on the search input.
- Caddyfile setup heredoc now emits the global { auto_https disable_redirects }
  block so Caddy doesn't try to bind port 80 on dev.
- github_pull_requests.updatedAt now mirrors GitHub's PR.updated_at: drop
  Drizzle's $onUpdate hook on the column, populate from pr.updated_at in
  sync, initial-sync, and webhook upsert paths. Check-run webhooks no
  longer touch updatedAt (they don't bump GitHub's PR.updated_at).
  Schema-only Drizzle change — no SQL migration generated; drizzle-kit
  generate should be a no-op.

* feat(desktop): collapsible project sections + review fixes

Address PR review feedback and add a collapse toggle on each project
section in the v2 workspaces list view.

- Detect merged PRs from `mergedAt` instead of `state === "merged"`.
  GitHub's PR state is only "open"|"closed", so the old branch was dead
  code and every merged PR was being ranked as "closed".
- Map remaining check_run conclusions explicitly: `neutral` → success,
  `stale` and `startup_failure` → failure. Previously fell through to
  "pending" so completed checks rendered with a spinner.
- Project + device filter triggers fall back to a project/host lookup
  built from pre-search-and-device-filter data, so the trigger no longer
  collapses to "All projects" / "Unknown device" when the active
  selection is filtered out by search.
- V2WorkspaceProjectIcon: track failed owner instead of bare boolean,
  so switching to a different (working) owner re-tries the image.
- Hover card: show review status badge and checks block for draft PRs
  too (previously gated on state === "open" only).
- Project sections are now collapsible — chevron toggle in the section
  header, state persisted via the existing v2-project-local-meta store.

* fix(desktop,trpc): address review comments on v2 workspaces filters PR

- Fix @superset/trpc test: add `verifyOrgMembershipWithSubscription` to
  the `../integration/utils` module mock so bun can resolve the named
  export pulled in transitively via `requireActiveOrgMembership`. The
  paywall PR added that export but the test mock wasn't updated, which
  was failing CI on main.
- Drop the auto-stamp narrative comment on github_pull_requests.updatedAt.
- Don't blank the list while machineId is unresolved: treat
  "this-device" as no-op until machineId is known.
- Auto-expand a project section when one of its workspaces is the
  active route, even if the user previously persisted it as collapsed.
- Split the inline helper components out of V2WorkspacePrHoverCardContent
  and V2WorkspacesHeader into one-component-per-folder modules per
  AGENTS.md.
…t-sh#3867)

Renames Cloud → Remote workspaces, swaps the (Coming Soon) text for
proper Beta/Coming-soon Badges, adds an Automations row (Pro+),
and reorders Features so they read top-to-bottom by surface
(workspaces → automations → mobile → integrations → collab).
The marketing /pricing page mirrors the desktop billing comparison.
…et-sh#3876)

Previously, machineId was sourced from `hostServiceCoordinator.getConnection`,
which returns null whenever no host-service instance is running. After macOS
sleep killed the detached host-service child, every v2 workspace failed the
`workspace.hostId === machineId` check and got routed through the relay as
if it were remote, causing data not to load until app restart.

machineId is a deterministic per-device value (`getHostId()`), so expose it
via a dedicated `device.getMachineId` tRPC query and gate provider render
on it so the context value is non-nullable.

Note: this only fixes the misclassification. Reviving the host-service on
wake (e.g. via `powerMonitor`) is a separate change.
Switch host-service-client from httpBatchLink to httpLink so calls
to the local host service no longer wait on the slowest procedure
in a batch.
Mirrors the host-service v2 path so chat TUIs (claude-code et al.)
parse kitty CSI-u sequences from xterm.js. v1 already enables
kittyKeyboard in the renderer; the env claim makes consumers honor it.
…uperset-sh#3869)

* fix(agents): correct copilot flag order and mastracode prompt mode

- copilot: reorder `promptCommand` from `copilot -i --allow-tool=write` to
  `copilot --allow-tool=write -i`. With the old order, the rendered shell
  command landed as `copilot -i --allow-tool=write "PROMPT"`, which
  commander.js parsed as `-i=--allow-tool=write` and rejected the prompt
  with `error: too many arguments`.
- mastracode: add `promptCommand: "mastracode --prompt"`. The previous
  default-from-`command` rendered `mastracode "PROMPT"`, but mastracode's
  TUI silently drops positional args (only the headless `--prompt`/`-p`
  path actually executes the input). Trade-off: prompt-mode now runs
  headless since upstream has no `interactive + auto-execute` flag like
  copilot's `-i` or gemini's `--prompt-interactive`.
- bump `mastracode` desktop dep `0.15.0-alpha.3` → `0.16.0` to match the
  current published release.

* fix(agents): keep mastracode interactive after handling prompt

Chain headless prompt execution with a TUI relaunch so the user lands
in an interactive session on the same thread the prompt seeded. Without
the suffix, `mastracode --prompt` executed and exited, breaking the
expected "interactive + handles prompt" UX.

The TUI auto-resumes the most recent thread (per mastracode 0.13+
behavior), so chaining `; mastracode` after the headless run drops
the user back into the conversation populated by the prompt.

* fix(agents): fix copilot flag order in legacy permissions migration

The migration backfill restored `copilot -i --allow-all` for users
seeded before superset-sh#3546, which has the same flag-ordering bug as the
registry: `-i` consumes `--allow-all` as its prompt value and the
real prompt heredoc errors with `too many arguments`. Reorder to
`copilot --allow-all -i` so the prompt lands directly after `-i`.
The yolo permissions intent is preserved via the unchanged suffix.

* fix(desktop): revert internal mastracode bump to align workspace versions

sherif flagged the workspace mismatch — packages/chat and
packages/host-service still pin 0.15.0-alpha.3, so bumping desktop
alone broke multi-version consistency. The runtime upgrade is
already covered by the user-installed CLI; the internal dep just
needs to track the rest of the workspace.
* feat(web): show Pro badge in account dropdown

Adds a `billing.activePlan` tRPC query and renders a Pro/Enterprise
badge next to the user's name in the AgentsHeader dropdown and mobile
drawer when the active org has a paid subscription.

* feat(desktop): show Pro badge in OrganizationDropdown

Renders a Pro/Enterprise badge next to the org name in the desktop
OrganizationDropdown trigger (topbar and expanded sidebar variants),
using the existing useCurrentPlan hook.

* fix: address PR review comments

- billing.activePlan: drop redundant status check; the WHERE clause
  already restricts to ACTIVE_SUBSCRIPTION_STATUSES.
- AgentsHeader: derive planLabel only when on a paid tier so a stale
  "Pro" string can't surface if loading-state logic changes.
…set-sh#3881)

* fix(desktop): show local diff stats in v1 workspace hover card

The v1 workspace hover modal was rendering pr.additions/pr.deletions from
the GitHub PR snapshot, while the sidebar row showed local working-tree
stats — so the two surfaces could disagree and the modal LOC appeared to
flicker as PR data refetched.

Centralize via a new useLocalDiffStats hook (wraps useGitChangesStatus)
and use it in both surfaces. Drop the PR-stats fallback in the sidebar
row so before-hover and after-hover stay consistent.

* refactor(desktop): pass diffStats into hover card as a prop

Lift the LOC source up to the parent (WorkspaceListItem already computes
it for the inline +/-) and pass the same value into the hover card, so
the modal renders whatever it's handed instead of fetching its own
copy. Also render the diff stats in the no-PR and no-remote branches —
they were previously gated behind PR presence.

Reverts the worktreePath addition to getWorktreeInfo since the hover
card no longer needs it.

* fix(desktop): remove flickery PR-based diff stats from workspace list view

The workspace list row was reading additions/deletions from the GitHub
PR snapshot, which can drift from the local working tree and refetches
on its own cadence — values appeared to flicker. List rows don't need
LOC, so drop the display (and the now-unused githubStatus prefetch).

* refactor(desktop): inline diff stats reducer back into list item

The useLocalDiffStats hook was only called in one place; the other three
imports were just for the LocalDiffStats type on a prop. Inline the
useMemo back into WorkspaceListItem and use a structural type on the
prop instead — drops the hook file and its index export.
…rset-sh#3846)

* feat(desktop): add Create/Import project to v2 workspace picker

- Add "Create new project" and "Import project" entries to the v2
  ProjectPickerPill in the New Workspace modal; wired to the same
  flows used by the sidebar Add Repository button.
- Move v2 sidebar Add Repository button next to New Workspace; rename
  dropdown items to "Create new project" / "Import project".
- Refactor: openNewProject() and folderImport.start() now return
  promises (resolved by the modal/flow) instead of callbacks crossing
  a zustand store; consumers await and react in normal React flow.
- Extract useFinalizeProjectSetup() and useHostProjectIds (with
  exported queryKey) into renderer/react-query/projects/. After a
  project is created/imported the cached host project list is
  invalidated so needsSetup re-evaluates correctly.

* fix(desktop): address review feedback on workspace picker PR

- Picker import toast: "Project imported and selected." (no longer
  tells user to open from sidebar — project is already selected here).
- useHostProjectIds: log host fetch errors instead of swallowing.
- openNewProject: JSDoc the single-in-flight semantics.
- Drop redundant JSDoc / dead type re-export from earlier refactor.

* fix(desktop): pin Create/Import actions below scrolling project list

Move the action group out of CommandList so it stays visible when the
project list overflows, matching v1 behavior. Tightened max-height
to 280px to leave room for the footer.

* fix(desktop): gate workspace modal init on org context being ready

If session is still loading, activeOrganizationId is null and the
v2Projects query filters to []. Initializing the selection in that
state would lock in null and skip the real project list once the
session resolves. Wait until activeOrganizationId is non-null before
marking initialized.

* refactor(desktop): drop reportError wrapper and use shared ProjectSetupResult

- useFolderFirstImport: inline onError?.(message) — the wrapper added a
  hop without buying memoization (onError was already its only dep).
- NewProjectModal: use ProjectSetupResult from react-query/projects
  instead of an inline result shape, avoiding type drift.
…erset-sh#3884)

* fix(host-service): dedupe PR refresh calls with repo-keyed cache

Multiple projects targeting the same GitHub repo each fired their own
GraphQL query every 10s, and force=true paths bypassed the per-project
debounce entirely. Replace with a single repo-keyed response cache so N
projects on the same repo collapse to 1 call, branch-sync/tRPC bursts
share the in-flight promise, and the polling cadence drops to 20s.

* fix(host-service): cache PR detail fetches to skip repeat gh pr view

The PR detail panel re-invokes `gh pr view` on every mount, so re-renders,
tab switches, and click-back patterns each shell out fresh. Wrap the
procedure in a 30s TTL cache keyed on `owner/name#prNumber`, sharing the
in-flight promise across concurrent callers and evicting on failure so
transient errors don't poison subsequent reads.

* fix(host-service): bypass repo cache for explicit PR refreshes

The branch-sync follow-up and the `refreshByWorkspaces` tRPC mutation
both fire when external state has just changed (local SHA moved, or a PR
was just merged) — exactly the case where the 10s settled cache returns
stale data. Thread a bypassCache option from those call sites down to
the cache lookup so explicit refreshes always fetch fresh while the
polling tick keeps deduping. Bypass paths still write to the cache so
subsequent polls benefit.
…d git status (superset-sh#3838)

* fix(desktop): make file tree resilient to slow directory loads

- Time out listDirectory after 5s and retry up to 3x with linear backoff
- Abort in-flight requests on unmount and workspace/root change
- Show a loading spinner in FilesTab while the workspace query resolves
- Enable abortOnUnmount globally on the workspace tRPC client

* refactor(desktop): simplify useFileTree cancellation, drop FilesTab workspace name

- Use AbortController membership in the active set as the "still relevant" check, replacing isMountedRef + activeContextRef + isRequestCurrent
- Inline single-use helpers (markDirectoryLoading, clearDirectoryLoading, shouldRetryListDirectory, getListDirectoryRetryDelayMs)
- Drop the workspaceName prop from FilesTab/WorkspaceSidebar/WorkspaceContent; the files header is just "Explorer"

* fix(desktop): only show files-tab spinner on initial load, not refetches

* feat(host-service): plumb AbortSignal through listDirectory

When the renderer aborts a listDirectory query (timeout or workspace switch),
the host-service was running fs.readdir + per-symlink fs.stat to completion
and discarding the result. Node's fs API doesn't honor AbortSignal, but we
can short-circuit between operations — useful in symlink-heavy directories
(node_modules) where the per-entry stat loop dominates.

* refactor(host-service): batch listDirectory stat calls for cancellable abort

Process per-entry symlink stats in batches of 16 with a signal check
between batches. With Promise.all-over-everything, all stats kick off in
the same microtask and an in-flight abort can't interrupt any of them.
Batching bounds the zombie work to one batch (~16 stats) per abort.

* feat(host-service): tRPC query timeout middleware + retry on TIMEOUT

Hung host-service IPC (slow git, slow filesystem ops) was leaving the
renderer spinning indefinitely. Replace the bespoke per-hook retry/timeout
in useFileTree with a single server-side middleware that bounds every
query procedure.

Server (queryProcedure builder + middleware):
- t.middleware races next() against a per-procedure timeout, rejecting
  with a TRPCError({ code: "TIMEOUT" }). Default 5s, override via
  `.meta({ timeoutMs })` on procedures that legitimately take longer
  (search, listCommits, getDiff, getStatus, getBranchSyncStatus,
  getPullRequestThreads, readFile).
- Switch all query procedures in filesystem and git routers to
  queryProcedure; mutations remain on protectedProcedure.

Client (workspace-client QueryClient):
- defaultOptions.queries.retry retries TIMEOUT errors up to 2 times with
  linear backoff (300ms, 600ms). Other errors keep the previous single
  retry.

useFileTree:
- Drop withAbortableTimeout, ListDirectoryTimeoutError, retry-timer set,
  retry recursion, loadDirectoryRef, activeLoadAbortControllersRef.
- Just await utils.filesystem.listDirectory.fetch(input). React Query's
  retry policy handles TIMEOUT; abortOnUnmount cancels in-flight on
  unmount/workspace switch. ~120 lines removed.

Side benefit: git.getStatus / listBranches / listCommits etc. all gain
hung-IPC protection automatically. The Changes tab no longer spins
forever on a slow `git status`.

* docs(host-service): add QUERY_TIMEOUTS.md reference

Documents the queryProcedure / timeoutMiddleware pattern: where it
lives, how to set per-procedure budgets via .meta({ timeoutMs }),
the current budget table, and what timeouts do (and don't) interrupt.
… file (superset-sh#3885)

* feat(desktop): persist large-diff expand state, auto-expand on file click

Lift `showFullDiff` out of `DiffFileEntry` local state into a persisted
`expandedFiles` array on `DiffPaneData`, synced through the existing
paneLayout collection. Clicking a file from the v2 changes tab now adds
its path to `expandedFiles` (and clears it from `collapsedFiles`), so a
large file auto-renders its diff instead of showing the deferred
placeholder, and the decision survives navigation/reload.

* fix(desktop): guard collapsedFiles in openDiffPane against legacy panes

Older persisted DiffPane snapshots may not have collapsedFiles set; the
existing setCollapsed helper already uses `?? []` for the same reason.
Match the defensive read here so reusing an existing diff pane doesn't
TypeError on undefined.filter.
…og (superset-sh#3888)

Reconnect/close/error chatter no longer streams into xterm scrollback.
The transport now buffers up to 200 log entries and exposes them via
the runtime registry; a pane-header button reveals a popover with the
log and a clear action, and only appears when there's something to
show. Removes the move-to-background button from the v2 terminal pane
header.
* feat(api): backend prereqs for CLI v1

Lands the cloud + desktop changes the new CLI depends on, ahead of the
CLI itself, so the next CLI PR can stack cleanly:

- task: list filters/pagination, byIdOrSlug, list+create kept under
  prior `task.all` / `task.createFromUi` paths so the shipped CLI on
  main keeps compiling against this backend during the rollout
- task.update: accept-but-ignore deprecated `branch` field for the
  same reason
- automation: list/create/update/delete + run-now coverage CLI uses
- host: list/checkAccess/setOnline shape CLI talks to
- v2-workspace: workspace.list + create entry points
- web: cli/authorize and oauth/consent flows; proxy preserves original
  search params on the sign-in redirect (path + search round-trips
  through `?redirect=` directly — no cookie stash)
- desktop/mobile: drop unused sessionHosts collection registrations
- host-service: project router shape adjustments

CLI sources land in a follow-up PR on top of this one.

* fix(api): tighten cli-authorize redirect_uri, jwt fallback, task search

Address bot review on PR superset-sh#3889:

- cli/authorize: parse redirect_uri with `new URL()` and check the
  parsed `hostname` + empty userinfo. The earlier prefix-match accepted
  `http://127.0.0.1:80@evil.example` as a loopback callback.
- trpc.jwtProcedure: re-throw TRPCError instances from verifyJWT so an
  explicitly-rejected token (revoked/forged) doesn't silently fall
  through to the session cookie path. Non-TRPC parse errors still fall
  through (covers expired/missing tokens for desktop's session caller).
- task.list: escape `%` and `_` in the search input before interpolating
  into the ilike pattern.

* fix(api): validate automation v2ProjectId/workspace consistency

PR superset-sh#3889 follow-up:

- automation.create: when both `v2ProjectId` and `v2WorkspaceId` are
  supplied, require them to agree. Always derive the stored
  `v2ProjectId` from the workspace, since the workspace is the ground
  truth.
- automation.create: when only `v2ProjectId` is supplied (no workspace),
  verify the project belongs to the active organization. Previously a
  caller-supplied id was inserted as-is.
* chore(marketing): update trusted by section

* chore(marketing): tweak trusted by section

* chore(marketing): tweak trusted by section
…uperset-sh#3895)

Better-auth's apiKey plugin (`enableSessionForAPIKeys: true`) populates
ctx.session for x-api-key requests, but jwtProcedure was throwing on
the very first line if the request didn't carry an Authorization
Bearer header — so any procedure marked jwtProcedure rejected api-key
auth before reaching the session fallback. The CLI's `superset hosts`,
`workspaces.list`, etc. all 401'd with `--api-key sk_live_…`.

Accept any of: a verifiable Bearer JWT, a successful Bearer JWT
fallback, or an x-api-key-derived session. Throw only if none of
those produce identity. The TRPCError re-throw on explicit JWT
rejection is preserved.

Trailing error message updated to reflect the broader contract.
…perset-sh#3897)

Each file in the v2 changes sidebar now reveals a more-actions
dropdown on hover, with Open Diff / Open Diff in New Tab / Open File
/ Open File in New Tab / Open in Editor (mirrors the right-click
menu, plus new Open File entries that route through the existing
file-open pane). Click-modifier shortcut hints (⇧/⌘) are shown next
to the actions that have them.
…-lines strip (superset-sh#3899)

- Match ChangesHeader chrome on each file header (border-y + bg-muted/30)
- Split filename into dir + basename so the basename always stays visible
  on narrow widths instead of being truncated behind ellipsis
- Add a Copy path button next to the filename, drop Copy file contents
- Move StatusIndicator next to the +/- diff stats
- Blend diff body with the terminal pane surface color
- Flatten the "N unmodified lines" expander flush to the pane edges
  (kills pierre/diffs' wrapper / content / expand-button rounding +
  inline gaps for both line-info and line-info-basic separators)
- Standardize icon-button padding, drop the inline DiffViewModeToggle
  in favour of a co-located DiffPaneHeaderExtras component
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.

9 participants