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
+52 -3
View File
@@ -303,7 +303,8 @@ interface PluginRegistry {
registerSettingsRoute(path: string, Component: React.ComponentType): void;
// Named slot injection. Initial slots: "task-sidebar", "settings-nav",
// "main-nav-footer", "chat-input-actions", "chat-top-bar", "main-top-bar",
// "plugin-settings" (see "Named slots" below).
// "app-status-bar-left", "app-status-bar-right", and "plugin-settings"
// (see "Named slots" below).
registerComponent(slot: string, Component: React.ComponentType<{ slotProps?: unknown }>): void;
// WS action handler, bridged into the existing lib/ws dispatch.
registerWsHandler(action: string, handler: (payload: unknown) => void): void;
@@ -395,6 +396,8 @@ plugins at once. Available slots:
| `chat-input-actions` | Chat composer toolbar, beside the model picker, mic, and send button | `{ taskId, taskTitle, activeSessionId, sessionIds }` |
| `chat-top-bar` | Session top bar, beside the CPU/DB metrics and the document/editor/debug controls | `{ taskId, taskTitle, workspaceId, activeSessionId, sessionIds }` |
| `main-top-bar` | Default app top bar (Home / Kanban / Tasks), beside the CPU/DB metrics and the view/display controls | `{ workspaceId, workspaceLabel, currentPage }` |
| `app-status-bar-left` | Default-left item in the global status surface | `AppStatusBarSlotProps` |
| `app-status-bar-right` | Default-right item in the global status surface | `AppStatusBarSlotProps` |
| `plugin-settings` | A plugin's own settings page (**Settings > Plugins > `<plugin>`**), at the top above the settings form | `{ pluginId, status }` |
`plugin-settings` is the one exception to "every plugin's component renders":
@@ -467,8 +470,7 @@ example.
### Session top bar
Register a `chat-top-bar` component to surface at-a-glance status in the
session top bar, beside the first-party CPU/DB metrics and the
document/editor/debug controls. The host passes the current context as
session top bar, beside first-party document/editor/debug controls. The host passes the current context as
`slotProps`:
```ts
@@ -545,6 +547,53 @@ show.
registry.registerComponent("plugin-settings", makeSettingsStatus(host));
```
### Global Status bar
Register `app-status-bar-left` or `app-status-bar-right` for app-wide, compact
status UI. Kandev mounts exactly one presentation: a 24 px bar on tablet and
desktop, or an in-flow Status drawer section on phone. Keep bar content small;
render a touch-usable row when `presentation` is `"mobile-drawer"`.
```js
function StatusContribution({ slotProps }) {
const { placement, presentation, activeTaskId } = slotProps ?? {};
return host.jsx(
"span",
{ className: presentation === "bar" ? "truncate text-xs" : "block min-h-11 px-3 py-2" },
`${placement}: ${activeTaskId ?? "no active task"}`,
);
}
registry.registerComponent("app-status-bar-left", StatusContribution);
registry.registerComponent("app-status-bar-right", StatusContribution);
```
Each contribution receives this exact context:
```ts
type AppStatusBarSlotProps = {
placement: "left" | "right";
presentation: "bar" | "mobile-drawer";
density: "full" | "compact";
pathname: string;
activeWorkspaceId: string | null;
activeTaskId: string | null;
activeSessionId: string | null;
};
```
The IDs are hints; use `host.store` for full records. Each component registration
is one opaque item: Kandev does not inspect or separately reorder its children.
The slot chooses the default side. A user can Cmd-drag (macOS) or Ctrl-drag
(other desktop platforms) with a mouse across the full bar, and Kandev preserves
that backend-owned order across reloads, restarts, and plugin disable/enable.
Phone lists the saved left sequence followed by the saved right sequence and does
not offer drag ordering. There is no keyboard-arrow, touch, or plugin-priority
ordering API. Enable, disable, and uninstall update the live surface without a
reload, and each contribution has its own error boundary. A full-bleed route
(`topbar: false`) owns its own chrome; mount the host Status trigger there if that
route should expose Status.
## Three integration patterns
Both example repos are full, working plugins — read them rather than