Tab-based multi-chat system supporting concurrent parent and subagent sessions, per-session stop controls, layout/scroll fixes, and attachment display improvements.
125 lines
4.5 KiB
TypeScript
125 lines
4.5 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { HOME_TAB, openTab, type TabState } from "./tab-state";
|
|
import {
|
|
bindParentSessionToChatTab,
|
|
closeChatTabsForSession,
|
|
createBlankChatTab,
|
|
createParentChatTab,
|
|
createSubagentChatTab,
|
|
openOrFocusParentChatTab,
|
|
openOrFocusSubagentChatTab,
|
|
resolveChatIdentityForTab,
|
|
syncParentChatTabTitles,
|
|
syncSubagentChatTabTitles,
|
|
} from "./chat-tabs";
|
|
|
|
function baseState(): TabState {
|
|
return {
|
|
tabs: [HOME_TAB],
|
|
activeTabId: HOME_TAB.id,
|
|
};
|
|
}
|
|
|
|
describe("chat tab helpers", () => {
|
|
it("reuses an existing parent chat tab for the same session (prevents duplicate live tabs)", () => {
|
|
const existing = createParentChatTab({ sessionId: "parent-1", title: "Parent" });
|
|
const state = openTab(baseState(), existing);
|
|
|
|
const next = openOrFocusParentChatTab(state, { sessionId: "parent-1", title: "Renamed" });
|
|
|
|
expect(next.tabs.filter((tab) => tab.type === "chat")).toHaveLength(1);
|
|
expect(next.activeTabId).toBe(existing.id);
|
|
});
|
|
|
|
it("reuses an existing subagent tab for the same child session key (prevents duplicate child viewers)", () => {
|
|
const existing = createSubagentChatTab({
|
|
sessionKey: "agent:child-1:subagent:abc",
|
|
parentSessionId: "parent-1",
|
|
title: "Child",
|
|
});
|
|
const state = openTab(baseState(), existing);
|
|
|
|
const next = openOrFocusSubagentChatTab(state, {
|
|
sessionKey: "agent:child-1:subagent:abc",
|
|
parentSessionId: "parent-1",
|
|
title: "Child updated",
|
|
});
|
|
|
|
expect(next.tabs.filter((tab) => tab.type === "chat")).toHaveLength(1);
|
|
expect(next.activeTabId).toBe(existing.id);
|
|
});
|
|
|
|
it("binds a newly-created parent session id onto a draft chat tab without disturbing sibling tabs", () => {
|
|
const draft = createBlankChatTab();
|
|
const sibling = createParentChatTab({ sessionId: "existing-1", title: "Existing" });
|
|
const state = {
|
|
tabs: [HOME_TAB, draft, sibling],
|
|
activeTabId: draft.id,
|
|
} satisfies TabState;
|
|
|
|
const next = bindParentSessionToChatTab(state, draft.id, "new-session-1");
|
|
|
|
expect(next.tabs.find((tab) => tab.id === draft.id)?.sessionId).toBe("new-session-1");
|
|
expect(next.tabs.find((tab) => tab.id === sibling.id)?.sessionId).toBe("existing-1");
|
|
});
|
|
|
|
it("closes a deleted parent session and all of its subagent tabs (prevents orphan child tabs)", () => {
|
|
const parent = createParentChatTab({ sessionId: "parent-1", title: "Parent" });
|
|
const child = createSubagentChatTab({
|
|
sessionKey: "agent:child-1:subagent:abc",
|
|
parentSessionId: "parent-1",
|
|
title: "Child",
|
|
});
|
|
const unrelated = createParentChatTab({ sessionId: "parent-2", title: "Other" });
|
|
const state = {
|
|
tabs: [HOME_TAB, parent, child, unrelated],
|
|
activeTabId: child.id,
|
|
} satisfies TabState;
|
|
|
|
const next = closeChatTabsForSession(state, "parent-1");
|
|
|
|
expect(next.tabs.map((tab) => tab.id)).not.toContain(parent.id);
|
|
expect(next.tabs.map((tab) => tab.id)).not.toContain(child.id);
|
|
expect(next.tabs.map((tab) => tab.id)).toContain(unrelated.id);
|
|
});
|
|
|
|
it("syncs parent and subagent titles from persisted session metadata", () => {
|
|
const parent = createParentChatTab({ sessionId: "parent-1", title: "Draft title" });
|
|
const child = createSubagentChatTab({
|
|
sessionKey: "agent:child-1:subagent:abc",
|
|
parentSessionId: "parent-1",
|
|
title: "Child draft",
|
|
});
|
|
const state = {
|
|
tabs: [HOME_TAB, parent, child],
|
|
activeTabId: parent.id,
|
|
} satisfies TabState;
|
|
|
|
const parentSynced = syncParentChatTabTitles(state, [{ id: "parent-1", title: "Real title" }]);
|
|
const fullySynced = syncSubagentChatTabTitles(parentSynced, [
|
|
{ childSessionKey: "agent:child-1:subagent:abc", task: "Long task", label: "Research branch" },
|
|
]);
|
|
|
|
expect(fullySynced.tabs.find((tab) => tab.id === parent.id)?.title).toBe("Real title");
|
|
expect(fullySynced.tabs.find((tab) => tab.id === child.id)?.title).toBe("Research branch");
|
|
});
|
|
|
|
it("resolves chat identity for parent and subagent tabs", () => {
|
|
const parent = createParentChatTab({ sessionId: "parent-1", title: "Parent" });
|
|
const child = createSubagentChatTab({
|
|
sessionKey: "agent:child-1:subagent:abc",
|
|
parentSessionId: "parent-1",
|
|
title: "Child",
|
|
});
|
|
|
|
expect(resolveChatIdentityForTab(parent)).toEqual({
|
|
sessionId: "parent-1",
|
|
subagentKey: null,
|
|
});
|
|
expect(resolveChatIdentityForTab(child)).toEqual({
|
|
sessionId: "parent-1",
|
|
subagentKey: "agent:child-1:subagent:abc",
|
|
});
|
|
});
|
|
});
|