Files
João Salavisa 545cd0b599 fix(ui): move task plugins into mobile menu
Move task plugin actions and destinations into the shared mobile Plugins menu, preserving a compact task header and accessible touch targets. Add focused mobile coverage, plugin authoring documentation, and process-probe stability handling.
2026-09-23 08:14:51 +01:00

10 KiB

created, status, requirements, system_design, legacy_specs
created status requirements system_design legacy_specs
2026-09-22 implemented
REQ-UI-MOBILE-TASK-CHROME-001
../../specs/ui/system-design/mobile-task-chrome.md

Implementation Plan: Mobile Task Plugin Menu

Overview

Move task-scoped chat-top-bar plugin contributions out of the fixed phone task header and into the shared app menu's existing Plugins section. First add the responsive slot contract and host composition with component regressions, then prove the result with the packaged plugin fixture on the managed Pixel 5 project and capture the two phone states needed for the PR.

Confirmed root cause

SessionMobileTopBar renders TaskTopBarPluginActions inside MobileTopBarActions, whose shrink-0 cluster also owns the hamburger. Every registered chat-top-bar component is opaque and rendered inline, while ChatTopBarSlotProps has no presentation field or phone containment contract. The supplied 390-CSS-pixel phone screenshot shows two contributions consuming the middle of the fixed row and reducing the title/branch region to a cramped remainder. The code path is deterministic even though the document itself does not overflow.

The closest shipped correction is the listing main-top-bar slot: its phone presentation leaves the persistent header, renders in the shared app menu, and receives presentation: "mobile". The task slot should follow that boundary while preserving task and session identity.

Scope

In scope

  • Remove chat-top-bar plugin components from the persistent phone task header.
  • Render those components inside the shared phone menu's existing Plugins group.
  • Add a public desktop/mobile presentation field without changing existing task, workspace, active-session, or session-list context.
  • Contain multiple and long contributions in the menu and preserve 44px phone controls supplied through host UI primitives.
  • Keep tablet and desktop task-top-bar placement unchanged.
  • Add component and managed mobile E2E regressions using the real packaged plugin fixture.
  • Capture a synthetic long-title header and the opened Plugins menu for the PR.

Out of scope

  • Moving first-party approval, archive, port-forwarding, executor, or change-request controls.
  • Changing plugin registration lifecycle, backend APIs, persistence, or permissions.
  • Redesigning plugin navigation or the shared menu hierarchy outside the combined Plugins section.
  • Changing listing main-top-bar behavior.

Technical approach

Public slot contract

  • Export ChatTopBarSlotProps from apps/packages/plugin-sdk/src/index.ts and use it as the canonical host type in apps/web/components/task/task-top-bar-plugin-actions.tsx.
  • Add presentation: "desktop" | "mobile". Default the host component to desktop so existing call sites and plugin behavior remain compatible.
  • Update docs/plans/plugins/PLUGIN-API.md, docs/public/plugins-authoring.md, and the SDK exact-consumer test with the phone placement, containment, and touch contract.

Phone composition

  • Add a reactive registration-presence helper beside TaskTopBarPluginActions; it prevents the app menu from rendering an empty Plugins group when no chat-top-bar slot is registered.
  • Give TaskTopBarPluginActions a mobile wrapper that uses the available width, wraps contributions, constrains direct children, and normalizes host buttons to a minimum 44px active target. Keep its desktop return path unchanged.
  • Remove the plugin slot from MobileTopBarActions. Pass the mobile presentation through AppNavSheet instead, with the same task, workspace, active-session, and task-session IDs.
  • Thread the optional content through AppNavSections into MobilePluginNavSection. Render it above plugin destination rows under one localized Plugins heading, and keep the section absent when it has neither contributions nor destinations.
  • Preserve the menu's existing single vertical scroll owner, safe-area padding, link dismissal, Escape behavior, and focus return. A plugin control owns its interaction and does not close the menu merely because it was activated.

Regression and PR evidence

  • Extend the packaged E2E plugin with deterministic chat-top-bar contributions that expose the received presentation and include compact and wider content.
  • Extend apps/web/e2e/tests/plugins/mobile-plugin-topbar.spec.ts with a task scenario that creates a synthetic long-title task, verifies the fixed header and menu geometry, activates a plugin control, and checks zero document horizontal overflow.
  • Use prCapture in that scenario to write mobile-task-plugin-header.png and mobile-task-plugin-menu.png. The closed screenshot shows recovered task hierarchy; the open screenshot shows where plugin controls moved. Validate both manifest entries before PR publication.

ASCII UI preview

UI-01: Phone task header with plugin contributions

Entry point: /t/:taskId below md, with two chat-top-bar registrations.

Current behavior confirmed by the supplied screenshot and source trace:

+--------------------------------------------------+
| < | Long task title... v | [plugin] [status] | = |
|   | branch +changes      |                   |   |
+--------------------------------------------------+
    ^ task identity is squeezed by an unbounded,
      non-shrinking plugin cluster

Target persistent header:

+--------------------------------------------------+
| < | Long task title with useful context... v | = |
|   | repo / branch +changes                    |   |
+--------------------------------------------------+
    fixed row; plugin contributions are absent
    first-party contextual controls may remain before the menu

Target opened shared menu:

+--------------------------------------------------+
| Menu                                           x |
|--------------------------------------------------|
| Workspace                                        |
| [ Current workspace                         v ]  |
|                                                  |
| Home                    [Quick Chat] [Terminal]  |
| Tasks                                            |
|   ... existing task navigation ...               |
|                                                  |
| Plugins                                          |
|   [ plugin action ] [ session status 23% ]       |
|   [ additional controls wrap inside the menu ]   |
|   [ plugin destination row                    ]  |
|                                                  |
| Integrations                                     |
| Utilities                                        |
+--------------------------------------------------+
    one vertical scroll owner; no horizontal page scroll

Structural requirements: the phone header keeps task identity as the flexible region; the hamburger remains fixed and touch reachable; plugin contributions share one Plugins section, wrap within its width, and give host.ui.Button controls a 44px active target. The exact spacing, labels inside plugin-owned components, and number of first-party header controls remain governed by current design tokens and availability.

Tablet and desktop keep the current inline task top bar:

Task identity | metrics | plugin contributions | status/tools/actions

This preview maps to AC-UI-MOBILE-TASK-CHROME-001.5, .6, and .7.

Tests

  • AC-UI-MOBILE-TASK-CHROME-001.7: apps/web/components/task/mobile/session-mobile-top-bar.test.tsx first recorded the fixed-header placement failure. The task top-bar tests then prove the desktop/mobile context, containment wrapper, and reactive presence contract.
  • AC-UI-MOBILE-TASK-CHROME-001.5 and .7: apps/web/components/task/mobile/session-mobile-top-bar.test.tsx proves the plugin slot is not a child of the fixed action cluster and is supplied to the shared menu only while registrations exist.
  • AC-UI-MOBILE-TASK-CHROME-001.7: apps/web/components/plugins/mobile-plugin-nav-section.test.tsx and apps/web/components/navigation/app-nav-sheet.test.tsx prove the combined Plugins group, empty behavior, ordering, and context.
  • AC-UI-MOBILE-TASK-CHROME-001.6 and .7: apps/web/lib/plugins/sdk-contract.test.ts and the plugin SDK typecheck prove one public structural contract and unchanged desktop presentation.

E2E tests

  • AC-UI-MOBILE-TASK-CHROME-001.5, .6, and .7: apps/web/e2e/tests/plugins/mobile-plugin-topbar.spec.ts installs the packaged fixture, opens a long-title task in mobile-chrome, proves the closed header retains usable non-overlapping title/menu geometry, opens the app menu, proves both plugin contributions are contained and receive the mobile presentation, activates one, and asserts no document horizontal overflow. The existing listing scenario remains in the same focused run.

Work orders

Verification results

  • Installed the locked workspace dependencies.
  • Focused Vitest suite: 49 tests passed across the task top bar, phone header, shared Plugins section, navigation sheet, and public SDK contract.
  • Plugin SDK and web TypeScript checks passed; targeted ESLint completed with zero warnings.
  • Managed mobile-chrome Playwright run: 2 tests passed against the packaged fixture, including long-title geometry, context, 44px host-button size, activation, focus return, and horizontal-overflow assertions.
  • PR capture manifest contains the validated synthetic header and menu PNGs.
  • Public-doc validation passed 70 tests and 47 published pages; specification indexing and lint passed 299 decisions and 1,108 specifications.

Risks

  • Plugin components can render arbitrary markup. Host width constraints protect the menu, while the public presentation field lets cooperative plugins choose a clearer phone layout; the host cannot make opaque plugin copy meaningful.
  • Moving the phone instance into the drawer changes when a plugin component is visibly mounted. The contract must avoid promising background polling or local disclosure state while the menu is closed.
  • Extending the shared fixture affects every E2E test that installs it. Keep the new registrations inert outside task detail and run the existing listing scenario with the new task scenario.
  • The user-provided screenshot contains live task data. PR captures must use the synthetic fixture task instead of publishing that attachment.