Files
kandev/apps/web/components/task/task-top-bar-plugin-actions.tsx
T
José Almeida 9c16d337a2 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
2026-07-23 12:28:54 +01:00

73 lines
3.2 KiB
TypeScript

"use client";
import { useCallback, useMemo } from "react";
import { useOptionalAppStore } from "@/components/state-provider";
import { PluginSlot } from "@/components/plugins/plugin-slot";
import type { AppState } from "@/lib/state/store";
import type { TaskSession } from "@/lib/types/http";
/**
* Props forwarded to every plugin component registered for the `chat-top-bar`
* slot (`registry.registerComponent("chat-top-bar", Component)`). This is the
* session top bar's right-hand cluster, beside the document / editors / debug
* controls — the place for at-a-glance status a
* plugin wants to surface for the current task.
*
* A task can hold several sessions; the top bar is bound to one at a time
* (`activeSessionId`), so both it and the full `sessionIds` list are provided,
* mirroring the `chat-input-actions` slot. These are kandev session ids;
* resolving one to an agent/ACP transcript id (e.g. to key cost data on a
* session) is the plugin's job — do it server-side in the plugin backend
* through the Host data API, not here. See PLUGIN-API.md.
*/
export type ChatTopBarSlotProps = {
/** Task the top bar belongs to, or null before one exists. */
taskId: string | null;
/** Display title of the task, when known. */
taskTitle?: string;
/** Workspace the task lives in, when known. */
workspaceId: string | null;
/** Session the top bar is currently bound to, or null before one exists. */
activeSessionId: string | null;
/** Every kandev session id on the task (includes `activeSessionId`). */
sessionIds: string[];
};
const EMPTY_SESSIONS: TaskSession[] = [];
/**
* Plugin extension point in the session top bar, rendered alongside the
* first-party controls (document/editor menus and debug toggle).
* Renders every plugin component registered for the `chat-top-bar` slot (each
* isolated behind its own error boundary via `PluginSlot`) and forwards the
* current task, workspace, and all of its session ids as `slotProps`.
*/
export function TaskTopBarPluginActions(props: {
sessionId: string | null;
taskId: string | null;
taskTitle?: string;
workspaceId: string | null;
}) {
const { sessionId, taskId, taskTitle, workspaceId } = props;
// itemsByTaskId holds a stable per-task array reference (updated only when
// that task's sessions change), so selecting it avoids a new-array-per-render.
// Read optionally so the top bar can render in isolation (unit tests) without
// a StateProvider.
const selectSessions = useCallback(
(s: AppState): TaskSession[] =>
taskId ? (s.taskSessionsByTask.itemsByTaskId[taskId] ?? EMPTY_SESSIONS) : EMPTY_SESSIONS,
[taskId],
);
const taskSessions = useOptionalAppStore(selectSessions, EMPTY_SESSIONS);
const slotProps = useMemo<ChatTopBarSlotProps>(() => {
const sessionIds: string[] = taskSessions.map((session) => session.id);
// The active session may not yet be in the store list (freshly prepared);
// make sure the plugin always receives it.
if (sessionId && !sessionIds.includes(sessionId)) sessionIds.unshift(sessionId);
return { taskId, taskTitle, workspaceId, activeSessionId: sessionId, sessionIds };
}, [taskSessions, sessionId, taskId, taskTitle, workspaceId]);
return <PluginSlot name="chat-top-bar" slotProps={slotProps} />;
}