feat: add explicit drag handles to sidebar lists and refactor workspace tab model

This commit is contained in:
Mohamed Boudra
2026-03-04 11:57:22 +07:00
parent 952ff0dd72
commit b5b4e60916
11 changed files with 984 additions and 797 deletions
@@ -0,0 +1,164 @@
import { describe, expect, it } from "vitest";
import type { Agent } from "@/stores/session-store";
import { deriveWorkspaceTabModel } from "@/screens/workspace/workspace-tab-model";
import type { WorkspaceTab } from "@/stores/workspace-tabs-store";
function makeAgent(input: {
id: string;
provider?: Agent["provider"];
title?: string | null;
createdAt?: Date;
lastActivityAt?: Date;
requiresAttention?: boolean;
attentionReason?: Agent["attentionReason"];
}): Agent {
const createdAt = input.createdAt ?? new Date("2026-03-04T00:00:00.000Z");
const lastActivityAt = input.lastActivityAt ?? createdAt;
return {
serverId: "srv",
id: input.id,
provider: input.provider ?? "codex",
status: "idle",
createdAt,
updatedAt: createdAt,
lastUserMessageAt: null,
lastActivityAt,
capabilities: {
supportsStreaming: true,
supportsSessionPersistence: true,
supportsDynamicModes: true,
supportsMcpServers: true,
supportsReasoningStream: true,
supportsToolInvocations: true,
},
currentModeId: null,
availableModes: [],
pendingPermissions: [],
persistence: null,
runtimeInfo: {
provider: input.provider ?? "codex",
sessionId: null,
},
title: input.title ?? null,
cwd: "/repo/worktree",
model: null,
thinkingOptionId: null,
labels: {},
requiresAttention: input.requiresAttention ?? false,
attentionReason: input.attentionReason ?? null,
attentionTimestamp: null,
archivedAt: null,
};
}
describe("deriveWorkspaceTabModel", () => {
it("derives agent and terminal tabs from domain state, not UI membership", () => {
const model = deriveWorkspaceTabModel({
workspaceAgents: [
makeAgent({ id: "agent-a", title: "Build API" }),
makeAgent({ id: "agent-b", title: "" }),
],
terminals: [{ id: "term-1", name: "shell" }],
uiTabs: [],
tabOrder: [],
});
expect(model.tabs.map((tab) => tab.descriptor.tabId)).toEqual([
"agent_agent-a",
"agent_agent-b",
"terminal_term-1",
]);
const firstAgent = model.tabs[0]?.descriptor;
const secondAgent = model.tabs[1]?.descriptor;
expect(firstAgent?.kind === "agent" ? firstAgent.titleState : null).toBe("ready");
expect(secondAgent?.kind === "agent" ? secondAgent.titleState : null).toBe("loading");
expect(secondAgent?.label).toBe("");
});
it("keeps draft and file tabs as explicit UI tabs", () => {
const uiTabs: WorkspaceTab[] = [
{
tabId: "draft_123",
target: { kind: "draft", draftId: "draft_123" },
createdAt: 1,
},
{
tabId: "file_/repo/worktree/README.md",
target: { kind: "file", path: "/repo/worktree/README.md" },
createdAt: 2,
},
];
const model = deriveWorkspaceTabModel({
workspaceAgents: [makeAgent({ id: "agent-a", title: "A" })],
terminals: [],
uiTabs,
tabOrder: ["draft_123", "agent_agent-a", "file_/repo/worktree/README.md"],
});
expect(model.tabs.map((tab) => tab.descriptor.kind)).toEqual(["draft", "agent", "file"]);
});
it("applies stored order and appends newly-derived tabs deterministically", () => {
const model = deriveWorkspaceTabModel({
workspaceAgents: [makeAgent({ id: "agent-a" }), makeAgent({ id: "agent-b" })],
terminals: [{ id: "term-1", name: "zsh" }],
uiTabs: [],
tabOrder: ["terminal_term-1", "agent_agent-b"],
});
expect(model.tabs.map((tab) => tab.descriptor.tabId)).toEqual([
"terminal_term-1",
"agent_agent-b",
"agent_agent-a",
]);
});
it("prefers route tab, then focused tab, then first tab for active selection", () => {
const base = {
workspaceAgents: [makeAgent({ id: "agent-a" }), makeAgent({ id: "agent-b" })],
terminals: [],
uiTabs: [],
tabOrder: ["agent_agent-a", "agent_agent-b"],
};
expect(
deriveWorkspaceTabModel({
...base,
focusedTabId: "agent_agent-b",
}).activeTabId
).toBe("agent_agent-b");
expect(
deriveWorkspaceTabModel({
...base,
focusedTabId: "agent_agent-b",
routeTabId: "agent_agent-a",
}).activeTabId
).toBe("agent_agent-a");
});
it("covers regression: non-archived attention agent remains visible even if UI tabs omitted it", () => {
const offending = makeAgent({
id: "offender",
title: "Needs permission",
requiresAttention: true,
attentionReason: "permission",
});
const model = deriveWorkspaceTabModel({
workspaceAgents: [offending],
terminals: [],
uiTabs: [
{
tabId: "draft_123",
target: { kind: "draft", draftId: "draft_123" },
createdAt: 1,
},
],
tabOrder: ["draft_123"],
});
expect(model.tabs.some((tab) => tab.descriptor.tabId === "agent_offender")).toBe(true);
});
});