Files
kandev/apps/web/components/task/task-top-bar-plugin-actions.test.tsx
T
João Salavisa b277e7f1d8 feat: add chat-top-bar plugin slot (#1827)
* feat: add chat-top-bar plugin slot

Adds a new named plugin UI slot, chat-top-bar, rendered in the session
top bar beside the CPU/DB metrics and document/editor/debug controls.
Mirrors the existing chat-input-actions slot pattern: a wrapper
component (TaskTopBarPluginActions) assembles slotProps from the store
and renders <PluginSlot name="chat-top-bar" />, forwarding
{ taskId, taskTitle, workspaceId, activeSessionId, sessionIds }.

Updates PLUGIN-API.md, the public authoring guide, and the
PluginSlotName JSDoc to document the new slot. No backend changes
needed since slot names are an open string type.

* fix: address PR review — mobile chat-top-bar slot, gate by archived state

- Wire the chat-top-bar plugin slot into the mobile session top bar
  (session-mobile-top-bar.tsx), matching desktop's slotProps shape.
- Gate the plugin cluster behind !isArchived in TopBarRight, matching
  every other interactive control in that group (port-forward, PR/MR
  buttons, layout preset, editors menu).
- Extract RemoteExecutorIndicator, ApproveButton, and MobileTopBarDialogs
  to keep the mobile top bar under lint complexity/length limits.

* fix: gate mobile chat-top-bar plugin slot by archived state

Threads isArchived through TaskLayout -> SessionMobileLayout ->
SessionMobileTopBar -> MobileTopBarActions so the mobile top bar
hides plugin content on archived tasks, matching the desktop
TopBarRight behavior fixed in the previous commit.

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
2026-07-20 18:52:56 +01:00

86 lines
2.8 KiB
TypeScript

import { cleanup, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { pluginRegistry } from "@/lib/plugins/registry";
import { TaskTopBarPluginActions, type ChatTopBarSlotProps } from "./task-top-bar-plugin-actions";
const SLOT = "chat-top-bar";
// Minimal store: the wrapper only reads taskSessionsByTask.itemsByTaskId.
const mockState = {
taskSessionsByTask: {
itemsByTaskId: {
t1: [
{ id: "s1", task_id: "t1" },
{ id: "s2", task_id: "t1" },
],
},
},
};
vi.mock("@/components/state-provider", () => ({
useOptionalAppStore: (selector: (s: typeof mockState) => unknown) => selector(mockState),
}));
describe("TaskTopBarPluginActions", () => {
afterEach(() => {
cleanup();
pluginRegistry.unregisterPlugin("plugin-a");
});
it("renders nothing when no plugin registered a chat-top-bar component", () => {
const { container } = render(
<TaskTopBarPluginActions sessionId="s1" taskId="t1" taskTitle="Demo" workspaceId="w1" />,
);
expect(container.innerHTML).toBe("");
});
it("forwards task/workspace context, the active session, and all session ids", () => {
pluginRegistry.forPlugin("plugin-a").registerComponent(SLOT, ({ slotProps }) => {
const ctx = slotProps as ChatTopBarSlotProps;
return (
<div data-testid="plugin-topbar">
{`${ctx.taskId}|${ctx.workspaceId}|${ctx.activeSessionId}|${ctx.sessionIds.join(",")}`}
</div>
);
});
render(
<TaskTopBarPluginActions sessionId="s2" taskId="t1" taskTitle="Demo" workspaceId="w1" />,
);
expect(screen.getByTestId("plugin-topbar").textContent).toBe("t1|w1|s2|s1,s2");
});
it("includes the active session id even when the store list omits it", () => {
pluginRegistry.forPlugin("plugin-a").registerComponent(SLOT, ({ slotProps }) => {
const ctx = slotProps as ChatTopBarSlotProps;
return <div data-testid="plugin-topbar">{ctx.sessionIds.join(",")}</div>;
});
// taskId with no store entry -> only the active session propagates.
render(
<TaskTopBarPluginActions
sessionId="s9"
taskId="t-unknown"
taskTitle="Demo"
workspaceId="w1"
/>,
);
expect(screen.getByTestId("plugin-topbar").textContent).toBe("s9");
});
it("propagates only the active session when taskId is null", () => {
pluginRegistry.forPlugin("plugin-a").registerComponent(SLOT, ({ slotProps }) => {
const ctx = slotProps as ChatTopBarSlotProps;
return (
<div data-testid="plugin-topbar">{`${ctx.taskId}|${ctx.activeSessionId}|${ctx.sessionIds.join(",")}`}</div>
);
});
render(<TaskTopBarPluginActions sessionId="s9" taskId={null} workspaceId={null} />);
expect(screen.getByTestId("plugin-topbar").textContent).toBe("null|s9|s9");
});
});