Skip to content

feat(webui): cross-platform picker paths with env expansion; drop native picker - #28

Merged
fengzhi09 merged 1 commit into
mainfrom
feat/workspace-picker-paths
Sep 25, 2026
Merged

fengzhi09 merged 1 commit into
mainfrom
feat/workspace-picker-paths

Conversation

@fengzhi09

Copy link
Copy Markdown
Collaborator

What

User feedback: the picker's path input must work across win/mac/linux with system environment variables and absolute paths — too many "path illegal" failures — and the native OS picker is not wanted.

  • Server-side expandUserPath (lib/workspace.js): ~/~/foo/~\bar; allow-listed env vars $HOME/$USERPROFILE/$TMPDIR/$TEMP/$TMP and Windows-style %USERPROFILE%/%TEMP%/%TMP% with mid-path interpolation; alias fall-through (%TEMP% resolves via TMPDIR); unknown variables left literal (no env probing); wrapping quotes stripped. Expansion runs BEFORE containment validation.
  • Actionable containment errors: rejection returns {error, roots[]} and the picker renders "路径必须在以下位置之一: …" instead of a bare failure.
  • Native OS picker fully removed: /api/workspace/pick route + handler, lib helpers, webapp api wrapper, toolbar button, i18n keys, API docs — the UI was the only consumer.
  • 26 new tests (20 server expansion/containment cases incl. Windows shapes, 5 webapp source-scan, wire-shape extension for the 400+roots[] contract).

Acceptance (independent agent, live browser)

PASS-WITH-CONCERNS — all 5 ticket criteria met: ~/$HOME/文档 expansion browsed; out-of-roots error renders the allowed-roots list and recovers without reload; adversarial ~/.. and $HOME/../../etc both containment-rejected (realpath-based check after expansion); native picker gone (UI + grep); regression clean on Browse/mkdir/confirm/Recents/chip/conversation entry. Gates re-run fresh: typecheck 0, test:webapp 207/207, workspace-paths 20/20, build ✓, check:source ✓.

Non-blocking notes: relative paths resolve against server CWD (documented; not the browsed dir); UNC paths uncovered; %TEMP% with no TEMP/TMP/TMPDIR set falls back to HOME (intentional).

Full pnpm verify deferred to CI (windows-latest will exercise the Windows path shapes).

basic-features/03: cross-platform path input, env/tilde expansion, and
removal of the native OS picker per ticket feedback. The native picker
(zenity/kdialog/osascript/PowerShell) was both unreliable in the
target environments and out of scope; the in-product
WorkspacePickerModal is now the only workspace-pick surface.

1. Cross-platform path expansion — server/lib/workspace.js
   expandUserPath(input):
   - '~', '~/foo', '~\bar'           -> home + remainder
   - '$HOME', '$USERPROFILE'         -> POSIX-style
   - '%USERPROFILE%', '%TEMP%'       -> Windows-style
   - '%VAR%' / '$VAR' inside path    -> mid-path interpolation
   - 'C:\...', '/abs/path'           -> passes through; path.resolve
                                     handles backslashes natively on
                                     Windows, on POSIX a Windows-style
                                     path survives verbatim so the user
                                     sees an actionable ENOENT
   - '"abs"' / ''abs''                -> wrapping quotes stripped
   - empty / whitespace / non-string -> null (caller surfaces
                                     "path required" error)
   - allow-list for env vars: HOME / USERPROFILE / TMPDIR / TEMP /
     TMP. Unknown $VAR / %VAR% is left literal — strict allow-list
     is the standard shell-quoting answer; arbitrary interpolation
     would let an unprivileged user probe env-var presence. TMPDIR/
     TEMP/TMP fall through the alias chain so '%TEMP%' resolves
     when only TMPDIR is set (the typical POSIX case).

2. Containment error carries the allowed roots — the picker renders
   'Path must be under: ...' instead of a bare 'illegal':
   - server/lib/workspace.js#browseWorkspace and #assertWorkspacePath
     now return {ok:false, error, roots: [...]} on rejection;
     roots[] is the actionable hint.
   - lib/api.ts#browseWorkspace is rewritten to bypass the generic
     request() helper — that helper throws on non-OK and would
     strip the roots[] payload. The route returns the same body on
     200 and 400; the picker treats them identically.
   - components/panels.tsx WorkspaceBrowseTab stores errorRoots in
     state and renders `data-testid="workspace-picker-error-roots"`
     below the error message when present.
   - i18n: 'workspace.picker.mustBeUnder' (en + zh).

3. Native picker removed:
   - server/lib/workspace.js: pickDirectoryNative + tryKdialog +
     the entire native-spawn block deleted.
   - server/routes/workspace.js: handleWorkspacePick deleted;
     pickDirectoryNative import dropped.
   - server/app.js: 'POST /api/workspace/pick' route + 'routes/'
     listing entry removed.
   - webapp/components/panels.tsx: 'Open native picker' button +
     nativePick + nativeSupported deleted. Toolbar now reads
     'parent, path, home, root, mkdir' (no native).
   - webapp/lib/api.ts: pickWorkspaceNative wrapper deleted.
   - lib/i18n.ts: 'workspace.picker.native[Unsupported]' keys
     deleted (en + zh).
   - docs/API.md + API.zh-CN.md: section removed.
   - test/server/app-hono.test.js: 'POST /api/workspace/pick'
     route listing entry removed.

Tests
- test/lib/workspace-paths.check.mjs (new): 20 cases for
  expandUserPath (POSIX + Windows shapes, mid-path interpolation,
  trailing separators, wrapping quotes, allow-list strictness)
  and the containment error payload. The Windows-shape assertions
  use the verbatim env-var value; the platform-dependent join is
  exercised separately by integration tests on POSIX.
- test/routes/workspace.check.mjs: 'browse containment rejection'
  test pins the new error payload (status 400 + error + roots[],
  no path).
- webapp/test/workspace-picker-paths.test.ts (new): 5 cases
  scanning source for the native-picker removal and the errorRoots
  UI slot. Same source-scan pattern as workspace-picker-wire.test.ts
  from basic-features/02 — the webapp suite has no jsdom / RTL so
  a real render smoke test is out of scope, and the previous
  ticket's acceptance feedback established this is the lightweight
  alternative the webapp side can afford.

Live self-check (mandatory per acceptance feedback):
  PORT=18092 MCODE_WEBUI_DEV_FRONTEND_PORT=18093
  MCODE_WEBUI_DATA_DIR=/tmp/dev-wpp-check pnpm webui:dev; agent-browser:
    - chip click                 -> dropdown opens (no crash)
    - choose-new                 -> Level-2 modal opens
    - browse tab                 -> path input visible
    - '~/acer09' + Enter         -> listing shows real subdirs
                                  (taikong-jiqi / pelican-saturn /
                                  qingming / qingming-3d / review —
                                  confirms tilde expansion landed
                                  on /home/acer09/acer09)
    - '/var/log/private-out-of-roots-dir' + Enter
                              -> error renders with the roots list:
                                  'Path must be under: /home/acer09,
                                   /home/acer09/.../demo002, /tmp'
    - no-project                 -> chip text becomes 'tmp'
                                  (no-workspace)
    - toolbar no longer renders 'Open native picker'
  Dev server stopped after.

Gates
- pnpm typecheck (root)                 - 0 errors
- pnpm --filter @mavis/webui webapp:typecheck - 0 errors
- pnpm test:webapp                     - 207 / 207 / 0 fail
- pnpm build                            - passes (6253 source files)
- pnpm check:source                     - passes (4562 files)
- targeted server tests (workspace-paths,
  workspace.check, app-hono, models)    - 43 pass / 1 skipped /
                                           0 fail (the skip is a
                                           win32-only case)
@fengzhi09
fengzhi09 merged commit 645268f into main Sep 25, 2026
8 checks passed
@fengzhi09
fengzhi09 deleted the feat/workspace-picker-paths branch September 25, 2026 17:22
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