Skip to content

fix(desktop): v2 preset commands run reliably in new tabs#4107

Merged
Kitenite merged 4 commits into
mainfrom
debug-presets-new-tabs
May 5, 2026
Merged

fix(desktop): v2 preset commands run reliably in new tabs#4107
Kitenite merged 4 commits into
mainfrom
debug-presets-new-tabs

Conversation

@Kitenite
Copy link
Copy Markdown
Collaborator

@Kitenite Kitenite commented May 5, 2026

Summary

  • Bug: v2 presets opening multiple new tabs (new-tab-per-command mode) only ran the command in the LAST tab — earlier tabs sat empty until clicked. The v2 workspace only renders the active tab, so each addTab in the loop flipped activeTabId and unmounted the prior tab before its TerminalPane could call terminal.createSession. Background tabs never spawned a PTY.
  • Fix: move session creation off the pane and onto the call site. useV2TerminalLauncher is the single seam in v2 renderer code that calls terminal.createSession.mutate(...). One method: create({ command?, terminalId? }) => Promise<string> (mints when terminalId omitted, adopts when given — relying on host-service createSession idempotency).
  • Every v2 site that creates a terminal pane now awaits launcher.create(...) before writing the pane into the store: preset hook, hotkeys (NEW_GROUP / SPLIT_*), pane action button, context menu splits, useWorkspacePaneOpeners.addTerminalTab. Multi-pane / per-command presets fan out with Promise.all so they don't pay serial latency.
  • TerminalPane.mount is back to plain mount() → connect(). It never calls createSession; if a terminalId arrives without a live session, it's a launching-call-site bug, not the pane's problem.
  • TerminalPaneData.initialCommand is gone — pane data is just the terminalId now.

Test plan

  • new-tab-per-command preset with 3 commands — all 3 tabs run their commands without needing to be clicked
  • new-tab-single, new-tab-multi-pane, active-tab-single, active-tab-multi-pane preset modes still work
  • Cmd+T / NEW_GROUP hotkey opens a working empty terminal
  • Split right / down / auto via hotkey, pane action button, and context menu all work
  • No "Terminal session not found; create it before connecting." errors on preset launch

Summary by CodeRabbit

  • Refactor
    • Centralized terminal creation via a new launcher for more consistent behavior across presets, hotkeys, pane actions, and context menus.
    • Simplified terminal initialization and connection flow for faster, more reliable session startup.
  • New Features
    • Preset execution and related handlers now support async operations, improving responsiveness and error handling.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 5, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 47855d15-ac68-4e3a-af6d-4fdc15f378c3

📥 Commits

Reviewing files that changed from the base of the PR and between b267fe6 and afdffef.

📒 Files selected for processing (1)
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useV2TerminalLauncher/useV2TerminalLauncher.ts

📝 Walkthrough

Walkthrough

This PR centralizes terminal session creation behind a new TerminalLauncher abstraction and hook, replaces ad-hoc crypto UUID generation with launcher.create() across workspace wiring (hotkeys, presets, pane openers, context menus, pane actions), removes TerminalPaneData.initialCommand, and refactors TerminalPane to mount-and-connect using an existing session URL instead of creating sessions locally.

Changes

Terminal Launcher Integration (single cohort)

Layer / File(s) Summary
Data Shape
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/types.ts
Removed initialCommand?: string from TerminalPaneData.
Launcher Implementation
.../hooks/useV2TerminalLauncher/useV2TerminalLauncher.ts
New TerminalLauncher interface and useV2TerminalLauncher hook that calls trpcClient.terminal.createSession.mutate(...) and returns create(options?: CreateOptions): Promise<string>.
Barrel Export
.../hooks/useV2TerminalLauncher/index.ts
Re-exports type TerminalLauncher and useV2TerminalLauncher.
Core Hook/API Changes
.../hooks/useV2PresetExecution/useV2PresetExecution.ts, useWorkspaceHotkeys/useWorkspaceHotkeys.ts, useWorkspacePaneOpeners/useWorkspacePaneOpeners.ts, useDefaultContextMenuActions/useDefaultContextMenuActions.tsx, useDefaultPaneActions/useDefaultPaneActions.tsx
These hooks now accept launcher: TerminalLauncher and replace crypto.randomUUID() with await launcher.create(...) for new terminal IDs; memoization/deps updated to include launcher; some handlers become async / return Promise.
Preset Handling
.../useV2PresetExecution/useV2PresetExecution.ts
All preset execution flows use launcher.create({ command }) and pass preset-derived title into created panes; makeTerminalPane no longer includes initialCommand.
Terminal Pane Lifecycle
.../hooks/usePaneRegistry/components/TerminalPane/TerminalPane.tsx
Removed local session creation flow and theme init; TerminalPane now mounts runtime and connects via workspace WebSocket URL (uses useWorkspaceWsUrl) relying on upstream session creation.
Wiring / Page
.../page.tsx
Instantiate launcher = useV2TerminalLauncher() and thread it into useV2PresetExecution, useDefaultContextMenuActions, useWorkspacePaneOpeners, useDefaultPaneActions, and useWorkspaceHotkeys.
Prop Typing
.../components/V2PresetsBar/V2PresetsBar.tsx
executePreset prop widened to return `void

🎯 4 (Complex) | ⏱️ ~50 minutes

sequenceDiagram
  participant User as "User/UI"
  participant Page as "Page / Hooks"
  participant Launcher as "TerminalLauncher (TRPC)"
  participant Pane as "TerminalPane (Runtime)"
  participant WS as "Workspace WebSocket"

  rect rgba(100,150,240,0.5)
  User->>Page: trigger new tab / split / preset
  end

  rect rgba(120,200,160,0.5)
  Page->>Launcher: call create({ command? }) -> Promise<terminalId>
  Launcher->>Launcher: trpcClient.terminal.createSession.mutate(...)
  Launcher-->>Page: terminalId
  end

  rect rgba(240,180,120,0.5)
  Page->>Pane: create/add pane with terminalId
  Pane->>Pane: mount runtime
  Pane->>WS: connect to websocketUrl for terminalId
  WS-->>Pane: stream terminal IO
  end
Loading

"🐰
I hop and bind each terminal's seed,
One launcher grows where UUIDs did breed.
Hooks call, sessions wake—
Panes mount, websockets take,
Smooth sprouts where scattered ids recede."

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main bug fix: v2 preset commands now run reliably in new tabs, directly addressing the core issue where commands failed to execute in background tabs.
Description check ✅ Passed The PR description is comprehensive and well-structured, covering the bug, the fix approach, affected code paths, and a detailed test plan. However, it does not follow the provided template structure with sections like 'Type of Change', 'Testing', etc.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch debug-presets-new-tabs

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

❤️ Share

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

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 5, 2026

Greptile Summary

This PR fixes the new-tab-per-command preset bug by introducing useV2TerminalLauncher — a single hook that mints a terminalId and fires createSession before any pane is written to the store, ensuring background tabs get their PTY and initial command regardless of whether they're ever mounted. TerminalPane is simplified to mount + connect only, and useEnsurePersistedTerminals handles the rehydration path on workspace open.

  • useV2TerminalLauncher centralises all createSession calls; every terminal-creation site (presets, hotkeys, splits, pane openers) is migrated from bare crypto.randomUUID() to launcher.create({ command? }).
  • useEnsurePersistedTerminals walks the persisted pane layout on workspace mount and calls launcher.ensure(terminalId) for each terminal, covering host-service / daemon restart scenarios.
  • TerminalPaneData.initialCommand is removed from the store type since commands are now forwarded at session-creation time, not stored in pane data.

Confidence Score: 3/5

The core fix works correctly, but the launcher hook returns a new object literal on every render, causing the rehydration effect to re-fire createSession mutations for all persisted terminals on every render cycle.

The design of useV2TerminalLauncher returning { create, ensure } as a plain (unstabilised) object means every downstream useEffect, useCallback, and useMemo that lists launcher in its deps reacts to a false change on every render. The most damaging case is useEnsurePersistedTerminals, whose effect is documented as 'runs once per workspace mount' but will actually call launcher.ensure() — a live createSession RPC — for every persisted terminal on every render of V2WorkspacePage. The fix is a one-line useMemo wrap on the return value. The rest of the changes are clean and address the reported bug correctly.

apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useV2TerminalLauncher/useV2TerminalLauncher.ts — the missing useMemo on the return value is the root of the instability that propagates to all consumers.

Important Files Changed

Filename Overview
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useV2TerminalLauncher/useV2TerminalLauncher.ts New central launcher hook — returns an unstable object literal each render, causing useEnsurePersistedTerminals to fire repeated tRPC mutations; also omits terminal dimensions that the old mount path forwarded to createSession.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useEnsurePersistedTerminals/useEnsurePersistedTerminals.ts New rehydration hook — logic is correct but the useEffect dep on launcher (unstable object reference) causes it to re-run on every render instead of once per workspace mount.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/TerminalPane.tsx Correctly removes the async createSession call from mount, delegating PTY creation to the launcher; loses user-visible error feedback path as a side effect.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useV2PresetExecution/useV2PresetExecution.ts Cleanly migrated all preset modes to use launcher.create({ command }) before writing panes to the store; logic is correct and simpler than before.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx Wires launcher into all consumer hooks correctly; downstream instability flows from the unstable launcher object returned by useV2TerminalLauncher.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useDefaultContextMenuActions/useDefaultContextMenuActions.tsx Correctly replaced crypto.randomUUID() calls with launcher.create() for split actions; signature change to accept launcher is clean.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useWorkspaceHotkeys/useWorkspaceHotkeys.ts All four terminal-creation hotkey paths correctly updated to use launcher.create() instead of bare crypto.randomUUID().
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/types.ts Removes initialCommand from TerminalPaneData — correct, since commands are now passed at createSession time via the launcher, not stored in pane data.

Sequence Diagram

sequenceDiagram
    participant Caller as Preset / Hotkey / Split
    participant Launcher as useV2TerminalLauncher
    participant HostSvc as Host Service (tRPC)
    participant Store as WorkspaceStore
    participant Pane as TerminalPane (mount)
    participant WS as WebSocket

    Caller->>Launcher: launcher.create({ command? })
    Launcher->>HostSvc: createSession(terminalId, command)
    Note over HostSvc: Spawns PTY, buffers output
    Launcher-->>Caller: terminalId (sync)
    Caller->>Store: addTab / addPane (pane with terminalId)
    Note over Store: Active tab may flip here (new-tab-per-command)

    Note over Pane: Mounts whenever tab becomes active
    Pane->>Pane: mount() - attach DOM container
    Pane->>WS: connect(terminalId)
    WS-->>HostSvc: WebSocket attaches
    HostSvc-->>WS: Flush buffered PTY output

    Note over Caller,Store: Rehydration path (workspace open)
    Caller->>Launcher: launcher.ensure(terminalId)
    Launcher->>HostSvc: createSession (idempotent - adopt or spawn)
Loading

Comments Outside Diff (1)

  1. apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/TerminalPane.tsx, line 130-147 (link)

    P2 createSession failures are now silent to the user

    The removed async block wrote the error into the terminal display via terminalRuntimeRegistry.getTerminal(...).writeln(...), giving users a visible message when the host-service was unreachable or the session couldn't start. With this code gone, a createSession failure is logged to the console only (in useV2TerminalLauncher), and the pane mounts, calls connect(), and then sits with an unresponsive blank terminal. There's no way for the user to distinguish "still connecting" from "failed."

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/TerminalPane.tsx
    Line: 130-147
    
    Comment:
    **`createSession` failures are now silent to the user**
    
    The removed async block wrote the error into the terminal display via `terminalRuntimeRegistry.getTerminal(...).writeln(...)`, giving users a visible message when the host-service was unreachable or the session couldn't start. With this code gone, a `createSession` failure is logged to the console only (in `useV2TerminalLauncher`), and the pane mounts, calls `connect()`, and then sits with an unresponsive blank terminal. There's no way for the user to distinguish "still connecting" from "failed."
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
Fix the following 4 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 4
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useV2TerminalLauncher/useV2TerminalLauncher.ts:1-2
`useV2TerminalLauncher` returns a plain object literal (`{ create, ensure }`) on every render. Even though `create` and `ensure` are individually stabilised by `useCallback`, the containing object is a new reference on every call, so `launcher` itself is always a fresh value. `useEnsurePersistedTerminals` has `[store, launcher]` in its effect deps — because `launcher` is never the same reference, the effect fires on every render of `V2WorkspacePage` and calls `launcher.ensure()` (a live `createSession` tRPC mutation) for every terminal in the persisted layout each time. Wrapping the return value in `useMemo` makes the object stable so the effect runs only when `create`/`ensure` actually change.

```suggestion
import { useWorkspaceClient } from "@superset/workspace-client";
import { useCallback, useMemo } from "react";
```

### Issue 2 of 4
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useV2TerminalLauncher/useV2TerminalLauncher.ts:78-79
The returned object also needs to be wrapped in `useMemo` so its reference is stable across re-renders. Without this, every downstream `useEffect` / `useCallback` / `useMemo` that lists `launcher` in its dependency array will treat each render as a change and re-execute — most importantly, `useEnsurePersistedTerminals`'s effect will fire `createSession` mutations for all persisted terminals on every render cycle (e.g. every tab switch).

```suggestion
	return useMemo(() => ({ create, ensure }), [create, ensure]);
}
```

### Issue 3 of 4
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useV2TerminalLauncher/useV2TerminalLauncher.ts:39-58
**Terminal created without measured dimensions**

The old `TerminalPane` mount path called `terminalRuntimeRegistry.getDimensions()` and forwarded `cols`/`rows` to `createSession`. `launcher.create()` omits those fields entirely, so the host-service spawns the PTY at whatever default size it uses. The terminal will resize to fit its container on the first `resize` event, but until then the shell's line-wrap width is wrong — this can corrupt output for commands that query `$COLUMNS` or use readline immediately on startup (e.g. an `initialCommand` that runs a curses UI).

### Issue 4 of 4
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/TerminalPane.tsx:130-147
**`createSession` failures are now silent to the user**

The removed async block wrote the error into the terminal display via `terminalRuntimeRegistry.getTerminal(...).writeln(...)`, giving users a visible message when the host-service was unreachable or the session couldn't start. With this code gone, a `createSession` failure is logged to the console only (in `useV2TerminalLauncher`), and the pane mounts, calls `connect()`, and then sits with an unresponsive blank terminal. There's no way for the user to distinguish "still connecting" from "failed."

Reviews (1): Last reviewed commit: "fix(desktop): v2 preset commands run rel..." | Re-trigger Greptile

Comment on lines +1 to +2
import { useWorkspaceClient } from "@superset/workspace-client";
import { useCallback } from "react";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 useV2TerminalLauncher returns a plain object literal ({ create, ensure }) on every render. Even though create and ensure are individually stabilised by useCallback, the containing object is a new reference on every call, so launcher itself is always a fresh value. useEnsurePersistedTerminals has [store, launcher] in its effect deps — because launcher is never the same reference, the effect fires on every render of V2WorkspacePage and calls launcher.ensure() (a live createSession tRPC mutation) for every terminal in the persisted layout each time. Wrapping the return value in useMemo makes the object stable so the effect runs only when create/ensure actually change.

Suggested change
import { useWorkspaceClient } from "@superset/workspace-client";
import { useCallback } from "react";
import { useWorkspaceClient } from "@superset/workspace-client";
import { useCallback, useMemo } from "react";
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useV2TerminalLauncher/useV2TerminalLauncher.ts
Line: 1-2

Comment:
`useV2TerminalLauncher` returns a plain object literal (`{ create, ensure }`) on every render. Even though `create` and `ensure` are individually stabilised by `useCallback`, the containing object is a new reference on every call, so `launcher` itself is always a fresh value. `useEnsurePersistedTerminals` has `[store, launcher]` in its effect deps — because `launcher` is never the same reference, the effect fires on every render of `V2WorkspacePage` and calls `launcher.ensure()` (a live `createSession` tRPC mutation) for every terminal in the persisted layout each time. Wrapping the return value in `useMemo` makes the object stable so the effect runs only when `create`/`ensure` actually change.

```suggestion
import { useWorkspaceClient } from "@superset/workspace-client";
import { useCallback, useMemo } from "react";
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +78 to +79
return { create, ensure };
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 The returned object also needs to be wrapped in useMemo so its reference is stable across re-renders. Without this, every downstream useEffect / useCallback / useMemo that lists launcher in its dependency array will treat each render as a change and re-execute — most importantly, useEnsurePersistedTerminals's effect will fire createSession mutations for all persisted terminals on every render cycle (e.g. every tab switch).

Suggested change
return { create, ensure };
}
return useMemo(() => ({ create, ensure }), [create, ensure]);
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useV2TerminalLauncher/useV2TerminalLauncher.ts
Line: 78-79

Comment:
The returned object also needs to be wrapped in `useMemo` so its reference is stable across re-renders. Without this, every downstream `useEffect` / `useCallback` / `useMemo` that lists `launcher` in its dependency array will treat each render as a change and re-execute — most importantly, `useEnsurePersistedTerminals`'s effect will fire `createSession` mutations for all persisted terminals on every render cycle (e.g. every tab switch).

```suggestion
	return useMemo(() => ({ create, ensure }), [create, ensure]);
}
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +39 to +58

const create = useCallback(
(options?: CreateOptions): string => {
const terminalId = crypto.randomUUID();
const mutation = trpcClient.terminal.createSession.mutate({
terminalId,
workspaceId,
themeType,
initialCommand: options?.command,
});
terminalSessionPending.track(terminalId, mutation);
mutation.catch((error) => {
console.error(
"[useV2TerminalLauncher] Failed to create terminal session:",
{ terminalId, error },
);
});
return terminalId;
},
[trpcClient, workspaceId, themeType],
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Terminal created without measured dimensions

The old TerminalPane mount path called terminalRuntimeRegistry.getDimensions() and forwarded cols/rows to createSession. launcher.create() omits those fields entirely, so the host-service spawns the PTY at whatever default size it uses. The terminal will resize to fit its container on the first resize event, but until then the shell's line-wrap width is wrong — this can corrupt output for commands that query $COLUMNS or use readline immediately on startup (e.g. an initialCommand that runs a curses UI).

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useV2TerminalLauncher/useV2TerminalLauncher.ts
Line: 39-58

Comment:
**Terminal created without measured dimensions**

The old `TerminalPane` mount path called `terminalRuntimeRegistry.getDimensions()` and forwarded `cols`/`rows` to `createSession`. `launcher.create()` omits those fields entirely, so the host-service spawns the PTY at whatever default size it uses. The terminal will resize to fit its container on the first `resize` event, but until then the shell's line-wrap width is wrong — this can corrupt output for commands that query `$COLUMNS` or use readline immediately on startup (e.g. an `initialCommand` that runs a curses UI).

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useV2PresetExecution/useV2PresetExecution.ts (2)

21-21: 💤 Low value

Drop the as TerminalPaneData cast if the type now only requires terminalId.

Per the PR summary, initialCommand was removed from TerminalPaneData. If the type is now structurally just { terminalId: string } (with optional fields), the cast is redundant and hides any future shape mismatch. Prefer letting inference do the work so a future required field on TerminalPaneData surfaces here as a compile error.

♻️ Suggested change
-		data: { terminalId } as TerminalPaneData,
+		data: { terminalId } satisfies TerminalPaneData,

(or simply data: { terminalId } if CreatePaneInput<PaneViewerData> already discriminates the kind).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/useV2PresetExecution/useV2PresetExecution.ts
at line 21, Remove the unnecessary type assertion on the pane data: in
useV2PresetExecution (the line currently using "data: { terminalId } as
TerminalPaneData") drop the "as TerminalPaneData" so the object is passed as
"data: { terminalId }" and let TypeScript infer the shape; this surfaces any
future required fields on TerminalPaneData or mismatches from
CreatePaneInput<PaneViewerData> instead of hiding them behind an explicit cast.

77-132: ⚡ Quick win

De-duplicate the multi-pane builder between new-tab-multi-pane and active-tab-multi-pane.

Both branches build panes from preset.commands with the same fallback semantics and the same non-empty tuple cast. Extracting a small local helper keeps the two cases in lockstep and isolates the cast to one place.

♻️ Suggested refactor
+			const buildMultiPanes = (): [
+				CreatePaneInput<PaneViewerData>,
+				...CreatePaneInput<PaneViewerData>[],
+			] => {
+				const panes =
+					preset.commands.length > 0
+						? preset.commands.map((command) =>
+								makeTerminalPane(launcher.create({ command }), title),
+							)
+						: [makeTerminalPane(launcher.create(), title)];
+				return panes as [
+					CreatePaneInput<PaneViewerData>,
+					...CreatePaneInput<PaneViewerData>[],
+				];
+			};
+
 			try {
 				switch (plan) {
 					...
 					case "new-tab-multi-pane": {
-						const panes =
-							preset.commands.length > 0
-								? preset.commands.map((command) =>
-										makeTerminalPane(launcher.create({ command }), title),
-									)
-								: [makeTerminalPane(launcher.create(), title)];
-						state.addTab({
-							panes: panes as [
-								CreatePaneInput<PaneViewerData>,
-								...CreatePaneInput<PaneViewerData>[],
-							],
-						});
+						state.addTab({ panes: buildMultiPanes() });
 						break;
 					}
 					...
 					case "active-tab-multi-pane": {
-						const panes =
-							preset.commands.length > 0
-								? preset.commands.map((command) =>
-										makeTerminalPane(launcher.create({ command }), title),
-									)
-								: [makeTerminalPane(launcher.create(), title)];
+						const panes = buildMultiPanes();
 						if (!activeTabId) {
-							state.addTab({
-								panes: panes as [
-									CreatePaneInput<PaneViewerData>,
-									...CreatePaneInput<PaneViewerData>[],
-								],
-							});
+							state.addTab({ panes });
 							break;
 						}
 						for (const pane of panes) {
 							state.addPane({ tabId: activeTabId, pane });
 						}
 						break;
 					}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/useV2PresetExecution/useV2PresetExecution.ts
around lines 77 - 132, The two switch cases "new-tab-multi-pane" and
"active-tab-multi-pane" duplicate the logic that builds the panes array from
preset.commands (including the fallback to a single launcher.create() and the
non-empty tuple cast); extract that logic into a small helper (e.g.,
buildPanesForPreset or createPanesFromPreset) that returns the correctly typed
panes array (doing the CreatePaneInput<PaneViewerData> non-empty tuple cast in
one place) and replace the duplicated code in both cases to call this helper
before calling state.addTab or iterating to call state.addPane; keep references
to makeTerminalPane and launcher.create inside the helper so the cast and
creation logic are centralized.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/useV2TerminalLauncher/useV2TerminalLauncher.ts:
- Line 78: The returned launcher object from useV2TerminalLauncher is a new
object each render causing downstream hooks (that depend on launcher) to re-run;
fix this by memoizing the launcher reference—wrap the { create, ensure } return
in useMemo (or store in a ref) inside useV2TerminalLauncher so that the object
identity remains stable while still exposing the useCallback-stable create and
ensure functions; update the return to return the memoized launcher (reference
the create and ensure symbols and the useV2TerminalLauncher hook) so effects
depending on launcher no longer retrigger every render.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/page.tsx:
- Around line 90-95: The returned launcher object from useV2TerminalLauncher is
recreated each render which invalidates downstream dependency arrays (causing
hooks like useV2PresetExecution and useWorkspaceHotkeys to recompute); update
useV2TerminalLauncher to wrap the returned object in useMemo so it returns a
stable reference (e.g., memoize the object containing the create and ensure
callbacks with useMemo and depend on [create, ensure]) so calls to
useEnsurePersistedTerminals, useV2PresetExecution, and other consumers stop
rebinding on every render.

---

Nitpick comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/useV2PresetExecution/useV2PresetExecution.ts:
- Line 21: Remove the unnecessary type assertion on the pane data: in
useV2PresetExecution (the line currently using "data: { terminalId } as
TerminalPaneData") drop the "as TerminalPaneData" so the object is passed as
"data: { terminalId }" and let TypeScript infer the shape; this surfaces any
future required fields on TerminalPaneData or mismatches from
CreatePaneInput<PaneViewerData> instead of hiding them behind an explicit cast.
- Around line 77-132: The two switch cases "new-tab-multi-pane" and
"active-tab-multi-pane" duplicate the logic that builds the panes array from
preset.commands (including the fallback to a single launcher.create() and the
non-empty tuple cast); extract that logic into a small helper (e.g.,
buildPanesForPreset or createPanesFromPreset) that returns the correctly typed
panes array (doing the CreatePaneInput<PaneViewerData> non-empty tuple cast in
one place) and replace the duplicated code in both cases to call this helper
before calling state.addTab or iterating to call state.addPane; keep references
to makeTerminalPane and launcher.create inside the helper so the cast and
creation logic are centralized.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f91e28df-2a95-453c-8e56-03ebc66fd78b

📥 Commits

Reviewing files that changed from the base of the PR and between ed48e49 and 23eb68b.

📒 Files selected for processing (12)
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useDefaultContextMenuActions/useDefaultContextMenuActions.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useDefaultPaneActions/useDefaultPaneActions.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useEnsurePersistedTerminals/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useEnsurePersistedTerminals/useEnsurePersistedTerminals.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/TerminalPane.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useV2PresetExecution/useV2PresetExecution.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useV2TerminalLauncher/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useV2TerminalLauncher/useV2TerminalLauncher.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useWorkspaceHotkeys/useWorkspaceHotkeys.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useWorkspacePaneOpeners/useWorkspacePaneOpeners.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/types.ts
💤 Files with no reviewable changes (1)
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/types.ts

Comment on lines +90 to +95
const launcher = useV2TerminalLauncher();
useEnsurePersistedTerminals({ store, launcher });
const { matchedPresets, executePreset } = useV2PresetExecution({
store,
launcher,
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect the launcher hook to confirm the returned object/methods are memoized.
fd -t f 'useV2TerminalLauncher.ts' --exec cat {}

Repository: superset-sh/superset

Length of output: 2648


Wrap the returned launcher object in useMemo to avoid invalidating downstream dependency arrays.

The create and ensure methods are memoized with useCallback, but useV2TerminalLauncher returns a new object { create, ensure } on every render. Each fresh reference will cause useV2PresetExecution and other hooks to recompute unnecessarily, rippling through useWorkspaceHotkeys and triggering hotkey rebinding.

Memoize the return value:

const memoizedLauncher = useMemo(() => ({ create, ensure }), [create, ensure]);
return memoizedLauncher;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/page.tsx
around lines 90 - 95, The returned launcher object from useV2TerminalLauncher is
recreated each render which invalidates downstream dependency arrays (causing
hooks like useV2PresetExecution and useWorkspaceHotkeys to recompute); update
useV2TerminalLauncher to wrap the returned object in useMemo so it returns a
stable reference (e.g., memoize the object containing the create and ensure
callbacks with useMemo and depend on [create, ensure]) so calls to
useEnsurePersistedTerminals, useV2PresetExecution, and other consumers stop
rebinding on every render.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useV2TerminalLauncher/useV2TerminalLauncher.ts (1)

79-79: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Memoize the returned launcher object.

create and ensure are callback-stable, but { create, ensure } is a new object every render. Any useEffect/useMemo depending on launcher will retrigger every render, which is especially problematic for the persisted-terminal ensure flow added in this PR.

Suggested fix
-import { useCallback } from "react";
+import { useCallback, useMemo } from "react";
...
-	return { create, ensure };
+	return useMemo(() => ({ create, ensure }), [create, ensure]);

You can confirm the downstream dependency churn with:

#!/bin/bash
set -euo pipefail

echo "=== useV2TerminalLauncher ==="
fd 'useV2TerminalLauncher\.ts$' apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace \
  | while read -r f; do
      echo "--- $f ---"
      sed -n '1,140p' "$f"
    done

echo
echo "=== launcher-dependent hooks/components ==="
rg -n -C2 '\blauncher\b' apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace --iglob '*.ts' --iglob '*.tsx'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/useV2TerminalLauncher/useV2TerminalLauncher.ts
at line 79, The hook useV2TerminalLauncher returns a fresh object { create,
ensure } each render which breaks referential equality for consumers; wrap the
returned launcher in React.useMemo (e.g. create a const launcher = useMemo(() =>
({ create, ensure }), [create, ensure]) and return launcher) so the object is
stable across renders while keeping create and ensure callback-stable.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/desktop/src/renderer/lib/terminal/terminal-session-pending.ts`:
- Around line 39-40: awaitReady currently does a one-shot lookup on the pending
Map and returns immediately if no entry exists, which races with effect-driven
launcher.ensure calls; change awaitReady (in terminal-session-pending) to create
and register a deferred promise when no pending entry exists (store a resolve in
the pending Map) or implement a short retry/poll loop so callers (e.g.,
TerminalPane's useEffect) will wait until launcher.ensure (from
useEnsurePersistedTerminals) registers the real pending promise; reference
awaitReady, pending, launcher.ensure, useEnsurePersistedTerminals and
TerminalPane when making the change so the deferred promise is created/returned
by awaitReady before the rehydration effect runs.

---

Duplicate comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/useV2TerminalLauncher/useV2TerminalLauncher.ts:
- Line 79: The hook useV2TerminalLauncher returns a fresh object { create,
ensure } each render which breaks referential equality for consumers; wrap the
returned launcher in React.useMemo (e.g. create a const launcher = useMemo(() =>
({ create, ensure }), [create, ensure]) and return launcher) so the object is
stable across renders while keeping create and ensure callback-stable.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6a1afedd-785a-4760-afe7-8db525da1348

📥 Commits

Reviewing files that changed from the base of the PR and between 23eb68b and 133324d.

📒 Files selected for processing (3)
  • apps/desktop/src/renderer/lib/terminal/terminal-session-pending.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/TerminalPane.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useV2TerminalLauncher/useV2TerminalLauncher.ts

Comment thread apps/desktop/src/renderer/lib/terminal/terminal-session-pending.ts Outdated
Presets that opened multiple new tabs (`new-tab-per-command` mode) only
ran their command in the LAST tab — earlier tabs sat empty until the
user clicked them. The v2 workspace renders only the active tab, so
each `addTab` in the loop flipped `activeTabId` and unmounted the prior
tab before its `TerminalPane` could call `terminal.createSession`.
Background tabs never spawned a PTY.

Move session creation off the pane and onto the call site:

- `useV2TerminalLauncher` is the single seam in v2 renderer code that
  calls `terminal.createSession.mutate(...)`. One method, async:
  `create({ command?, terminalId? }) => Promise<string>` mints a fresh
  id when terminalId is omitted, adopts the given id otherwise (relying
  on host-service createSession idempotency).
- Every v2 site that creates a terminal pane now awaits
  `launcher.create(...)` before writing the pane into the store: preset
  hook, hotkeys (NEW_GROUP / SPLIT_*), pane action button, context menu
  splits, `useWorkspacePaneOpeners.addTerminalTab`. Multi-pane / per-
  command presets fan out via `Promise.all` so they don't pay serial
  latency.
- `TerminalPane.mount` is back to plain `mount() + connect()`. It never
  calls createSession; if a terminalId arrives without a live session,
  it's a launching-call-site bug, not the pane's problem.
- `TerminalPaneData.initialCommand` is gone — pane data is just the
  terminalId now.
@Kitenite Kitenite force-pushed the debug-presets-new-tabs branch from c298364 to b267fe6 Compare May 5, 2026 21:07
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

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

413-416: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Make the disconnected banner text selectable.

This error/status text is rendered without select-text cursor-text, so users still cannot copy it in the renderer.

💡 Proposed fix
 			{connectionState === "closed" && (
-				<div className="flex items-center gap-2 border-t border-border px-3 py-1.5 text-xs text-muted-foreground">
+				<div className="flex items-center gap-2 border-t border-border px-3 py-1.5 text-xs text-muted-foreground select-text cursor-text">
 					<span>Disconnected</span>
 				</div>
 			)}

As per coding guidelines, Error text must be selectable by users with explicit select-text cursor-text classes; renderer sets user-select: none on body.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/usePaneRegistry/components/TerminalPane/TerminalPane.tsx
around lines 413 - 416, The disconnected banner rendered in TerminalPane (check
the connectionState === "closed" block inside the TerminalPane component) is not
selectable due to global renderer styles; update the banner div to include the
explicit classes "select-text" and "cursor-text" so the "Disconnected" status
can be copied—i.e., add those classes to the existing className on the div that
renders the disconnected message.
♻️ Duplicate comments (1)
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useV2TerminalLauncher/useV2TerminalLauncher.ts (1)

52-52: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Memoize the returned launcher object so consumers' dependency arrays stay stable.

{ create } is a fresh object literal on every render even though create itself is useCallback-stabilized. Every consumer that lists launcher in a dependency array (useV2PresetExecution [store, launcher, resolvePresetCommands], useDefaultPaneActions [launcher], useDefaultContextMenuActions […, launcher], useWorkspacePaneOpeners [store, launcher], useWorkspaceHotkeys via threaded deps) will re-create their callbacks/memoized values on every parent render, defeating the memoization in this file.

♻️ Proposed fix
-import { useCallback } from "react";
+import { useCallback, useMemo } from "react";
@@
-	return { create };
+	return useMemo(() => ({ create }), [create]);
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/useV2TerminalLauncher/useV2TerminalLauncher.ts
at line 52, The returned launcher object literal in useV2TerminalLauncher is
recreated each render which breaks consumers' dependency stability; instead
memoize the launcher object (containing the create callback) so its reference is
stable — e.g. use useMemo or useRef inside useV2TerminalLauncher to return a
stable object like launcher = useMemo(() => ({ create }), [create]) and then
return that launcher from the hook (keep the create useCallback as-is).
🧹 Nitpick comments (2)
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useWorkspaceHotkeys/useWorkspaceHotkeys.ts (1)

47-57: 💤 Low value

Consider surfacing launcher.create() failures to the user, the way useV2PresetExecution does.

In NEW_GROUP, SPLIT_AUTO, SPLIT_RIGHT, and SPLIT_DOWN the await launcher.create() call is unguarded. If the host-service mutation rejects (e.g., daemon down, schema validation failure on the protected procedure — see packages/host-service/src/trpc/router/terminal/terminal.ts:92-95), the rejection becomes an unhandled promise rejection and the user sees nothing — no tab, no error toast. useV2PresetExecution already wraps the same call in try/catch with a toast.error; mirroring that pattern here keeps hotkey-driven creation paths consistent with preset-driven ones.

♻️ Example shape (apply to each `SPLIT_*` and `NEW_GROUP` callback)
-	useHotkey("NEW_GROUP", async () => {
-		const terminalId = await launcher.create();
-		store.getState().addTab({
-			panes: [
-				{
-					kind: "terminal",
-					data: { terminalId } as TerminalPaneData,
-				},
-			],
-		});
-	});
+	useHotkey("NEW_GROUP", async () => {
+		try {
+			const terminalId = await launcher.create();
+			store.getState().addTab({
+				panes: [
+					{ kind: "terminal", data: { terminalId } as TerminalPaneData },
+				],
+			});
+		} catch (err) {
+			console.error("[useWorkspaceHotkeys] NEW_GROUP failed:", err);
+			toast.error("Failed to open new terminal", {
+				description: err instanceof Error ? err.message : "Terminal session creation failed.",
+			});
+		}
+	});

Also applies to: 204-255

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/useWorkspaceHotkeys/useWorkspaceHotkeys.ts
around lines 47 - 57, The NEW_GROUP, SPLIT_AUTO, SPLIT_RIGHT and SPLIT_DOWN
hotkey handlers call launcher.create() without handling rejections; wrap each
async callback (the function passed to useHotkey for those keys) in a try/catch,
call const terminalId = await launcher.create() inside the try, then on success
call store.getState().addTab(...) as existing, and on failure call
toast.error(...) (mirror the pattern used in useV2PresetExecution) so
launcher.create() failures surface to the user instead of causing unhandled
promise rejections.
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useV2PresetExecution/useV2PresetExecution.ts (1)

85-183: LGTM — preset execution now mints sessions before tabs/panes are written, fixing the new-tab-per-command race.

Each plan branch awaits launcher.create({ command }) (parallelized with Promise.all where multiple sessions are needed) before calling state.addTab / state.addPane, so background tabs that flip activeTabId and unmount before mounting still get their PTY queued — exactly the behavior the PR aims to fix. The wrapping try/catch with toast covers user-visible failures.

One thing worth keeping in mind (not a blocker for this PR): if Promise.all rejects partway, sessions that already created on host-service become orphaned until the next workspace open. If useEnsurePersistedTerminals/launcher.ensure lands as the PR description suggests, that path covers re-adoption; otherwise a follow-up to clean up dangling sessions on partial failure may be worthwhile.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/useV2PresetExecution/useV2PresetExecution.ts
around lines 85 - 183, The code can leave orphaned terminal sessions if
Promise.all rejects partway; update executePreset to track any terminal IDs
created before a failure in the parallel branches ("new-tab-multi-pane",
"new-tab-per-command", "active-tab-multi-pane") and, in the catch, iterate those
IDs to clean them up (e.g. call the launcher cleanup API for each created id) or
switch to using the planned resilience helper (useEnsurePersistedTerminals /
launcher.ensure) once available; ensure tracking/cleanup logic is added around
the launcher.create calls so partial successes are removed on error.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/useV2TerminalLauncher/useV2TerminalLauncher.ts:
- Around line 19-27: The PR description advertises missing APIs
(launcher.ensure, useEnsurePersistedTerminals, terminalSessionPending) but the
implementation only defines TerminalLauncher.create; either update the PR text
or implement the missing features—preferably implement: add an
ensure(terminalId: string): Promise<void> method to the TerminalLauncher
interface and its concrete launcher implementation to idempotently
confirm/rehydrate a session, add a useEnsurePersistedTerminals hook that reads
persisted terminal IDs and calls launcher.ensure for each on mount, and
introduce a shared terminalSessionPending map (or similar store) used by
create() and ensure() to track in-flight session creation so callers can await
the same Promise; update any types and tests referencing TerminalLauncher to
include ensure and wire the hook to the launcher.

---

Outside diff comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/usePaneRegistry/components/TerminalPane/TerminalPane.tsx:
- Around line 413-416: The disconnected banner rendered in TerminalPane (check
the connectionState === "closed" block inside the TerminalPane component) is not
selectable due to global renderer styles; update the banner div to include the
explicit classes "select-text" and "cursor-text" so the "Disconnected" status
can be copied—i.e., add those classes to the existing className on the div that
renders the disconnected message.

---

Duplicate comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/useV2TerminalLauncher/useV2TerminalLauncher.ts:
- Line 52: The returned launcher object literal in useV2TerminalLauncher is
recreated each render which breaks consumers' dependency stability; instead
memoize the launcher object (containing the create callback) so its reference is
stable — e.g. use useMemo or useRef inside useV2TerminalLauncher to return a
stable object like launcher = useMemo(() => ({ create }), [create]) and then
return that launcher from the hook (keep the create useCallback as-is).

---

Nitpick comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/useV2PresetExecution/useV2PresetExecution.ts:
- Around line 85-183: The code can leave orphaned terminal sessions if
Promise.all rejects partway; update executePreset to track any terminal IDs
created before a failure in the parallel branches ("new-tab-multi-pane",
"new-tab-per-command", "active-tab-multi-pane") and, in the catch, iterate those
IDs to clean them up (e.g. call the launcher cleanup API for each created id) or
switch to using the planned resilience helper (useEnsurePersistedTerminals /
launcher.ensure) once available; ensure tracking/cleanup logic is added around
the launcher.create calls so partial successes are removed on error.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/useWorkspaceHotkeys/useWorkspaceHotkeys.ts:
- Around line 47-57: The NEW_GROUP, SPLIT_AUTO, SPLIT_RIGHT and SPLIT_DOWN
hotkey handlers call launcher.create() without handling rejections; wrap each
async callback (the function passed to useHotkey for those keys) in a try/catch,
call const terminalId = await launcher.create() inside the try, then on success
call store.getState().addTab(...) as existing, and on failure call
toast.error(...) (mirror the pattern used in useV2PresetExecution) so
launcher.create() failures surface to the user instead of causing unhandled
promise rejections.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9fc02001-25b5-4aaf-80d1-eade92b522cd

📥 Commits

Reviewing files that changed from the base of the PR and between 133324d and b267fe6.

📒 Files selected for processing (11)
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/V2PresetsBar/V2PresetsBar.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useDefaultContextMenuActions/useDefaultContextMenuActions.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useDefaultPaneActions/useDefaultPaneActions.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/TerminalPane.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useV2PresetExecution/useV2PresetExecution.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useV2TerminalLauncher/index.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useV2TerminalLauncher/useV2TerminalLauncher.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useWorkspaceHotkeys/useWorkspaceHotkeys.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useWorkspacePaneOpeners/useWorkspacePaneOpeners.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/types.ts
💤 Files with no reviewable changes (1)
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/types.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useV2TerminalLauncher/index.ts

Comment on lines +19 to +27
export interface TerminalLauncher {
/**
* Awaits `terminal.createSession` and returns the terminalId. Callers
* should await this before writing the pane into the store, so the pane's
* WebSocket connect doesn't race ahead of the session existing on
* host-service.
*/
create(options?: CreateOptions): Promise<string>;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm whether ensure / useEnsurePersistedTerminals / terminalSessionPending exist anywhere in the repo.
rg -nP --type=ts -C2 '\b(useEnsurePersistedTerminals|terminalSessionPending)\b'
rg -nP --type=ts -C2 '\blauncher\.ensure\s*\('
rg -nP --type=ts -C2 'ensure\s*\(\s*[a-zA-Z_]*terminalId'

Repository: superset-sh/superset

Length of output: 46


PR description claims features that do not exist in the implementation.

The PR objectives reference a launcher.ensure(terminalId) method, a useEnsurePersistedTerminals rehydration hook, and a terminalSessionPending shared map. None of these appear anywhere in the codebase. The current TerminalLauncher interface only exposes create().

Before merge, either update the PR description to reflect what actually ships, or complete the implementation to match the stated objectives. This ensures reviewers and the test plan align with the actual code.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/useV2TerminalLauncher/useV2TerminalLauncher.ts
around lines 19 - 27, The PR description advertises missing APIs
(launcher.ensure, useEnsurePersistedTerminals, terminalSessionPending) but the
implementation only defines TerminalLauncher.create; either update the PR text
or implement the missing features—preferably implement: add an
ensure(terminalId: string): Promise<void> method to the TerminalLauncher
interface and its concrete launcher implementation to idempotently
confirm/rehydrate a session, add a useEnsurePersistedTerminals hook that reads
persisted terminal IDs and calls launcher.ensure for each on mount, and
introduce a shared terminalSessionPending map (or similar store) used by
create() and ensure() to track in-flight session creation so callers can await
the same Promise; update any types and tests referencing TerminalLauncher to
include ensure and wire the hook to the launcher.

Kitenite added 2 commits May 5, 2026 14:59
…re stable

The hook returned a fresh `{ create }` object every render, invalidating
the [launcher] dep array in every consumer (preset hook, hotkeys, pane
actions, context menu, openers) on each parent render and pointlessly
recomputing their useCallback / useMemo bodies. Wrap in useMemo so
launcher's reference is stable across renders.
@Kitenite Kitenite merged commit 0db7cd4 into main May 5, 2026
9 checks passed
@Kitenite Kitenite deleted the debug-presets-new-tabs branch May 5, 2026 22:24
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 5, 2026

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ✅ Neon database branch

Thank you for your contribution! 🎉

saddlepaddle pushed a commit that referenced this pull request May 6, 2026
* fix(desktop): v2 preset commands run reliably in new tabs

Presets that opened multiple new tabs (`new-tab-per-command` mode) only
ran their command in the LAST tab — earlier tabs sat empty until the
user clicked them. The v2 workspace renders only the active tab, so
each `addTab` in the loop flipped `activeTabId` and unmounted the prior
tab before its `TerminalPane` could call `terminal.createSession`.
Background tabs never spawned a PTY.

Move session creation off the pane and onto the call site:

- `useV2TerminalLauncher` is the single seam in v2 renderer code that
  calls `terminal.createSession.mutate(...)`. One method, async:
  `create({ command?, terminalId? }) => Promise<string>` mints a fresh
  id when terminalId is omitted, adopts the given id otherwise (relying
  on host-service createSession idempotency).
- Every v2 site that creates a terminal pane now awaits
  `launcher.create(...)` before writing the pane into the store: preset
  hook, hotkeys (NEW_GROUP / SPLIT_*), pane action button, context menu
  splits, `useWorkspacePaneOpeners.addTerminalTab`. Multi-pane / per-
  command presets fan out via `Promise.all` so they don't pay serial
  latency.
- `TerminalPane.mount` is back to plain `mount() + connect()`. It never
  calls createSession; if a terminalId arrives without a live session,
  it's a launching-call-site bug, not the pane's problem.
- `TerminalPaneData.initialCommand` is gone — pane data is just the
  terminalId now.

* fix(desktop): memoize useV2TerminalLauncher return so consumer deps are stable

The hook returned a fresh `{ create }` object every render, invalidating
the [launcher] dep array in every consumer (preset hook, hotkeys, pane
actions, context menu, openers) on each parent render and pointlessly
recomputing their useCallback / useMemo bodies. Wrap in useMemo so
launcher's reference is stable across renders.
MocA-Love added a commit to MocA-Love/superset that referenced this pull request May 21, 2026
superset-sh#4101 import agents as v2 terminal presets with live link:
- useV2PresetExecution.ts: drop upstream useV2AgentConfigs + useWorkspace
  import. useWorkspace arrives with superset-sh#4067 (cycle 44a WorkspaceProvider);
  fork keeps its workspaceId/projectId props until then.
- V2AgentsSettings.tsx: adopt upstream initialAgentPresetId prop and
  useV2AgentConfigs re-import (fork retains the hook elsewhere).
- PresetEditorDialog.tsx / V2PresetsSection.tsx: adopt upstream Switch /
  Link / ExternalLink additions for live-link UX.
- useMigrateV1PresetsToV2/: re-delete fork-removed hook; drop import
  from V2PresetsSection.tsx (call sites were stub-only).
- PresetEditorDialog.tsx: dedupe Switch import (one survived from each
  side of the merge).

superset-sh#4107 v2 preset commands in new tabs (skipped): introduces
useV2TerminalLauncher hook that takes a launcher prop through
useV2PresetExecution / useDefaultContextMenuActions / useDefaultPaneActions
/ useWorkspacePaneOpeners. This pattern is tightly coupled to superset-sh#4067's
WorkspaceProvider (workspaceId/projectId stop being passed down by page.tsx
and instead come from useWorkspace()). Deferred to cycle 44a so the
launcher and provider land together.

superset-sh#4112 include agent args (skipped): depends on superset-sh#4107.

superset-sh#4057 / superset-sh#4083 fix-compat:
- RunInWorkspacePopoverV2.tsx / OpenInWorkspaceV2.tsx: drop iconUrl from
  project map and ProjectThumbnail props; pass githubOwner={null} until
  cycle 41 (superset-sh#3823 v2 project icon settings) lands.

Phase 4 cycle 39 net: 3 cherry-picks landed (superset-sh#4057, superset-sh#4083, superset-sh#4101);
2 deferred to cycle 44a (superset-sh#4107, superset-sh#4112). useV2PresetExecution preserves
fork's workspaceId/projectId calling convention.
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.

1 participant