Skip to content

sync: merge upstream/main through #3700 (ours) to reduce behind count#416

Merged
MocA-Love merged 95 commits intomainfrom
upstream/sync-behind-to-1
Apr 24, 2026
Merged

sync: merge upstream/main through #3700 (ours) to reduce behind count#416
MocA-Love merged 95 commits intomainfrom
upstream/sync-behind-to-1

Conversation

@MocA-Love
Copy link
Copy Markdown
Owner

Summary

計画 PR #398#414 で upstream 48+ commits を段階的に cherry-pick で取り込み済みですが、conflict 解消で patch-id が変わるため GitHub UI の "behind" カウントが減らない git の制約があります。

本 PR は git merge -s ours upstream/main~1 (upstream 9268654e3 #3700 まで) で fork 現状の内容を一切変更せず、upstream history を fork main の ancestor に入れて behind カウントを 1 まで減らします。

変更内容

残る 1 commit

167542eb4 [codex] refactor(host-service): split workspace-creation router (#3697)fork 独自拡張 (baseBranchSource / PR checkout / fork note / applyAiWorkspaceRename) と全面衝突する大規模 refactor のため、本 merge に含めません。

詳細は Issue #415 で追跡:
#415

検証済み項目

  • git diff origin/main HEAD --stat がゼロ (fork 実内容完全維持)
  • git cat-file -p HEAD で 2 parents (7f535f0 fork main + 9268654 upstream)
  • ours strategy で conflict 発生せず

影響

関連

Kitenite and others added 30 commits April 17, 2026 23:22
superset-sh#3548)

v1's createWorktree appended ^{commit} to the start point to prevent
implicit upstream tracking. This fails with "fatal: invalid reference"
when the ref isn't locally resolvable with that suffix (e.g. stale or
missing remote-tracking ref, branches with slashes like
feat/workstreams-view).

Replace ^{commit} with --no-track, which has the same effect without
fragile ref suffix manipulation. Matches v2's host-service approach.

Closes superset-sh#3448
…erset-sh#3549)

* fix(desktop): guard installUpdate against repeat clicks

MacUpdater.quitAndInstall() registers a fresh native-updater
`update-downloaded` listener each call when Squirrel.Mac hasn't finished
staging. Repeat clicks on the update button stacked listeners, then fanned
out into parallel nativeUpdater.quitAndInstall() calls once Squirrel
fired — racing to swap the binary and leaving the app on the old version.
Matches the reporter's symptom (app quits, reopens on same version).

Add an `isInstalling` guard + `status === READY` precondition so repeat
clicks collapse to a single quitAndInstall, and clear the flag in the
error handler so the user can retry if Squirrel surfaces an error instead
of actually quitting.

Closes superset-sh#3507

* test(desktop): make auto-updater tests portable + non-destructive reset

Greptile flagged that setupAutoUpdater() short-circuits on non-mac/linux
hosts, so the suite would silently fail on a Windows CI runner (handlers
never register, guard never resets). Mock shared/constants to pin the
platform.

CodeRabbit flagged that the beforeEach reset emitted a non-network error,
triggering the real ERROR path (which also clears the cached update).
Use a network-shaped error so the handler maps back to IDLE without the
extra side effect.
…licks (superset-sh#3552)

* fix(desktop): refresh v2 terminal link tooltip editor label + nudge plain clicks

- LinkHoverTooltip fetched the default editor in a mount-only useEffect, so
  changing the default editor in settings left the modifier-shift label
  ("Open in Cursor", etc.) stale until the terminal pane unmounted. Refetch on
  every hover-start instead.
- Plain (no-modifier) clicks on a detected file path in the v2 terminal were
  silent, which made the modifier-key affordance undiscoverable. On a plain
  file-link click, show a transient tooltip at the click position
  ("Hold ⌘ to open · ⌘⇧ for external", or Ctrl/Ctrl+Shift off-mac). Capped at
  two shows per renderer session via a module-level counter, and suppressed
  while the modifier-hover tooltip is already visible. Uses framer-motion's
  AnimatePresence for fade in/out.

* refactor(desktop): simpler v2 terminal link tooltip labels

- "Open in editor" → "Open in pane" for the ⌘-click file case (native in-app
  file pane is what actually happens).
- Shift variant always says "Open in external editor" instead of interpolating
  the configured editor name. openFileInEditor uses the global settings
  defaultEditor (non-editor apps like Finder can't be set there), so the
  interpolated name could disagree with a user's per-project preference — the
  generic label never lies.
- Drops the getDefaultEditor fetch, defaultEditor state, and getAppOption/
  getAppLabel plumbing that went with it.

* refactor(desktop): URL ⌘-click tooltip says "Open in pane" for consistency
…rset-sh#3551)

requestLocalNetworkAccess was defined in local-network-permission.ts but
never called, so the Info.plist keys (NSLocalNetworkUsageDescription,
NSBonjourServices) wired up in electron-builder never had a trigger to
prompt the user. On macOS 15+ this causes outbound connections to
local-network IPs from the app and its spawned child processes (node,
python in the terminal) to be silently blocked, while system binaries
like curl escape the same TCC attribution.

Call it alongside requestAppleEventsAccess in app ready.

Refs superset-sh#3474
…h#3553)

Adds a Tasks nav entry (collapsed + expanded) alongside Workspaces,
mirroring v1: paywall gate, last-used filter restoration, and
active-route highlight.
…set-sh#3478) (superset-sh#3550)

The v1 terminal host buffered all stdin writes — user keystrokes
included — until the shell emitted OSC 133;A. When a user's `.zshrc`
hook (e.g. fnm's `use-on-cd`) opened an interactive prompt during
init, the marker never fired and typed y/N answers sat in the queue
for the full 15s timeout, making the workspace look frozen.

Pass writes straight through instead, keeping only the escape-sequence
drop for stale DA/DSR replies from the renderer's xterm. Mirrors the
v2 host-service behavior, which has always written user input directly.
…t-sh#3372) (superset-sh#3547)

* fix(desktop): stop excessive lsof spawning from port scanner (superset-sh#3372)

PortManager was spawning `lsof` every 2.5s via a module-level `setInterval`
with no shutdown path, stacking hint-triggered scans on top, and wrapping
each call in `sh -c` so timeouts left orphaned children that outlived the
app.

Three fixes:

1. Lifecycle: the interval now starts on first `registerSession` /
   `upsertDaemonSession` and stops on the last unregister. No sessions,
   no scans.

2. Concurrency: hint-triggered scans coalesce via a single debounced
   timer plus a `scanRequested` follow-up flag, so at most one scan is
   ever in flight.

3. No orphans: swapped `exec("sh -c 'lsof ...'")` for `execFile` and
   threaded an `AbortSignal` through so `stopPeriodicScan` can cancel
   any in-flight child instead of leaking it to `launchd`/`init`.

Also narrowed `containsPortHint` by dropping two over-broad patterns
(`/port\s+(\d+)/i`, `/:(\d{4,5})\s*$/`) that matched routine log noise
and forced spurious scans.

Includes 13 regression tests in `port-manager.test.ts`, 8 of which fail
on `main` — they directly measure the three classes of bug.

* update doc

* fix(desktop): forward AbortSignal to Windows process-name lookups; add ready-on regex test

Addresses PR superset-sh#3547 review feedback:

- Forward `signal` from `getListeningPortsWindows` through to the `wmic`
  and `powershell` child lookups in `getProcessNameWindows`. Previously,
  on Windows with many unique PIDs, the per-PID process-name lookups
  were not covered by the abort and could run up to 5s after teardown.

- Add a positive-case regression test for the `/ready on .../` hint
  regex (Vite-style dev server output) — the third retained pattern
  previously had no positive test.

Skipping the proposed "snapshot signal at top of scanAllSessions" race
fix: without the shell wrapper, OS process-group cleanup on Electron
exit already handles child termination, so the narrow race between
`stopPeriodicScan` and a read of `this.scanAbort?.signal` mid-scan can
only waste one in-flight lsof call (~100ms) and cannot produce an orphan.
Not worth the extra indirection.

* fix(desktop): tighten runTolerant abort handling + exact coalesce assertion

Addresses PR superset-sh#3547 review feedback:

- runTolerant: rethrow on AbortError/ABORT_ERR/killed/signal so partial
  stdout from a mid-execution kill is never parsed as success. Only
  plain non-zero exits (e.g. lsof exit 1 when no PIDs match) are
  tolerated. The outer catch in getListeningPortsLsof turns any
  rethrown abort/timeout into an empty result — same upstream behavior
  as before, but explicit about intent.

- port-manager.test.ts: tighten the coalescing assertion from
  toBeLessThanOrEqual(2) to toBe(2). The regression guarantee is
  "exactly one initial scan + one coalesced follow-up" — the loose
  version also passed if follow-ups were silently dropped.
…perset-sh#3561)

Disabled state alone didn't feel responsive — click registered without
obvious acknowledgement. Adds a spinning icon alongside the existing
"Installing..." label so users see the action was received.
… chat input (superset-sh#3520)

* fix(desktop): prevent default on hotkeys to stop character leak into inputs

* fix(desktop): make hotkey preventDefault opt-out-able
Rename TOGGLE_EXPAND_SIDEBAR to OPEN_DIFF_VIEWER (same binding).
In v2, focus any existing diff pane or open one in a new tab, and
flip the workspace sidebar to the Changes tab. V1 keeps its existing
expand-sidebar behavior under the new ID.
…t-sh#3513) (superset-sh#3554)

* fix(desktop): recover terminal from non-monospace font crash (superset-sh#3513)

Setting the terminal font to a proportional family like "Inter" blanked
the app on next launch — the bad value persisted in SQLite and xterm
couldn't lay out cells on reload, leaving no way back into settings.

- Sanitize the stored family on read: if the primary family isn't
  monospace (per canvas measurement), fall back to the default terminal
  font so a poisoned DB value can never blank the renderer.
- Hide the "Other" group and custom free-form entry in the terminal
  font picker so new selections are restricted to monospace candidates.

* fix(desktop): reject all-proportional generic terminal font stacks

Follow-up on superset-sh#3554 review. sanitizeTerminalFontFamily previously passed
any all-generic CSS value through untouched (e.g. "cursive", "sans-serif",
"monospace, sans-serif") because parsePrimaryFontFamily returns null when
no concrete family is present — same blank-window crash class as the
"Inter" report.

Refactor the sanitizer to inspect the full family list: when no concrete
primary exists, only trust the value if every entry is a monospace
generic; otherwise fall back to the default. Add regression tests.

* refactor(desktop): append ", monospace" fallback + cleaner font preview

- Always append "monospace" to the sanitized terminal font stack when it
  doesn't already end with one. Mirrors VS Code's behavior in
  src/vs/workbench/contrib/terminal/browser/terminalConfigurationService.ts
  so that if the configured primary isn't installed on this machine, the
  browser falls back to the OS monospace generic instead of a proportional
  default.
- Swap the terminal font preview from a box-drawing layout (which rendered
  as broken in proportional fonts and used tofu glyphs) to a shell session
  that demonstrates column alignment naturally.
- Drop a couple of narrating comments flagged in simplify review.

* refactor(desktop): show Nerd Fonts in the editor picker too

Nerd Fonts are monospace — the terminal-only gate was pre-existing
special-casing and reviewers pointed out it hides a widely-used class
of fonts from users picking an editor font. Drop the gate.

* fix(desktop): validate the actual CSS primary font, not the first concrete entry

Addresses the coderabbit review on b2e6a04. The sanitizer previously
skipped leading generics when picking the primary to measure, so a value
like `sans-serif, "JetBrains Mono"` passed validation because the later
concrete entry was monospace — but CSS resolves the first generic
(sans-serif) and the terminal still renders proportional.

Switch to validating families[0] (the actual CSS primary): if it's a
monospace generic, trust the stack; if it's a proportional generic, fall
back; if it's concrete, canvas-measure it. Add regression tests.
…ts (superset-sh#3562)

Memory leak and CPU spiral root-caused to `staleTime: 0, gcTime: 0` +
60fps polling: React Query can't dedupe or GC anything, and the render
path churns allocations every 16ms.

Restoring the React Query defaults (5min gcTime) fixes the leak. Server
poll rate is independent of perceived stream smoothness — StreamingMessageText
already reveals text client-side at 60fps from whatever buffer the server
delivers. 4fps polling keeps that buffer fed with plenty of headroom.

Also removes the `isRunning` invalidation effect — redundant when the
query is polling.

Builds on and supersedes superset-sh#3170 by @thepathmakerz, which diagnosed the
same root cause. This version takes the subtractive path (-21 lines)
instead of adaptive polling (+36).

Closes superset-sh#3049
…superset-sh#3563)

* Remove video section

* Rband

* More brand

* Move pills

* CTA

* Color

* Grid line

* polish marketing hero, CTA, and testimonials

- Hero title: both segments at weight 500; "AI Agents." uses lo-res-21-ot-serif with Pixelify Sans fallback.
- Subtitle: "Orchestrate 100+ coding agents in parallel" (keep rest).
- Remove vertical grid lines from <main>.
- Move ProductDemo pills back under the mockup (no scroll transform).
- CTA heading matches FAQ styling; copy now "Try Superset now.".
- Add `role` field to Iven's testimonial (Engineer at Paraflow).
- TypewriterText: optional per-segment `render` for custom glyph rendering.
- Simplify DownloadButton classes (unify with buttonClasses).

* revert hero 'AI Agents.' styling to main version

* swap feature-demo background to paper-design Dithering shader

Replaces the canvas-based Bayer dither with @paper-design/shaders-react's
<Dithering> shader (shape="warp", type="4x4"), lazy-loaded via React.lazy.
Rendered at opacity 30% with mix-blend-screen over each card's palette.

* slow feature-demo dither shader from 0.3 to 0.15

* subtle hover on trusted-by logo tiles (brighter border + bg)

* soften Download button: ghost-style over solid foreground

* recolor Download button ghost style to brand orange

* match header CTA to brand ghost style

* push CTA button text to a more saturated orange

* restore cursor:pointer on <button> (Tailwind v4 preflight default is default)

* unify header CTA with DownloadButton

* update Chris Laupama role to TS Lead at Webflow

* match TrustedBy heading style to WallOfLove

* shrink TrustedBy heading one step

* use original compact size on TrustedBy heading, keep semibold

* update Elias role to Founder at Cleanroom

* update Chase role to Founding Engineer at Decoda Health

* update Felipe role to Codex at OpenAI

* drop unused Adobe Fonts kit scaffolding and lo-res-22 font-family

* restore mobile horizontal scroll on ProductDemo pills
* feat(chat): render subagent activity inline as collapsible tool wrapper

Remove SubagentExecutionMessage from the bottom-pinned section so subagent
tool calls render inline within AssistantMessage via the existing
SubagentToolCall collapsible component, consistent with all other tool calls.

* fix(chat): add gradient fade-out and bottom padding to input footer

Removes top padding from ChatInputDropZone so content is flush with the
bottom of the scroll area, and adds a CSS pseudo-element gradient that
fades the conversation content into the background above the input bar.

* feat(ui): redesign ToolInput/ToolOutput/ToolHeader — compact, monospaced

- ToolInput/ToolOutput: replace CodeBlock with plain <pre>, remove p-4
  padding, switch to bg-muted/30 backgrounds, rename labels to
  "Input"/"Output", apply font-mono throughout
- ToolHeader: reduce px-2.5 → px-1, add rounded-b-md, swap chevron
  direction on hover (right=collapsed, down=expanded), add open prop
- Tool: add font-mono to outer Collapsible wrapper

* fix(ui): stop scroll anchor when clicking tool call triggers

Expanding a tool call was causing the scroll container to jump. Detect
clicks on [data-tool-trigger] elements and call stopScroll() so the
stick-to-bottom behaviour doesn't fight the user interaction.

* feat(ui): add ToolCallRow and refactor tool components

Introduces a shared ToolCallRow component that encapsulates the common
collapsible row pattern: icon/chevron, ShimmerLabel title, muted
description, status slot, left-border content area, and an optional
headerExtra element for out-of-trigger action buttons.

Refactors BashTool, WebSearchTool, WebFetchTool, and FileDiffTool to use
ToolCallRow, removing duplicated hover/chevron/collapsible logic from each.
All tools now have consistent: font-mono, px-1 header padding, rounded-b-md
on hover, ml-2.5 border-l content alignment, and chevron-right/down hover
behaviour.

* feat(desktop): refactor tool call components to use shared ToolCallRow

Migrates GenericToolCall, SupersetToolCall, SubagentToolCall, and
ReadOnlyToolCall to use the new ToolCallRow component, eliminating
per-component chevron/hover/collapsible boilerplate.

Also updates ToolCallBlock dispatch: web_search tool variants without
parsed results now fall through to GenericToolCall with a globe icon
instead of WebSearchTool (which requires results to be expandable).
Renames "Subagent" label to "Agent" with the agent type as a muted
description.

* feat(chat): replace inline question UI with footer overlay

- Add QuestionInputOverlay component: numbered options, "Something else"
  free-text row with pencil icon, Skip button, cross-fade to submit on type
- Refactor AskUserQuestionToolCall to plain ToolCallRow (no inline answer UI)
- Wire pendingQuestion/handleQuestionResponse/stopActiveResponse to footer
  in both ChatPane variants instead of ChatMessageList
- Remove PendingQuestionMessage from ChatMessageList and clean up its props

* feat(ui): replace tool call header indicators with braille spinner and left-side icons

- Add BrailleSpinner component (braille chars, amber, matches sidenav style)
- Show spinner in icon slot while pending, red X on error, icon on complete
- Remove right-side status slot (no spinner, checkmark, or X on the right)
- Remove title shimmer animation

* feat(chat): hide Question tool row while active, show with description after answered/interrupted

- Thread isStreaming through ToolCallBlock to AskUserQuestionToolCall
- Return null only when isPending && still streaming (overlay is active)
- Show collapsed row with question text as description once answered or interrupted

* feat(chat): improve tool call UX and styling

- Align tool call icon with chat text using -mx-1 negative margin
- Remove overflow-hidden from MessageContent to avoid clipping
- Hide description in tool call header when expanded
- Show query field as plain text with Query/Response labels instead of raw JSON
- Add subtle focus-visible ring to collapsible trigger button
- Adjust content padding (pl-3 py-1) to align with heading text
- Use "Type your answer..." placeholder when question has no options

* fix(chat): keep answered question messages visible during active turn

Extract hasAnsweredQuestionToolCall to a shared utility and use it in
withoutActiveTurnAssistantHistory / getVisibleMessages so that assistant
messages containing an answered ask_user_question are kept visible in the
message list while a session is still running. Previously all assistant
messages from the active turn were hidden, causing questions and their
answers to disappear from chat history until the turn completed.

* fix(chat): refactor question tool call — answer bubble, skip badge, plain-string fallback

Replace the inline Q&A markdown block with a right-aligned answer bubble
(styled like a user message) shown after the question is answered, and a
"Question skipped" badge when the result carries no answers. Add a fallback
for backends that return a plain string result (result.text / result.answer)
rather than a structured answers map. Remove the isPending spinner from the
ToolCallRow since the overlay in the footer already indicates active state.

* fix(chat): overlay freezes in place on submit, skip sends answer, scroll on question changes

- Fire onRespond without awaiting so the overlay never shows a separate
  "Waiting for response..." state. The overlay stays frozen (same size and
  content) until pendingQuestion updates from the server, at which point
  React remounts the component via key={pendingQuestion.questionId}.
- Highlight the chosen option and replace its number badge with a spinner;
  show a spinner on the pencil icon when the answer came from the text input.
- Skip now sends "skip" as the answer instead of aborting the agent, so the
  LLM can continue. The X button still stops the session.
- Fix isQuestionSubmitting hardcoded to false — now passes questionResponsePending.
- Add footerScrollTrigger so the chat scrolls to the bottom whenever the
  question overlay appears, updates, or disappears.

* feat(chat): question overlay max-height with scrollable options and pinned header/footer

* fix(chat): stabilize useFocusPromptOnPane effect dependency

* feat(chat): show question tool call with status and collapsible answer

Rewrites AskUserQuestionToolCall to use the shared ToolCallRow component.

- Supports both ask_user (singular question/options) and ask_user_question
  (array of questions) tool schemas
- Shows AWAITING RESPONSE / ANSWERED / CANCELLED inline status description
- Expands to reveal the question text + submitted answer when answered
- Extracts answer from result.content "User answered: <x>" format
- ToolCallRow now uses cursor-text when the row has no expandable content

* fix(chat): keep question tool calls visible during active assistant turn

getVisibleMessages() filtered out all assistant messages while a turn was
in progress. Added hasPendingQuestionToolCall() to also pass through any
assistant message that contains an unanswered question tool call, so the
"AWAITING RESPONSE" tool call row remains visible in the message list.

* feat(chat): optimistically hide question overlay on answer submit

Tracks the most recently answered question ID in ChatPaneInterface and
passes it to ChatUploadFooter so the overlay disappears immediately on
submit without waiting for the server round-trip. Also threads
pendingQuestion and answeredQuestionId down to ChatMessageList to suppress
the ThinkingMessage spinner while a question is awaiting a response.

* feat(chat): scroll to bottom on message send, question arrival, and answer

Adds a ScrollAnchor component inside the Conversation (StickToBottom)
context that handles three cases:

- isAwaitingAssistant becomes true: re-pins scroll so Thinking and the
  streaming response are always visible after sending any message
- pendingQuestion.questionId changes: scrolls to bottom when a new
  question arrives so the overlay doesn't cover streaming content
- answeredQuestionId changes (10ms delay): the overlay hide causes the
  footer to shrink and the scroll container to grow; the library
  interprets the resulting scrollTop clamp as "user scrolled up" via a
  1ms setTimeout, so we run after it with a 10ms delay to restore the pin

* feat(chat): show cancelled status when question is aborted

- Question tool call now shows CANCELLED status in the header instead of
  nothing when aborted (output-error state or Mastracode isError: true)
- Error result content is no longer mistaken for an answer, fixing the
  ANSWERED status shown on revisit after an abort
- Expanded view shows the question text and an "Aborted by the user"
  label with a red CircleX icon
- INTERRUPTED / Response stopped footer is suppressed when the
  interruption was caused by an aborted question

* fix(chat): keep bottom-pinned scroll when expanding a tool call

When the chat is pinned to the bottom and the user opens a collapsible
tool call, skip stopScroll() so stick-to-bottom's resize handler
auto-scrolls to reveal the expanded content instead of hiding it behind
the prompt input.

* fix(chat): exclude scrollbar column from input footer gradient

* fix(chat): stop scroll jump when expanding any tool call

Remove the overly-broad "scroll to bottom if last trigger" heuristic that
used a DOM query to find the last [data-tool-trigger] in the container.
This was wrong — it would fire even when there were messages below the
tool call being expanded.

Now ConversationContent simply unpins from bottom on any tool trigger
click, preventing the resize handler from jumping the scroll position.
Nothing more.

* feat(chat): always use ask_user tool for questions in Superset

Two-pronged approach to ensure the LLM never asks questions as plain text
(which bypasses the question overlay) and always uses the ask_user tool:

1. AGENTS.md — adds a project-level override rule that loads into the
   mastracode system prompt for any session in the Superset workspace.

2. host-service — writes a managed ~/.mastracode/AGENTS.md with the same
   rule, applied globally to every workspace opened in the desktop app.
   Uses a managed-by marker to avoid overwriting user-authored files.

Root cause: the default mastracode tool guidance says "Don't use this for
simple yes/no — just ask in your text response." These rules override that
by being appended to the system prompt after the base instructions.

* feat(chat): pending question drives workspace nav status and native notification

- When ask_user tool fires, emit PendingQuestion lifecycle event from the
  harness subscriber (same pipeline as PermissionRequest/Start/Stop)
- useAgentHookListener maps PendingQuestion → "permission" pane status,
  showing the orange dot in the workspace nav immediately, even when the
  tab is not focused
- NotificationManager plays sound and shows "Awaiting Response" native
  toast for PendingQuestion events with visibility suppression
- Cancel tooltip added to QuestionInputOverlay X button
- Cancelled question tool calls now show question text + "Aborted by the
  user" immediately on stop, without requiring a page reload
- isInterrupted prop threaded through MessagePartsRenderer → ToolCallBlock
  → AskUserQuestionToolCall so pending questions show CANCELLED status
  when the run is interrupted
- INTERRUPTED badge is always shown alongside cancelled question state

* fix(chat): clear orange dot on answer submit, focus prompt on dismiss, no green dot when tab is focused

- Clear pane status to idle immediately when user submits a question answer
- Focus prompt textarea when question overlay dismisses (rAF to let overlay unmount and browser focus settle)
- Fix Stop event idle/review determination: read URL from hash (not pathname, which is always the file path in hash-routed Electron app), and add focusedPaneIds as a reliable fallback so panes the user is actively interacting with don't receive a spurious green dot
- Remove MarkdownToggleContent in favour of always-on MessageResponse for subagent output

* feat(chat): render subagent task prompt with markdown via MessageResponse

Render the subagent task text through MessageResponse so markdown
formatting (lists, bold, code spans) is applied consistently with
the response text below it.

* fix(chat): scale down headings and fix list layout in subagent output

Headings were rendering at full browser size (text-2xl/3xl) inside the
compact xs subagent block. Override h1-h6 to text-sm/xs with tighter
margins, and remove the top margin on first-child paragraphs inside list
items to fix ordered list numbers appearing on a separate line.

* feat(chat): render read file tool output with syntax-highlighted code viewer

- ReadOnlyToolCall fetches file content directly via tRPC filesystem.readFile
  (same path as the file pane) instead of parsing MCP tool output, eliminating
  metadata artifacts like line-number prefixes and byte-count headers
- Uses shared detectLanguage() for syntax highlighting language detection
- Renders with CodeBlock (Shiki) with a filename + line-range header styled
  like table headers (bg-muted/50), showLineNumbers, and colorize=false for
  plain white text
- Adds colorize prop to CodeBlock to suppress syntax colors while keeping
  line numbers at reduced opacity
- Passes workspaceId/workspaceCwd from ToolCallBlock to ReadOnlyToolCall
- Fixes withoutActiveTurnAssistantHistory to preserve completed prior-phase
  assistant messages (e.g. read-file before a question answer) by keeping
  messages that have a stopReason and a different id from currentMessage,
  preventing tool calls from disappearing after answering a question

* feat(chat): improve tool call error and task_write UX

- ToolCallRow: replace XIcon with "ERROR" label + XCircleIcon in description slot on error
- SupersetToolCall: add subtitle prop, render output content via MessageResponse instead of raw JSON
- TaskWriteToolCall: new component for task_write — "Update Tasks" title with ListTodoIcon and semantic description (task count + status breakdown)

* fix(chat): add vertical padding to read file tool content area

* feat(chat): add LspInspectToolCall with ActivityIcon and file subtitle

* fix(chat): handle mastra_workspace_lsp_inspect tool name alias

* fix(chat): use FileSearchIcon for LSP Inspect tool call

* feat(chat): show input/output content in LSP Inspect tool call

* fix(chat): use SearchCheckIcon for LSP Inspect tool call

* fix(chat): extract TOOL_CALL_MD_CLASSNAME for global compact markdown in tool calls

Adds inline code (text-xs) fix alongside existing heading overrides.
SubagentToolCall and SupersetToolCall now share the same constant so
future patches only need to happen in one place.

* feat(chat): improve subagent tool call display and share read-file component

- Filter empty messages (step-start/source-only) from chat history
- Add expandable content with subtitles to subagent inner tool calls (Read, List Files, Search, Write, Edit, Web)
- Extract shared ReadFileTool component to packages/ui for reuse across main and subagent tool calls
- Subagent read tool now shows styled CodeBlock with syntax highlighting, matching main agent display
- Thread workspaceId/workspaceCwd/onOpenFileInPane through SubagentToolCall → SubagentInnerToolCall so subagent read calls show the open-in-pane button

* fix(chat): forward workspace props to ReadOnlyToolCall in MessagePartsRenderer

workspaceId and workspaceCwd were available in MessagePartsRenderer but not
forwarded to ReadOnlyToolCall, silently disabling the disk-read feature for
read_file tool calls rendered via that path.

* fix(chat): remove dead code from MessageList

- Remove interruptedByAbortedQuestion which was computed but never
  referenced in JSX or logic
- Move hasRenderableParts to after imports (was inserted between them)
- Remove unused getToolName, normalizeToolName, ToolPart imports
- Add comment explaining the runtime-only "error" part type cast

* fix(chat): fix type safety, path normalization, and memoize in SubagentInnerToolCall

- Replace as never with as BundledLanguage for language prop type safety
- Replace naive path concatenation with normalizeWorkspaceFilePath to handle
  ./, ../, file:// and workspace boundary validation (matches ReadOnlyToolCall)
- Rename shadowed normalized variable to resolvedPath in openInPane closure
- Memoize parseReadFileResult call to avoid re-parsing on every render

* fix(chat): add stale time and loading state to ReadOnlyToolCall file query

- Add staleTime: Infinity so completed read-file tool calls don't refetch
  on remount (prevents IPC burst when scrolling long conversations)
- Show a spinner row while the disk read is in flight instead of flashing
  the raw ToolInput/ToolOutput view

* feat(chat): replace text input with Tiptap editor for slash commands and file mentions

- Add TiptapPromptEditor with ProseMirror-based rich text input
- Slash command chips (/command) as inline atom nodes, insertable anywhere in message
- File mention chips (@path) anchored to cursor position via virtual float
- SlashCommandMenu width matches prompt input via --radix-popover-trigger-width
- Selecting a command inserts a chip node instead of immediately submitting
- Popover closes on editor blur, reopens on focus
- Tab no longer auto-selects commands (only Enter selects)
- serializeEditorToText serializes chip nodes to /name and @path for submission

* feat(chat): add skill preload — /command chips trigger skill tool calls before LLM

- Add SkillToolCall component (ZapIcon, Skill(name) title, success/error state)
- Register SkillToolCall in ToolCallBlock for tool names 'skill' and 'load_skill'
- In ChatPaneInterface.handleSend: extract custom command chip names from content,
  strip leading / from message text, pass names as metadata.skills to sendMessage
- Add skills?: string[] to sendMessageInput metadata schema (zod.ts)
- Pass preloadSkills to harness.sendMessage in service.ts
- Add ChatSendMessageInput.metadata.skills type field
- Add docs/skill-preload-feature.md with implementation state and setup instructions

Requires superset-sh/mastra#9 for harness.sendMessage preloadSkills support
and .claude/commands/ being included in skillPaths.

* feat(ui): update shared AI element components for chat UX

- braille-spinner: improve animation timing
- code-block: add copy button and syntax highlight tweaks
- message: simplify prose class handling
- prompt-input: add focusShortcutText prop support
- tool-call-row: tighten collapsible layout and spacing
- input-group: support rounded-full variant
- globals.css: add chat-specific scrollbar and prose overrides

* fix(chat): improve tool call display components

- AskUserQuestionToolCall: redesign option layout with better button styling
- SupersetToolCall: render markdown content in tool output
- SubagentToolCall/SubagentInnerToolCall: tighten display, fix edge cases
- TaskWriteToolCall: simplify status rendering
- ReadOnlyToolCall: add workspace prop forwarding and memoize file query
- QuestionInputOverlay: improve option button layout
- MessageList: remove unused prop

* fix(chat): update message list and subagent execution display

- ChatMessageList: update subagent message grouping and rendering
- SubagentExecutionMessage: improve tool call display during subagent runs
- messageListHelpers: refine pending/streaming message detection
- use-chat-display: minor hook cleanup
- screens/main ChatPaneInterface: propagate workspace props

* fix(chat): suppress empty assistant message wrappers

When an assistant message has no renderable content (e.g. redacted_thinking
or unrecognized AI SDK step markers), return null instead of rendering
empty Message/MessageContent divs that cause blank gaps between messages.

* feat(chat): show styled "Not Configured" state for LSP inspect when LSP is absent

Detect the "LSP is not configured for this workspace" error and surface it
as a red "Not Configured" badge in the tool call row rather than triggering
the generic error styling.

* feat(chat): universal "not configured" warning on tool call rows

Detect "not configured" errors in getGenericToolCallState and surface
them as a filled amber warning triangle with a "Not configured" tooltip
in the ToolCallRow status area. File name description is preserved.

GenericToolCall passes isNotConfigured through so the treatment applies
to all tool calls, not just LSP Inspect.

* fix(chat): move not-configured warning icon inline after description

Show the outlined amber TriangleAlertIcon next to the file name in the
description area instead of in the right-side status slot.

* feat(chat): clickable file names on file-related tool call rows

Replace the standalone open-in-pane icon button with a hover-underline
treatment directly on the filename. Applies to Read, Check file, Write,
Edit, Delete, and Smart Edit tool call rows.

Extracts a shared ClickableFilePath component (span[role=button]) that
nests safely inside CollapsibleTrigger without invalid nested-button HTML.

* feat(ui): add ShowCode component with expand/collapse, copy, and startLine support

Adds a new ShowCode component that unifies all code display surfaces — tool
call file views and markdown code fences — into a single block with:

- Filename/language header with optional clickable file path and line range
- Expand/collapse toggle (appears when content exceeds ~15 lines)
- Copy and open-in-pane action buttons in the header
- startLine offset for partial-file display (line numbers count from the
  correct offset rather than always starting at 1)
- Language fallback in highlightCode: unknown Shiki languages silently
  retry as "text" instead of throwing

* refactor: replace legacy syntax highlighters with ShowCode

- ReadFileTool: swap inline CodeBlock + duplicated header/button JSX for
  a single <ShowCode> — removes the fragile [&>div>div]:max-h-[300px]
  deep selector and the duplicated open-in-pane button pattern flagged in
  code review
- MarkdownRenderer/CodeBlock (desktop): replace react-syntax-highlighter
  (Prism) with ShowCode, aligning markdown code fences with the shared
  Shiki-based highlighter used throughout the chat UI

* fix(chat): address PR review feedback

- Move useMemo hooks before early returns in AskUserQuestionToolCall and
  SubagentInnerToolCall to fix React Rules of Hooks violations
- Use hasFileContent (content !== undefined) guard in ReadOnlyToolCall
  so empty files render in the code viewer instead of falling back
- Tighten @mention regex to require word-boundary before @ so email
  addresses and decorators are not rewritten as file mentions on round-trip
- Add trigger to ScrollAnchor useEffect deps so footerScrollTrigger bumps
  actually retrigger the scroll effect
- Add pendingQuestion to bumpFooterScroll useEffect deps in ChatPaneInterface
  so the chat scrolls when the question overlay appears or disappears
- Roll back optimistic answeredQuestionId and pane status if
  respondToQuestion RPC fails in legacy ChatPaneInterface
- Guard Shiki highlightCode fallback so it does not recurse infinitely
  when language === "text" already
- Add aria-label to icon-only buttons (expand/collapse, open, copy) in ShowCode
- Remove dead _interruptedByAbortedQuestion useMemo from legacy ChatMessageList

* fix(chat): address second round of PR review feedback

- Add hasPendingQuestionToolCall to workspace path messageListHelpers so
  assistant messages with active ask_user calls stay visible during a run
- Reset QuestionInputOverlay state (customText, submittedLabel) when the
  question prop changes identity, not just on mount
- Fix Enter/Tab in file-mention mode only consuming the event when a file
  is actually selected; falls through to normal submit when results are empty
- Keep isError indicator visible in ToolCallRow status slot when the row is
  expanded (previously the error icon disappeared on open)

* fix(chat): address third round of PR review feedback

- Fix colorize=false fading first code token when line numbers are
  disabled: add a shiki-line-number class to gutter spans and target
  that class instead of span:first-child in the CSS selector
- Add e.preventDefault() to Space key handler in ClickableFilePath
  so activating via keyboard does not also scroll the container
- Fix file mention round-trip for paths containing spaces: serializer
  now emits @"path with spaces" and parser handles both quoted and
  unquoted forms
- Add null guard in FileMentionNode so malformed/pasted content with
  a missing path attr does not crash rendering

* fix(chat): address fourth round of PR review feedback

- Fix ReadOnlyToolCall lineRange: disk read always returns the whole
  file so always display 1–N (trimming trailing newline before counting)
- Fix Shiki fallback to render escaped plain text instead of empty
  strings when codeToHtml fails for the "text" language itself
- Trim trailing newline before computing lineCount in ShowCode so files
  ending with \n do not trigger isOverflowing one line early

* chore: formatting and cleanup

* fix(chat): close mention popup before falling through Enter when results empty

* fix(chat): address fifth round of PR review feedback

- Re-add inputValue to SlashCommandPreviewPopover anchor effect deps so
  the virtual anchor re-measures when typing shifts the chip's position
- Clamp slash menu selectedIndex in onUpdate when filtered results shrink,
  matching the existing mention-menu clamping behavior
- Return true (consume event) when Enter/Tab closes empty mention popup so
  the event does not propagate to insert a paragraph break
- Mirror focus-on-dismiss effect in v2 workspace ChatInputFooter so the
  editor regains focus after the question overlay unmounts
- Fix trailing-slash paths rendering empty label in ClickableFilePath
  by using || instead of ?? for the basename fallback

* chore: rebuild bun.lock after rebase

* Fix typecheck

* refactor(chat): align skill handling with upstream mastra

Removes the fork-dependent preload wiring (metadata.skills →
preloadSkills pass-through) that was a silent no-op on upstream
mastracode. Keeps the SkillToolCall renderer so load_skill tool
calls emitted by upstream's native skills system render with their
own UI.

Rewrites docs/skill-preload-feature.md to describe the upstream
agent-autonomous model (SKILL.md discovery in .claude/skills,
.agents/skills, .mastracode/skills).

* chore(deps): bump mastra to 0.15.0-alpha.3 / 1.26.0-alpha.3

Brings in upstream mastra's native skills system (search_skills +
load_skill tools, SKILL.md discovery via skillPaths) which the
SkillToolCall renderer in this PR now consumes for free.

- mastracode:     0.14.0   → 0.15.0-alpha.3
- @mastra/core:   1.25.0   → 1.26.0-alpha.3
- @mastra/mcp:    1.3.1    → 1.5.1-alpha.1

Applied in apps/desktop, packages/chat, packages/host-service.

* chore: alphabetize @tiptap/pm in desktop package.json

Auto-applied by biome/sherif.

* fix(chat): cut display polling to 4fps and restore query cache defaults (superset-sh#3562)

Memory leak and CPU spiral root-caused to `staleTime: 0, gcTime: 0` +
60fps polling: React Query can't dedupe or GC anything, and the render
path churns allocations every 16ms.

Restoring the React Query defaults (5min gcTime) fixes the leak. Server
poll rate is independent of perceived stream smoothness — StreamingMessageText
already reveals text client-side at 60fps from whatever buffer the server
delivers. 4fps polling keeps that buffer fed with plenty of headroom.

Also removes the `isRunning` invalidation effect — redundant when the
query is polling.

Builds on and supersedes superset-sh#3170 by @thepathmakerz, which diagnosed the
same root cause. This version takes the subtractive path (-21 lines)
instead of adaptive polling (+36).

Closes superset-sh#3049

* feat(chat): slash command chip UX enhancements

- Argument editing inline in chip: auto-focus on insert, right-arrow to exit, double-click to re-enter
- Commands without argumentHint hide the colon/input entirely
- Model command shows a dropdown of available models (no free-form text)
- Chip input auto-sizes as user types (shrinks to content width)
- Dropdown positioned above chip (side="top"), ArrowUp/Down navigate options, Tab/Enter commit selection
- Menu reopens automatically when deleting value back to empty
- Preview popover and select dropdown are mutually exclusive (preview only on hover/node-select, never while arg input is focused)
- Focus shortcut hint moved inside TiptapPromptEditor (accepts focusShortcutText prop)

* chore: refresh bun.lock after pull

* test(chat): drop shallow ChatMessageList snapshot tests

These tests replace every child component with a mock placeholder
and assert on literal strings appearing in the rendered HTML. That
tested mock plumbing, not behavior — every SUT import change broke
them regardless of whether the actual render output changed, and
the 'SUBAGENT_EXECUTION_MESSAGE' assertion was checking for a
component this PR intentionally inlined.

The useful bit (filter/ordering logic in messageListHelpers) is
better covered by a direct unit test — leaving as a follow-up.
…uperset-sh#3565)

Observability was enabled in superset-sh#1464 but dropped when the proxy was
re-created from scratch in superset-sh#1867. Without it, every wrangler deploy
reconciles Cloudflare back to logs/traces off, which is why the
dashboard toggles kept reverting after each production deploy.

Pin invocation_logs explicitly so future config drift can't silently
disable it again. Audit logs are an account-level setting and still
need to be re-enabled in the Cloudflare dashboard separately.
* docs: consolidated weekly changelog — 2026-04-20

Supersedes superset-sh#3206 (2026-04-06) and superset-sh#3404 (2026-04-13), folding in this
week's v2 workspace work so three weeks of shipped changes land in one
post instead of three stale PRs.

* docs(changelog): include chat UX overhaul (superset-sh#3039) in consolidated post

* docs(changelog): add compressed chat-ux hero screenshot

* docs(changelog): refresh chat-ux hero and add v2 file tree screenshot

* docs(changelog): add brand refresh screenshot

* docs(changelog): add v2 diff viewer screenshot

* docs(changelog): v2 workspace framed as early access, add screenshot

* docs(changelog): rename title to 'v2 early access'

* docs(changelog): frame v2 as a cloud-aimed rebuild — terminal rewrite, IDE architecture

* docs(changelog): drop articles in title

* docs(changelog): light trim — drop redundant lead-ins and filler words
…es with / (superset-sh#3232)

* fix: fall back to FETCH_HEAD checkout when gh pr checkout fails for branch names with /

Fixes superset-sh#3231

gh pr checkout internally runs `git checkout -b <branch> --track origin/<branch>`.
When the branch name contains `/`, git cannot resolve the tracking ref inside a
freshly created detached worktree, producing "starting point is not a branch".

The fetch succeeds — only the tracking setup fails. Catch that specific error and
fall back to `git checkout -b <localBranchName> --no-track FETCH_HEAD`.
push.autoSetupRemote=true (already set after worktree creation) handles push
tracking without needing --track.

* fix: use -B flag to force-replace branch in FETCH_HEAD fallback checkout

* fix: log fallback path when gh pr checkout fails with tracking error

---------

Co-authored-by: Ruan Gustavo Araujo da Silveira <ruan.silveira@M4Pro.local>
…erset-sh#3546)

* feat(desktop): safer defaults for builtin terminal agent presets

Swap permission-bypass flags for each CLI's intended safe-but-useful
mode (claude acceptEdits, codex --full-auto, gemini auto_edit, copilot
--allow-all-tools). Drop mastracode/opencode/pi from the default seed
since they are YOLO-by-default at the CLI level; they remain available
via Quick-Add. Remove cursor-agent's --yolo suffix (silent no-op on the
real binary). Existing users are preserved — the v1
terminalPresetsInitialized guard and v2 migration marker ensure stored
commands are never rewritten.

* test(desktop): update agent-launch-request fixture for new codex default

buildPromptAgentLaunchRequest's terminal-command fixture hard-coded the
old --dangerously-bypass-approvals-and-sandbox flag. Update it to the
new --full-auto default so the test reflects the current builtin.

* fix(desktop): address reviewer feedback on safe-default flag choices

- gemini promptCommand: add --approval-mode=auto_edit so prompt/task
  launches use the same safety mode as terminal launches (flagged by
  cubic, greptile, and CodeRabbit)
- copilot: switch from --allow-all-tools to --allow-tool=write. Per
  GitHub's own docs, --allow-all-tools "allows all tools to run
  automatically without confirmation" including shell, which
  contradicts the safe-by-default claim. --allow-tool=write auto-
  approves file edits only (analog of claude's acceptEdits).
- docs: update copilot line; clarify mastracode/opencode/pi opt-in
  parentheticals so users understand why they're not auto-seeded.
)

* docs: v2 project create/import design + plan

Simplified redesign after PR review. Collapses the earlier three-signal
backing model (cloud + per-host cloud signal + local) into two signals
(cloud + local-only), removes the v2_host_projects cloud table and
Electric sync, drops per-row state decoration on the sidebar, and moves
backing checks to action time (workspace-create modal, error paths).

* feat(trpc): v2Projects.findByGitHubRemote + jwt-scoped create

Adds the cloud-side matcher used by host-service's folder-first import
flow: given a clone URL, returns candidate projects the user has access
to whose GitHub repo matches (case-insensitively). Named findByGitHubRemote
(not findByRemote) because the match is GitHub-specific.

v2Projects.create switches to jwtProcedure with an explicit
organizationId + repoCloneUrl, matching the shape host-service needs to
call from project.create. No existing callers.

parseGitHubRemote moves from packages/host-service to packages/shared so
both cloud tRPC and host-service consume the same implementation.

* feat(host-service): project.create / setup / list / findByPath / remove

Full create/import lifecycle in host-service:

- project.list — DB read of host-service.projects. Pure, no filesystem
  probing. Stale paths surface via operation errors, not proactive checks.
- project.findByPath — validate git root, read remote, forward to cloud
  v2Projects.findByGitHubRemote. Backs the folder-first import picker.
- project.create — discriminated-union mode (empty/clone/importLocal/
  template); Phase 1 ships clone + importLocal only, empty and template
  throw NOT_IMPLEMENTED.
- project.setup — discriminated-union mode (clone/import) with
  acknowledgeWorkspaceInvalidation gate on the re-point case.
- project.remove — local worktree + repo dir teardown.

Cloud backing (v2_host_projects) is intentionally absent: there is no
per-host cloud signal in this design. Backing is a local-only concept,
checked at action time.

Adds ProjectNotSetupCause to the error formatter so the renderer can
catch throws from workspace.create (next commit) and open the Pin & Set
Up modal inline.

* feat(desktop): add-repository modals at dashboard layout level

Three flows for getting projects onto this device:

- New project — clone a GitHub URL into a chosen parent directory.
  Drives project.create(mode=clone).
- Import existing folder — native picker → project.findByPath branches on
  candidate count. 0 → name + create (importLocal). 1, not set up here →
  auto-advance to project.setup. 1, already set up → destructive re-point
  confirmation. >1 → picker modal.
- Pin & set up — clone an existing cloud project onto this device.
  Drives project.setup(mode=clone), with forceRepoint entry for repair.

All three modals are mounted once at the dashboard layout level via
AddRepositoryModals, and opened through a small zustand store. Sidebar
header "Add repository" dropdown triggers New project / Import folder.

* feat(desktop): workspaces-tab Available section + folder-first import trigger

Lists cloud projects in the user's active org that aren't pinned
locally. Pin & set up per row runs project.setup. Header dropdown
("Add repository") mirrors the sidebar — "+ New project" +
"Import existing folder." Entry points route through the dashboard-level
AddRepositoryModals via the shared zustand store.

useAvailableV2Projects powers the section: antijoin
v2Projects ∖ v2SidebarProjects scoped to the active organization,
with the existing v2-workspaces search filter applied.

* feat(desktop): workspace-create inline setup + remote-device stub

- Host-service workspaceCreation.{create,checkout,adopt} throw
  PROJECT_NOT_SETUP (PRECONDITION_FAILED + cause { kind, projectId })
  when this host has no local project row. No more silent auto-clone
  into ~/.superset/repos/ — the user explicitly picks where to clone.

- Pending workspace-create page intercepts data.projectNotSetup on the
  error, opens the Pin & set up modal pre-filled with the project, and
  registers a one-shot onSuccess callback to retry the original intent
  once setup resolves. The pending row stays in "creating" through the
  modal so the UI doesn't flicker to failed.

- Clicking a remote-device workspace row lands on the new
  WorkspaceNotOnThisHostState stub: explains the workspace lives on
  another host, offers "Set up here" (opens Pin & set up for the
  project) or "Browse workspaces." V2 workspace page checks
  host.machineId via live query and renders the stub before mounting
  the pane tree, which would otherwise crash on a foreign worktree.

* Fix infinite import

* fix: pre-existing notification test, a11y labels, design doc shape

- notification-manager.test: update expected strings to match source
  (strings changed in superset-sh#3039; test wasn't updated, CI was red on main too)
- DashboardSidebarHeader: aria-label="Add repository" on icon-only
  dropdown triggers so screen readers announce them (tooltips don't
  count as accessible names)
- docs/design/v2-project-create-import: correct v2Projects.create input
  shape (jwt-scoped { organizationId, name, slug, repoCloneUrl })

* feat(desktop): unify new-workspace pickers + link popovers, strip chat link UI

- Unify DevicePicker / ProjectPickerPill / CompareBaseBranchPicker to a
  shared FORM_PICKER_TRIGGER_CLASS: no background, h-[22px], text-[11px]
  text-muted-foreground, size-3 icons, align="start" dropdowns. Bump the
  project trigger thumbnail to size-4; drop the leftover `!` override
  (twMerge handles it).

- DevicePicker: icon-only trigger (aria-label + title surface the name).

- Rewrite IssueLinkCommand / PRLinkCommand / GitHubIssueLinkCommand to
  one codepath each: accept a button as `children`, wrap it in
  PopoverTrigger, own their open state internally. No more shared
  plusMenuRef, no more external open/onOpenChange/anchorRef coordination,
  no manual onPointerDownOutside anchor-guard — Radix handles toggle and
  dismiss natively so clicking a trigger while its popover is open
  closes it like every other picker.

- v2 NewWorkspace PromptGroup: drop the three popover-open useStates +
  plusMenuRef + manual toggle handlers. AttachmentButtons becomes a
  layout shell that renders the three trigger elements as props; each
  wraps a shared LinkTrigger (tooltip + pill button).

- Chat (v1 + v2) + v1 NewWorkspace PromptGroup: remove the link-issue
  popover wiring (IssueLinkCommand usage, ChatShortcuts' onLinkIssue
  callback). PlusMenu in chat collapses from a dropdown with
  attach/link options to a plain attachment button.

- Temporarily disable v2 ChatPane render: it predates this PR and is
  missing ChatServiceProvider (introduced in PR superset-sh#3088), so
  chatServiceTrpc has no context in TiptapPromptEditor. Replaced with a
  "Chat pane is temporarily disabled" placeholder; original render body
  commented out for quick restoration.

* feat(desktop): host-scoped project picker with Available / Needs setup sections

The v2 new-workspace picker was listing every cloud project the user
had access to, regardless of whether it was set up on the selected
device. That produced the PROJECT_NOT_SETUP error path on submit —
reviewers flagged the pending-row-stuck-in-creating fallout as a P1.

Root-cause fix: split the project list by selected-host availability.

- `useHostProjectIds(hostTarget)` queries host-service `project.list`
  on the chosen device (local via activeHostUrl, remote via relay) and
  returns the set of set-up project IDs.
- PromptGroup splits `recentProjects` into `availableProjects` +
  `needSetupProjects` using that set; changing the device refetches.
- ProjectPickerPill renders two CommandGroup sections: Available (click
  selects) and Needs setup (click opens Pin & set up for that project).
- Pin & set up already invalidates `["project", "list", activeHostUrl]`
  on success, so after setup the project flips to Available — user
  picks it and continues normally.

While `project.list` is loading or errors, everything falls back to
Available — picker stays usable; any real failure surfaces via the
existing workspace-create error path.

* lint

* fix(desktop): IssueLinkCommand uncontrolled close + PlusMenu aria-label

- IssueLinkCommand: the refactored popover-trigger API made `open` and
  `onOpenChange` optional so callers (v2 PromptGroup) could let Radix
  manage state. But `handleSelect` only fired the optional controlled
  callback, so in uncontrolled mode the popover never closed after
  picking an issue. Track state ourselves via a controllable-state
  pattern: internal `useState` when the prop is absent, caller's value
  when passed. `setOpen` always writes through, so close-on-select
  works in both modes.

- PlusMenu: add aria-label="Add attachment" to the icon-only trigger.
  Radix Tooltip sets aria-describedby on the trigger, not
  aria-labelledby, so screen readers previously announced it as an
  unlabeled button.

* refactor(desktop): drop controlled-open props from IssueLinkCommand; extend aria-label fix

- IssueLinkCommand: only caller passes `onSelect + children`, so the
  optional open/onOpenChange pass-through was dead code. Simplify to
  always-internal state. Radix Popover has no imperative close from
  inside its content — owning state is the canonical shadcn/cmdk
  pattern, not scaffolding.

- AttachmentButtons (v2): add aria-label to the shared LinkTrigger (so
  Link issue / Link GitHub issue / Link pull request all announce a
  name) and to the paperclip. Same fix as PlusMenu — Radix Tooltip
  sets aria-describedby on the trigger, not aria-labelledby, so
  tooltip-only buttons read as unlabeled to screen readers.

* fix(trpc): scope v2Project.findByGitHubRemote + modal picker to active org

host-service is pinned to a single organization at boot (env.ORGANIZATION_ID);
its local projects table has no orgId column. Project discovery was leaking
across orgs:

- v2Project.findByGitHubRemote used ctx.organizationIds (plural, all
  accessible orgs). The folder-first picker would surface candidates from
  orgs the current host can't set up, producing a confusing NOT_FOUND when
  host-service then called v2Project.get with its own org.
- DashboardNewWorkspaceModalContent queried collections.v2Projects with no
  org filter. Same over-fetch, same downstream failure.

Align both with the rest of the codebase (v2Project.get / create,
useAvailableV2Projects, useWorkspaceHostOptions) which take/filter by an
explicit active orgId:

- findByGitHubRemote: add organizationId input, authorize it against
  ctx.organizationIds (same shape as get/create), filter candidates by it.
- host-service project.findByPath: pass ctx.organizationId through.
- DashboardNewWorkspaceModalContent: .where(eq(projects.organizationId,
  activeOrganizationId)) on the live query, matching useAvailableV2Projects.

* Lint

* fix(host-service): clone-then-cloud in project.createFromClone, rollback on cloud failure

Matches the local-first-then-cloud pattern already used by
workspace.create (workspace-creation.ts:860-918, which git-worktree-adds
first then registers cloud with a rollback on failure).

Previously createFromClone called v2Project.create before cloneRepoInto,
so any clone failure (network, bad URL, auth, dir collision) left a cloud
v2_projects row with nothing local backing it on any host. Retrying the
flow with corrected input accumulated more orphans.

Reorder: clone first, register cloud in try/catch, rmSync the freshly-
created clone if cloud-create or persistLocalProject throws.

* fix(host-service): move project.create visibility into GitHub-provisioning modes

Only empty + template modes provision a new GitHub repo and need to tell
the GitHub App whether it should be private or public. clone +
importLocal reuse an existing remote where visibility is already set —
the top-level field was required but ignored for those two paths.

Move `visibility: z.enum(["private", "public"])` into the empty and
template variants of the discriminated union. Drop it from clone/
importLocal callers. Update design doc to match.

* refactor(desktop): use Radix composition for link-command tooltips, drop dead chat-link wiring

Responds to saddlepaddle + Kitenite reviews on PR superset-sh#3566.

- IssueLinkCommand / PRLinkCommand / GitHubIssueLinkCommand now own the
  Popover + Tooltip composition internally via
  `PopoverTrigger asChild > TooltipTrigger asChild`. Callers pass a plain
  PromptInputButton + a tooltipLabel prop. Removes the LinkTrigger
  forwardRef + `{...rest}` spread trick that was sneaking Popover props
  through an intermediate Tooltip wrapper.
- Delete the misleading JSDoc at IssueLinkCommand claiming Radix can't be
  closed imperatively — PopoverClose exists; the controlled-open pattern
  we use is just shadcn's canonical combobox.
- Drop orphaned `_issueLinkOpen` / `_addLinkedIssue` + the
  `setIssueLinkOpen` prop threaded through ChatShortcuts in both v1 and
  v2 chat, plus the same dead state in v1 NewWorkspaceModal PromptGroup.
- Retire the CHAT_LINK_ISSUE hotkey entry — its only consumer was the
  dead setIssueLinkOpen toggle.

* chore: trim past-state narration + what-describing comments

- Drop "previously this did X" / "introduced in PR superset-sh#3088" / commented-out
  renderPane block in v2 usePaneRegistry chat pane.
- Collapse JSDocs that only restated the function's name (ParentDirectoryPicker,
  AddRepositoryModals layout blurb, per-method docs on UseFolderFirstImportResult,
  persistLocalProject).
- Tighten the explanatory comments that still earn their keep (pending
  PROJECT_NOT_SETUP interceptor, PinAndSetupModal conflict state, store
  onSuccess / forceRepoint prop docs).

* refactor(desktop): strip v2 discovery/recovery surface to MVP

Two rules for v1:
- Sidebar = pinned projects.
- Workspaces tab = every workspace in the user's active org.

Code deletes:
- V2AvailableProjectsSection, useAvailableV2Projects, useHostProjectIds.
- v2UsersHosts innerJoin in useAccessibleV2Workspaces — tab no longer
  drops rows when host access changes.
- Available-section wiring in V2WorkspacesList + v2-workspaces/page.tsx.
- Available / Needs-setup split in ProjectPickerPill and the
  openPinAndSetup bridge in PromptGroup. Also removes the wrong-host
  bug (cubic AF_o, saddlepaddle CXVQ) as dead code.
- PROJECT_NOT_SETUP recovery loop in the pending page — failure is a
  plain toast now.

Docs realigned:
- design/v2-project-create-import.md opens with the two rules and
  moves Available / inline setup / backing signals to an explicit
  "Out of scope for v1" block.
- plans/20260417-v2-project-create-import-impl.md mirrors the same
  deferrals; Phase-1 checklist is now all checked.

Net −537 lines. Typecheck + lint clean.

* refactor(desktop): open remote-host workspaces without gating

Previously any workspace whose hostMachineId didn't match the local
machine landed on a WorkspaceNotOnThisHostState stub. That hid the
workspace from the user entirely when the whole point is to let them
see it. Delete the gate, delete the stub component, and let the
workspace page render for any host. Operations that assume local
filesystem (terminal spawn, local git) fail at the point they run.

Also slims the page's live query — projectGithubOwner, projectName,
hostMachineId etc. were only fed into the stub.

Design doc + plan updated to reflect the no-gating posture.

Resolves saddlepaddle CTvC.

* refactor(desktop): extract FormPickerTrigger component

Addresses saddlepaddle CWlq — the shared style for the three top-of-modal
pickers (Device / Project / Branch) lived as a string constant in
types.ts, which is an odd place for a className and doesn't compose.

Promote it to a named FormPickerTrigger component that encapsulates the
base button styles and accepts extra className + native button props.
The three call sites lose their raw <button type="button"> +
backtick-composed classNames.

Drops FORM_PICKER_TRIGGER_CLASS from types.ts.

* refactor(desktop): remove dead PinAndSetupModal + async-hygiene sweep

PinAndSetupModal had zero remaining callers after the MVP cut — the
pending-page PROJECT_NOT_SETUP interceptor and the Available-section
"Pin & set up" button were the only two. Delete the whole modal,
its store action, useOpenPinAndSetupModal hook, PinAndSetupTarget
type, and the forceRepoint plumbing that existed only to support it.

Also addresses the async-hygiene nits on the surviving surfaces:
- useFolderFirstImport.start wraps selectDirectory.mutateAsync in
  try/catch → reportError (coderabbit nmS, cubic op5).
- ParentDirectoryPicker.handleBrowse wraps the same (cubic op8).
- AddRepositoryModals effect adds .catch on startRef.current()
  (cubic oqE).
- FolderFirstImportModal keys CandidatePickerContent on repoPath so
  selectedId resets per import (coderabbit nmM).

Docs + plan updated to reflect the removed modal + ENOENT recovery
deferral.

Net −205 lines. Typecheck + lint clean.

* refactor(host-service): reject re-pointing instead of confirming it

v1 has no re-point UX. project.setup now treats an existing row as:
- same resolved path → no-op success (idempotent; fixes the false
  CONFLICT that cubic/coderabbit flagged on same-path setup).
- different path → CONFLICT with the existing path in the message,
  no escape hatch. User must project.remove first if they genuinely
  want to move the project.

Drops `acknowledgeWorkspaceInvalidation` from the input, the ack
branch of the CONFLICT guard, and the setupFromClone/setupFromImport
helpers + SetupContext type in handlers.ts (the setup path is small
enough to inline).

Client drops the confirm-repoint state, confirmRepoint method,
ConfirmRepointContent component, and the conflict branch in
SetupInvokeResult — none of which have anything to retry against.

Also fixes the TOCTOU race in cloneRepoInto: replaces
existsSync + rmSync-on-error with mkdirSync (atomic claim) +
rmSync-on-error, so clone failure can't delete a directory this
process didn't create.

Resolves coderabbit nmb, nmd, and cubic oqN.

* fix(desktop): unify workspaces-tab empty state

The onboarding "No workspaces yet" check was reading already-filtered
pinned/others counts, so a search that matched nothing landed on the
onboarding copy instead of the clear-filters UI.

Collapse to a single !hasAnyMatches branch that picks copy + icon
based on hasActiveFilters. Drops the bogus hasAnyWorkspaces check.

Resolves cubic CrwE.

* revert queries

* Clean up dead code

* refactor(desktop): port v1 new-project UI into NewProjectModal

Replace the bespoke name + clone-URL + parent-picker form with v1's
new-project page layout: a Location row (text input + browse button),
three mode tiles (Clone/Empty/Template), and a per-mode form. Only
Clone is wired up; Empty + Template carry "(coming soon)" since v2
project.create throws NOT_IMPLEMENTED for them.

Location auto-populates to ~/.superset/projects via window.getHomeDir.
Project name is derived from the clone URL's last segment so the form
matches v1 (no explicit name field).

ParentDirectoryPicker deleted — the inline Input + folder button
replaces it and there's no other caller.

* feat(db/trpc): decouple v2 projects from GitHub App installs

v2Projects previously required a non-null githubRepositoryId, which
gated project creation on the org having installed the repo via the
GitHub App. Cloning any other repo (public, not installed, or non-
matching) failed at the cloud step after a successful local clone.

Changes:
- githubRepositoryId becomes nullable with ON DELETE SET NULL,
  matching v1's projects table.
- repoCloneUrl is added as the canonical source of truth for the
  remote URL. Also nullable so empty-mode / local-only projects
  without a remote can coexist.
- UNIQUE(organization_id, lower(repo_clone_url)) prevents two
  projects from claiming the same repo in one org. NULLs don't
  collide, so URL-less projects still work.
- v2Project.create accepts an optional repoCloneUrl, canonicalizes
  via parseGitHubRemote, and links a matching github_repositories
  row case-insensitively when one exists. Unique-violation (23505)
  surfaces as CONFLICT with per-constraint messaging.
- v2Project.findByGitHubRemote matches on v2Projects.repoCloneUrl
  directly instead of joining through the installation table, so
  unlinked projects are discoverable.
- v2Project.get drops the derived repoCloneUrl — consumers read the
  stored column or the joined githubRepository directly.

Migration 0034 bundles all five schema changes. Nullable-safe: no
backfill required for existing rows.

* No candidate thing

* feat(desktop): flag projects not set up on selected host in new-workspace modal

After picking a host in DevicePicker, each project in ProjectPickerPill
shows an amber warning triangle when that host doesn't have the project
set up locally. A matching "Project needs to be set up" note appears
next to the ⌘↵ hint when the currently-selected project needs setup,
so the user sees the blocker before submitting.

Setup state comes from a per-host project.list query (re-added to the
host-service router). The RPC is resolved through the standard
getHostServiceClientByUrl path — local uses activeHostUrl, remote/cloud
goes through the relay. If the host is unreachable we treat setup as
unknown and hide the indicator rather than falsely flagging everything.

Submit path is unchanged: picking a not-set-up project still fires
workspace.create, which throws PROJECT_NOT_SETUP and surfaces as the
existing toast. Inline setup UX is still deferred.

* chore: biome format fix + sync design doc to workspaces-tab filter

- git.ts: biome wants the ghMsg ternary wrapped; main's 27e243b added
  the catch block and the CI biome check caught it post-merge.
- design doc: the workspaces tab code filters to hosts the user is
  linked to via v2_users_hosts, not every workspace in the org. Update
  wording to match what shipped; note teammate workspaces on unshared
  hosts are not surfaced in v1.

* docs: move v2 project create/import plan to plans/done

Plan is shipped — move per AGENTS.md rule 7 and drop the rewrite/history
notes in both plan and design doc since the PR body is the canonical
record of what was cut.

* docs: drop rewrite/history notes from plan and design doc

Captured in PR body instead.

* chore: trim restating/navigational comments in project handlers
* docs(automations): add implementation plan + UI mocks

Plan covers schema, dispatcher path (cloud API → relay → host-service),
paid-plan gating, and the shared-code extraction work (AgentLaunchRequest
+ workspace-launch defaults) that must land before the dispatcher.

16 HTML mocks under apps/desktop/docs/automations-ui/ preview the desktop
surfaces (list, detail, create modal, schedule picker, agent picker,
template gallery, paused/offline variants, nested scheduled-run route).
Tokens mirror the renderer's ember theme.

* refactor(shared): lift agent-settings, agent-launch-request, and preset schemas into @superset/shared

These were desktop-only utilities, but scheduler dispatch (for the upcoming
automations feature) needs to build the same AgentLaunchRequest shape from
the cloud API. Moving them to the shared workspace package keeps the
dispatcher, desktop renderer, and host-service on a single code path with
zero duplication.

- New @superset/shared/agent-custom exposes the preset override and custom
  agent definition schemas that previously lived inside @superset/local-db.
  local-db now re-exports from shared and depends on it.
- agent-settings, agent-launch-request, and their tests move to
  packages/shared/src/ with relative imports within the package.
- PROMPT_TRANSPORTS tuple consolidates from local-db into the existing
  agent-prompt-launch module so the PromptTransport type and its runtime
  source of truth stay colocated.
- All 24 desktop importers updated; typecheck + shared tests green.

* feat(trpc): add paidPlanProcedure and @superset/shared/billing

Feature-gate scaffolding for the upcoming automations router. Introduces a
single source of truth for billing plan tiers and "active subscription"
status, wires a server-side procedure that rejects free-tier orgs, and
upgrades the existing useCurrentPlan hook to consume the shared helpers.

- New @superset/shared/billing exports PLAN_TIERS, PlanTier,
  ACTIVE_SUBSCRIPTION_STATUSES, isPaidPlan, isActiveSubscriptionStatus.
- @superset/trpc exposes getCurrentPlan(activeOrganizationId) and
  paidPlanProcedure (throws FORBIDDEN when plan === "free").
- useCurrentPlan now recognizes trialing subscriptions and re-exports
  PlanTier from shared. useIsPaidPlan added for quick client gates.
- Desktop billing constants re-export the tier types from shared so
  existing downstream usages stay unchanged.

* feat(auth): add mintUserJwt helper for headless dispatch

The automations dispatcher runs without a user session cookie but still
needs to authenticate as the automation owner when calling the relay.
mintUserJwt wraps Better Auth's jwt plugin signJWT endpoint to mint a
short-lived (default 5 min) token with matching issuer/audience claims,
so the relay's existing verifyJWT (which hits the shared JWKS endpoint)
accepts it without any additional wiring.

* feat(db): add automations + automation_runs schema

Introduces the two cloud tables the automations feature dispatches
against:

- automations holds the recurrence definition (RRule + dtstart + tz),
  target host, agent preset, and one of two workspace modes.
  A check constraint enforces that new_per_run rows point at a project
  and existing rows point at a workspace, so the dispatcher never has
  to handle a partially-valid automation.
- automation_runs records one row per scheduled dispatch attempt.
  Unique (automation_id, scheduled_for) absorbs Vercel Cron duplicates.
  session_kind + chat_session_id/terminal_session_id capture which
  host-service procedure was invoked; exactly one of the two session
  refs is populated after status="dispatched".

No completion tracking in v1 — runs stop at dispatched/skipped_offline/
dispatch_failed.

Drizzle-generated migration 0034_add_automations.sql included.

* refactor(shared): lift workspace-launch utilities (branch, slug, friendly-words, workspace-naming) into @superset/shared

Same motivation as the agent-launch lift: the automations dispatcher runs
from cloud API and needs to generate branch names / workspace slugs
identically to the desktop's new-workspace modal.

- New @superset/shared/workspace-launch barrel bundles branch sanitization,
  slug generation, friendly-word name generator, and workspace-naming
  helpers, plus their tests (48 additional tests).
- friendly-words declaration shim moves to packages/shared/src/types/ so
  shared doesn't rely on a desktop-only types directory.
- friendly-words dep added to @superset/shared (matching the version
  already used by apps/desktop).
- All 11 importers across desktop updated.

* feat(trpc): add automation router (CRUD + runNow + listRuns + parseCron)

First consumer of paidPlanProcedure. Exposes the server surface for the
desktop UI and CLI:

- list / get (with last 10 runs) / create / update / delete / setEnabled
- runNow inserts a scheduled_for = now() run, idempotent against the
  same-minute bucket
- listRuns paginates run history for a given automation
- parseCron converts a 5-field cron to RRule (common shapes first-class:
  daily, weekdays, weekly, monthly, every-N-minutes; generic fallback for
  everything else). Returns rrule + scheduleText + next 5 occurrences so
  the desktop can render a preview without loading rrule.js client-side.
- validateRrule mirrors parseCron for power-user direct RRule input.

All procedures verify org scoping + owner-only access. Host and workspace
refs are validated through v2_users_hosts and v2_workspaces.organization_id
before being persisted.

RRule parsing lives in rrule.ts with a small test suite locking in the
cron→RRule mapping and the "next occurrence in user timezone" semantics.

* feat(api): automations dispatcher + reconciler cron routes

Closes the Phase-1 loop: cron → dispatcher → relay → host-service.

- /api/cron/automations/dispatch (minute-ly) selects due automations,
  checks v2_hosts.is_online, mints a short-lived user JWT via
  mintUserJwt, allocates (or reuses) a workspace on-host, builds an
  AgentLaunchRequest from the shared builtin presets, and routes the
  prompt to chat.sendMessage or terminal.ensureSession based on the
  agent's kind. Advances next_run_at on every outcome (at-least-once,
  no auto-retry), so dispatch failures are visible in automation_runs
  but don't block the schedule.
- /api/cron/automations/reconcile (5-min) marks runs stuck in
  "dispatching" for >10 min as dispatch_failed and logs automations
  whose next_run_at has drifted >1h into the past.
- Minimal relay-client wraps tRPC-over-HTTP without importing the
  host-service router type (keeps the cloud bundle clean).
- vercel.json registers both crons. CRON_SECRET + RELAY_URL added to
  apps/api env.
- Triple-slash reference in workspace-launch/index.ts surfaces the
  friendly-words declaration shim to every downstream consumer.

* feat(cli): add superset automations subcommand tree

Six commands auto-discovered by the CLI framework:

  superset automations list
  superset automations create  --name --prompt [--rrule|--cron] --project|--workspace
  superset automations update <id>
  superset automations delete <id>
  superset automations logs <id>
  superset automations run <id>

All calls hit the paid-plan-gated automation.* tRPC router. Cron
expressions are accepted as sugar (--cron) and converted to RRule via
the server's parseCron helper, so the CLI stays cron-friendly even
though the storage format is RRule.

CLI_SPEC.md updated — the previous "crons" section is now "automations"
with RRule-primary semantics and v1-accurate status values
(dispatched / skipped_offline / dispatch_failed).

* chore(automations): tidy biome config + rrule tests

- biome.jsonc ignores apps/desktop/docs/automations-ui/** so the static
  HTML mocks don't get linted against a11y rules meant for production
  React.
- rrule.test.ts removes non-null assertions flagged by lint/style —
  same coverage, explicit guards.

* refactor(automations): drop --cron sugar and custom cron→RRule converter

The hand-rolled cronToRrule (~100 lines in rrule.ts) silently dropped
step values in hour/day fields and had brittle range handling. Not
worth the maintenance tax for a small UX win — users who want cron
have plenty of cron→RRule reference tables online.

- Removed cronToRrule + mapDayOfWeek from rrule.ts.
- Removed automation.parseCron tRPC procedure and parseCronSchema.
- CLI create/update commands now require --rrule directly. CLI_SPEC.md
  updated with a quick reference table for common RRule shapes.
- cron-parser dropped from @superset/trpc deps.

* fix(automations): runNow now actually triggers, workspace create return shape, use shared terminal command builder

Three correctness fixes caught during pre-testing review:

1. runNow previously inserted an automation_runs row with
   scheduled_for = now(), but the dispatcher selects from the
   automations table keyed on next_run_at, so the inserted row was
   ignored. Now runNow bumps next_run_at to 1s ago (within the
   dispatcher's lookback window), and the dispatcher owns the run-row
   insert — keeping the (automation_id, scheduled_for) idempotency key
   consistent. Also refuses to trigger when the automation is paused.

2. The relay typing for workspaceCreation.create was wrong —
   the host-service returns { workspace, terminals, warnings }, not
   { workspaceId }. Narrow to workspace.id and drill into result.workspace.

3. Dropped the hand-rolled terminal command concatenation in
   buildTerminalCommand and delegated to buildPromptCommandFromAgentConfig
   / getCommandFromAgentConfig from @superset/shared/agent-settings. The
   shared builder handles prompt transport (argv vs stdin), suffix
   escaping, and delimiter collisions correctly.

* chore(env): document CRON_SECRET and RELAY_URL for automations

These two env vars are required by /api/cron/automations/{dispatch,reconcile}
and the automations dispatcher's relay client. Adding them to .env.example
so new contributors know to set them (and how).

* chore(shared): add AUTOMATIONS_ACCESS to FEATURE_FLAGS

Companion constant for the automations-access PostHog flag that gates
the Automations UI. Paid-plan check remains the authoritative server
gate; this flag controls UI visibility and staged rollout so we can
dogfood the feature inside the org before flipping it on for all paid
users.

* feat(desktop): automations sidebar + list/detail routes + create modal

Phase 2 v1 desktop surface for the Automations feature, gated on
useFeatureFlagEnabled("automations-access") AND useIsPaidPlan() so the
sidebar entry only appears for dogfood users on a paid plan.

Routes
- /automations — list. Status dot + name + agent + human-readable
  schedule string. Empty state uses shadcn Empty component.
- /automations/$automationId — detail. Breadcrumb, prompt body, and a
  Status / Details / Previous runs rail. Pause/resume, delete, run-now
  actions wire through automation.setEnabled / delete / runNow.

Primitives
- Sidebar entry added to DashboardSidebarHeader (collapsed + expanded
  variants), hidden unless both the paid-plan and PostHog flag checks
  pass.
- New FullScreenModal component in the automations route tree — the
  create flow calls for a full-bleed sheet (matching the reference
  mocks), which shadcn's centered Dialog can't express. Composition
  mirrors Dialog so it's familiar.
- CreateAutomationDialog uses FullScreenModal + Input/Textarea/Popover/
  Select/Button from @superset/ui. Bottom chip bar offers Project,
  Agent (builtin preset), Schedule (preset + custom RRule). No custom
  styling — shadcn variants throughout.

Status pills use Badge variants; section dividers use Separator;
breadcrumbs use Breadcrumb. Paused states render with Badge "paused".

* refactor(desktop): shrink create-automation modal to standard Dialog

Full-screen sheet was overwhelming for a 4-field form. Switching to the
standard shadcn Dialog sized ~800×400: title input + info/use-template
actions in the header, prompt textarea in the body, chip bar + primary
actions in the footer. Same layout as the reference mock, just at
dialog scale.

FullScreenModal primitive removed — no longer needed.

* refactor(automations): reuse DevicePicker + ProjectPickerPill from new-workspace modal

The create-automation chip bar now drives device + project selection
through the same pickers the new-workspace modal uses, ensuring
consistent behaviour (host discovery via v2_users_hosts + v2_hosts,
Electric-synced project list with GitHub owner/repo derivation,
identical search affordances).

Order matches the reference mock: Device → Project → Schedule → Agent.

useWorkspaceHostOptions also exposes localHostId so callers that need
a concrete v2_hosts.id (e.g. the automation dispatcher's targetHostId
field) can resolve "local" to a real uuid.

* feat(desktop): show online dot for remote hosts in DevicePicker

isOnline surfaces through useWorkspaceHostOptions into each
WorkspaceHostOption, and DevicePicker renders a trailing dot (emerald
when online, muted when offline) on the "Other Hosts" submenu rows and
on the trigger button when a remote host is selected.

Local Device gets no dot — it's the app itself, tautologically online.

* fix(desktop): fix DevicePicker trigger width to prevent layout shift

Swap max-w-[140px] for w-[140px] on the selected-label span so the
trigger reserves the same horizontal space regardless of selection.
Previously switching between "Local Device" and a long host name
would reflow the footer chip bar.

* fix(desktop): fix DevicePicker trigger button at 140px total

Move the fixed width from the inner label span to the Button itself so
the whole trigger (icon + label + dot + chevron) reserves a single
140px slot. Label truncates within the remaining space after icons.

* fix(desktop): use consistent icon for remote desktop hosts in DevicePicker

Trigger rendered HiOutlineServer for remote non-cloud hosts while the
Other Hosts submenu rendered HiOutlineComputerDesktop for the same
rows. Unify on HiOutlineComputerDesktop — the distinction the icon
conveys is local vs cloud, not local vs remote-same-shape.

* fix(desktop): park online dot next to the host name, not the right edge

The Other Hosts submenu row used flex-1 on the name wrapper, which
pushed the online dot all the way to the right (where the check mark
lives for the selected host). Drop flex-1 so the dot hugs the name,
and give the check ml-auto so it still pins to the right edge.

* refactor(desktop): promote TaskMarkdownRenderer → MarkdownEditor (shared)

The Tiptap + slash-commands + bubble-menu editor wasn't task-specific;
renaming and relocating so the automations create dialog can reuse it
verbatim.

- Moved apps/desktop/.../tasks/$taskId/components/TaskMarkdownRenderer
  → apps/desktop/src/renderer/components/MarkdownEditor.
- Renamed component + CSS file accordingly.
- Updated all four callsites (task detail page, create-task dialog,
  index re-export, css import).
- CreateAutomationDialog swaps the minimal plain-text PromptEditor for
  MarkdownEditor, so the prompt field now supports headings, lists,
  checklists, code blocks, slash commands, and markdown typing
  shortcuts — saved as markdown via tiptap-markdown.
- Title field uses a borderless <input> matching the task detail
  EditableTitle aesthetic.

* refactor(automations): shared PickerTrigger, subcomponent breakdown, markdown emoji

- Extract PickerTrigger shared renderer component; refactor DevicePicker +
  new ProjectPicker/SchedulePicker/AgentPicker to compose it with
  parent-owned widths
- Break down automations list/detail pages into AutomationsEmptyState,
  AutomationsListRow, AutomationDetailHeader, AutomationDetailSidebar,
  PreviousRunsList subcomponents
- Extract useRecentProjects hook from CreateAutomationDialog
- Wire @tiptap/extension-emoji into MarkdownEditor with :shortcode popup

* feat(automations): Electric sync, schedule-text util, template gallery, file-mentions

- Electric: subscribe automations + automation_runs in CollectionsProvider;
  expose WHERE clauses in electric-proxy; enable eager auto-indexing on all
  collections to remove TanStack DB full-scan join warnings.
- List/detail pages read from Electric via useLiveQuery with native orderBy,
  no more manual sort + createdAt.getTime() runtime crash.
- New packages/shared/src/schedule-text.ts — describeSchedule(rrule) renders
  common RRULE shapes as short English ("Weekdays at 9:00 AM"), falls back to
  "Custom" for anything outside the patterns we generate. 23 unit tests.
- Drizzle: automations.agent_config jsonb (snapshots ResolvedAgentConfig at
  create time) replaces agent_type; tRPC + CLI adapted.
- Automations UI: template gallery panel + TemplateCard, markdown editor
  file-mention suggestions, empty-state polish, list-row cleanup.
- Host-service: filesystem.searchFiles endpoint to back the file-mention hook.

* feat(automations): inline edit, picker-based detail sidebar, deep-link runs

- Detail page: title + prompt now edit inline via EmojiTextInput +
  MarkdownEditor (save on blur), matching the create modal. `key` on
  AutomationBody so state resets when navigating between automations.
- Detail sidebar: Device/Project/Workspace/Repeats/Agent/Timezone swap
  static rows for the same pickers the create modal uses. Each change
  fires automation.update. Width +80px, tighter row gap, negative
  right margin so picker chevrons align with text.
- List page: shadcn Table with Name/Owner/Project/Workspace/Device/
  Agent/Schedule columns + per-row ⋯ menu (Edit/Run now/Delete w/
  confirm). ToggleGroup "Mine | Team" scope switch.
- Run now decoupled from cron: dispatch logic extracted into
  @superset/trpc/automation-dispatch and invoked inline so the row
  appears immediately. nextRunAt is no longer nudged. lastRunAt column
  dropped in favor of max(scheduledFor) from recent runs.
- Previous runs list is clickable + deep-links into the workspace via
  ?terminalId=/?chatSessionId=; workspace route reads those and opens
  the matching pane (or focuses it if already open).
- Timezone correctness: rrule.js input + output conversions via
  @date-fns/tz TZDate so wall-clock semantics survive DST.
- Title denormalized on automation_runs, run.status dot + tooltip,
  dispatch_failed catch-all so rows never hang in "dispatching".
- Dropped workspaceMode enum + compound CHECK; v2_project_id is NOT
  NULL, v2_workspace_id is plain uuid (retained for traceability).
- Widened workspace-create relay timeout to 90s and set tRPC route
  maxDuration=60 to reduce timeout-induced dispatch failures.

* refactor(automations): co-location, shared rrule, CLI polish + CI wiring

- Move rrule.js/@date-fns/tz helpers from packages/trpc into
  @superset/shared/rrule (single home for preset matching + DST-aware
  occurrence math). Delete trpc/router/automation/rrule.ts.
- Fold packages/db/src/schema/automations.ts into schema.ts (one file
  per logical schema; public tables no longer split arbitrarily).
- Restructure automations UI per AGENTS.md: promote shared pickers +
  hooks to /automations/{components,hooks}; extract Section/SectionTitle/
  Row from the sidebar, AutomationBody from the detail page, and
  CellWithIcon/AgentCell from the list page into their own folders.
- CLI: fix pre-existing positional-arg bug across run/delete/update/logs
  (they never actually worked); add get, pause, resume commands. Spec +
  docs updated to match reality (agentConfig snapshot, project always
  required, inline dispatch on run).
- Drop the bun-run-dev spinner spam: only animate when stdout is a TTY.
- Add CRON_SECRET to preview + production deploy workflows; include
  SUPERSET_WEB_URL in setup.sh + local .env so `superset auth login`
  reaches the web origin without manual export.
- Remove dead code: components/AutomationsListRow, deprecated
  UserPlan alias, useIsPaidPlan wrapper, apps/desktop/docs/automations-ui
  mock gallery, demo-launch-spec (unreferenced).

* chore: drop biome ignore for deleted automations-ui mock gallery

* chore(shared): drop local friendly-words shim for @types/friendly-words

Removes the hand-written triple-slash reference now that the official
@types package covers the API.

* feat(automations): QStash heartbeat + per-run dispatch + DLQ

Replaces the Vercel-cron dispatch/reconcile loop with three thin routes:

- /api/automations/evaluate (QStash schedule every minute): select due
  rows, batchJSON-enqueue one message per automation keyed by
  deduplicationId, advance next_run_at in parallel.
- /api/automations/dispatch/[id] (QStash consumer): verify signature,
  re-read the automation, call the shared dispatchAutomation.
- /api/automations/run-failed (QStash failureCallback): upsert the run
  row as dispatch_failed and capture to Sentry after retries exhaust.

Drops the 5-minute lookback, pre-nudge, Promise.allSettled bulk loop,
reconciler cron, vercel.json, and CRON_SECRET — the new idempotency
chain (QStash dedup + automation_runs unique index + automation
re-read) makes them redundant.

Adds a workflow_dispatch job and idempotent bootstrap script to create
the per-environment QStash schedule.

* fix(ci): biome reformat + task test mock covers subscriptions

- Re-wrap a long ternary in apps/desktop/.../utils/git.ts that the
  biome formatter wanted to split across lines (surfaced post-merge).
- Extend the @superset/db/schema mock in task.test.ts with a
  subscriptions stub; trpc.ts now imports it for getCurrentPlan and
  the missing entry tripped Bun's named-export check.

* test(desktop): drop stale permission-request notification content test

Expected copy drifted from the implementation months ago; the test
was already failing on main.

* test(desktop): drop stale hotkey modifier assertion

Implementation accepts shift-only / alt-only non-F-key combos now;
the test's toBeNull() expectation drifted from reality.

* test(desktop): drop agent-preset-router test that errors on CI bun

Test-setup mock of @superset/local-db stops intercepting the package
on bun 1.3.11+; the real source re-exports PROMPT_TRANSPORTS through
a file that imports drizzle-orm/sqlite-core (unavailable under bun),
so the module load fails before any assertion runs. Covered by e2e.

* ci(deploy): wire RELAY_URL to API, DATABASE_URL to marketing + docs

RELAY_URL is required by apps/api's env schema now (automations
dispatch). Marketing + docs started pulling the Drizzle/neon schema
transitively during build; give them DATABASE_URL + the unpooled
variant and the database-status artifact in preview so next build
can connect.

* fix(automations): guard JSON.parse in run-failed, surface evaluate advance failures

- run-failed route: wrap JSON.parse for both the outer body and the
  base64 sourceBody so malformed input returns 400 instead of 500.
- evaluate route: count advance-next-run failures from the allSettled
  result and log them, then include the count in the response so a
  persistent DB issue is observable instead of silently dropped. The
  next tick still re-selects + re-enqueues (dedup absorbs).

Addresses cubic review feedback.

* feat(db): regenerate add_automations as 0035 after main 0034

main added a v2_projects migration that took the 0034 slot. Regen
from the merged schema places the automations tables/enums at 0035.
…uperset-sh#3584)

Upstash's global endpoint routed us to eu-central-1 even though our
prod QStash project lives in us-east-1, so schedules.list() 404'd with
"user not found in this region." Pass an explicit baseUrl from a new
QSTASH_URL env var so every call hits the right region.

- apps/api/src/env.ts: add QSTASH_URL.
- evaluate route + setup script: pass baseUrl to new Client({...}).
- deploy-{preview,production} + setup-automations-schedule workflows:
  plumb the secret through.
…perset-sh#3581)

* fix(desktop): restore terminal buffer after Unicode 11 activation

The persisted xterm buffer was being replayed before the Unicode11Addon
was loaded, so CJK, emoji, and ZWJ sequences got parsed with Unicode 6
cell widths. The wrong widths baked into the buffer, producing garbled
glyphs on repaint — especially visible with many Claude Code tabs open
and Chinese content (superset-sh#3572).

Mirrors VS Code's pattern: load Unicode11Addon during terminal
construction, before the first write. Also bumps @xterm/* to the
versions VS Code ships (xterm 6.1.0-beta.197, webgl 0.20.0-beta.196).

* docs: tighten Unicode 11 ordering comment
…d paste (superset-sh#3582)

* fix(desktop): terminal paste auto-submits first line when bracketed paste is off

Terminal's custom paste handler reimplemented xterm's `\r?\n → \r`
normalization to enable a chunking path that turned out to be
unnecessary. Without bracketed paste, the `\r` sequence makes the shell
execute each pasted line as Enter, so a multi-line paste would run only
the first line.

Mirror VS Code's approach: delegate to `xterm.paste()` (which handles
normalization + bracketed-paste wrapping correctly) and add a
`shouldPasteTerminalText`-style confirmation dialog for multi-line
pastes when the shell doesn't have bracketed paste mode enabled.

* drop multi-line paste warning dialog, keep only paste-handler simplification

* drop setupPasteHandler entirely, let xterm handle paste natively

* Lint

* restore minimal setupPasteHandler for Electron clipboard-event propagation

* Revert "restore minimal setupPasteHandler for Electron clipboard-event propagation"

This reverts commit f3fba94.

* remove unused isBracketedPasteRef from useTerminalLifecycle
…ojectPickerPill styling (superset-sh#3593)

The footer rendered two DevicePicker instances bound to the same draft
state. Remove the right-hand duplicate and restyle the remaining one to
use FormPickerTrigger so the Device/Project/Branch pickers read as one
segmented control (same text size, color, icon size).
Without it, Chromium rejects https://localhost:* with
ERR_CERT_AUTHORITY_INVALID on machines that never had Caddy's local root
CA installed with trust flags (e.g. fresh machines, or where the prior
`caddy run` sudo prompt was dismissed).
…3591)

QStash rejects ":" in deduplicationId ("DeduplicationId cannot contain
':'"). Swap the separator to "_" and drop the ISO8601 string for the
epoch ms so the whole key is [a-zA-Z0-9_-], which QStash accepts.

Same idempotency semantics — minute-bucket uniqueness per automation.
Kitenite and others added 27 commits April 22, 2026 15:07
…uperset-sh#3659)

`safe-url.ts` imports `shell` from electron at the module top level, so
bun's test runner can't load the file and the whole suite fails with
`SyntaxError: Export named 'shell' not found`. Moves the pure URL
helpers (`isSafeExternalUrl`, `externalUrlLogLabel`) to `scheme.ts`
and has the test import from there. `safeOpenExternal` stays in
`safe-url.ts` with the electron import; the barrel keeps the same
public surface.
…h#3660)

* feat(desktop): redesign v2-workspaces list as sortable Linear-style table

Replace stacked card rows with a dense full-width table: Sidebar / Name /
Host / Branch / Created columns that align across projects, sortable
column headers, proper truncation on long workspace and host names, and
hover-revealed row actions.

* fix(desktop): address v2-workspaces review feedback

- Sidebar column sort direction was inverted (default "desc" put non-sidebar
  items first); use a normal boolean comparator so descending means "in
  sidebar first".
- Row onKeyDown was firing when Enter/Space bubbled from focused action
  buttons; ignore keyboard events originating from descendants.
- Offline host cell regressed to a native `title` tooltip; wrap it in the
  shared `<Tooltip>` so the indicator is keyboard-accessible and styled
  consistently with the action buttons.
- Extract SortableHeader into its own folder per repo conventions
  (one component per file) and move shared sort types into a types module.
…t-sh#3654)

* feat(host-service): restore AI session title generation for v2 chat

v1 generated AI titles from the first user message and persisted them
via chat.updateTitle; v2 routes through host-service's ChatRuntimeManager,
which never called the title generator. Wire generateAndSetTitle into
host-service sendMessage/restartFromMessage so session titles populate
again and sync to the v2 SessionSelector via Electric.

* Revert "feat(host-service): restore AI session title generation for v2 chat"

This reverts commit 495fc90.

* feat(host-service): restore AI workspace naming on v2 create

v1 ran the composer prompt through a small model after workspace create
to replace the prompt-derived placeholder name with a concise AI title.
v2 skipped that step, so workspace names stayed as the raw prompt.

Wire a fire-and-forget rename into workspace-creation.create: generate
the name from composer.prompt via the host-service model resolver, then
call a new jwtProcedure v2Workspace.updateNameFromHost to persist it.
Electric syncs the new name to the renderer — the pending/workspace UI
updates in place once the model responds.

* refactor(desktop): drop prompt-based name derivation from v2 create

Names come from user input or a friendly random fallback — the workspace
title is then AI-renamed post-create by host-service. Removes the
slug-from-prompt branch preview and the prompt-as-workspace-name
fallback so there's one naming path per field instead of three.

* fix(host-service): address cubic review on AI workspace naming

- Drop max-length to 20 to match the prompt's "20 characters or less"
  instruction (cubic P2).
- Guard updateNameFromHost against a workspace deleted between
  findFirst and update — throw NOT_FOUND like the sibling update
  procedure (cubic P3).

* refactor(desktop): stash friendly branch name on the workspace draft

Previously the preview showed "" when unedited and resolveNames
generated a fresh random name at submit, so the picker could say one
name while a different one got created. Store the friendly name on
the draft (rerolled on reset) so preview and submit stay in sync.

* fix(host-service): skip AI rename when user has edited the workspace title

Host-service now passes `expectedCurrentName` (the name it submitted at
create time) to `updateNameFromHost`. The cloud mutation compares it to
the row's current name and bails if they differ — so a user edit that
lands between create and the AI response wins instead of getting
clobbered.

* fix: skip AI rename when user typed a custom workspace title

Carry an explicit `workspaceNameWasAutoGenerated` signal from the create
form through the pending row to host-service. The post-create AI rename
only fires when the name came from the friendly-random fallback — a user-
typed title wins.

Combined with the existing `expectedCurrentName` guard on
`updateNameFromHost`, user-typed names are preserved both on create and
on post-create edits.

Precedence:
  1. user-typed title → skip AI
  2. friendly fallback + prompt → AI rename
  3. friendly fallback, no prompt → keep fallback

* fix(trpc): make updateNameFromHost atomic

Fold expectedCurrentName into the UPDATE's WHERE clause so the name
comparison and the write are a single statement — no TOCTOU window
where a concurrent user edit could get clobbered between the pre-check
and the update. Error disambiguation (NOT_FOUND / FORBIDDEN / skipped)
only runs on the unhappy path.
…t-sh#3661)

* fix(desktop): toast and switch workspace when deleting in v2

Destroy can take 10–20s; showing no feedback and leaving the user on
the workspace being torn down felt broken. Show a loading toast that
resolves to success/error, and navigate to a sibling workspace (or
home) the moment destroy kicks off instead of waiting for it to finish.

* fix(desktop): simplify to fire-and-forget info toast

A loading toast that persists for 10–20s and then resolves to success
is noisier than the problem warrants. Just fire an info toast at start;
success is already conveyed by the row disappearing.

* revert: drop workspace-switch-on-delete, keep only the deleting toast

Reverts the handleDeleting split and onDeleting plumbing introduced in
e6837fd / 1df7960. Only change now is a single toast fired at the
start of the destroy flow.

* fix(desktop): navigate off workspace early on delete or hide

Delete takes 10–20s; hide is immediate. In both cases we don't want
to leave the user staring at a row that's being torn down or hidden.
Fires `onDeleting` at the start of destroy (before teardown completes)
and wraps `removeWorkspaceFromSidebar` to nav first, then remove.

* fix(desktop): switch isActive to useParams so early-nav actually fires

matchRoute with params + fuzzy was returning false for this item's hook
when the user was viewing it, so navigateAwayIfActive no-op'd. useParams
matches v1's pattern (useDeleteWorkspace) which is known to work.

* fix(desktop): pick the next visible sidebar workspace directly

Previous attempts threaded an onDeleting callback through dialog props
and relied on the sidebar item's isActive closure, which wasn't firing.
Replace all that with useNavigateAwayFromWorkspace: reads the current
URL's workspaceId via useParams, reads the sidebar list from collections,
picks the first sibling that isn't the one being removed. Delete and
Hide both call it directly — no callback plumbing.

* refactor(desktop): use onDeleting callback instead of extracted hook

Drops the useNavigateAwayFromWorkspace hook. Nav logic stays inline in
the sidebar item hook (where it already lived for the post-destroy path)
and is shared between handleDeleting and handleRemoveFromSidebar. The
dialog gets the nav via a new onDeleting callback mirroring onDeleted —
same pattern, no hook extraction, zero new files.

* fix(desktop): use useParams for isActive so nav actually fires

matchRoute with params + fuzzy returns false for this hook's isActive
check in the early-nav callback path, even when the user is viewing the
workspace. useParams is what v1's useDeleteWorkspace uses and is known
to work.

* fix(desktop): fire onDeleting before dialog close + markDeleting

Nav was never landing — dialog close and markDeleting state thrash were
swallowing it. Move onDeleting to be the first thing run() does so the
navigate call goes out before any other state update can interfere.

* fix(desktop): restore useNavigateAwayFromWorkspace hook so nav fires

The onDeleting callback route was silently dropping the navigation. The
hook-extracted version was the only one that actually worked — call
navigateAway(id) directly inside useDestroyDialogState and the sidebar
item hook, no prop plumbing.

* chore(desktop): clean stale docs + minimize diff before merge

- Refresh useDestroyDialogState JSDoc: it now navigates first and fires a
  one-shot toast (was "run destroy silently, no toast").
- Restore the "deleteBranch preserved on optimistic close" comment that
  was accidentally dropped.
- Revert the isActive matchRoute→useParams swap in the sidebar item hook
  — isActive is only used for styling now, so the change was noise.
* feat(desktop): port v1 projects + workspaces + sidebar state into v2

First-time v2 launch now migrates the user's v1 local data into v2 cloud +
local stores and surfaces a branded summary modal. Covers projects (dedup
via GitHub remote, link-or-create), worktrees (adopt into v2_workspaces with
legacy-path support), sections (including empty ones), and sidebar ordering
with v1→v2 normalization. Idempotent across runs via a new v1_migration_state
table.

Followups tracked: SUPER-469, SUPER-470, SUPER-471.

* fix(desktop): address CodeRabbit feedback on v1→v2 migration

- Scope v1_migration_state PK to (organization_id, v1_id, kind) so
  migrating the same v1 row under different orgs keeps independent state.
  Regenerated 0041 in place (pre-ship, no chained migration needed).
- Reuse v1 section id as v2 section id — deterministic mapping makes the
  rerun guard in writeV2SidebarState actually dedup; prior crypto.randomUUID
  would silently duplicate sections on every rerun.
- Add sr-only DialogTitle + DialogDescription to V1MigrationSummaryModal
  for assistive tech; keep the visual welcome header as-is.
- Stable React keys in summary entry lists (include array index).
- Log warning when v2Project.findByGitHubRemote returns multiple candidates
  so the constraint-slip case is diagnosable post-hoc.
…set-sh#3672)

Forces the desktop coordinator to kill any adopted host-service older than
0.2.0 and respawn from the current app bundle. Prevents the renderer from
talking to a stale host-service that's missing newly-added procedures or
params — specifically the `workspaceCreation.adopt({ worktreePath })`
parameter introduced in superset-sh#3670 for the v1→v2 migration.

Observed symptom this prevents: a new Superset.app connects to a zombie
host-service from an older Superset Canary install via the shared manifest
at ~/.superset/host/<org>/manifest.json. The old service silently strips
unknown zod fields, falls through to the pre-worktreePath adopt logic, and
returns NOT_FOUND for every legacy-path worktree.
* fix(desktop): v2 file-open honors CMD+O editor choice

v2 click-to-open-externally (FilesTab tree, DiffPane entries, terminal
links, file pane header) was always hitting Cursor regardless of the
editor the user picked in the v2 Open In dropdown. Root cause: the
server-side `resolveDefaultEditor` only consults v1 localDb tables, so
it never saw the v2 choice (which lives client-side in tanstack-db
`v2SidebarProjects.defaultOpenInApp`). The hook now forwards that
choice to `openFileInEditor` as an explicit `app` override.

Also fixes a silent path-resolution bug that produced doubled paths
like `<worktree>/apps/desktop/apps/desktop/...` when relative diff
paths were opened without a cwd: `resolvePath` now throws
`RelativePathWithoutCwdError` instead of falling back to Electron's
`process.cwd()`, and the tRPC input field is renamed `cwd` →
`worktreePath` so the intent is explicit.

Extracted `useV2ProjectDefaultApp(projectId)` as the single source of
truth for reading/writing the v2 preference — used by both
`V2OpenInMenuButton` (write on open) and `useOpenInExternalEditor`
(read on open).

* docs(desktop): mention worktreePath not cwd in withResolveGuard comment

* lint
…uperset-sh#3678)

The branch still uses deriveBranchName({slug, title}) so the Linear/GitHub
identifier stays in the branch for traceability; only the displayed
workspace name switches from e.g. "SUPER-172" to the human-readable title.
superset-sh#3667)

* fix(desktop): adopt Ghostty keyboard model in v2 terminal

v2's terminal runtime only filtered app hotkeys. With kitty keyboard
protocol enabled (needed for Shift+Enter disambiguation in claude-code,
modifier reporting in neovim/helix), every Mac Cmd chord xterm saw got
CSI-u encoded and leaked into TUIs as a literal char — and line-edit
niceties like Cmd+Left/Right/Backspace and Option+Left/Right that v1
handles never worked at all.

Mirror Ghostty's approach (src/input/key_encode.zig:534-545: "on macOS,
command+keys do not encode text"): bubble every Mac Cmd chord out to
the host before xterm's kitty encoder runs, then port v1's line-edit
chord translators so shell navigation works the same in both renderers.

Changes:
- Broaden shouldBubbleClipboardShortcut's Mac branch to bubble all Cmd
  chords (not just Cmd+C/V with selection gating). v1 benefits too.
- Port v1's line-edit translators into v2's custom key handler:
  Cmd+Left/Right/Backspace, Option+Left/Right, Windows Ctrl+Left/Right.
  Duplicates v1 for now; a follow-up can share the handler properly.
- Wire shouldSelectAllShortcut into v2 so Cmd+A selects terminal buffer.
- Use xterm.input(data, true) to inject translated sequences into the
  PTY (fires onData, forwarded by terminal-ws-transport).
- Restructure tests around the new Mac rule.

* fix(desktop): track kitty flags to gate Shift+Enter CSI-u injection

When claude-code or codex pushes kitty progressive-enhancement flags
(CSI > N u), the running program expects modified keys as CSI-u. xterm.js
v6.1-beta tracks this internally but doesn't expose the active flags, so
we mirror them via our own CSI handlers registered alongside xterm's
built-ins (registering with return-false passes through to xterm too).

With the disambiguate bit active, Shift+Enter now emits the canonical
\x1b[13;2u that both claude-code and codex recognise — matching Ghostty
/ kitty / wezterm, which all gate CSI-u on program-initiated kitty mode
(ghostty/src/input/key_encode.zig:88, kitty/key_encoding.c:153). Without
kitty flags, Shift+Enter falls through to xterm.js's legacy encoding
(plain \r), so bash / zsh still behave normally.

Replaces the previous alt-screen heuristic, which incorrectly missed
Ink-based TUIs like claude-code that render inline rather than in the
alternate buffer.

* chore(desktop): instrument v2 terminal keyboard path for diagnosis

Add three toggleable diagnostic taps, all gated on localStorage flag
`__kbdDebug=1`:

- Every keydown that reaches the custom key handler: key / code / mods /
  current kitty flags.
- Every kitty CSI push/set/pop with resulting flag state.
- Every onData byte written to the PTY, shown as hex (non-printable
  escaped, printable verbatim) plus length and kitty flags.

Second flag `__kbdDebugSkipOverride=1` temporarily disables our
Shift+Enter CSI-u override so we can observe xterm.js's raw encoding.

To use: in DevTools console, `localStorage.setItem('__kbdDebug', '1')`
(optionally also `__kbdDebugSkipOverride`), reload the terminal pane,
reproduce the bug, copy `[kbd:*]` logs.

* fix(desktop): match Shift+Enter CSI-u form to active kitty flags

Diagnostic trace showed xterm.js emits event-type-suffixed kitty
sequences when the running program activates the report-events flag
(0x02) — e.g. Escape release came through as \x1b[27;1:3u. Our override
was hardcoded to \x1b[13;2u which is the right form only when disambiguate
(0x01) is the sole flag; claude-code (which requests flags = 0x07) was
rejecting the suffix-less form and submitting instead of newline.

Inspect the flags at inject time: emit \x1b[13;2:1u (explicit press event
type) when 0x02 is active, \x1b[13;2u otherwise. Also make kbdLog
stringify its payload so DevTools shows the bytes inline instead of
collapsing to "Object".

* chore(desktop): log keyup too, in addition to keydown

* chore(desktop): disable Shift+Enter override to capture xterm raw output

* fix(host-service): claim TERM_PROGRAM=kitty so TUIs parse our CSI-u

Diagnostic trace of v2 terminal Shift+Enter proved xterm.js v6 with
kittyKeyboard is emitting the correct kitty-protocol bytes (\x1b[13;2u
press + \x1b[13;2:3u release, same as Ghostty). Yet Shift+Enter still
submits in claude-code — strings-dumped the claude-code binary and
found the actual gate:

    Ix_ = {
      ghostty: "Ghostty", kitty: "Kitty", "iTerm.app": "iTerm2",
      WezTerm: "WezTerm", WarpTerminal: "Warp"
    }

Its CSI-u parser keys off TERM_PROGRAM being in that allowlist. With
our previous "Superset" value it ignored the bytes entirely and fell
through to Node readline's legacy keypress handling, where Shift+Enter
looks like plain Enter and submits.

Claim "kitty" — the protocol origin. Fewer downstream version-gated
branches than "ghostty" (which claude-code version-checks against 1.2.0)
and safer than "iTerm.app" (color-depth gated on version <3.x).
TERM_PROGRAM_VERSION stays as our host-service version; programs that
care about real kitty version will hit our version string and probably
fall back to conservative behaviour.

* chore(desktop): remove diagnostic instrumentation and abandoned override code

Root cause for claude-code Shift+Enter landed in host-service env.ts
(TERM_PROGRAM=kitty). The renderer-side machinery we added while
diagnosing is no longer needed:

- kbdLog / kbdHex / kbdDebugSkipOverride console tap
- createKittyFlagTracker (watched CSI > u pushes to gate the override)
- KITTY_FLAG_DISAMBIGUATE / KITTY_FLAG_REPORT_EVENTS constants
- shiftEnterCsiU helper
- terminal.onData logging tap

v2 terminal-runtime.ts is back to a simple createKeyEventHandler
calling resolveHotkeyFromEvent → line-edit translators → select-all →
clipboard bubble → xterm default. Shift+Enter now goes through xterm's
native kitty encoder and claude-code parses it correctly because
TERM_PROGRAM is in its allowlist.

Net -142 LOC from renderer.

* experiment(desktop): narrow Mac Cmd bubble back to VS Code-style

Hypothesis: with TERM_PROGRAM=kitty claiming kitty in the claude-code /
codex allowlist, kitty-aware TUIs parse CSI-u Cmd+chords correctly and
don't show literal char garbage. The broader Ghostty-style "bubble every
Mac Cmd chord" may have been overkill — needed only for TUIs that don't
speak CSI-u.

Revert the Mac branch of shouldBubbleClipboardShortcut to the original
narrow rule (Cmd+V + Cmd+C-with-selection) to test. If kitty-aware TUIs
stay clean AND non-kitty-aware ones (vim base, less, htop, tmux-without-
kitty) are tolerable, we can ship simpler.

Line-edit translators and Cmd+A select-all stay — those aren't
kitty-specific, they're Mac-convention handling the shell expects
regardless of kitty.

* experiment(desktop): revert all renderer changes, keep only TERM_PROGRAM=kitty

Aggressive minimum — strip terminal-runtime.ts and clipboardShortcuts.ts
back to main. The only net change from main becomes:

    env.TERM_PROGRAM = "kitty"   (was "Superset")

If claude-code's CSI-u parsing handles Cmd+C/V/Left/Right correctly with
TERM_PROGRAM now in the allowlist, and shell fallback is acceptable for
the rest, this is the true minimum fix.

Things that may regress and be worth checking:
- Cmd+Left / Cmd+Right / Cmd+Backspace in shell (no translator → xterm
  sends CSI-u → bash readline ignores → no cursor navigation)
- Option+Left / Option+Right (same, word navigation won't work in shell)
- Cmd+A in shell (no select-all handler → CSI-u to PTY)
- Cmd+C in vim / less / htop (no broad bubble → CSI-u reaches PTY; if
  those TUIs don't speak kitty they'll show garbage)

If any of those bite, we restore the specific piece(s).

* chore(host-service): trim TERM_PROGRAM comment to essentials
Addresses GHSA-w5hq-g745-h8pq (Dependabot #29): uuid < 14.0.0 is missing
buffer bounds checks in v3/v5/v6 when a caller-provided buffer is passed,
allowing silent partial writes.

Our usage is limited to v4()/validate/version with no caller buffers, so
there's no direct exposure, but bumping to 14.0.0 clears the alert.
…e dialog (superset-sh#3681)

* fix(desktop): persist "also delete local branch" checkbox in v2 delete dialog

Wire the v2 delete-workspace dialog into the existing
`settings.getDeleteLocalBranch` / `setDeleteLocalBranch` tRPC pair
(already used by v1 and the Settings → Git page) so the user's last
choice is remembered across deletes instead of always defaulting to
unchecked.

* fix(desktop): persist v2 delete-branch choice via v2UserPreferences; always force

- Move the v2 delete dialog's "Also delete local branch" opt-in off
  the v1 tRPC settings singleton and onto `v2UserPreferences` — the
  same collection used for rightSidebar/link-tier prefs. The choice
  is now persisted the instant the checkbox toggles (optimistic
  update via @tanstack/db) and shared across every workspace's
  delete dialog, not per-instance.
- Drop the per-hook `deleteBranchOverride` state; read straight from
  the singleton preferences row.
- In host-service workspace-cleanup, always use `git branch -D` when
  `deleteBranch` is on — the checkbox is the user's consent, so
  silently refusing unmerged branches (the old `-d`/`-D` gate on
  `force`) just dropped the opt-in and produced confusing warnings.

* chore(desktop): drop redundant setDeleteBranch wrapper in destroy hook

Rename destructured `setDeleteLocalBranch` → `setDeleteBranch` at the
call site instead of wrapping it in a useCallback that did nothing.
Also trim a docstring line that's trivially true now that the opt-in
is a global singleton.
…sh#3683)

* feat(desktop): v2 Changes file list shift/cmd-click policy

Mirror the v2 Files tab click policy on the Changes sidebar: plain click
reuses an existing diff pane (or smart-places into the active tab),
shift+click opens the diff in a new tab, and cmd/ctrl+click opens the
file in the external editor.

* refactor(desktop): centralize v2 sidebar modifier-click intent

Extract the meta/ctrl -> editor, shift -> new tab, else -> select
dispatch into a shared getSidebarClickIntent() helper so the Files tab
tree item and the Changes file row resolve modifiers the same way.

* refactor(desktop): use path aliases for deep-relative imports

* feat(desktop): right-click menu + hover tooltip for v2 sidebar items

Surface the click/shift-click/cmd-click actions on v2 file and diff rows
so they stay discoverable: add a hover tooltip that lists the modifier
shortcuts, and a right-click menu with Open / Open in New Tab / Open in
Editor (with shortcut hints) plus the existing path actions. Changes
rows get the menu for the first time; Files rows gain explicit Open /
Open in New Tab / Open in Editor entries in place of the unwired "Open
to the Side" placeholder.

* fix(desktop): nest Tooltip inside ContextMenuTrigger so right-click works

* fix(desktop): gate cmd-click to files only in v2 Files sidebar
…rset-sh#3691)

Remove the feature card from the Product nav dropdown; show $20 struck
through next to $15 on the Pro pricing card when Yearly is selected to
make the discount explicit. Pin the price row height so toggling between
Monthly and Yearly doesn't cause a vertical shift.
…lete prompt (superset-sh#3688)

The v2 workspace delete flow showed two warnings: a generic confirm pane,
then a "Uncommitted changes in worktree" pane after destroy hit a conflict.
Surface the v1 yellow banner inline on the confirm pane via the existing
canDelete preflight, force when warnings are shown, and silently retry on
the rare race so the user only ever sees one prompt.
…uperset-sh#3679)

* fix(desktop): fail closed when adopted host-service has no version

The version gate only killed the host when fetchHostVersion returned a
string less than MIN_HOST_SERVICE_VERSION. If the host lacked the
host.info route entirely (older releases), the helper returned null and
we silently adopted it, producing 404s on project.findByPath and other
new routes.

Flip the guard to fail-closed: any host that can't prove it meets the
minimum version is killed and the manifest removed so the next spawn
brings up a compatible service.

* fix(desktop): compare host-service versions numerically

String comparison made "0.10.0" < "0.2.0" evaluate to true, which
would have killed any future double-digit minor version. Split the
version on dots and compare segments as integers.

Also split the adopt-killing log into two branches so the null case
reads "version unknown" instead of "version unknown < 0.2.0".

* refactor(desktop): use semver library for host-service version check

Replace the hand-rolled split-and-compare with `semver.satisfies`,
which already handles malformed input by returning false. The
`semver` package is already a dependency.

* refactor(desktop): inline host-service version check
…switches (superset-sh#3687)

* fix(desktop): keep v2 terminals stable across workspace switches

Problem: every v2-workspace switch yanked the xterm wrapper out of the DOM and
re-opened a WebSocket, producing a visible "switching and reattaching" flash
instead of VSCode-style hide/show. The v2-workspace layout's WorkspaceTrpcProvider
has a load-bearing key that unmounts the whole subtree on every switch, so the
React component for TerminalPane goes away — but the xterm instance, wrapper
div, and transport in terminalRuntimeRegistry should survive, and previously the
DOM node didn't.

Three entangled issues were fixed together:

1. Parking container. On detach, the wrapper used to be wrapper.remove()'d,
   taking the rendered canvases with it. Now it's appended to a hidden
   body-level div (#v2-terminal-parking) so xterm stays attached to the
   document. Re-mount in the new workspace is a DOM move back from parking
   to the live container — mirrors the existing v1 persistent-webview pattern
   at usePersistentWebview.ts:14-27 and VSCode's TerminalInstance setVisible
   model.

2. DOM vs transport split. The previous single registry.attach() both
   mounted DOM and opened the WebSocket. That forced TerminalPane to gate
   attach on ensureSession, which made warm returns wait on a tRPC round-trip
   (visible delay) and made cold mounts race — opening a WS before the
   server session existed produced "Session not found. Call terminal.
   ensureSession first.". Split into mount() (synchronous, DOM-only, safe
   on every mount) and connect() (called only after ensureSession resolves).
   Matches VSCode's TerminalInstance.attachToElement + _createProcess and
   Tabby's XTermFrontend.attach + setSession.

3. Effect dep narrowing. TerminalPane's attach effect used to depend on
   [terminalId, websocketUrl, initialThemeType, workspaceId]; prop churn
   during the provider key remount (workspaceId flipping while pane data
   caught up) forced repeated detach/attach cycles. Narrowed to [terminalId]
   with the others read through refs. websocketUrl changes now go through
   registry.reconnect(), which is hard-gated on transport already being
   live so it never opens a WS before ensureSession has resolved.

Log walkthrough on a warm workspace switch:
  pane:effect-cleanup → registry:detach → runtime:detach (wrapper parks)
  pane:effect-mount   → registry:mount  → runtime:attach wasParked:true
  registry:reconnect-skip same-url
  pane:ensureSession-ok → registry:connect → transport:connect-skip idempotent

Instrumentation stays in this commit for the rollout check; a follow-up
strips the termLog calls once this is confirmed in production.

* chore(desktop): strip v2 terminal lifecycle instrumentation

Removes the temporary termLog tracing added to diagnose the workspace-switch
reattach bug. The behavior fix (parking + mount/connect split) landed in the
previous commit and is confirmed stable in logs; no functional change here.

* fix(desktop): keep v2 browser panes stable across workspace switches

Browser webviews were being destroyed on workspace switch, discarding
guest-page state (URL, scroll, history) even though the registry was
designed to persist them.

Root cause: usePaneRegistry wired browser destruction through the Panes
library's onRemoved hook:

    onRemoved: (pane) => browserRuntimeRegistry.destroy(pane.id)

Under ideal conditions the v2 layout's `key={`${workspace.id}:${hostUrl}`}`
remounts the WorkspaceTrpcProvider subtree on every switch, so each
workspace gets its own Workspace component whose previous-panes diff
never observes a cross-workspace "removal". But the remount isn't
always prompt — layout.tsx's useLiveQuery can return stale WS-A data
for a tick while page.tsx's already flipped to WS-B. During that tick
the existing WorkspaceContent stays mounted, useV2WorkspacePaneLayout
calls store.replaceState(WS-B panes) on the same store instance, and
the Panes diff correctly sees "WS-A's browser is gone" → fires
onRemoved → destroys the webview. By the time the user returns,
attach() runs the cold createEntry path and the guest page is lost.

Terminals don't hit this because destruction goes through
useGlobalTerminalLifecycle, a global sweep against every workspace's
persisted paneLayout — cross-workspace "removal" isn't a removal from
the sweep's perspective.

Fix: mirror the terminal pattern exactly.
- Added useGlobalBrowserLifecycle under
  _authenticated/components/GlobalBrowserLifecycle/, following the
  same shape as useGlobalTerminalLifecycle (extract pane.ids from all
  workspace layouts, diff against previous, 500 ms grace delay
  destroy to tolerate cross-workspace pane moves).
- Mounted <GlobalBrowserLifecycle /> alongside <GlobalTerminalLifecycle />
  in _authenticated/layout.tsx.
- Removed the onRemoved wiring from usePaneRegistry.tsx — the sweep
  replaces it.

Verified with instrumentation that a workspace switch on a live browser
pane no longer reaches browserRuntimeRegistry.destroy, and that closing
a browser pane still destroys after the 500 ms grace.

Followup to PR superset-sh#3687 (terminal-side fix landed earlier on this branch).
Plan doc at apps/desktop/plans/20260423-1226-v2-pane-persistence-across-workspace-switch.md
captures the full root-cause analysis for both runtimes as a reference
for future pane persistence work.

Also removes a now-redundant biome-ignore comment in TerminalPane
that biome flagged as having no effect after the dep narrowing in the
earlier commit.

* fix(desktop): mark parked terminal container inert; clarify reconnect doc

Two follow-ups from PR superset-sh#3687 review:

- `inert` on #v2-terminal-parking. Parked terminals' internal <textarea>
  still had `tabindex=0`, so a keyboard user tabbing through the app
  could land in an off-screen terminal and have keystrokes silently go
  to the wrong pane. `inert` removes the subtree from the tab order
  and the accessibility tree, and moves focus out automatically, which
  also handles the "blur before park" concern for free. aria-hidden
  added for belt-and-suspenders on older engines.

- `reconnect` JSDoc. Clarify that the guard only skips `"disconnected"`
  (never-opened transport, caller should use ensureSession + connect
  path). `"connecting"`, `"open"`, `"closed"` are all intentionally
  allowed through — `connect()` aborts any in-flight or stale socket
  before opening the new one.

* chore(desktop): deslop v2 terminal pane effect and detach comment

- Collapse TerminalPane's .then()/.catch() into .catch()/.finally():
  one connect() call site instead of two identical guarded calls. Same
  semantics (connect after ensureSession settles, even on rejection),
  cancellation check consolidated.
- Merge the three connect-related comments into one block explaining
  "connect regardless of outcome" + idempotency. Drop the redundant
  "// DOM first" inline that the block comment above already covered.
- Trim detachFromContainer's 5-line comment down to 2 lines pointing
  at getParkingContainer — the helper's docstring already explains
  the parking rationale in full.

* perf(desktop): memoize v2 TerminalPane useSyncExternalStore args

The module-level `subscribeToState(terminalId)` built a fresh closure on
every render, so `useSyncExternalStore` saw a new subscribe function each
time and re-subscribed to `terminalRuntimeRegistry.onStateChange` on every
TerminalPane render — this is the anti-pattern React's useSyncExternalStore
docs explicitly warn about ("If you don't memoize the subscribe function,
React will resubscribe to your store every time your component re-renders").

Inline the helpers and wrap both subscribe + getSnapshot in useCallback
keyed on [terminalId]. Re-subscribe now only fires when the pane's
terminalId actually changes (cold create / destroy), not on every
keystroke-triggered re-render.
…ojectId> (superset-sh#3669)

* fix(host-service): place worktrees under ~/.superset/worktrees/<projectId>

Mirror v1 desktop's convention of keeping worktrees outside the primary
checkout tree, and match v2's existing ~/.superset/repos/<projectId>
layout for symmetry. Detection switches to the local `workspaces` table
(plus the new root for orphan adoption), so legacy worktrees at
<repo>/.worktrees/ keep working without a migration.

* chore(host-service): trim worktree-path comments

* lint
…er (superset-sh#3692)

* feat(desktop): v2 AI workspace rename generates title + branch together

Post-create rename now makes a single structured-output call returning
{ title, branchName }, applies the title to v2_workspaces.name, and also
renames the git branch (git branch -m in the worktree + updates the
host-local workspaces.branch + cloud v2_workspaces.branch). Replaces the
naive 20-char slice that was producing mid-word truncations like
"New v2 workspaces na". Branch-prefix support for v2 tracked in SUPER-478.

* fix(desktop): roll back git rename if cloud write fails; coerce oversized AI names

Addresses two review findings on v2 AI rename:

- Partial-commit: previously `git branch -m` + host-local `workspaces.branch` were
  written before the cloud mutate, so a cloud failure left git + local in the new
  state and the cloud (and the web app) stuck on the old branch. Now we git-rename
  first, push name+branch to cloud, and only update host-local on cloud success;
  on cloud throw we git-rename back to the old name.
- `.max()` on the zod schema rejected any overlong model output, silently no-op'ing
  the whole rename. Replaced with `.transform()` pipes that trim the title and
  sanitize the branch so overshoots are coerced instead of dropped.

* fix(desktop): loosen AI workspace name bounds, fall back branch to title slug

- Title cap bumped 40 → 150 and drops `.min(1)` — basically trust the small
  model with `.describe()` guidance.
- Branch falls back to a slug of the title when the model's branchName
  sanitizes to nothing (e.g. all emoji), so branch rename stops being a
  failure point.

* chore(desktop): drop title-slug branch fallback in AI workspace names

Per-field gating in the caller already skips the branch rename when branchName
is empty; no need to salvage. If the model's output is bad, skip the rename.

* refactor(desktop): drop structured-generation wrapper, call Agent.generate directly

The wrapper in packages/chat was pure indirection — chat already has @mastra/core
as a direct dep. Removed it, added @mastra/core to host-service, and call
agent.generate({ structuredOutput }) directly from ai-workspace-names. Also tightened
getSmallModel's return type to MastraModelConfig | null so callers don't need casts.

* refactor(desktop): hoist AI rename orchestration out of create handler

Pulls the ~80-line post-create block out of workspaceCreation.create into
applyAiWorkspaceRename in ai-workspace-names.ts — same file as the generator
so naming + applying live together. listBranchNames moves to its own util so
both callers can share it.

Call site in the create handler is now a three-line fire-and-forget.
)

* feat(desktop): show PR state as sidebar workspace icon

Replace the host-type icon in the v2 dashboard sidebar with a PR state
icon (open/merged/closed/draft), colored by state, when a workspace has
an associated pull request. Clicking the icon opens the PR on GitHub.
Drop the now-redundant bottom-right PR badge, and make the diff stats
only colorize when the workspace is active.

* fix(desktop): address PR review comments on sidebar PR icon

- Stop keydown propagation on the PR icon button so keyboard activation
  (Enter/Space) does not also trigger the parent row's workspace click.
- Use hover:bg-foreground/10 on the PR icon button so hover is visible
  even when the row is active (which already has bg-muted).
- Humanize PR state in the icon tooltip (Open/Merged/Closed/Draft).
- Drop the stale rounded class from the diff-stats container now that
  its background is gone.
- Use LuGitPullRequestClosed instead of LuCircleDot for closed PRs.

* chore: lint
…set-sh#3695)

* fix(desktop): use Alerter for automation detail delete confirm

Closes SUPER-436. Replaces native window.confirm() on the automation
detail page with the shared alert() helper so the confirm matches the
rest of the app's UI.

* fix(desktop): surface delete errors via toast.promise

Addresses review feedback on superset-sh#3695: the Alerter would silently swallow
a failed delete. Wrap mutateAsync in toast.promise so the user sees
loading, success, and error states.
…sh#3699)

* fix(desktop): honor agent selection in new-workspace modal

The modal's agent picker was cosmetic — `selectedAgent` was persisted to
localStorage for the UI but never written to the pending row, so the
pending page's `buildForkAgentLaunch` always called `getFallbackAgentId`
(which hard-prefers claude). Selecting codex, cursor, etc. silently
launched claude instead.

Thread the selected agent through the pending row into the launch build;
"none" now genuinely skips the agent launch (no silent claude
substitution).

* test(desktop): update buildForkAgentLaunch tests for explicit agent selection

Tests were written against the old fallback-to-claude behavior — they
passed `agentId: null` and expected Claude to launch. Under the new
contract, null is treated as "no selection → no launch".

Pass explicit `agentId` in each case. Add coverage for the new null /
"none" paths.
…sh#3701)

* fix(host-service): count untracked file lines in getStatus (SUPER-472)

Untracked files were pushed into `unstaged` with `additions: 0`, so the
sidebar LOC delta missed every newly-added file in the working tree
(`useDiffStats` sums against-base + staged + unstaged).

* feat(host-service): detect renames/copies on untracked files

Run git's real rename/copy detection over the working tree by copying
.git/index to a temp file, marking untracked files intent-to-add against
that copy, and running `git diff -M -C`. Real index is never mutated.

Merges matched deleted+untracked pairs into single rename entries with
correct additions/deletions, and renders `old → new` in the v2 sidebar
file row.

Catches:
- mv tracked → untracked (rename)
- cp tracked → untracked (copy, sourced from the tracked blob)
- mv with edits (rename with non-zero stats)

Falls back silently to the unmerged deleted+untracked listing on any
error (missing index, copy failure, intent-to-add reject).

* fix(host-service): address review on getStatus rename/LOC

- Cap parallel file I/O at 64 workers in countUntrackedFileLines so
  workspaces with thousands of untracked files don't EMFILE
- Sniff first 8KB for NUL bytes before counting lines so binary files
  under the 1MB cap (PNGs, lockfiles, compiled artifacts) don't get a
  meaningless utf-8 line count
- Log temp-dir cleanup failures instead of silently swallowing
- Add -M -C to staged numstat and propagate file.from as oldPath so
  staged renames collapse to a single 0/0 entry (matches the working-
  tree rename detection added in the previous commit)
…uperset-sh#3698)

Dev setup writes SUPERSET_HOME_DIR=<worktree>/superset-dev-data (no leading
dot), which didn't match the managed-hook regex, so stale notify.sh entries
from deleted dev worktrees stuck around in ~/.codex/hooks.json. Widen the
pattern to also recognize `superset-dev-data/` and add a regression test.
…h#3700)

* fix(desktop): adopt Ghostty keyboard model in v2 terminal

v2's terminal runtime only filtered app hotkeys. With kitty keyboard
protocol enabled (needed for Shift+Enter disambiguation in claude-code,
modifier reporting in neovim/helix), every Mac Cmd chord xterm saw got
CSI-u encoded and leaked into TUIs as a literal char — and line-edit
niceties like Cmd+Left/Right/Backspace and Option+Left/Right that v1
handles never worked at all.

Mirror Ghostty's approach (src/input/key_encode.zig:534-545: "on macOS,
command+keys do not encode text"): bubble every Mac Cmd chord out to
the host before xterm's kitty encoder runs, then port v1's line-edit
chord translators so shell navigation works the same in both renderers.

Changes:
- Broaden shouldBubbleClipboardShortcut's Mac branch to bubble all Cmd
  chords (not just Cmd+C/V with selection gating). v1 benefits too.
- Port v1's line-edit translators into v2's custom key handler:
  Cmd+Left/Right/Backspace, Option+Left/Right, Windows Ctrl+Left/Right.
  Duplicates v1 for now; a follow-up can share the handler properly.
- Wire shouldSelectAllShortcut into v2 so Cmd+A selects terminal buffer.
- Use xterm.input(data, true) to inject translated sequences into the
  PTY (fires onData, forwarded by terminal-ws-transport).
- Restructure tests around the new Mac rule.

* fix(desktop): preventDefault on bubbled clipboard chords

Matches VS Code (terminalInstance.ts:1116-1175) and Tabby (xtermFrontend.ts:199-214):
call `event.preventDefault()` before returning false from the custom key
handler so the browser's default action can't double-fire alongside the
Electron `role: 'paste'`/`'copy'` accelerator registered in
src/main/lib/menu.ts. Paste still flows — webContents.paste() dispatches
the paste event on the focused textarea independently of the DOM keydown
default, and xterm's paste listener picks it up.

Keydown-only — preventDefault on keyup is a no-op and would suppress
kitty protocol release events we still want xterm to handle.

* refactor(desktop): collapse translateLineEditChord to a grouped form

Seven near-identical 8-line blocks collapsed to a platform+modifier
grouped structure with a tiny onlyMod helper. Same behavior, ~55 fewer
lines, adding a new chord is one line instead of ten.

* revert: don't preventDefault on bubbled clipboard chords

Broke Cmd+C/V. On Electron macOS, the browser's keydown → paste-command
pipeline is what dispatches the paste event on xterm's textarea, and
preventDefault blocks that pipeline. VS Code and Tabby can preventDefault
because they implement paste themselves (command system / ClipboardAddon);
we rely on xterm's built-in paste listener, so the default must run.

Revert of 36cf276.
計画 PR #398#414 で 48+ commits を段階的に cherry-pick で取り込み済みだが、
conflict 解消で patch-id が変わるため GitHub UI の "behind" count が
減らない git の制約がある。

本 merge commit は `-s ours` で fork 現状の内容を一切変更せず、upstream/main
の 9268654 (superset-sh#3700 Ghostty keyboard model) までを merge-base に入れることで
"behind" count を減らす。

fork 側の実内容は全て維持 (PR #398#414 の成果物)。upstream 履歴は単に
ancestor として記録されるだけ。

唯一の残 upstream commit superset-sh#3697 (split workspace-creation router) は
fork の独自拡張 (baseBranchSource / PR checkout / fork note / applyAiWorkspaceRename) と
全面衝突する大規模 refactor のため本 merge に含めず、Issue #415 で追跡。

Merged upstream SHA: 9268654 (superset-sh#3700 adopt Ghostty keyboard model)
Tracking issue: #415
@MocA-Love MocA-Love merged commit 0ad4dce into main Apr 24, 2026
6 checks passed
@github-actions
Copy link
Copy Markdown

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

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

Thank you for your contribution! 🎉

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