Files
oh-my-pi/packages/coding-agent/test/agent-hub-ordering.test.ts
T
can1357 42d81f189f feat(coding-agent): redesigned agent hub layout for task clarity
- Redesigned agent hub entries as two-line cards, separating identity and status from task descriptions.
- Added explicit model and thinking level badges to agent status information.
- Updated agent hub rendering to support multi-line entries and adaptive vertical scrolling based on row height.
- Simplified status display by using glyphs instead of redundant status labels.
- Adjusted rendering logic to prioritize displaying agent task summaries on an independent indented line.
2026-07-13 03:03:26 +02:00

169 lines
5.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Regression: the agent hub row order must be stable while the hub is open.
*
* The hub is sorted by lastActivity on first open, but after that keyboard
* selection must not jump around as agents heartbeat or update activity. New
* agents that appear while the hub is open are appended at the end.
*/
import { afterEach, beforeAll, describe, expect, it, setSystemTime, vi } from "bun:test";
import { IrcBus } from "@oh-my-pi/pi-coding-agent/irc/bus";
import { AgentHubOverlayComponent } from "@oh-my-pi/pi-coding-agent/modes/components/agent-hub";
import { SessionObserverRegistry } from "@oh-my-pi/pi-coding-agent/modes/session-observer-registry";
import { initTheme } from "@oh-my-pi/pi-coding-agent/modes/theme/theme";
import { AgentRegistry } from "@oh-my-pi/pi-coding-agent/registry/agent-registry";
import type { AgentSession } from "@oh-my-pi/pi-coding-agent/session/agent-session";
import { visibleWidth } from "@oh-my-pi/pi-tui/utils";
interface GeometryStub {
setRows(n: number): void;
restore(): void;
}
function stubStdoutGeometry(cols: number): GeometryStub {
const rowsDesc = Object.getOwnPropertyDescriptor(process.stdout, "rows");
const colsDesc = Object.getOwnPropertyDescriptor(process.stdout, "columns");
let rows = 24;
Object.defineProperty(process.stdout, "rows", { configurable: true, get: () => rows, set: () => {} });
Object.defineProperty(process.stdout, "columns", { configurable: true, get: () => cols, set: () => {} });
const restoreOne = (key: "rows" | "columns", desc: PropertyDescriptor | undefined) => {
if (desc) Object.defineProperty(process.stdout, key, desc);
else Object.defineProperty(process.stdout, key, { configurable: true, value: undefined, writable: true });
};
return {
setRows(n: number) {
rows = n;
},
restore() {
restoreOne("rows", rowsDesc);
restoreOne("columns", colsDesc);
},
};
}
function makeHub(agents: AgentRegistry) {
return new AgentHubOverlayComponent({
observers: new SessionObserverRegistry(),
hubKeys: [],
onDone: () => {},
requestRender: () => {},
registry: agents,
irc: new IrcBus(agents),
focusAgent: async () => {},
});
}
function renderedAgentIds(hub: AgentHubOverlayComponent): string[] {
// Entry first lines are ` <cursor> <status-glyph> <id> …`; task lines are
// indented deeper and chrome lines never carry the cursor slot.
const ids: string[] = [];
for (const raw of hub.render(120)) {
const match = /^ (?:❯| ) (\S+) (\S+)/u.exec(Bun.stripANSI(raw));
if (match) ids.push(match[2]!);
}
return ids;
}
describe("Agent hub row ordering", () => {
let geometry: GeometryStub | undefined;
beforeAll(async () => {
await initTheme();
});
afterEach(() => {
vi.useRealTimers();
setSystemTime();
vi.restoreAllMocks();
geometry?.restore();
geometry = undefined;
AgentRegistry.resetGlobalForTests();
});
it("freezes the initial lastActivity order while the hub is open", () => {
vi.useFakeTimers();
let hub: AgentHubOverlayComponent | undefined;
try {
geometry = stubStdoutGeometry(120);
const agents = new AgentRegistry();
setSystemTime(1000);
const sessionA = {} as AgentSession;
agents.register({ id: "A", displayName: "Alpha", kind: "sub", session: sessionA });
setSystemTime(2000);
const sessionB = {} as AgentSession;
agents.register({ id: "B", displayName: "Beta", kind: "sub", session: sessionB });
setSystemTime(3000);
const sessionC = {} as AgentSession;
agents.register({ id: "C", displayName: "Gamma", kind: "sub", session: sessionC });
hub = makeHub(agents);
expect(renderedAgentIds(hub)).toEqual(["C", "B", "A"]);
// Bump A's lastActivity far ahead of the others. The hub is already open,
// so the captured order must not change.
setSystemTime(4000);
agents.setActivity("A", "still running");
// Registering a new agent schedules a coalesced row refresh; the
// existing rows must stay put once the scheduled refresh runs.
setSystemTime(5000);
const sessionD = {} as AgentSession;
agents.register({ id: "D", displayName: "Delta", kind: "sub", session: sessionD });
expect(renderedAgentIds(hub)).toEqual(["C", "B", "A"]);
vi.advanceTimersByTime(100);
expect(renderedAgentIds(hub)).toEqual(["C", "B", "A", "D"]);
} finally {
hub?.dispose();
vi.useRealTimers();
setSystemTime();
}
});
it("truncates lines and sanitizes newlines to prevent terminal wrapping", () => {
geometry = stubStdoutGeometry(80);
const agents = new AgentRegistry();
const sessionA = {} as AgentSession;
agents.register({
id: "RevAgentStream",
displayName: "Agent runtime + compaction reviewer",
kind: "sub",
session: sessionA,
});
const observers = new SessionObserverRegistry();
vi.spyOn(observers, "getSessions").mockReturnValue([
{
id: "RevAgentStream",
kind: "subagent",
label: "Subagent",
status: "active",
description: "Complete the assignment below, thoroughly:\n- check performance\n- check leaks",
lastUpdate: Date.now(),
},
]);
const hub = new AgentHubOverlayComponent({
observers,
hubKeys: [],
onDone: () => {},
requestRender: () => {},
registry: agents,
irc: new IrcBus(agents),
focusAgent: async () => {},
});
const lines = hub.render(80);
for (const line of lines) {
const cleanLine = Bun.stripANSI(line);
expect(cleanLine.includes("\n")).toBe(false);
expect(cleanLine.includes("\r")).toBe(false);
const width = visibleWidth(line);
expect(width).toBeLessThanOrEqual(78);
}
hub.dispose();
});
});