Files
kandev/docs/decisions/2026-08-06-plugin-code-host-dashboard-parity.md
T

10 KiB

ADR-2026-08-06-plugin-code-host-dashboard-parity: Code-Host Plugins Reuse Native Dashboard Primitives

Status: accepted Date: 2026-08-06 Area: frontend, protocol

Context

GitHub and GitLab dashboards share one interaction model, but their list and toolbar components remained provider-coupled. The first Bitbucket plugin therefore reproduced their appearance with plugin CSS while inventing a second dashboard review action and an intermediate launch dialog. Visual similarity did not preserve the native task and review workflow.

Decision

Kandev owns provider-neutral code-host dashboard primitives under apps/web/components/integrations/ and exposes them through host.ui. GitHub, GitLab, and compatible plugins render their result list, row, toolbar, scope controls, task preset menu, linked-task indicator, and semantic change-request icons through those same host components. Plugins select icon semantics such as open, merged, or closed; they do not copy SVG paths from first-party providers.

Task presets use the same provider-neutral semantic icon keys as first-party presets. The host resolves those keys to its Tabler components, so Review, Address feedback, and Fix CI render the exact eye, message, and tool glyphs everywhere. Plugins do not pass custom icon components or rely on a generic fallback when a native semantic equivalent exists. Provider brand glyphs are different: the plugin owns that component and Kandev reuses it across native surfaces.

Kandev also owns common task change-request linking anatomy. Code-host plugins invoke the host task-link dialog contract with provider copy and a submit callback; they do not rebuild the input, validation, loading state, footer, toast, close behavior, or responsive surface inside an arbitrary plugin modal. A Link-submenu child names the target only (for example, Bitbucket Pull Request) because the parent already supplies the verb. The singleton plugin modal host mounts inside AppShell's theme, tooltip, and toast provider tree so host-owned workflows retain the same runtime contexts as first-party dialogs.

Runtime React components remain in the Kandev host repository and are delivered by the versioned host.ui contract. They do not move to a separate shared frontend repository: host-owned React, Radix contexts, theme behavior, and release compatibility must remain atomic. A future published SDK package may provide TypeScript contracts and pure build-time helpers once multiple plugin repositories need them, but it must not duplicate runtime UI implementations.

A code-host dashboard row is an inert result container. Its title opens the provider, its metadata describes the change request, and its sole creation control is the compact Task preset dropdown. Selecting a preset opens the host's real TaskCreateDialog directly; provider linking runs after successful task creation. Dashboard rows do not add a Review action or a provider-owned intermediate launch modal. Review begins from linked task context through the existing review-provider registry on desktop and mobile.

When the selected repository already exists in the workspace, the dialog keeps its normal REST create transport. For a first-use plugin repository, the plugin may inject the dialog's provider-neutral createTask transport and forward only native task fields through an authenticated plugin action. The plugin server resolves the pull request and trusted repository descriptor from its connection before calling Host Tasks.Create; browser input never grants repository-provider authority. A per-dialog launch identifier makes transport retries idempotent without limiting one pull request to one task.

Kandev also owns task-scoped change-request status presentation. A review-provider summary may include normalized status/check data; the host review registry preserves it, refreshes it through the existing provider lifecycle, and automatically mounts the same shared control in both first-party locations: the task topbar and the status row above the chat composer (including passthrough sessions). The host owns the exact trigger, delayed desktop hover popover, native mobile drawer, accessibility, touch geometry, and responsive behavior. Providers do not register a visual status slot. They supply data, refresh it around every 90 seconds, and retain provider API ownership.

The shared status body is the provider-neutral presentation extracted from GitHub's CI popover. Its common header, grouped checks, review/comment summaries, links, loading, empty, and error states render identically for built-ins and plugins. Provider-only controls such as automation or merge actions are optional callbacks/capabilities inside that anatomy, not alternate markup. Opening Review routes through the registered review provider on desktop or the current mobile session.

Kandev likewise owns the common change-request detail presentation exposed through host.ui.ChangeRequestDetail. GitHub and compatible review providers map their native payloads into one normalized detail model and pass supported action callbacks. The host owns header, branch/state metadata, description, reviews, checks, threaded comments, action placement, loading/error states, scroll ownership, and responsive behavior. Providers retain queries, authorization, mutation semantics, capability detection, and normalization. A plugin may still own a review-panel adapter, but not a parallel visual design for those common surfaces.

Every successful PR-from-task creation and watch-created task records the same durable task-to-change-request association exposed by manual linking. Watch-owned links remain authoritative in watch state and are unioned with manual link state when serving task review/status and workspace association queries; implementations do not copy them into a second store merely for presentation. Durable association, suppression, reservation, and watch keys use provider ID, provider connection scope, immutable repository ID, and change-request number. Repository paths, display keys, and clone URLs are mutable lookup attributes and never durable identity. A changed connection epoch pauses an existing watch before provider access; resuming explicitly rebinds it and discards its old cursor.

Task-scoped refresh also performs bounded branch-based discovery, equivalent to GitHub's external-PR detection. It derives repository and checkout branch only from the host-verified task, exact-matches an open provider change request's source branch, and stores the result idempotently. Browser-supplied repository or branch values never authorize automatic linking, and discovery failure does not hide an existing link.

The native task dialog's branch picker follows the same provider-neutral trust boundary. For a persisted plugin-owned repository, the host resolves the active manifest owner and invokes its declared workspace-scoped repositories.branches action with a descriptor derived only from the stored repository row. The action returns normalized branch names; the browser cannot substitute provider identity, host, namespace, clone URL, or repository ID. GitHub remains on its first-party service path, while future code-host plugins reuse this action instead of adding another host-native provider branch.

Provider adapters continue to own API queries, normalization, authorization, task-link storage, provider data, and capability handling. Shared host components own presentation and interaction geometry; they accept normalized data and callbacks and contain no GitHub, GitLab, or Bitbucket API logic.

Repository discovery is a provider-neutral, server-searchable paged contract. The host passes optional query, opaque cursor, and requested limit fields; providers return either the legacy repository array or { repositories, nextCursor }. The host follows pages, rejects repeated cursors, and always binds returned provider identities to the manifest owner. Registered plugin-owned provider icons are used in native repository controls rather than a generic branch glyph. These rules keep large installations usable without adding provider-specific search or pagination branches to Kandev. Provider cursors are bound to the original query, state, provider connection scope, and immutable repository identity; using one after any of those inputs changes fails before a provider request.

Task indicator and CI refreshes are summary operations. They fetch only normalized change-request metadata, reviews, checks, and an inexpensive accurate unresolved-comment count when the provider exposes one. Files, diffs, commits, and comment bodies load lazily when Review opens. Both summary and detail responses remain bounded, with explicit truncation instead of exceeding the authenticated plugin action response limit.

Pipeline/build status is read from the pull request's source (head) commit. The destination commit describes the merge target and cannot represent the CI result for the proposed change.

Consequences

Code-host plugins require a host release containing these additive host.ui exports. Dashboard fixes now apply uniformly to first-party and plugin providers, including themes, touch targets, dropdown behavior, loading/error states, and row density. Provider adapters remain independently releasable, while common review/status visuals enter the versioned host contract. New semantic icon or presentation needs require an additive host contract release rather than a plugin-local visual approximation. Plugin repositories avoid a second runtime UI dependency and its version-skew/release- coordination cost. Task-link, status, and detail fixes land once in the host and remain consistent for first-party and plugin providers, without moving provider polling, credentials, or API payloads into Kandev.

Alternatives Considered

  • Keep copying GitHub/GitLab markup and CSS into each plugin. Rejected because the first live implementation already drifted in behavior despite similar styling.
  • Export GitHub's PRList directly. Rejected because its status hooks, payloads, and task associations are GitHub-specific rather than a reusable plugin contract.
  • Permit each provider to choose its own dashboard actions. Rejected for first-party- parity code hosts because it makes task creation and review placement inconsistent.
  • Put shared React components in a dedicated repository/package. Rejected because it would split host UI contexts and versioning; only pure types/helpers may be published later if repeated plugin-authoring demand justifies them.