Skip to content

fix(skills): match plugin workspace folders across Windows path spellings - #1883

Merged
vastsa merged 4 commits into
vastsa:mainfrom
SeanLiu326:fix/plugin-workspace-windows-spelling
Oct 26, 2026
Merged

vastsa merged 4 commits into
vastsa:mainfrom
SeanLiu326:fix/plugin-workspace-windows-spelling

Conversation

@SeanLiu326

Copy link
Copy Markdown
Contributor

What

isPluginWorkspace never matched on Windows, so the bundled pi-desktop/plugin-development skill was never offered for a workspace a plugin was already loaded from.

Root cause

apps/desktop/electron/main/builtin-skills.ts built its containment prefix with a literal / and compared spellings verbatim:

const prefix = workspacePath.endsWith("/") ? workspacePath : `${workspacePath}/`;
return pluginPaths.some((path) => path === workspacePath || path.startsWith(prefix));

The two inputs arrive from different worlds:

  • pluginPaths — plugins.listLoaded().map((loaded) => loaded.path), stored verbatim from the folder the user picked (dialog.showOpenDialog → native backslashes on Windows).
  • workspacePath — the host's session.get/workspace.get answer, which can persist a forward-slash spelling or the //?/X:/… long-path variant (electron/main/path-utils.ts exists precisely because both spellings occur).

Even with both sides natively backslashed, the /-suffixed prefix (C:\dev\repo/) never matched C:\dev\repo\plugins\my-plugin. On macOS/Linux both sides are forward-slashed, so only Windows broke. The manifest.json-at-root probe still rescued the single-plugin-at-root layout; a monorepo with plugins in a subdirectory got nothing. Both callers are affected: session launch (runtime/session-launch.ts:546) and the composer command build (ipc/composer-ipc.ts:63).

Fix

Containment now folds both sides before comparing — separators unified to /, the Windows verbatim prefix stripped in either spelling, trailing separators dropped, and case folded everywhere but Linux (the same platform split the worktree ref folding uses). The sibling-prefix boundary is preserved: repo-x never counts as inside repo. The comparison lives in a new dependency-free module (plugin-workspace-contains.ts) so its behavior is tested directly, following the plugin-session-push-gate.ts precedent.

Tests

apps/desktop/test/plugin-workspace-contains.test.mjs — separator/long-prefix/case unification, the workspace-root case, and the sibling-prefix boundary on both drive and POSIX shapes. 2/2 pass locally (Windows box, which is the broken platform); plugin-skills.test.mjs stays green.

…ings

isPluginWorkspace built its containment prefix with a literal "/" and
compared spellings verbatim, but the two inputs arrive from different
worlds: the plugin folder comes from a native dialog (backslashes on
Windows) while the workspace comes from the host, which can persist a
forward-slash or //?/X:/… spelling. On Windows no plugin path ever
matched, so the bundled plugin-development skill was never offered for a
workspace a plugin was already loaded from — the manifest.json probe only
rescued the single-plugin-at-root layout.

Containment now folds both sides (separators unified, Windows verbatim
prefix stripped, case folded everywhere but Linux, like the worktree ref
folding) in a dependency-free module so its boundary behavior — sibling
prefixes like repo-x must not count — is tested directly.

vastsa commented Oct 26, 2026

Copy link
Copy Markdown
Owner

The Windows separator defect is real, and I tested the new helper directly. It introduces a regression for a valid POSIX filesystem-root workspace: pluginWorkspaceContains('/', '/tmp/plugins/example') returns false because trimming trailing slashes turns / into the empty string; the existing prefix check returns true. Please preserve filesystem roots during normalization and add coverage for / (plus drive-root containment) before landing. Keep the sibling boundary test. This is a behavioral regression, independent of CI.

Folding a bare root by trimming its trailing separators turned "/" into the empty string, so pluginWorkspaceContains("/", "/tmp/plugins/example") answered false where the prefix check it replaced answered true. Preserve the POSIX root through the fold, compare every absolute path against it directly, and cover the root cases (/ and C:/) alongside the sibling boundary.
@SeanLiu326

Copy link
Copy Markdown
Contributor Author

Good catch — fixed in 1739a9c30 (branch now merges current main, head f487f436e).

The regression: fold trimmed trailing separators before deciding anything, so a bare root folded to "", hit the empty-workspace guard, and answered false. The prefix check it replaced treated / as "/" and matched every absolute path, so this was strictly narrower than the old behavior.

The fix: a path made only of separators is now recognized as the filesystem root and preserved through the fold (/, /// → /), and the POSIX root compares plugin.startsWith("/") directly instead of the ${workspace}/ spelling that would become //. Drive roots need no special case — C:/ folds to c: and the ordinary c:/… prefix test keeps working, including the //?/C:/ verbatim spelling.

Coverage added (kept the sibling boundary test untouched):

  • pluginWorkspaceContains("/", "/tmp/plugins/example") → true, ("/", "/") → true, ("///", …) → true, ("/", "tmp/plugins/example") → false
  • drive roots: ("C:/", "C:/dev/repo/plugins/p"), ("C:\", "C:\…"), ("C:/", "C:"), verbatim //?/C:/, and ("C:/", "D:/…") → false

test/plugin-workspace-contains.test.mjs + test/plugin-skills.test.mjs — 18/18 pass (cases use consistent casing so they hold on Linux too), tsc --noEmit clean.

vastsa commented Oct 26, 2026

Copy link
Copy Markdown
Owner

The revised root normalization addresses the regression from the previous review. Independently ran all 18 plugin-workspace/plugin-skill tests and Desktop typecheck, and bundled/imported the production isPluginWorkspace entry point to verify Windows nested-plugin detection, POSIX / workspaces and sibling rejection. All pass. The original Windows prefix check fails the nested-backslash case; the revised implementation fixes it while preserving root containment. Refreshing the integration candidate and proceeding to land this focused fix; CI status is not a blocker in this review.

@vastsa
vastsa merged commit c9184e7 into vastsa:main Oct 26, 2026
6 checks passed

This branch was previously deployed

1 inactive deployment
Preview — 8ad4a264 Deployed Oct 26, 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.

2 participants