Files
kandev/docs/specs/plugins/system-design/plugin-shortcut-settings.md
T
2026-09-04 17:38:05 +00:00

8.6 KiB

status, system, requirements, created, owners
status system requirements created owners
current plugins
REQ-PLUGINS-PLUGIN-SHORTCUT-SETTINGS-001
2026-09-04
kandev

Plugin Shortcut Settings System Design

Purpose and boundaries

The plugin system owns plugin keybinding declarations and their namespaced override identities, so it also owns where users configure those bindings. This design moves their presentation from the core shortcut list to each installed plugin's existing detail route without changing the manifest, frontend registry, dispatcher, HTTP settings contract, or backend storage.

The UI system continues to own the shared settings-save coordinator described by Settings Manual Save. Portable persistence continues to follow ADR 0041.

Requirement mapping

Requirement Design sections
REQ-PLUGINS-PLUGIN-SHORTCUT-SETTINGS-001 Settings surfaces, Draft and save flow, Conflict and dispatch behavior, Responsive behavior, Failure and compatibility

Settings surfaces

KeyboardShortcutsSettings in apps/web/components/settings/general-settings.tsx continues to load all installed plugin declarations for conflict calculation, but KeyboardShortcutsCard renders only the static entries from CONFIGURABLE_SHORTCUTS. It no longer renders a dynamic Plugin Shortcuts group.

PluginDetail in apps/web/components/settings/plugins/plugin-detail.tsx adds a host-owned plugin-shortcut card between the plugin's operator configuration area and its manifest card. The card renders only when the selected PluginRecord has at least one parseable ui.keybindings declaration. It remains outside the canManage gate so a member can manage a personal shortcut while the existing plugin-authored settings slot, schema-driven configuration, lifecycle actions, and danger zone remain administrator-only.

An active bundle may register a replacement component at the exact /settings/plugins/{id} route. SettingsRoutes keeps that plugin-owned content but wraps it in PluginRootSettingsRoute, which appends the same host-owned shortcut card. Nested plugin settings routes remain plugin-owned without the card. This preserves the public route extension while ensuring a plugin cannot displace the user's only shortcut configuration surface.

A focused component under apps/web/components/settings/plugins/ owns the plugin shortcut draft and card. It reuses the existing ShortcutRecorder behavior and shared conflict label derivation from the keyboard-shortcut components instead of copying the recording, clear, reset, dirty, or warning logic. Within the plugin detail context, a row displays the manifest keybinding description without repeating the plugin name; conflict labels stay qualified by plugin name so warnings are unambiguous.

The card's visible description explains that these shortcuts are personal and run only while the plugin is active. A disabled or errored plugin therefore keeps the editor available without implying that its handler is currently registered.

Data and contracts

PluginRecord.ui.keybindings remains the declaration source. The helpers in apps/web/lib/keyboard/plugin-shortcuts.ts continue to parse each manifest default and derive the stable override key plugin:{pluginId}:{keybindingId}. StoredShortcutOverrides remains one map containing both core and plugin entries under userSettings.keyboardShortcuts.

No backend DTO or endpoint changes. The editor persists through the existing user-settings update request's keyboard_shortcuts field. Plugin configuration continues to use the plugin config endpoint and is a separate save contributor.

Draft and save flow

  1. The plugin detail route reads the selected plugin plus the complete installed plugin list through usePlugins, and reads the current portable shortcut overrides from the settings store.
  2. The shortcut component builds all plugin entries for conflict comparison, filters the visible list by the selected plugin ID, and seeds saved and draft override snapshots from userSettings.keyboardShortcuts. Initial hydration and later higher-revision settings updates replace the saved baseline and rebase locally changed or deleted keys onto that complete incoming map.
  3. A recorder mutation changes only the local draft. The component registers a contributor such as plugin-shortcuts:{pluginId} with useSettingsSaveContributor; discard restores the saved snapshot. A draft cannot become saveable until the initial portable-settings baseline has been incorporated.
  4. Save rebases the local delta onto the latest settings-store map immediately before sending the complete override map through updateUserSettings. A response advances the baseline only when its revision is not older than the store, and edits made while the request is pending remain in the draft.
  5. If an administrator also changed schema-driven plugin configuration, the route coordinator invokes both independent contributors. Existing partial success and retry behavior applies; a shortcut-only save never calls the plugin config endpoint and therefore never restarts the plugin.

Conflict and dispatch behavior

Both shortcut surfaces resolve conflicts from the union of translated core entries and every installed plugin entry using findShortcutConflicts. Each surface filters only the rows it displays, not the comparison set. This retains core-versus-plugin and plugin-versus-plugin warnings after the rows move to separate pages.

usePluginShortcuts is unchanged. It reads the same override map on each keydown, invokes only currently registered plugin handlers, skips disallowed editable targets, and suppresses a plugin handler when its effective combo matches a Kandev-owned shortcut. Disabled and errored plugins have no live handler registration even though their saved declaration and override remain editable.

Responsive behavior

Desktop keeps the existing direct plugin detail route and adds the shortcut card inline in that page. On phones, the entry path remains the shipped Settings navigation sheet, Plugins list, and full-card tap into the direct plugin detail route, following apps/web/e2e/tests/plugins/mobile-plugin-settings-row.spec.ts. A drawer is not introduced because the shortcut list is persistent detail content rather than a temporary choice.

The detail page's settings scroll container remains the single vertical scroll owner. Shortcut rows can stack their label and controls on narrow widths; record, clear, and reset actions provide at least a 44 px active touch dimension on phone/coarse-pointer presentation. The shared floating save surface remains the primary persistence action and retains its existing dynamic-viewport and safe-area behavior. Shared manifest data, drafts, conflict calculation, and save callbacks are identical across viewports; only row composition and touch geometry vary.

Failure and compatibility

An unparseable manifest default remains a skipped declaration with the existing developer warning; backend manifest validation should make this exceptional. If no parseable bindings remain, no empty card is rendered. A failed user-settings save leaves the contributor dirty and retryable under the shared save coordinator.

Namespaced override IDs and the backend field do not change, so the relocation requires no migration and does not reset existing user choices. Uninstalling a plugin continues to make any orphaned override inert; reinstalling the same plugin identity can resolve that key again under existing behavior.

Verification

Component tests cover core-only rendering, selected-plugin rendering, empty declarations, qualified conflict warnings, drafts, resets, route replacement, and persistence boundaries. Deferred-response cases cover initial hydration, higher-revision synchronization, complete-map rebasing, stale save responses, and edits made during an in-flight save. The existing authenticated member plugin-settings scenario proves that the shortcut editor remains available while operator controls stay hidden and that the General shortcut page no longer contains the plugin row. The existing mobile plugin-detail scenario proves direct entry, touch geometry, single-axis containment, an acknowledged Save, and reload while the fixture plugin is disabled. Existing plugin dispatcher tests remain the regression evidence for dispatch precedence and editor-target rules.