* style: add subtle teal tint to dark mode surfaces Replace neutral gray surfaces with a teal-tinted palette across the app and website, giving Paseo a warmer, more recognizable feel. App uses a restrained tint (G-R ~3), website is slightly stronger (G-R ~6) as a brand showcase. * fix(opencode): handle message.part.delta events for assistant text streaming OpenCode v2 streams assistant text via `message.part.delta` events (with field "text" or "reasoning"), but the translator only handled `message.part.updated`. This caused assistant messages to be silently dropped during live streaming. * feat: show agent short ID in tab context menu and tooltip (#161) Add agent short ID to the "Copy agent id" menu item as trailing hint text and to the tab tooltip next to the title. Add leading icons to all workspace tab context menu items. * add batch close rpc for workspace tabs
162 lines
5.0 KiB
TypeScript
162 lines
5.0 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import {
|
|
buildBulkCloseConfirmationMessage,
|
|
classifyBulkClosableTabs,
|
|
closeBulkWorkspaceTabs,
|
|
} from "@/screens/workspace/workspace-bulk-close";
|
|
import type { WorkspaceTabDescriptor } from "@/screens/workspace/workspace-tabs-types";
|
|
|
|
function makeAgentTab(id: string): WorkspaceTabDescriptor {
|
|
return {
|
|
key: `agent_${id}`,
|
|
tabId: `agent_${id}`,
|
|
kind: "agent",
|
|
target: { kind: "agent", agentId: id },
|
|
};
|
|
}
|
|
|
|
function makeTerminalTab(id: string): WorkspaceTabDescriptor {
|
|
return {
|
|
key: `terminal_${id}`,
|
|
tabId: `terminal_${id}`,
|
|
kind: "terminal",
|
|
target: { kind: "terminal", terminalId: id },
|
|
};
|
|
}
|
|
|
|
function makeFileTab(path: string): WorkspaceTabDescriptor {
|
|
return {
|
|
key: `file_${path}`,
|
|
tabId: `file_${path}`,
|
|
kind: "file",
|
|
target: { kind: "file", path },
|
|
};
|
|
}
|
|
|
|
describe("workspace bulk close helpers", () => {
|
|
it("classifies agent, terminal, and passive tabs for shared bulk close handling", () => {
|
|
const groups = classifyBulkClosableTabs([
|
|
makeAgentTab("a1"),
|
|
makeTerminalTab("t1"),
|
|
makeFileTab("/repo/README.md"),
|
|
]);
|
|
|
|
expect(groups).toEqual({
|
|
agentTabs: [{ tabId: "agent_a1", agentId: "a1" }],
|
|
terminalTabs: [{ tabId: "terminal_t1", terminalId: "t1" }],
|
|
otherTabs: [{ tabId: "file_/repo/README.md" }],
|
|
});
|
|
});
|
|
|
|
it("describes mixed destructive bulk close operations in the confirmation copy", () => {
|
|
const message = buildBulkCloseConfirmationMessage(
|
|
classifyBulkClosableTabs([
|
|
makeAgentTab("a1"),
|
|
makeAgentTab("a2"),
|
|
makeTerminalTab("t1"),
|
|
makeFileTab("/repo/README.md"),
|
|
]),
|
|
);
|
|
|
|
expect(message).toBe(
|
|
"This will archive 2 agent(s), close 1 terminal(s), and close 1 tab(s). Any running process in a closed terminal will be stopped immediately.",
|
|
);
|
|
});
|
|
|
|
it("keeps terminal-only confirmations explicit about stopping running processes", () => {
|
|
const message = buildBulkCloseConfirmationMessage(
|
|
classifyBulkClosableTabs([makeTerminalTab("t1")]),
|
|
);
|
|
|
|
expect(message).toBe(
|
|
"This will close 1 terminal(s). Any running process in a closed terminal will be stopped immediately.",
|
|
);
|
|
});
|
|
|
|
it("uses one mixed closeItems RPC for agent and terminal tabs, then applies local cleanup", async () => {
|
|
const groups = classifyBulkClosableTabs([
|
|
makeAgentTab("a1"),
|
|
makeTerminalTab("t1"),
|
|
makeTerminalTab("t2"),
|
|
makeFileTab("/repo/README.md"),
|
|
]);
|
|
const closedTabIds: string[] = [];
|
|
const cleanupCalls: Array<{ tabId: string; target?: WorkspaceTabDescriptor["target"] }> = [];
|
|
const closeItems = vi.fn(async () => ({
|
|
agents: [{ agentId: "a1", archivedAt: "2026-04-01T04:00:00.000Z" }],
|
|
terminals: [
|
|
{ terminalId: "t1", success: true },
|
|
{ terminalId: "t2", success: false },
|
|
],
|
|
requestId: "req-1",
|
|
}));
|
|
|
|
const result = await closeBulkWorkspaceTabs({
|
|
groups,
|
|
client: { closeItems },
|
|
closeTab: async (tabId, action) => {
|
|
closedTabIds.push(tabId);
|
|
await action();
|
|
},
|
|
closeWorkspaceTabWithCleanup: (input) => {
|
|
cleanupCalls.push(input);
|
|
},
|
|
logLabel: "all tabs",
|
|
});
|
|
|
|
expect(closeItems).toHaveBeenCalledTimes(1);
|
|
expect(closeItems).toHaveBeenCalledWith({
|
|
agentIds: ["a1"],
|
|
terminalIds: ["t1", "t2"],
|
|
});
|
|
expect(result).toEqual({
|
|
agents: [{ agentId: "a1", archivedAt: "2026-04-01T04:00:00.000Z" }],
|
|
terminals: [
|
|
{ terminalId: "t1", success: true },
|
|
{ terminalId: "t2", success: false },
|
|
],
|
|
requestId: "req-1",
|
|
});
|
|
expect(closedTabIds).toEqual(["agent_a1", "terminal_t1", "file_/repo/README.md"]);
|
|
expect(cleanupCalls).toEqual([
|
|
{ tabId: "agent_a1", target: { kind: "agent", agentId: "a1" } },
|
|
{ tabId: "terminal_t1", target: { kind: "terminal", terminalId: "t1" } },
|
|
{ tabId: "file_/repo/README.md" },
|
|
]);
|
|
});
|
|
|
|
it("still closes passive tabs when the mixed closeItems RPC fails", async () => {
|
|
const groups = classifyBulkClosableTabs([
|
|
makeAgentTab("a1"),
|
|
makeTerminalTab("t1"),
|
|
makeFileTab("/repo/README.md"),
|
|
]);
|
|
const closedTabIds: string[] = [];
|
|
const cleanupCalls: Array<{ tabId: string; target?: WorkspaceTabDescriptor["target"] }> = [];
|
|
const warn = vi.fn();
|
|
|
|
const result = await closeBulkWorkspaceTabs({
|
|
groups,
|
|
client: {
|
|
closeItems: async () => {
|
|
throw new Error("rpc failed");
|
|
},
|
|
},
|
|
closeTab: async (tabId, action) => {
|
|
closedTabIds.push(tabId);
|
|
await action();
|
|
},
|
|
closeWorkspaceTabWithCleanup: (input) => {
|
|
cleanupCalls.push(input);
|
|
},
|
|
warn,
|
|
logLabel: "others",
|
|
});
|
|
|
|
expect(warn).toHaveBeenCalledTimes(1);
|
|
expect(result).toBeNull();
|
|
expect(closedTabIds).toEqual(["file_/repo/README.md"]);
|
|
expect(cleanupCalls).toEqual([{ tabId: "file_/repo/README.md" }]);
|
|
});
|
|
});
|