b277e7f1d8
* 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>
86 lines
2.8 KiB
TypeScript
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");
|
|
});
|
|
});
|