Skip to content

RFC: desktop channel-feature registry to replace the per-feature tab/settings/sidebar wiring (behavior-preserving; complements #3275) #3280

Description

@jmreijnen-parkbee

Motivation

Adding a channel-scoped feature to the desktop client today means editing a spread of centrally-owned files in lockstep: the ChannelScreen dispatch if/else, the two near-identical *ChannelContent tab shells, the settings section wiring (a union type + a values array + a render switch with a never exhaustiveness gate + a nav-groups map), the scattered binding parsers plus the glyph mapper, and a hand-rolled read/write hook per feature. A correct new feature must touch all of them consistently — the client-side equivalent of the relay's per-kind "fork tax" (companion proposal: #3167).

This isn't hypothetical. #3275 ("host MCP Apps as channel tabs") adds a genuinely new tab type today by extending those shared shells directly. Sequence/board/docs-style surfaces do the same. Every such addition edits the same central files and grows the switch/if-else chains.

Proposed solution

A ChannelFeaturePlugin registry for the desktop client — modeled on the existing shared/features/ flag manifest ("typed definition list + resolver hook + gate"), but for channel features. A plugin declares, in one object: how it classifies a channel (parseBinding off the kind-39000 about), and the surfaces it contributes — tabs, settingsPanel, sidebar group/create-actions, headerAction, and glyph.

  • One generic ChannelFeatureShell renders a plugin's tabs (lazy) — collapsing the two hand-rolled *ChannelContent shells into one. The ChannelScreen dispatch if/else becomes a classify + shell render.
  • The settings section list becomes a single descriptor array (render per entry), retiring the parallel union / values-array / switch / nav-map.
  • Sidebar grouping, create-actions, and glyphs are driven by the plugins.
  • A shared useReplaceableEvent / usePublishReplaceable hook pair removes the duplicated "fetch → subscribe → newest-wins" read logic and the "sign → publish" write logic that each feature currently re-implements.

Built-in surfaces (chat/forum, the settings sections) register through the same path — dogfooded, so it's a behavior-preserving refactor, not a new privileged plugin lane. Because every icon/affordance (tab, glyph, sidebar group, create button) is a declared field, a feature like #3275's MCP-App tabs plugs in without editing any shared file.

Alternatives considered

  • Keep hand-wiring each feature into the shared shells (status quo) — the fork-tax remains, the central switch/if-else chains keep growing, and feat(desktop): host MCP Apps as channel tabs #3275-style additions keep editing central files.
  • A heavier dynamic plugin/loader system — unnecessary. This is compile-time registration, exactly like the existing shared/features/ manifest; no runtime plugin machinery.

Additional context

We have a complete working implementation, verified behavior-preserving against the existing desktop Playwright specs (channel tabs, sidebar hierarchy, settings), with downstream-specific features split into an opt-in pack that a vanilla build omits. It's the client-side companion to the relay-side registry proposed in #3167.

It also directly complements #3275 (host MCP Apps as channel tabs): a channel-feature registry would give those App tabs — and Sequence/board/docs — a first-class extension point rather than each tab type extending the shared shells. Happy to align on the shape (registry surface, how MCP-App tabs would register) before writing the PR, and to split it into review-sized commits.

Searched open issues and PRs for duplicates (channel feature registry / tab registry / feature plugin): none found.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions