feat(webui): workspace picker entry coverage — home chip dropdown and sidebar project-row switch - #27
Merged
Merged
Conversation
added 2 commits
September 26, 2026 00:01
The picker built in basic-features/01 only had an entry point inside
the conversation view's WorkspacePanel. Two surfaces were dead:
1. The home-screen workspace chip ("demo002" button on the
greeting screen, chat.tsx HomeState) was a button with no
onClick — clicking it did nothing.
2. Sidebar project rows (session-tree.tsx ProjectNode) only
expanded/collapsed on click; there was no "switch to this
workspace" action.
This ticket adds both entry points without changing the existing
conversation-view entry or the picker itself.
Implementation
A. WorkspaceChipDropdown (new components/workspace-picker.tsx) —
Two-level pattern, pr-22 style:
- Level 1: an antd Dropdown anchored to the home chip, with the
same mavis-dropdown classes the composer uses so the chrome
reads native.
* 最近 — recents from /api/workspace/recent; the active
row carries a ✓ glyph. Single click switches.
* 选择新项目 — opens the full WorkspacePickerModal (Level 2).
* 不需要项目 — switches to no-workspace (tmpdir), same path
as the existing Recents tab's no-workspace
button.
- Level 2 is the existing WorkspacePickerModal (exported from
panels.tsx) — one component, one source of truth, mounted
independently by the home chip. Both call sites use the same
instance type; nothing shared.
B. ProjectRowSwitchAction (new components/workspace-picker.tsx) —
A 22px icon button on the hover slot next to the existing
'new task in this project' plus. Click calls
runAction(api.setWorkspace(repoPath)) with the project's first
repoPath. event.stopPropagation() keeps single-click expand/
collapse unchanged.
Files
- components/workspace-picker.tsx (new) — WorkspaceChipDropdown +
ProjectRowSwitchAction + the dropdown chrome. lastSegment is a
duplicate of shell.tsx#workspaceLeaf so the chip's display can
live in this module without dragging in the entire shell.
- components/panels.tsx — WorkspacePickerModal is now exported
(was a module-local function), so the home chip's Level 2 can
mount it independently.
- components/chat.tsx — HomeState now mounts WorkspaceChipDropdown
in place of the inert button. Removed the manual
workspace.dir.split('/').filter(Boolean).pop() display; the
dropdown owns it now.
- components/session-tree.tsx — ProjectNode's hover slot now
carries ProjectRowSwitchAction alongside the existing RowAction.
- lib/i18n.ts — 4 new bilingual keys
(workspace.chipDropdown.recent|chooseNew|noProject,
workspace.projectRow.switch).
- webapp/test/workspace-chip.test.ts (new) — pins lastSegment
parsing, the active-row predicate, and the row order.
Out of scope (per ticket)
- Sidebar git-branch chip and quick-action chips from the pr-22
reference screenshots.
- Sidebar per-file 'switch' affordance (the chip dropdown covers
workspace-level switching; the per-directory case is reachable
through Level 2's directory picker).
Gates
- pnpm --filter @mavis/webui webapp:typecheck — 0 errors
- pnpm test:webapp — 200 / 200 / 0 fail
- pnpm build — passes (6253 source files)
- pnpm check:source — passes (4560 files)
- pnpm typecheck (root) — 0 errors
Acceptance round 2 found the workspace chip crashed the entire app
on click: rc-dropdown threw 'React.Children.only expected to receive
a single React element child' on overlay mount, and the uncaught
render error unmounted the document.
Root cause: in components/workspace-picker.tsx, <Dropdown> was given
an overlayClassName but no popupRender or menu prop, so antd had
nothing to render into the overlay and rc-dropdown threw. composer.tsx
already uses the popupRender={() => <customPanel/>} pattern for its
permission / model selectors; match it here.
Fix:
- components/workspace-picker.tsx: <Dropdown> now declares
popupRender={() => dropdownContent}, matching composer.tsx#SelectPanel.
Comment explains the antd overlay contract so a future refactor
doesn't reintroduce the regression.
Test: the webapp test suite has no jsdom / @testing-library/react
('chat-virtual-list.test.ts: no jsdom in this suite' is explicit), so
mounting the Dropdown here would require pulling in a heavyweight
new harness. Instead, the new test in workspace-chip.test.ts reads
the source file and asserts the <Dropdown> mount has 'popupRender='
or 'menu=' as a static-source pin — same regression-against-future-
refactor shape as webapp-picker-wire.test.ts (the wire-shape pin from
the previous branch), but on the JSX surface rather than a wire
field. A real render smoke test would be preferred if the infra
existed; this is the lightweight alternative.
Live self-check (mandatory per acceptance feedback): started an
isolated dev instance (PORT=18092 MCODE_WEBUI_DEV_FRONTEND_PORT=18093
MCODE_WEBUI_DATA_DIR=/tmp/dev-wpe-check pnpm webui:dev) and verified
every path with agent-browser:
- chip click → dropdown renders, no crash
- 选择新项目 click → Level-2 modal opens (title 切换工作区,
Recents / Browse tabs visible)
- Escape → Level-2 modal closes cleanly
- 不需要项目 click → chip text becomes 'tmp'
(no-workspace state)
- sidebar 切换到此工作区 click → chip text becomes 'town'
(sidebar project switch)
- document.body has children throughout; the chip click is the only
way to reach the dropdown and there is no remaining crash path.
Gates
- pnpm --filter @mavis/webui webapp:typecheck — 0 errors
- pnpm test:webapp — 201 / 201 / 0 fail (the new static-source test
pins the popupRender contract)
- pnpm build — passes
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Follow-up to #26: the workspace picker only existed inside the conversation view's right panel — the home screen had no path to it (dead workspace chip), and sidebar project rows could never switch the workspace (click = expand only). Both were reproduced and reported by the user.
WorkspacePickerModal(Recents/Browse, same single-source component) / 不需要项目 → no-workspace (tmpdir) mode.stopPropagationon click + mousedown keeps expand/collapse and drag-select intact.WorkspacePickerModalexported frompanels.tsxas the single source of truth; conversation-view entry unchanged.popupRenderon the Dropdown).Acceptance (2 rounds, independent agent)
Round 1 FAIL — the
<Dropdown>was missingpopupRender,React.Children.onlycrashed and unmounted the whole app on chip click. Fixed in023303b; developer now live-self-checks render paths before handing off (new workflow rule).Round 2 PASS — live-verified on an isolated instance: dropdown renders/3× open-close cycles; recents row switch (chip label +
/api/stateagree); 选择新项目 → Level-2 browse → switch; 不需要项目 → tmpdir; sidebar hover switch + expand/collapse + "+" unaffected; conversation-view entry regression clean; static tripwire test judged legitimate.Gates (fresh, re-run by acceptance):
pnpm typecheck0 errors ·pnpm test:webapp201/201 ·pnpm build✓ ·pnpm check:source4560 files ✓. Fullpnpm verifydeferred to CI.Non-blocking nits noted for later: Escape doesn't close the dropdown (click-away does); no-workspace chip shows the tmpdir leaf name.