feat(plugins): add task-row tags and sidebar actions (#2609)

Co-authored-by: ayattara <alassane.yattara@savoirfairelinux.com>
Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
This commit is contained in:
Alassane
2026-08-21 13:12:30 -04:00
committed by GitHub
parent b0ca65971c
commit 64bc820c45
23 changed files with 518 additions and 75 deletions
@@ -0,0 +1,33 @@
# ADR-2026-08-18-plugin-task-row-metadata: Keep Task Row Plugin Metadata Generic
**Status:** accepted
**Date:** 2026-08-18
**Area:** frontend
## Context
Plugins need compact, read-only content on sidebar and `/tasks` rows. The first
consumer was a tags plugin, but naming the public slot after tags would make one
plugin's data model part of the host contract.
## Decision
The host exposes `task-row-metadata` with `TaskRowMetadataSlotProps`. The slot
accepts contributions from any plugin and describes only the host surface:
task, workspace, workflow step, and `sidebar` or `task-list` presentation. The
host owns the conditional wrapper and emits no DOM when the slot is empty.
Primary task-menu actions use the same registration on kanban, desktop sidebar,
and phone task-sheet menus. Edit-group actions remain card-only.
## Consequences
Plugins can show tags, ownership, provider state, or other compact metadata
without new host hooks. Host layouts stay stable when no plugin contributes.
The generic contract cannot promise tags-specific styling or behavior.
## Alternatives Considered
`task-row-tags` and `TaskRowTagsSlotProps` were rejected because they couple the
host API to one plugin. Reusing `task-card-tags` was rejected because dense rows
need a surface discriminator and different layout ownership.
+1
View File
@@ -179,6 +179,7 @@ Read individual ADRs for full context. Create new ones via `/record decision` or
| 2026-08-17-separate-task-activity-from-summary-freshness | [Separate Task Activity From Summary Freshness](2026-08-17-separate-task-activity-from-summary-freshness.md) | accepted | backend, frontend, protocol | 2026-08-17 |
| 2026-08-17-release-pr-ruleset-bypass | [Give Stable Release PRs an Administrator Token Bypass](2026-08-17-release-pr-ruleset-bypass.md) | accepted | infra, workflow, security | 2026-08-17 |
| 2026-08-18-never-started-agent-stall-terminal | [Treat Never-Started Agent Stalls as Terminal](2026-08-18-never-started-agent-stall-terminal.md) | accepted | backend, frontend, protocol | 2026-08-18 |
| 2026-08-18-plugin-task-row-metadata | [Keep Task Row Plugin Metadata Generic](2026-08-18-plugin-task-row-metadata.md) | accepted | frontend | 2026-08-18 |
| 2026-08-20-settings-prompt-editor-provider-ownership | [Settings Prompt Editors Own Monaco Completion Providers](2026-08-20-settings-prompt-editor-provider-ownership.md) | accepted | frontend | 2026-08-20 |
| 2026-08-14-kandev-native-agent-rich-output | [Keep Agent Rich Output Host Native](2026-08-14-kandev-native-agent-rich-output.md) | accepted | backend, frontend, protocol, security | 2026-08-14 |
| 2026-08-20-acp-client-non-underscore-extension-methods | [Route Non-Underscore Inbound Client Methods to the Extension Handler](2026-08-20-acp-client-non-underscore-extension-methods.md) | accepted | backend, protocol | 2026-08-20 |
+15 -6
View File
@@ -698,7 +698,8 @@ interface PluginRegistry {
// "settings-nav", "chat-input-actions", "task-create-input-actions",
// "new-session-input-actions", "chat-top-bar",
// "main-top-bar", "app-status-bar-left", "app-status-bar-right",
// "plugin-settings", "task-card-indicators", "task-card-tags", and
// "plugin-settings", "task-card-indicators", "task-card-tags",
// "task-row-metadata", and
// "sidebar-workspace-actions".
// "task-card-indicators" renders a small icon/badge beside the PR status
// icon on every kanban card and forwards
@@ -710,6 +711,10 @@ interface PluginRegistry {
// same `{ taskId: string, workspaceId: string | null, workflowStepId: string | null }`
// shape as `slotProps` (`workspaceId` is null with no active workspace, and
// `workflowStepId` is null when the task has no workflow step assigned).
// "task-row-metadata" renders compact, read-only metadata on the sidebar
// task tree and `/tasks` list. It forwards
// `{ taskId, workspaceId, workflowStepId, surface }`, where `surface` is
// "sidebar" or "task-list". The slot is plugin-agnostic.
// "chat-input-actions", "task-create-input-actions", and
// "new-session-input-actions" render composer actions for task/Quick Chat,
// task creation, and new-session creation. Each forwards the typed
@@ -1197,11 +1202,10 @@ console, and the menu still closes either way (Radix's own close-on-select,
independent of the async result).
Group `"primary"` renders each visible action as its own flat, top-level menu
item instead of nesting it under `Edit` — positioned between the "Move
to"/"Send to workflow" submenus and the "Link" submenu. Visibility filtering,
registration order, and `run()`/error handling are identical to the `"edit"`
group; the two groups are independent lists (an action only ever belongs to
one).
item instead of nesting it under `Edit`. It appears on cards and on the shared
desktop/mobile task-row menu. Group `"edit"` remains card-only. Visibility
filtering, registration order, and `run()`/error handling are identical; the
two groups are independent lists (an action only ever belongs to one).
`"task-card-indicators"` (documented above with the other slots) is the
matching read-only surface: a small icon/badge rendered beside the PR status
@@ -1213,6 +1217,11 @@ in its own row on the card instead of the cramped title row
`"task-card-indicators"` shares with `PRTaskIcon`. Use it for a contribution
that needs its own width, e.g. a row of tag chips.
`"task-row-metadata"` is a separate, plugin-agnostic slot for compact,
read-only metadata in the sidebar task tree and `/tasks` list. Its shape is
`{ taskId, workspaceId, workflowStepId, surface }`, with `surface` equal to
`"sidebar"` or `"task-list"`. The host emits no wrapper when the slot is empty.
### Task filters
`registerTaskFilter` adds one section to the kanban board's display dropdown
+1
View File
@@ -120,6 +120,7 @@
"delete_task_kandev",
"list_tasks_kandev",
"move_task_kandev",
"record_step_decision_kandev",
"remove_task_dependency_kandev",
"set_task_title_kandev",
"step_complete_kandev",
+18 -14
View File
@@ -262,7 +262,7 @@ slice shapes in a released plugin.
| registerTaskAction | Child action inside the task menu's native Link section | Active ui.bundle | Action is revoked on unload; host supplies current task/workspace and desktop/mobile presentation | registry.registerTaskAction({ id: "link-pr", placement: "link", ... }) |
| registerReviewProvider | Normalized task reviews, workspace associations, unlink, and shared Review panel | ui.bundle and matching `repository_providers[]` id | Snapshots/subscriptions are owner-scoped and revoked on unload; host owns status chrome, indicators, unlink UI, and responsive Review placement | registry.registerReviewProvider({ id: "acme", ...reviews }) |
| registerTaskPanel | { id, title, icon?, Component, mobileEnabled? }; adds a row to the task workspace's "+" (add panel) menu; Component receives { panelId, taskId, sessionId, presentation } | Active ui.bundle | Panel renders behind its own error boundary; slow/failed reloads preserve it, a ready generation missing it closes it, and disable/uninstall closes every owned instance | registry.registerTaskPanel({ id: "notes", title: "Notes", Component: NotesPanel }) |
| registerTaskMenuAction | { id, label, icon?, group: "edit" \| "primary", visible?(context), run(context) }; "edit" nests inside the card's Edit submenu, "primary" renders as a flat top-level item between "Move to"/"Send to workflow" and "Link" | Active ui.bundle | Action is revoked on disable/uninstall; a throwing/rejecting run is caught and logged | registry.registerTaskMenuAction({ id: "enhance", label: "Enhance", group: "edit", run: doEnhance }) |
| registerTaskMenuAction | { id, label, icon?, group: "edit" \| "primary", visible?(context), run(context) }; "edit" is card-only inside Edit, while "primary" is a flat item on cards and desktop/mobile task-row menus | Active ui.bundle | Action is revoked on disable/uninstall; a throwing/rejecting run is caught and logged | registry.registerTaskMenuAction({ id: "enhance", label: "Enhance", group: "primary", run: doEnhance }) |
| registerTaskFilter | { id, label, getOptions(), matches(context, selected) }; adds a client-side, multi-select filter section to the kanban board's display dropdown, alongside Workflow/Repository | Active ui.bundle | Filter is revoked on disable/uninstall; selections are ephemeral (not persisted); matches is only called for a non-empty selection, and a throw is caught, logged, and treated as non-matching | registry.registerTaskFilter({ id: "tags", label: "Tags", getOptions: listTagOptions, matches: taskHasSelectedTag }) |
| host.React / host.jsx | Shared React instance and React.createElement alias | Active ui.bundle | No cleanup; never bundle a second React/Radix runtime | const h = host.jsx |
| host.context | Versioned provider-neutral reads/subscriptions for active workspace, all workspace ids, native task creation, and exact provider repository identity | Active ui.bundle | Subscriptions are generation-owned and revoked on unload; records are stable SDK shapes, not private app state | const ids = host.context.getWorkspaceIds() |
@@ -451,6 +451,7 @@ to strings, but an unmounted name renders nowhere.
| plugin-settings | Top of this plugin's Settings > Plugins page | { pluginId, status }; owner-scoped to the plugin being viewed |
| task-card-indicators | Kanban card, beside the PR status icon | { taskId, workspaceId, workflowStepId } |
| task-card-tags | Kanban card, its own row below the badges row | { taskId, workspaceId, workflowStepId } |
| task-row-metadata | Sidebar task tree and `/tasks` rows | TaskRowMetadataSlotProps |
| sidebar-workspace-actions | Desktop New Task row or phone navigation action group, after Quick Terminal and Quick Chat | SidebarWorkspaceActionsSlotProps |
AppStatusBarSlotProps is { placement, presentation, density, pathname,
@@ -1950,21 +1951,24 @@ func (p *plugin) OnEvent(ctx context.Context, event *pluginsdk.Event) error {
For critical workflows, periodically reconcile from host.Tasks() or another
Host reader because event queues are bounded and delivery is best-effort.
### 6. Kanban-aware contribution
### 6. Task contributions
`registerTaskMenuAction` adds an item to the kanban card's context/dropdown
menu; group `"edit"` nests it inside the `Edit` submenu, group `"primary"`
renders it as a flat, top-level item positioned between the "Move
to"/"Send to workflow" submenus and the "Link" submenu.
`registerTaskMenuAction` adds an item to native task menus. Group `"edit"` is
card-only and nests inside the kanban card's `Edit` submenu. Group `"primary"`
renders as a flat top-level item on cards and desktop/mobile task-row menus.
`task-card-indicators` (see the named slots table) is the matching read-only
surface, rendered beside the PR status icon on every card. `task-card-tags`
is a sibling read-only surface with the same `slotProps` shape, mounted in its
own row instead of that cramped title-row spot; reach for it when a
contribution (e.g. a row of tag chips) needs its own width. Both
`visible(context)` and `run(context)` receive the card's actual `presentation`:
`"desktop"` on the desktop kanban and `"mobile"` on the phone kanban. Use it
when an action needs to choose responsive behavior; the host supplies it
through the card composition.
contribution (for example, a row of tag chips) needs its own width.
`visible(context)` and `run(context)` receive the actual `presentation`.
The value is `"desktop"` or `"mobile"` for the current native task menu.
`task-row-metadata` is the plugin-agnostic compact counterpart for the sidebar
task tree and `/tasks` list. It receives `{ taskId, workspaceId,
workflowStepId, surface: "sidebar" | "task-list" }`. Use it for read-only
metadata such as labels, ownership, provider state, or tags. An empty slot
adds no wrapper or spacing.
```js
registry.registerTaskMenuAction({
@@ -1982,9 +1986,9 @@ With no plugin action registered, the card's `Edit` item stays flat; once any
plugin registers one for group `"edit"`, it becomes `Edit > Edit task`
followed by each visible action. A `run` that throws or rejects is caught and
logged, not left to crash the card; the menu closes either way. Group
`"primary"` actions render and behave the same way, just as their own
top-level menu row instead of nested under `Edit`. Do not patch first-party
Kanban components directly.
`"primary"` actions render and behave the same way as their own top-level row,
including in desktop and phone task-row menus. Do not patch first-party task
components directly.
`registerTaskFilter` adds a client-side, multi-select filter section to the
kanban board's display dropdown, next to the built-in Workflow and Repository
+18 -3
View File
@@ -749,18 +749,23 @@ presentation }`. Every plugin panel shares one generic `"plugin-panel"` dockview
throws renders a per-panel error-boundary fallback without affecting the rest of
the layout. Decision:
ADR-2026-08-04-plugin-contribution-lifecycle-authority.
- **Kanban card contributions:** `registerTaskMenuAction({ id, label, icon?,
group: "edit", visible?, run })` adds an item to the kanban card's `Edit`
- **Task-menu and row contributions:** `registerTaskMenuAction({ id, label, icon?,
group: "edit" | "primary", visible?, run })` adds `"edit"` actions to the kanban card's `Edit`
submenu (the flat `Edit` item becomes `Edit > Edit task` once any plugin
registers one); `run(context)` receives `{ workspaceId, taskId, taskTitle,
workflowStepId, presentation }`, and a rejected `run` is caught and logged
without blocking the menu from closing. `registerComponent("task-card-indicators",
without blocking the menu from closing. `"primary"` actions render on cards
and desktop/mobile task-row menus. `registerComponent("task-card-indicators",
C)` renders `C` beside the PR status icon on every kanban card, receiving
`{ taskId, workspaceId, workflowStepId }` as `slotProps`.
`registerComponent("task-card-tags", C)` renders `C` in its own row on every
kanban card (below the badges row), receiving the same
`{ taskId, workspaceId, workflowStepId }` shape — for a contribution too wide
for the cramped title-row `task-card-indicators` spot, e.g. a row of tag chips.
`registerComponent("task-row-metadata", C)` renders plugin-agnostic,
read-only metadata on sidebar and `/tasks` rows. It receives
`{ taskId, workspaceId, workflowStepId, surface }`. An empty slot adds no
wrapper or spacing. Decision: ADR-2026-08-18-plugin-task-row-metadata.
- **Sidebar workspace actions:** `registerComponent("sidebar-workspace-actions", C)`
renders `C` after Quick Terminal/Quick Chat in the desktop sidebar's New Task
row and in the shared phone navigation sheet, forwarding
@@ -1090,6 +1095,16 @@ complete.
`context.presentation` is `"mobile"`; the same action invoked from desktop receives
`"desktop"`.
- **GIVEN** a plugin registers a `"primary"` task-menu action, **WHEN** a user
opens a desktop sidebar or phone task-sheet menu, **THEN** the same action is
present with the correct `presentation`. **GIVEN** the plugin unregisters
while the menu is open, **THEN** its action disappears without a reload.
- **GIVEN** any plugin registers `"task-row-metadata"`, **WHEN** sidebar or
`/tasks` rows render, **THEN** it receives `{ taskId, workspaceId,
workflowStepId, surface }`. **GIVEN** the slot is empty, **THEN** the host
renders no metadata wrapper or extra spacing.
- **GIVEN** a plugin has per-user state and deleting those rows returns an error,
**WHEN** an operator uninstalls it, **THEN** the request fails, the package and plugin
record remain installed for retry, and no successful-uninstall response is emitted.