Files
kandev/docs/plans/plugin-task-menu-submenus/task-01-host-submenu-contract.md
T
2026-09-23 11:00:23 +01:00

98 lines
5.4 KiB
Markdown

---
id: "01-host-submenu-contract"
title: "Host: task menu action submenu contract"
status: done
wave: 1
depends_on: []
plan: "plan.md"
requirements:
- REQ-PLUGINS-TASK-MENU-SUBMENUS-001
- REQ-PLUGINS-TASK-MENU-SUBMENUS-002
- REQ-PLUGINS-TASK-MENU-SUBMENUS-003
acceptance_criteria:
- AC-PLUGINS-TASK-MENU-SUBMENUS-001.1
- AC-PLUGINS-TASK-MENU-SUBMENUS-001.2
- AC-PLUGINS-TASK-MENU-SUBMENUS-001.3
- AC-PLUGINS-TASK-MENU-SUBMENUS-001.4
- AC-PLUGINS-TASK-MENU-SUBMENUS-001.5
- AC-PLUGINS-TASK-MENU-SUBMENUS-001.6
- AC-PLUGINS-TASK-MENU-SUBMENUS-002.1
- AC-PLUGINS-TASK-MENU-SUBMENUS-002.2
- AC-PLUGINS-TASK-MENU-SUBMENUS-002.3
- AC-PLUGINS-TASK-MENU-SUBMENUS-002.4
- AC-PLUGINS-TASK-MENU-SUBMENUS-002.5
- AC-PLUGINS-TASK-MENU-SUBMENUS-003.1
- AC-PLUGINS-TASK-MENU-SUBMENUS-003.2
- AC-PLUGINS-TASK-MENU-SUBMENUS-003.3
system_design:
- ../../specs/plugins/system-design/plugin-task-menu-submenus.md
---
# Task 01: Host task menu action submenu contract
- **Acceptance:**
1. `TaskMenuActionRegistration.items?(context)` is optional and synchronous in
the SDK and the web re-export; `run` stays required; a host that predates the
field renders the flat item (AC-PLUGINS-TASK-MENU-SUBMENUS-001.1).
2. A usable `items()` renders the action as a one-level submenu: `label` is the
unselectable trigger, children render in order under the same context, the
entry's own `disabled` reaches every child, and a child's own `disabled`
holds (AC-PLUGINS-TASK-MENU-SUBMENUS-001.2).
3. Every unusable result — absent, `null`, non-array, empty, a throw, a
thenable, or only-unusable children — falls back to the flat `run` item, with
the defect reported once per action and kind
(AC-PLUGINS-TASK-MENU-SUBMENUS-001.3,
AC-PLUGINS-TASK-MENU-SUBMENUS-001.4).
4. A child is read once inside the boundary's guard into a snapshot: blank or
non-string `id`/`label`, a non-callable `run`, a `disabled` that is neither
boolean nor `null`, and an `icon` that is none of name/component/element/
`null` all drop that child; a repeated `id` keeps the first occurrence; a
throwing getter or a Proxy fails only that child
(AC-PLUGINS-TASK-MENU-SUBMENUS-001.5).
5. No malformed registration can fail a render: an unusable action `label` or
`id` contributes no entry, the defect report never throws on a `Symbol`,
null-prototype or throwing-accessor registration, a `primary`-group submenu
whose entries are all unusable leaves the native `Edit` item flat, and the
key encoder neither throws on a lone surrogate nor collides for any two
plugin-authored ids (AC-PLUGINS-TASK-MENU-SUBMENUS-002.1 .. 002.4).
6. Icon resolution admits `memo`/`forwardRef` component objects, excludes
`lazy` and elements for non-menu surfaces, resolves own keys only, and keeps
the menu-only element tolerance documented
(AC-PLUGINS-TASK-MENU-SUBMENUS-002.5).
7. Submenu children of a `primary` action reach the command palette and the
sidebar's task commands, one command each, carrying the trigger label as
`context`; `edit` stays card-only; a caller that forces the flat `Edit` form
never reuses a prebuilt entry set
(AC-PLUGINS-TASK-MENU-SUBMENUS-003.1 .. 003.3).
- **Verification:**
- `cd apps/web && ./node_modules/.bin/tsc --noEmit --incremental false`
- `cd apps/web && pnpm vitest run components/plugins/task-menu-actions.test.tsx components/kanban-card-edit-submenu.test.tsx components/kanban-card-menu-items.test.tsx components/kanban-card-menu-grouping.test.tsx components/kanban-card-plugin-context.test.tsx lib/kanban/task-actions-menu-entries.test.ts lib/plugins/sdk-contract.test.ts lib/plugins/icons.test.ts components/task/task-switcher-context-menu.test.tsx components/task-command-choices.test.tsx components/task-commands.test.tsx`
- `cd apps/web && ./node_modules/.bin/eslint --max-warnings 0 <changed files>`
- `cd apps/web && pnpm i18n:check`
- `python3 scripts/list-docs.py validate`
- `node .github/scripts/pr-docs.cjs` (CI's documentation-coverage evaluator)
- **Files likely touched:**
- `apps/packages/plugin-sdk/src/index.ts`
- `apps/web/lib/plugins/types.ts`
- `apps/web/lib/plugins/icons.ts` (+ test)
- `apps/web/components/plugins/task-menu-actions.ts` (+ test)
- `apps/web/components/kanban-card-edit-submenu.tsx` (+ test)
- `apps/web/components/kanban-card-menu-items.tsx` (+ test)
- `apps/web/components/kanban-card-menu.tsx`
- `apps/web/components/task-command-choices.tsx` (+ test)
- `apps/web/components/task-command-items.tsx` (+ test)
- `apps/web/lib/kanban/task-actions-menu-entries.ts` (+ test)
- `apps/web/lib/plugins/sdk-contract.test.ts`
- `apps/web/AGENTS.md`, `docs/plans/plugins/PLUGIN-API.md`, `docs/public/plugins-authoring.md`
- **Dependencies:** None.
- **Parallelism:** sequential.
- **Inputs:**
- Requirements: docs/specs/plugins/requirements/plugin-task-menu-submenus.md
- System design: docs/specs/plugins/system-design/plugin-task-menu-submenus.md
- Existing submenu entry model and renderers: `apps/web/components/kanban-card-menu-items.tsx`
- Plugin contract documentation: `docs/plans/plugins/PLUGIN-API.md`, `docs/public/plugins-authoring.md`
- Consumer expectations: the plugin repository's `ui/bundle.js` registration and its tests
- **Output contract:** summary, files changed, exact verification commands with
results, the adversarial review rounds with their dispositions, task status →
`done`, plan checkbox update.