Skip to content

feat(desktop): Windows support - #9499

Open
waleedlatif1 wants to merge 72 commits into
stagingfrom
feat/desktop-windows
Open

waleedlatif1 wants to merge 72 commits into
stagingfrom
feat/desktop-windows

Conversation

@waleedlatif1

Copy link
Copy Markdown
Collaborator

Summary

Adds Windows (x64, NSIS) support to the desktop app. The macOS behaviour is unchanged.

Shell

  • Single instance: a relaunch focuses the existing window instead of opening another.
  • Per-channel AppUserModelID.
  • Windows File/Edit/View/Help menu (auto-hidden, Alt reveals it), Alt+Left/Right for Back/Forward.
  • Native frame whose caption follows Sim's theme.
  • Full-colour tray icon that opens Sim on left-click; with the tray on, Sim stays resident when the last window closes.
  • Foreground focus after the browser sign-in handoff; taskbar flash when a download finishes.

Feature gating

  • New platform field on the bridge (optional; older shells are all macOS).
  • One per-platform feature table (src/shared/platform-features.ts) drives the preload surface, IPC registration, executor registration and the background runner.
  • The web app now checks for each capability on the bridge instead of only checking that the desktop app is present.
  • Off on Windows for this release: Terminal, native local-file read/import, computer use, Help search, terminal themes, browser import and copy-password. The user-local/ VFS grants stay on.

Security

  • Windows paths are validated as text before any filesystem call. This refuses UNC, \\?\ and \\.\ paths, drive-relative and rooted paths, reserved device names, alternate data streams, and trailing dots or spaces. It closes a pre-consent SMB/NTLM leak via realpath.
  • File identity is compared as bigint.
  • Clipboard guard covers Shift+Insert, Ctrl+Insert and Shift+Delete; a Meta-only editing combo becomes Ctrl on Windows.
  • Windows-safe staged and downloaded file names.
  • Bounded retry of atomic renames when Windows holds a file lock.

Packaging and updates

  • NSIS one-click per-user installer with a separate install folder per channel (extraMetadata.name), multi-size .ico per channel, and signing of the .node files.
  • The NSIS updater drives ready from electron-updater, since there is no Squirrel step on Windows.
  • /api/desktop/update/latest.yml. A release without a Windows manifest means "no update", not 502.
  • /api/desktop/update/download?platform=windows.

CI

  • Windows release job with Azure Artifact Signing over OIDC, Authenticode and publisherName checks, and the packaged smoke test.
  • The Windows job is gated on its own signing probe and never blocks the macOS release.
  • package-smoke-windows leg in desktop-e2e.yml that runs the desktop unit suite and the packaged smoke test.

Rollout

No Windows artifacts are published until the Azure signing secrets and variables exist (see the README table). Until then the probe skips the job with a warning, so the macOS releases are unaffected.

Test plan

  • Targeted desktop and update-feed tests. Each new guard was reverted and its test went red.
  • CI Windows leg: unit suite, unsigned package, packaged smoke.
  • Windows Server 2025 EC2 QA box: build from source, NSIS install, launch, second-launch focus, uninstall. Results are in the PR comments.

- electron-builder: per-user one-click NSIS target with per-channel .ico,
  Authenticode for native addons, and platform-scoped node-pty prebuilds;
  the Computer Use helper moves under mac.
- node-pty win32 prebuilds pinned beside the darwin ones; ensure-pty-prebuilds
  fetches per host platform (ConPTY binaries on Windows) with Windows' own tar.
- build-native skips on Windows; install:local and package:share refuse to run
  off macOS.
- Committed icon*.ico generated from the per-channel dock icons by
  scripts/generate-windows-icons.ts; build.ts selects one per baked origin.
…NSIS updater

A per-platform descriptor picks the manifest (latest-mac.yml / latest.yml),
the exact release artifacts a feed may offer, and whether a native stage
follows the download. Windows has none: electron-updater's update-downloaded
fires after the installer's publisher signature is verified, so it drives
ready directly, and the confirmed restart installs silently and relaunches.
Self-update on Windows requires the packaged app-update.yml; otherwise the
manual-download pipeline offers the installer.
…tform

The bridge now carries an optional platform (absent shells are macOS). One
per-platform feature table drives the preload surfaces, the IPC table, the
background runner, and device registration, so Windows does not advertise
or accept the terminal, native file tools, computer use, terminal themes,
Chrome import, or password copy. The web app feature-detects the terminal,
folder grants, and password copy from the bridge, sends the platform with
chat capabilities, labels the tray setting for Windows, and keys the
title-bar lane on the bridge platform instead of the user agent.
The macOS app-menu shape (hide/hideOthers/unhide, a windowMenu whose Close
item takes Ctrl+W off macOS) is wrong on Windows. Build a Windows template
with the same items and accelerators, Exit via role quit, and hidden
Alt+Left/Alt+Right for history. The macOS template is unchanged.
A template image is invisible on a dark Windows taskbar. Use the
per-channel app icon with 1.25x/1.5x/2x representations, open Sim on a
left-click (Windows only shows the menu on right-click), and flash the
taskbar button when a download finishes in a background window.
…w chrome

A Windows relaunch arrives as second-instance and now surfaces the running
window instead of opening another. Claim the installer's AppUserModelID,
auto-hide the menu bar on native-frame windows, drop the menu from frameless
sheets, keep Sim resident in the tray when the last window closes, and lift
handoff-focused windows past Windows' foreground lock. The native caption
follows Sim's theme preference when the renderer reports one.
…enu on Windows

The preload sends the explicit sim-theme preference alongside the resolved
theme so main can follow an explicit choice on Windows. The credential
picker drops the app menu Windows attaches to every window, so its
accelerators cannot act inside the picker.
- update-feed: a per-platform descriptor (manifest, artifact names, installer
  preference). Windows builds publish independently, so a release without
  latest.yml is not a Windows candidate (404 no-release, never a 502), while
  a latest.yml naming anything but Sim-<v>-x64.exe rejects the release.
- New /api/desktop/update/latest.yml twin of latest-mac.yml, sharing one
  handler; the GitHub resolution moves into resolveDeploymentRelease.
- /api/desktop/update/download takes ?platform=mac|windows through a contract
  (default mac for shells that predate it); the update gate passes the shell's
  platform.
sim-setup desktop resolves the installer and feed for the host platform, and
the desktop and self-hosting docs cover the Windows installer, requirements,
endpoints, and the features Windows does not have yet.
Windows has no process groups, so teardown kills the app's tree with
taskkill /T before removal and retries removal while files unlock.
…lesystem

A model-supplied native path is now resolved without any fs call on
Windows: only drive-absolute and ~ paths pass, so a UNC path can no longer
make the shell authenticate to a remote host before consent, and device,
verbatim, drive-relative, and rooted paths are refused. Path segments in
native paths and localfs URIs must be plain NTFS names (no streams,
reserved devices, trailing dots or spaces, or invalid characters).
File identity checks compare bigint dev/ino, the missing Windows
O_NOFOLLOW is explicit, and the atomic store write retries a rename that
Defender or the indexer briefly blocks.
- desktop-release takes a platform input. The new build-windows job resolves
  the same channel identity (plus the package-name slug), signs through Azure
  Artifact Signing after an OIDC azure/login, and validates the exact
  Sim-<v>-x64.exe, its blockmap, latest.yml (version, url, path, sha512), each
  channel's updater cache, Authenticode on the installer and app exe, and the
  publisherName pinned in app-update.yml. Then it runs the packaged smoke and
  uploads with the manifest last.
- ci: a separate Azure signing probe gates the Windows stable and prerelease
  calls, which skip with a warning until configured. Nothing waits on them, so
  macOS publication never depends on the Windows build.
- desktop-e2e: a windows-latest leg runs the desktop unit suite and the
  packaged smoke against an unsigned unpacked x64 build.
Regenerating it inlines the bridge's relative imports (./local-files,
./title-bar, ./computer-use.generated), which the check cannot resolve.
The platform changes are additive and pass the check against the
existing snapshot.
… browser

The agent's clipboard guard also refuses Blink's Shift+Insert, Ctrl+Insert,
and Shift+Delete bindings off macOS, and a Cmd editing shortcut becomes
Ctrl there instead of the Windows key. Staged uploads and downloads get
NTFS-safe names on Windows. Password copy refuses in main as well as at
the bridge, and the credential storage comments describe DPAPI and the
confirmation fallback alongside Keychain and Touch ID.
Gate computer use through the platform feature table, guard the now-optional
terminal bridge in the e2e specs, and name the Windows File/Help menus in the
self-hosting docs and sim-setup output.
@waleedlatif1
waleedlatif1 requested a review from a team as a code owner October 31, 2026 04:43
@vercel

vercel Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
docs Skipped Skipped Oct 31, 2026 8:21am UTC

Request Review

Exclude the suites for subsystems Windows builds do not ship, keep POSIX
mode-bit assertions to POSIX, and build expected paths with path.join.
@greptile-apps

greptile-apps Bot commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 4/5

[Medium impact] Windows support reaches the desktop app, its update feed, and its release workflows.

Fix the closed-terminal path before merging so Chat cannot report a command that never ran.

Findings

  1. P1 Skipped commands look running ▶

Summary

Adds Windows desktop packaging, signing, menus, tray behavior, file access, and updates. The latest changes tighten terminal startup and Stop behavior.

  • Windows users can install Sim and use a native desktop shell.
  • Windows terminal sessions work with PowerShell and configured Git Bash.
  • Windows file access checks paths before touching the filesystem.
  • The desktop bridge exposes only features supported by each platform.
  • Windows installs download and update from Windows release artifacts.
  • Windows releases are signed and packaged in a separate CI path.

Reviews (3) · Last reviewed commit: "fix(desktop): type the agent's next comm..." · Reviewed by Greptile

Comment thread .github/workflows/desktop-e2e.yml Resolved
Comment thread apps/desktop/scripts/install-local.ts Resolved
electron-builder's dependency collector fails on a missing production
dependency but skips a missing optional one, and bun installs only the
host platform's prebuild, so each OS was failing on the other's packages.

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

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.

1 issue found and verified against the latest diff

Confidence score: 3/5

  • On / and app/(landing) pages, themePreference() in apps/desktop/src/preload/shell-theme.ts reads sim-theme instead of sim-landing-theme, so it can report a preference that doesn’t match the landing page theme. Read the landing-theme key for those routes.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/desktop/src/preload/shell-theme.ts">

<violation number="1" location="apps/desktop/src/preload/shell-theme.ts:20">
P2: `themePreference()` always reads the app's `sim-theme` key, but marketing pages (`/` and `app/(landing)` segments) theme from `sim-landing-theme`. On those pages the reported preference is from the wrong store, so the Windows caption and `nativeTheme.themeSource` follow the app choice while the page shows the landing toggle's theme. Read `sim-landing-theme` (default `light`) when the path is a landing path, mirroring `isLandingPath` in `apps/sim/lib/landing/routes.ts`.</violation>
</file>

This PR changes authentication, authorization, or input validation. Ultrareviews find 2.4x more serious bugs than standard reviews. Comment @cubic-dev-ai ultrareview to run one.

Turn on auto-fix | Re-trigger cubic

Comment thread .github/workflows/ci.yml Resolved
Comment thread apps/desktop/src/preload/shell-theme.ts Resolved
Comment thread apps/desktop/src/main/win32-path.ts Resolved
Comment thread apps/desktop/src/main/ipc.ts Resolved
Comment thread .github/workflows/desktop-e2e.yml Resolved
GitHub validates every job of a reusable workflow against the calling job's
permissions at startup, so the new id-token: write request in desktop-release
made the whole ci workflow fail to start.
isTerminalEnabled now requires the bridge to expose a terminal, which a
Windows shell omits.
CHANNEL_IDENTITIES (src/shared/channel-identity.ts) holds each channel's name,
app ID, and icons. The packager's channels.ts adds only the origin, install-local
reuses it, and the running app reads its name, AppUserModelID, and icons from
the same entries, so the runtime identity can no longer drift from the
installer's.
…er-platform layouts

buildMenuTemplate assembles the items both platforms share once and hands
them to buildWindowsMenu or buildMacMenu; each menu's contents are unchanged.
…the feed's select

feedErrorResponse answers both the manifest feed and the download redirect
when no release can be offered, and resolveDeploymentRelease returns the
release's assets for each route to read its own field.
addRepresentation decodes a PNG buffer directly, so each scale skips the
base64 data URL round trip.
…d loop

The array already puts latest.yml last, so one loop keeps the manifest after
the installer it names.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cubic-dev-ai review this PR

@cubic-dev-ai

cubic-dev-ai Bot commented Oct 31, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev-ai review this PR

@waleedlatif1 I have started the AI code review. It will take a few minutes to complete.

…suite

Browser import does not ship on Windows; its platform gate stays covered
by the import-service tests, which inject the platform.

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

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.

Review completed against the latest diff

Reply with feedback, questions, or to request a fix.

Turn on auto-fix | Re-trigger cubic

Comment thread apps/desktop/src/main/terminal/pty.ts Resolved
Comment thread packages/terminal-protocol/src/index.ts Resolved
Comment thread apps/desktop/src/main/terminal/pty.ts Resolved
Stop selected every direct child of the shell, so a server the user had
started in the background earlier in the same terminal was killed with
the command. The session records when the agent's command was typed, and
on Windows only children created since then are its processes. The
Windows suite starts a background child before the stubborn command and
checks it survives Stop.
…oss chunks

While the query is pending, a chunk's tail that could begin it is held
until the next chunk, which either completes the query or releases it.
cmd escapes with a caret, literal inside double quotes, and has no single
quotes; PowerShell escapes with a backtick.
Git for Windows' bin\bash.exe holds the pty and starts the real bash, so
the shell's direct children predate every command and Stop found nothing
to end. Stop now walks the shell's descendants and takes the outermost
ones created since the command was typed.
…rawn

Git Bash drops input typed while it is still switching from a native
program back to its prompt, so a command run straight after a stopped one
vanished. bash now reports its prompt's end (OSC 633 B, drawn by readline
at the end of PS1), as PowerShell already did, and a run into a shell
that reports it waits for the prompt, at most 2 s. The bash init file
also sets PROMPT_COMMAND before its DEBUG trap, so that assignment is no
longer announced as a command at startup.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cubic-dev-ai review this PR

@cubic-dev-ai

cubic-dev-ai Bot commented Oct 31, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev-ai review this PR

@waleedlatif1 I have started the AI code review. It will take a few minutes to complete.

if (!session.hasShellIntegration) {
await this.awaitShellStartup(session, latch)
}
await session.waitForPromptDrawn(latch.signal)

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 Skipped commands look running

If the user closes the terminal or the shell exits while waitForPromptDrawn() waits, closing resolves the wait and clears the busy state without aborting the call. run() then calls runCommand() on the dead session. Its writes do nothing, but its timer eventually returns status: 'running' even though nothing ran.

Check session.alive after this await and return SESSION_CLOSED before starting the command.

Suggested change
await session.waitForPromptDrawn(latch.signal)
await session.waitForPromptDrawn(latch.signal)
if (!session.alive) {
throw new TerminalError('SESSION_CLOSED', 'The shell closed before the command could start.')
}

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

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.

7 issues found across 125 files

Confidence score: 3/5

  • process-group.ts can force-kill an unrelated Windows process if the command exits and its PID is reused before taskkill runs. Retain and revalidate the process creation identity before stopping it.
  • index.ts can report a command as running after the shell exits during the wait, even though the command never started. Check session.alive after the wait and return SESSION_CLOSED.
  • describeRunningCommand in packages/terminal-protocol/src/index.ts splits on whitespace before removing quotes, so a quoted Windows executable path with spaces can be misidentified. Parse the quoted path before splitting it.
  • shell-integration.ts can use a stale $LASTEXITCODE for a later failing cmdlet. Track whether the accepted command updated it before using it to report the result.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/desktop/src/main/terminal/session.ts">

<violation number="1" location="apps/desktop/src/main/terminal/session.ts:626">
P2: `waitForPromptDrawn` keeps waiting while a command is running, so a `terminal_run` against a busy bash or PowerShell terminal stalls about 2 seconds before it reports BUSY. Resolve the wait when a command already holds the foreground.</violation>

<violation number="2" location="apps/desktop/src/main/terminal/session.ts:713">
P2: The `interrupted` flag is only set by `kill('SIGINT')`, so a Ctrl-C typed in the panel or sent with `terminal_input` does not set it. Set the flag where the `\u0003` byte is written so every Ctrl-C path is covered.</violation>
</file>

<file name="packages/terminal-protocol/src/index.ts">

<violation number="1" location="packages/terminal-protocol/src/index.ts:421">
P2: `describeRunningCommand` splits the shell line on whitespace before stripping quotes, so a quoted Windows path with spaces is cut in half. `& "C:\Program Files\nodejs\npx.cmd" create-app` under `pwsh` is labelled `Program`, and that label reaches the close dialog and tab title. Tokenize the segment quote-aware instead of using `split(/\s+/)`, and add a test for a quoted path with spaces.</violation>

<violation number="2" location="packages/terminal-protocol/src/index.ts:440">
P3: The new helper strips `.exe` from POSIX command labels too, changing macOS behavior for valid executables such as `/usr/local/bin/foo.exe`. Strip the extension only for Windows syntax while keeping `programName`'s Windows normalization.</violation>
</file>

<file name="apps/desktop/src/main/terminal/shell-integration.ts">

<violation number="1" location="apps/desktop/src/main/terminal/shell-integration.ts:408">
P2: This reports a stale native exit code for later failing cmdlets. Track whether the accepted command updated `$LASTEXITCODE` (or otherwise distinguish native failures) before using it; otherwise a cmdlet failure can be attributed the wrong exit status.</violation>
</file>

<file name="apps/desktop/src/main/terminal/process-group.ts">

<violation number="1" location="apps/desktop/src/main/terminal/process-group.ts:101">
P1: Windows Stop loses the process creation identity before invoking `taskkill`. If a command exits and Windows reuses its PID during this race, Stop can force-kill an unrelated process; retain and revalidate `(pid, createdAt)` immediately before killing the tree.</violation>
</file>

<file name="apps/desktop/src/main/terminal/index.ts">

<violation number="1" location="apps/desktop/src/main/terminal/index.ts:1393">
P2: Check `session.alive` after this wait and return `SESSION_CLOSED` before starting the command; otherwise a shell exit can make `run()` report a command as running even though it never ran.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Turn on auto-fix | Re-trigger cubic

commandRoots(await readProcesses(), shellPid, startedAt),
stillRunning: async (_shellPid, pids) => pids.filter(isRunning),
signal: (pids) => {
for (const pid of pids) killProcessTree(pid)

@cubic-dev-ai cubic-dev-ai Bot Oct 31, 2026 •

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: Windows Stop loses the process creation identity before invoking taskkill. If a command exits and Windows reuses its PID during this race, Stop can force-kill an unrelated process; retain and revalidate (pid, createdAt) immediately before killing the tree.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At apps/desktop/src/main/terminal/process-group.ts, line 101:

<comment>Windows Stop loses the process creation identity before invoking `taskkill`. If a command exits and Windows reuses its PID during this race, Stop can force-kill an unrelated process; retain and revalidate `(pid, createdAt)` immediately before killing the tree.</comment>

<file context>
@@ -1,34 +1,193 @@
+      commandRoots(await readProcesses(), shellPid, startedAt),
+    stillRunning: async (_shellPid, pids) => pids.filter(isRunning),
+    signal: (pids) => {
+      for (const pid of pids) killProcessTree(pid)
+    },
+    escalations: ['SIGTERM'],
</file context>
Fix with cubic

// SIGINT is delivered as a keystroke so the foreground process group gets
// it the way Ctrl-C would, rather than only the shell.
if (signal === 'SIGINT') {
if (this.pendingCommand) this.pendingCommand.interrupted = true

@cubic-dev-ai cubic-dev-ai Bot Oct 31, 2026 •

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: The interrupted flag is only set by kill('SIGINT'), so a Ctrl-C typed in the panel or sent with terminal_input does not set it. Set the flag where the \u0003 byte is written so every Ctrl-C path is covered.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At apps/desktop/src/main/terminal/session.ts, line 713:

<comment>The `interrupted` flag is only set by `kill('SIGINT')`, so a Ctrl-C typed in the panel or sent with `terminal_input` does not set it. Set the flag where the `\u0003` byte is written so every Ctrl-C path is covered.</comment>

<file context>
@@ -639,6 +710,7 @@ export class TerminalSession {
     // SIGINT is delivered as a keystroke so the foreground process group gets
     // it the way Ctrl-C would, rather than only the shell.
     if (signal === 'SIGINT') {
+      if (this.pendingCommand) this.pendingCommand.interrupted = true
       this.write('\u0003')
       return
</file context>
Fix with cubic

resolve()
}
const check = () => {
if (this.promptDrawn || !this.reportsPromptEnd || this.disposed) done()

@cubic-dev-ai cubic-dev-ai Bot Oct 31, 2026 •

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: waitForPromptDrawn keeps waiting while a command is running, so a terminal_run against a busy bash or PowerShell terminal stalls about 2 seconds before it reports BUSY. Resolve the wait when a command already holds the foreground.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At apps/desktop/src/main/terminal/session.ts, line 626:

<comment>`waitForPromptDrawn` keeps waiting while a command is running, so a `terminal_run` against a busy bash or PowerShell terminal stalls about 2 seconds before it reports BUSY. Resolve the wait when a command already holds the foreground.</comment>

<file context>
@@ -561,6 +607,31 @@ export class TerminalSession {
+        resolve()
+      }
+      const check = () => {
+        if (this.promptDrawn || !this.reportsPromptEnd || this.disposed) done()
+      }
+      timer = setTimeout(done, PROMPT_DRAW_MAX_MS)
</file context>
Suggested change
if (this.promptDrawn || !this.reportsPromptEnd || this.disposed) done()
if (this.promptDrawn || !this.reportsPromptEnd || this.disposed || this.foregroundCommand !== null) done()
Fix with cubic

.split('/')
.filter(Boolean)
.pop() ?? '')
? lastPathSegment(program.replace(/^['"]|['"]$/g, ''), syntax.windowsPaths ? /[\\/]/ : '/')

@cubic-dev-ai cubic-dev-ai Bot Oct 31, 2026 •

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: describeRunningCommand splits the shell line on whitespace before stripping quotes, so a quoted Windows path with spaces is cut in half. & "C:\Program Files\nodejs\npx.cmd" create-app under pwsh is labelled Program, and that label reaches the close dialog and tab title. Tokenize the segment quote-aware instead of using split(/\s+/), and add a test for a quoted path with spaces.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/terminal-protocol/src/index.ts, line 421:

<comment>`describeRunningCommand` splits the shell line on whitespace before stripping quotes, so a quoted Windows path with spaces is cut in half. `& "C:\Program Files\nodejs\npx.cmd" create-app` under `pwsh` is labelled `Program`, and that label reaches the close dialog and tab title. Tokenize the segment quote-aware instead of using `split(/\s+/)`, and add a test for a quoted path with spaces.</comment>

<file context>
@@ -371,15 +418,29 @@ export function describeRunningCommand(running: string): string {
-          .split('/')
-          .filter(Boolean)
-          .pop() ?? '')
+      ? lastPathSegment(program.replace(/^['"]|['"]$/g, ''), syntax.windowsPaths ? /[\\/]/ : '/')
       : '') || segment
   return truncate(label, MAX_RUNNING_COMMAND_LABEL, '…')
</file context>
Fix with cubic

}
if ($Global:__SimInCommand) {
$Global:__SimInCommand = $false
$exitCode = if ($succeeded) { 0 } elseif ($nativeExitCode -is [int] -and $nativeExitCode -ne 0) { $nativeExitCode } else { 1 }

@cubic-dev-ai cubic-dev-ai Bot Oct 31, 2026 •

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: This reports a stale native exit code for later failing cmdlets. Track whether the accepted command updated $LASTEXITCODE (or otherwise distinguish native failures) before using it; otherwise a cmdlet failure can be attributed the wrong exit status.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At apps/desktop/src/main/terminal/shell-integration.ts, line 408:

<comment>This reports a stale native exit code for later failing cmdlets. Track whether the accepted command updated `$LASTEXITCODE` (or otherwise distinguish native failures) before using it; otherwise a cmdlet failure can be attributed the wrong exit status.</comment>

<file context>
@@ -279,13 +314,115 @@ __sim_precmd() {
+    }
+    if ($Global:__SimInCommand) {
+      $Global:__SimInCommand = $false
+      $exitCode = if ($succeeded) { 0 } elseif ($nativeExitCode -is [int] -and $nativeExitCode -ne 0) { $nativeExitCode } else { 1 }
+      $markers += (__Sim-Osc "D;$exitCode")
+    }
</file context>
Fix with cubic

if (!session.hasShellIntegration) {
await this.awaitShellStartup(session, latch)
}
await session.waitForPromptDrawn(latch.signal)

@cubic-dev-ai cubic-dev-ai Bot Oct 31, 2026 •

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: Check session.alive after this wait and return SESSION_CLOSED before starting the command; otherwise a shell exit can make run() report a command as running even though it never ran.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At apps/desktop/src/main/terminal/index.ts, line 1393:

<comment>Check `session.alive` after this wait and return `SESSION_CLOSED` before starting the command; otherwise a shell exit can make `run()` report a command as running even though it never ran.</comment>

<file context>
@@ -1394,6 +1390,7 @@ export class TerminalService {
     if (!session.hasShellIntegration) {
       await this.awaitShellStartup(session, latch)
     }
+    await session.waitForPromptDrawn(latch.signal)
     if (session.isBusy) {
       throw new TerminalError(
</file context>
Suggested change
await session.waitForPromptDrawn(latch.signal)
await session.waitForPromptDrawn(latch.signal)
if (!session.alive) {
throw new TerminalError('SESSION_CLOSED', 'The terminal session closed before the command started.')
}
Fix with cubic

.split(separator)
.filter(Boolean)
.pop()
?.replace(/\.exe$/i, '') ?? ''

@cubic-dev-ai cubic-dev-ai Bot Oct 31, 2026 •

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.

P3: The new helper strips .exe from POSIX command labels too, changing macOS behavior for valid executables such as /usr/local/bin/foo.exe. Strip the extension only for Windows syntax while keeping programName's Windows normalization.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/terminal-protocol/src/index.ts, line 440:

<comment>The new helper strips `.exe` from POSIX command labels too, changing macOS behavior for valid executables such as `/usr/local/bin/foo.exe`. Strip the extension only for Windows syntax while keeping `programName`'s Windows normalization.</comment>

<file context>
@@ -371,15 +418,29 @@ export function describeRunningCommand(running: string): string {
+      .split(separator)
+      .filter(Boolean)
+      .pop()
+      ?.replace(/\.exe$/i, '') ?? ''
+  )
+}
</file context>
Fix with cubic

This branch was previously deployed

1 inactive deployment
Preview — 9fa43164 Deployed Oct 31, 2026 by vercel[bot]
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