feat(web): add global app status surface (#1869)

* feat(web): add app status bar

* feat(web): add app status bar visibility toggle

* feat(web): persist status bar item order

* fix(web): refine app status bar behavior

* fix(web): polish app status bar presentation

* test(web): split plugin slot coverage groups

* fix(web): address status surface review feedback

* fix(web): stabilize status bar overlay tests
This commit is contained in:
José Almeida
2026-07-23 12:28:54 +01:00
committed by GitHub
parent 7de788ceed
commit 9c16d337a2
141 changed files with 4727 additions and 771 deletions
+45 -4
View File
@@ -122,14 +122,15 @@ interface PluginRegistry {
registerSettingsRoute(path: string, Component: React.ComponentType): void;
// Named slot injection. Host renders all components registered for a slot via
// <PluginSlot name="..." props={...}/>. Initial slots: "task-sidebar",
// <PluginSlot name="..." slotProps={...}/>. Initial slots: "task-sidebar",
// "settings-nav", "main-nav-footer", "chat-input-actions", "chat-top-bar",
// "main-top-bar", "plugin-settings".
// "main-top-bar", "app-status-bar-left", "app-status-bar-right", and
// "plugin-settings".
// "chat-input-actions" renders icon buttons in the chat composer toolbar
// (beside the model picker, mic, and send) and forwards
// `{ taskId, taskTitle, activeSessionId, sessionIds }` as `slotProps`.
// "chat-top-bar" renders status in the session top bar (beside the CPU/DB
// metrics and the document/editor/debug controls) and forwards
// "chat-top-bar" renders status in the session top bar (beside the
// document/editor/debug controls) and forwards
// `{ taskId, taskTitle, workspaceId, activeSessionId, sessionIds }`. Both
// carry the active session plus every kandev session id on the task.
// "main-top-bar" renders status/actions in the default app top bar on the
@@ -155,6 +156,46 @@ interface PluginRegistry {
}
```
### App-status-bar slots
`app-status-bar-left` and `app-status-bar-right` are live named component slots.
Each registration is one opaque status item; the slot chooses its default side,
not a permanent side after user customization. Components receive
`slotProps` with this exact shape:
```ts
interface AppStatusBarSlotProps {
placement: "left" | "right";
presentation: "bar" | "mobile-drawer";
density: "full" | "compact";
pathname: string;
activeWorkspaceId: string | null;
activeTaskId: string | null;
activeSessionId: string | null;
}
```
`placement` matches registration slot. `presentation` identifies the mounted host;
the host mounts only one presentation at once. `density` is `full` on desktop and
phone drawer, `compact` on tablet. `pathname` and active IDs are current-context
hints, not entity payloads; read complete records from `host.store`.
Before customization, registration order is render order within each default side.
Users can Cmd-drag on macOS or Ctrl-drag elsewhere with a mouse to move any item
across the whole desktop/tablet bar. Kandev stores that order in backend user
settings; disabled contributions keep their place and return there when enabled.
Phone renders the saved left sequence followed by the saved right sequence, without
dragging. There is no cross-plugin priority API, keyboard-arrow ordering, or touch
ordering. Enable, disable, and uninstall update slots without reload. Each component
is isolated by an owner-aware error boundary, so plugins must tolerate remounting and
render a compact bar control or touch-usable drawer row for the supplied presentation.
The host neither inspects nor separately reorders children inside a registration, and
does not add a nested interactive wrapper.
A full-bleed plugin route (`topbar: false`) opts out of host chrome. It may mount
the host-provided Status drawer trigger when its own chrome should expose status;
otherwise status access is intentionally its responsibility.
## Registry internals (host side)
`apps/web/lib/plugins/registry.ts` holds a singleton `PluginRegistry` whose data