Skip to content

fix(server): installed editors no longer go undetected on Windows - #6221

Open
JstnMcBrd wants to merge 2 commits into
pingdotgg:mainfrom
JstnMcBrd:mcb/win-editor-discovery
Open

fix(server): installed editors no longer go undetected on Windows#6221
JstnMcBrd wants to merge 2 commits into
pingdotgg:mainfrom
JstnMcBrd:mcb/win-editor-discovery

Conversation

@JstnMcBrd

@JstnMcBrd JstnMcBrd commented Aug 11, 2026

Copy link
Copy Markdown

Hi, I'm a new user trying out T3 Code for the first time. I encountered a bug, so I thought I'd get some practice by using T3 Code to fix T3 Code.

Thanks for taking the time to review. I hope you'll find this valuable. Feedback is much appreciated.

AI Disclosure:
Much of this code and PR description were written with Claude Opus 5 in T3 Code.
However, I have reviewed and revised everything myself.

Problem

On Windows, the "Open in…" picker listed no editors and stayed disabled, even with VS Code installed.

server.getConfig resolves 22 editor commands through PATH under a 5s timeout that returns [] on expiry. The scan probed every (directory, command, extension) combination: ~40 directories × 24 candidate names × 22 commands ≈ 21,000 filesystem probes, 14.4s measured. It never finished in time. Other platforms (Linux, macOS) have no PATHEXT and probe one candidate per directory, so they stayed well inside the budget.

Solution

Read and cache each PATH directory once and match candidates against the listing. Same discovery: 14,375ms → 85ms.

The cache moved to an axis that actually repeats

Before: Keyed per command, and a pass resolves each command once — so the cache is never hit:

for each of 22 editors: cursor, trae, code, zed, idea, ...
  isCommandAvailable("code")
    PATHEXT = .COM .EXE .BAT .CMD .VBS .VBE .JS .JSE .WSF .WSH .MSC .CPL
    candidates = each extension, upper AND lower ................ 24 names

    for each of ~40 PATH directories:
      ├── stat("C:\Windows\system32\code.COM")   ✗   ← a real disk probe
      ├── stat("C:\Windows\system32\code.com")   ✗
      ├── stat("C:\Windows\system32\code.EXE")   ✗
      ├── ... 24 probes, all misses ...
      └──                                            = 24 probes / directory

    = ~960 probes for ONE command

    cache["win32|PATH|PATHEXT|code"] = result    <- written here, but a pass
                                                  resolves "code" only once,
                                                  so it is never read

 22 commands x ~960 probes  =  ~21,000 filesystem probes
                            =  14,375 ms   (measured on my machine)

 14.4s  >  5s timeout  ──►  availableEditors = []  ──►  Open disabled

After: Keyed per directory, one listing serves every command:

for each of 22 editors: cursor, trae, code, zed, idea, ...
  isCommandAvailable("code")
    for each of ~40 PATH directories:
      │
      ├── names = cache["C:\Windows\system32"]       <- filled by the FIRST command
      │     │       or readdir() on a miss              to touch this directory,
      |     |                                           reused by the other 21
      │     ▼
      │   { "cmd.exe", "notepad.exe", "where.exe", ... }   in memory
      │
      └── for each of 24 candidates:
            "code.com" in the set?  no  ──► skip.  no disk probe at all
            "code.cmd" in the set?  YES ──► stat(...)  ← the only probe,
                                                          and it confirms

~40 directory reads TOTAL for all 22 commands
                            =  71-101 ms   (measured on my machine)

 0.1s  <  5s timeout  ──►  ["vscode", "file-manager"]  ──►  Open enabled

Same 30s TTL, same monotonic-clock expiry — only the key changed.

Results are identical

A listing match only selects which candidates get stated — stat still decides what resolves, checking file type and the executable bit exactly as before.

Names fold to lowercase on every platform. This is necessary because the listing introduces a userspace name comparison that didn't exist before; previously the kernel did all name matching inside stat. Folding can only widen the candidate set, and the OS validates each one anyway, so an extra match costs one stat — whereas exact matching would miss a real command on a case-insensitive volume (Windows, default APFS).

Two behaviors preserved deliberately: a search-only directory (read permission denied) falls back to probing candidates directly, and explicit paths never consult the cache, so a just-installed binary stays immediately visible.

Changes

File
packages/shared/src/shell.ts Directory listings replace per-combination probing; cache keyed by directory
packages/shared/src/shell.test.ts +3 tests: case-insensitive match, permission-denied fallback, missing directory does zero probes
apps/server/src/process/externalLauncher.test.ts Mocks updated; memoization test asserts one directory read per pass
apps/server/src/process/externalLauncher.ts Stale comment
Before After
image image

38 tests pass; targeted lint, typecheck and format clean. Verified on Windows 11 — I have no macOS or Linux machine to test on.

Further Reading

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes NA
  • I included a video for animation/interaction changes NA

Note

Medium Risk
Changes shared PATH command-resolution used by editor discovery and other availability checks; incorrect listing/cache behavior could hide or mis-resolve installed commands.

Overview
Fixes Windows editor discovery timing out by replacing per-candidate stat probing with cached PATH directory listings.

resolveCommandPath now reads each PATH directory once (via listPathDirectory / PathListingCache), matches candidates against the lowercase name set, and only stats hits. Unreadable directories fall back to probing; missing/unusable ones are skipped. Same 30s TTL, but keyed by directory so one listing serves every command in a scan.

Tests cover case-insensitive Windows matches, listing-failure fallbacks, and updated editor-discovery memoization expectations.

Reviewed by Cursor Bugbot for commit 59849f9. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Fix editor detection on Windows by switching to directory-listing-based command resolution

  • Replaces per-command resolution caching with per-directory listing cache in shell.ts: each PATH entry is read once per 30-second window, with file names stored in lowercase for case-insensitive matching (fixing missed detections on Windows where extensions like .CMD differ in case).
  • PATH directories that fail listing for NotFound or BadResource reasons are skipped outright; directories that fail for other reasons (e.g. PermissionDenied) fall back to probing each candidate with stat.
  • Cache capacity is set to 256 entries (keyed per directory), shared across all command resolutions within the window.
  • Behavioral Change: command-level resolution results are no longer cached; caching now applies at the directory-listing level, so repeated lookups for different commands in the same directory share one cached listing instead of individual cached outcomes.

Macroscope summarized 59849f9.

Resolving a command probed every (directory, command, extension) combination: on Windows that is ~40 PATH directories times 24 candidate names, so editor discovery's 22 commands cost over 20,000 filesystem probes and took 14s on an ordinary machine. That overran the 5s timeout in server.getConfig, which yields an empty editor list on expiry, so no editors were ever detected and the Open button stayed disabled — intermittently, because the old per-command cache carried partial progress across reconnects and a lucky rescan occasionally finished in time. Other platforms have no PATHEXT and so probe one candidate per directory, which stayed well inside the timeout. Each PATH directory is now read once and candidates are matched against its listing, with the cache keyed by directory rather than by command; the same discovery takes 85ms. A listing match only selects which candidates to stat, so the stat still decides what resolves, and a directory that denies read permission falls back to probing directly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f5384c23-c118-44cf-af6e-d7a931fd39f0

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Aug 11, 2026
Comment thread packages/shared/src/shell.ts
@macroscopeapp

macroscopeapp Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR changes the core PATH resolution algorithm from per-command caching to per-directory listing cache, with new error-handling logic. While the bug fix intent is clear and tests are comprehensive, the algorithmic change to how Windows command detection works warrants human review.

You can customize Macroscope's approvability policy. Learn more.

Only three failure reasons prove a PATH entry can never hold a command: NotFound (ENOENT), BadResource (ENOTDIR/EISDIR/ELOOP) and BadArgument. Everything else — EBUSY, EIO and EPERM, the last two arriving unclassified as Unknown — is transient or unproven, and treating it as an empty listing ruled out every candidate without the stat fallback and cached that false negative for the full 30s window, so an installed command could briefly appear missing. Unproven failures now list as null, which makes the caller probe candidates directly the way it did before this cache existed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added size:L 100-499 changed lines (additions + deletions). and removed size:M 30-99 changed lines (additions + deletions). labels Aug 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L 100-499 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: No installed editors found

1 participant