61723934b9
* fix: isolate saved layouts from stale sessions * fix: preserve session tabs in saved layouts * fix: handle materialized saved layout visibility * fix: preserve sibling sessions on env switch * fix: load task sessions before saved layouts * fix: preserve sibling tabs on layout fast paths * test: cover layout session id deduplication
223 lines
6.2 KiB
TypeScript
223 lines
6.2 KiB
TypeScript
import { expect, it } from "vitest";
|
|
|
|
import { materializeReusableChatPanel, normalizeReusableSessionPanels } from "./session-panels";
|
|
import type { LayoutGroup, LayoutPanel, LayoutState } from "./types";
|
|
|
|
const CENTER_GROUP_ID = "group-center";
|
|
const OLD_SESSION_ID = "s-old";
|
|
const NEW_SESSION_ID = "s-new";
|
|
const SIBLING_SESSION_ID = "s-sibling";
|
|
const OLD_SESSION_PANEL_ID = `session:${OLD_SESSION_ID}`;
|
|
const NEW_SESSION_PANEL_ID = `session:${NEW_SESSION_ID}`;
|
|
const SIBLING_SESSION_PANEL_ID = `session:${SIBLING_SESSION_ID}`;
|
|
const CHAT_ID = "chat";
|
|
const CHAT_COMPONENT = "chat";
|
|
|
|
function panel(id: string, component = id): LayoutPanel {
|
|
return { id, component, title: id };
|
|
}
|
|
|
|
function chatPlaceholder(): LayoutPanel {
|
|
return { id: CHAT_ID, component: CHAT_COMPONENT, title: "Agent", tabComponent: "permanentTab" };
|
|
}
|
|
|
|
function oldSessionPanel(id = OLD_SESSION_PANEL_ID): LayoutPanel {
|
|
return {
|
|
id,
|
|
component: CHAT_COMPONENT,
|
|
title: "Old task",
|
|
tabComponent: "sessionTab",
|
|
params: { sessionId: OLD_SESSION_ID },
|
|
};
|
|
}
|
|
|
|
function layoutWithGroup(group: LayoutGroup): LayoutState {
|
|
return {
|
|
columns: [
|
|
{
|
|
id: "center",
|
|
groups: [group],
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
it("normalizes saved session chat panels to a reusable chat placeholder", () => {
|
|
const normalized = normalizeReusableSessionPanels(
|
|
layoutWithGroup({
|
|
id: CENTER_GROUP_ID,
|
|
activePanel: OLD_SESSION_PANEL_ID,
|
|
panels: [oldSessionPanel(), panel("changes")],
|
|
}),
|
|
);
|
|
|
|
expect(normalized.columns[0]?.groups[0]?.activePanel).toBe(CHAT_ID);
|
|
expect(normalized.columns[0]?.groups[0]?.panels).toEqual([chatPlaceholder(), panel("changes")]);
|
|
});
|
|
|
|
it("keeps normalized reusable chat placeholders idempotent", () => {
|
|
const once = normalizeReusableSessionPanels(
|
|
layoutWithGroup({
|
|
id: CENTER_GROUP_ID,
|
|
activePanel: CHAT_ID,
|
|
panels: [chatPlaceholder(), panel("plan")],
|
|
}),
|
|
);
|
|
const twice = normalizeReusableSessionPanels(once);
|
|
|
|
expect(twice).toEqual(once);
|
|
});
|
|
|
|
it("materializes reusable chat panels to the active task session", () => {
|
|
const materialized = materializeReusableChatPanel(
|
|
layoutWithGroup({
|
|
id: CENTER_GROUP_ID,
|
|
activePanel: CHAT_ID,
|
|
panels: [chatPlaceholder(), panel("plan")],
|
|
}),
|
|
NEW_SESSION_ID,
|
|
);
|
|
|
|
expect(materialized.columns[0]?.groups[0]?.activePanel).toBe(NEW_SESSION_PANEL_ID);
|
|
expect(materialized.columns[0]?.groups[0]?.panels[0]).toEqual({
|
|
id: NEW_SESSION_PANEL_ID,
|
|
component: CHAT_COMPONENT,
|
|
title: "Agent",
|
|
tabComponent: "sessionTab",
|
|
params: { sessionId: NEW_SESSION_ID },
|
|
});
|
|
});
|
|
|
|
it("materializes sibling task session tabs beside the active session", () => {
|
|
const materialized = materializeReusableChatPanel(
|
|
layoutWithGroup({
|
|
id: CENTER_GROUP_ID,
|
|
activePanel: CHAT_ID,
|
|
panels: [chatPlaceholder(), panel("plan")],
|
|
}),
|
|
NEW_SESSION_ID,
|
|
[SIBLING_SESSION_ID, NEW_SESSION_ID],
|
|
);
|
|
|
|
expect(materialized.columns[0]?.groups[0]?.activePanel).toBe(NEW_SESSION_PANEL_ID);
|
|
expect(materialized.columns[0]?.groups[0]?.panels.map((item) => item.id)).toEqual([
|
|
NEW_SESSION_PANEL_ID,
|
|
SIBLING_SESSION_PANEL_ID,
|
|
"plan",
|
|
]);
|
|
});
|
|
|
|
it("keeps tree and flat groups in sync when rewriting session panels", () => {
|
|
const materialized = materializeReusableChatPanel(
|
|
{
|
|
columns: [
|
|
{
|
|
id: "center",
|
|
groups: [],
|
|
tree: {
|
|
type: "leaf",
|
|
group: {
|
|
id: CENTER_GROUP_ID,
|
|
activePanel: OLD_SESSION_PANEL_ID,
|
|
panels: [oldSessionPanel()],
|
|
},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
NEW_SESSION_ID,
|
|
);
|
|
const group = materialized.columns[0]?.groups[0];
|
|
|
|
expect(group?.activePanel).toBe(NEW_SESSION_PANEL_ID);
|
|
expect(group?.panels[0]?.id).toBe(NEW_SESSION_PANEL_ID);
|
|
expect(materialized.columns[0]?.tree).toMatchObject({
|
|
type: "leaf",
|
|
group: {
|
|
activePanel: NEW_SESSION_PANEL_ID,
|
|
panels: [{ id: NEW_SESSION_PANEL_ID }],
|
|
},
|
|
});
|
|
});
|
|
|
|
it("drops duplicate chat session panels from reusable layouts", () => {
|
|
const materialized = materializeReusableChatPanel(
|
|
layoutWithGroup({
|
|
id: CENTER_GROUP_ID,
|
|
activePanel: OLD_SESSION_PANEL_ID,
|
|
panels: [oldSessionPanel(), oldSessionPanel("session:s-other"), panel("plan")],
|
|
}),
|
|
NEW_SESSION_ID,
|
|
);
|
|
|
|
expect(materialized.columns[0]?.groups[0]?.panels.map((item) => item.id)).toEqual([
|
|
NEW_SESSION_PANEL_ID,
|
|
"plan",
|
|
]);
|
|
});
|
|
|
|
it("deduplicates session chat panels across columns", () => {
|
|
const materialized = materializeReusableChatPanel(
|
|
{
|
|
columns: [
|
|
{
|
|
id: "center",
|
|
groups: [{ id: CENTER_GROUP_ID, panels: [oldSessionPanel()] }],
|
|
},
|
|
{
|
|
id: "right",
|
|
groups: [
|
|
{ id: "group-right", panels: [oldSessionPanel("session:s-other"), panel("plan")] },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
NEW_SESSION_ID,
|
|
);
|
|
|
|
expect(materialized.columns.map((column) => column.id)).toEqual(["center", "right"]);
|
|
expect(materialized.columns[0]?.groups[0]?.panels.map((item) => item.id)).toEqual([
|
|
NEW_SESSION_PANEL_ID,
|
|
]);
|
|
expect(materialized.columns[1]?.groups[0]?.panels.map((item) => item.id)).toEqual(["plan"]);
|
|
});
|
|
|
|
it("preserves groups that were already empty before normalization", () => {
|
|
const normalized = normalizeReusableSessionPanels({
|
|
columns: [
|
|
{
|
|
id: "center",
|
|
groups: [
|
|
{ id: CENTER_GROUP_ID, panels: [oldSessionPanel()] },
|
|
{ id: "empty-split", panels: [] },
|
|
],
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(normalized.columns[0]?.groups.map((group) => group.id)).toEqual([
|
|
CENTER_GROUP_ID,
|
|
"empty-split",
|
|
]);
|
|
expect(normalized.columns[0]?.groups[1]?.panels).toEqual([]);
|
|
});
|
|
|
|
it("removes groups that become empty only after duplicate session tabs are discarded", () => {
|
|
const materialized = materializeReusableChatPanel(
|
|
{
|
|
columns: [
|
|
{
|
|
id: "center",
|
|
groups: [
|
|
{ id: CENTER_GROUP_ID, panels: [oldSessionPanel()] },
|
|
{ id: "duplicate-session-only", panels: [oldSessionPanel("session:s-other")] },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
NEW_SESSION_ID,
|
|
);
|
|
|
|
expect(materialized.columns[0]?.groups.map((group) => group.id)).toEqual([CENTER_GROUP_ID]);
|
|
});
|