Skip to content

feat(ui): split the composer add menu into upload, modes and skills - #1716

Merged
jackwener merged 2 commits into
mainfrom
feat/composer-toolbar-split
Jul 31, 2026
Merged

feat(ui): split the composer add menu into upload, modes and skills#1716
jackwener merged 2 commits into
mainfrom
feat/composer-toolbar-split

Conversation

@jackwener

Copy link
Copy Markdown
Member

Summary

The composer hid three unrelated capabilities behind one + menu: file import, the Plan/Swarm/Graph switches with the 专家团 group, and — for Skills — nothing at all. Skills were reachable only by typing /, which is discoverable once you know it exists and invisible otherwise.

Each capability now gets its own named trigger in the composer toolbar:

  • Upload — a direct button (Upload mark) that opens the file picker in one click instead of two. Import still no-ops mid-turn (runImportAction / drop target), so it disables while streaming rather than vanishing.
  • Modes — one menu holding Plan / Swarm / Graph plus the 专家团 group. It stays reachable mid-turn (fix(ui): keep the composer + menu reachable while streaming #1444), and feat(ui): subtract permanent chrome toward the Paseo visual benchmark #1433's subtraction still holds: the switches are menu items, not standalone toolbar switches. The active-mode indicator chips are unchanged.
  • Skills — a picker panel with a search field, 全选 / 清除全部, and checkbox rows carrying name + description, plus a selection count on the trigger. It writes the same structured selection the / popup produces (useComposerSkillDraft), so both paths share one draft instead of a second selection model.

The panel is an absolute overlay anchored to its own trigger, so opening it never grows the composer box (composer-constant-footprint-contract) — the same discipline the mention popup follows. Esc / outside click dismiss it, and Esc returns focus to the trigger.

Copy is locale-aware in both zh and en; composer.add / composer.addTitle retire with the + trigger they labelled.

Verification

  • npm --workspace @maka/ui run typecheck + clean build + test:dist — 290 passed, including the new composer-skill-picker.test.ts (key/filter helpers, closed-trigger markup, localization) and the updated conversation-localization contracts.
  • Desktop tsc -p tsconfig.renderer.json and tsconfig.storybook.json — clean.
  • Electron e2e (Playwright): new e2e/composer-toolbar-controls.spec.ts (2 tests — three separate triggers present with no + left over; picker → chip → count badge → Esc focus return → 清除全部) and the existing composer-mode-indicator.spec.ts + composer-skill-invocation.spec.ts (7 tests, proving the / path still shares the same draft) — 9 passed.
  • biome lint, check-a11y, check-copy, check-console — clean.
  • Renderer production build (build:renderer) — clean.

Review focus

The Skills picker is new UI surface: the panel geometry (340px, list capped at 240px with its own scroll) and the checkbox row semantics (role="checkbox" buttons inside the labelled dialog) are the parts most worth a second opinion.

The composer hid three unrelated capabilities behind one + menu: file
import, the Plan/Swarm/Graph switches with the expert teams, and nothing
at all for Skills — those were reachable only by typing `/`, which is
discoverable once you know it exists and invisible otherwise.

Each capability now has its own named toolbar trigger:

- upload: a direct button (Upload mark) that opens the file picker in one
  click instead of two. Import still no-ops mid-turn, so it disables while
  streaming rather than vanishing.
- modes: one menu holding Plan/Swarm/Graph plus the 专家团 group. It stays
  reachable mid-turn (#1444), and #1433's subtraction still holds — the
  switches are menu items, not standalone toolbar switches.
- skills: a picker panel with a search field, 全选 / 清除全部, and checkbox
  rows. It writes the SAME structured selection `/` produces
  (useComposerSkillDraft), so both paths share one draft rather than a
  second selection model.

The panel is an absolute overlay anchored to its trigger, so opening it
never grows the composer box (composer-constant-footprint-contract).
CI's desktop contract suite caught six violations the @maka/ui tests and
the check-* scripts do not cover:

- role="dialog" belongs to the Astryx Dialog primitive, which owns focus
  trapping, Escape, and the top layer. The picker is a disclosure the
  trigger's aria-expanded/aria-controls already describe, so it is a
  labelled role="group" instead (dialog-source-contract).
- outline-offset must be var(--focus-ring-offset); the inset calc() is
  gone (focus-ring recipe).
- the count badge used a bare 3px pad, a line-height of 1, and a
  --font-size-2xs fallback literal. It now sits on --space-0-5,
  --leading-none, and --font-size-caption, and is sized to fit that
  smallest governed size.
- the primitives contract pinned the retired + trigger's button={{...}}
  shape; it now pins the standalone upload IconButton (which carries the
  import-pending state) and the modes trigger that kept the governed
  icon-only ghost/sm shape.
@jackwener
jackwener force-pushed the feat/composer-toolbar-split branch from f12b1b8 to 6cd2490 Compare July 31, 2026 17:20
@jackwener
jackwener merged commit 6baba56 into main Jul 31, 2026
3 checks passed
jackwener added a commit that referenced this pull request Jul 31, 2026
These 149 tests read the project's own source and CSS off disk and
asserted on its text. They imported nothing under test and ran no code,
so they could only ever catch what a linter catches — "don't write a
bare 3px", "don't hand-write role=dialog" — while charging every
refactor a rewrite of its own guards. #1711 deleted two of them to land
an Astryx migration; #1716 had to rewrite six to move three buttons.

Deleted: 149 test files (~675 tests) plus one orphaned source helper.
Kept: the 219 behavioral tests in the same directory, every e2e spec,
and the fast scripts/check-*.mjs commands (dead-css, a11y, copy,
console) that cover the same ground in seconds without a build.

Also kept, deliberately, seven static guards that protect non-cosmetic
invariants rather than styling: the preload/IPC surface pins, the
renderer token-leak gate, external-link noopener, and secret redaction.
Those are cheap and guard things review reliably misses.

The CSS governance doc kept pointing at deleted guards, so its
enforcement section now says what actually holds the line: conventions
plus the check-* scripts, verified on the rendered surface.
jackwener added a commit that referenced this pull request Jul 31, 2026
* chore(desktop): drop the source-scanning contract test suite

These 149 tests read the project's own source and CSS off disk and
asserted on its text. They imported nothing under test and ran no code,
so they could only ever catch what a linter catches — "don't write a
bare 3px", "don't hand-write role=dialog" — while charging every
refactor a rewrite of its own guards. #1711 deleted two of them to land
an Astryx migration; #1716 had to rewrite six to move three buttons.

Deleted: 149 test files (~675 tests) plus one orphaned source helper.
Kept: the 219 behavioral tests in the same directory, every e2e spec,
and the fast scripts/check-*.mjs commands (dead-css, a11y, copy,
console) that cover the same ground in seconds without a build.

Also kept, deliberately, seven static guards that protect non-cosmetic
invariants rather than styling: the preload/IPC surface pins, the
renderer token-leak gate, external-link noopener, and secret redaction.
Those are cheap and guard things review reliably misses.

The CSS governance doc kept pointing at deleted guards, so its
enforcement section now says what actually holds the line: conventions
plus the check-* scripts, verified on the rendered surface.

* chore(desktop): drop postcss and unused source helpers with the suite

The deleted CSS-parse contract was postcss's only consumer, and the
tailwind-compile contract was the only TS importer of tailwindcss —
which styles.css still needs through `@import "tailwindcss"`, so it
stays declared and is now ignored by knip instead. Nine helper exports
lost their last readers with the suite; knip must report zero for
apps/desktop.
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