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

162 lines
10 KiB
Markdown

# 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.