fix(agent-hub-ui): corrected agent hub render flow in editorContainer

- Fixed Agent Hub display to render in editorContainer instead of floating ui overlay.
- Fixed close flow to clear editorContainer, re-add the editor, and restore editor focus.
- Updated Agent Hub activation tests and removed the probe test fixture.
This commit is contained in:
can1357
2026-06-16 23:03:11 +02:00
parent 3459371724
commit 3225e6b4be
3 changed files with 31 additions and 64 deletions
@@ -1216,11 +1216,20 @@ export class SelectorController {
...this.ctx.keybindings.getKeys("app.session.observe"),
];
let hub: AgentHubOverlayComponent | undefined;
let overlayHandle: OverlayHandle | undefined;
// Render the hub inline in the editor slot — the same anchored region
// every other selector (model, session, tree, the `ask` tool) uses —
// rather than a floating overlay. A non-fullscreen overlay composited over
// a live transcript strands a stale copy in native scrollback every time a
// running subagent's progress grows the frame and scrolls the window; the
// hub is opened mid-run, so those copies stacked into a wall of duplicate
// "Agent Hub" frames bleeding the task tree behind them. As an editor-slot
// component it rides the normal append-only commit path: the transcript
// commits above it exactly once and the hub repaints in place.
const done = () => {
hub?.dispose();
overlayHandle?.hide();
this.ctx.editorContainer.clear();
this.ctx.editorContainer.addChild(this.ctx.editor);
this.ctx.ui.setFocus(this.ctx.editor);
this.ctx.ui.requestRender();
};
@@ -1251,12 +1260,8 @@ export class SelectorController {
return;
}
overlayHandle = this.ctx.ui.showOverlay(hub, {
anchor: "bottom-center",
width: "100%",
maxHeight: "100%",
margin: 0,
});
this.ctx.editorContainer.clear();
this.ctx.editorContainer.addChild(hub);
this.ctx.ui.setFocus(hub);
this.ctx.ui.requestRender();
}
@@ -125,18 +125,21 @@ describe("Agent hub Enter activation", () => {
const editor = {};
let capturedHub: AgentHubOverlayComponent | undefined;
let hideCalls = 0;
let editorRestoredCount = 0;
const focusedIds: string[] = [];
const focusResolved = Promise.withResolvers<void>();
const editorFocused = Promise.withResolvers<void>();
const focusTargets: unknown[] = [];
const editorContainer = {
clear: () => {},
addChild: (child: unknown) => {
if (child === editor) editorRestoredCount++;
else capturedHub = child as AgentHubOverlayComponent;
},
};
const ctx = {
keybindings: { getKeys: () => [] },
ui: {
showOverlay: (component: AgentHubOverlayComponent) => {
capturedHub = component;
return { hide: () => hideCalls++ };
},
setFocus: (target: unknown) => {
focusTargets.push(target);
if (target === editor) editorFocused.resolve();
@@ -144,6 +147,7 @@ describe("Agent hub Enter activation", () => {
requestRender: () => {},
},
editor,
editorContainer,
collabGuest: { agentRegistry: agents, hubRemote: undefined },
focusAgentSession: async (id: string) => {
focusedIds.push(id);
@@ -165,7 +169,7 @@ describe("Agent hub Enter activation", () => {
await editorFocused.promise;
expect(focusedIds).toEqual([AGENT_ID]);
expect(hideCalls).toBe(1);
expect(editorRestoredCount).toBe(1);
expect(focusTargets.at(-1)).toBe(editor);
capturedHub!.dispose();
});
@@ -182,17 +186,20 @@ describe("Agent hub double-← gating", () => {
function setup(agents: AgentRegistry) {
let shown: AgentHubOverlayComponent | undefined;
const editor = {};
const ctx = {
keybindings: { getKeys: () => [] },
ui: {
showOverlay: (component: AgentHubOverlayComponent) => {
shown = component;
return { hide: () => {} };
},
setFocus: () => {},
requestRender: () => {},
},
editor: {},
editor,
editorContainer: {
clear: () => {},
addChild: (child: unknown) => {
if (child !== editor) shown = child as AgentHubOverlayComponent;
},
},
collabGuest: { agentRegistry: agents, hubRemote: undefined },
focusAgentSession: async () => {},
session: { getToolByName: () => undefined, extensionRunner: undefined },
@@ -1,45 +0,0 @@
import { beforeAll, describe, it } 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";
describe("hub probe", () => {
beforeAll(async () => {
await initTheme();
});
it("dumps rendered rows", () => {
Object.defineProperty(process.stdout, "rows", { configurable: true, get: () => 40 });
Object.defineProperty(process.stdout, "columns", { configurable: true, get: () => 120 });
const agents = new AgentRegistry();
const sess = {} as AgentSession;
for (const letter of ["A", "B", "C", "D", "E"]) {
const id = `Repro${letter}`;
agents.register({
id,
displayName: `Advisor delivery bug reproducer (independent track ${letter})`,
kind: "sub",
parentId: id, // self-parent, mimicking the bug
session: sess,
status: "running",
});
}
const hub = new AgentHubOverlayComponent({
observers: new SessionObserverRegistry(),
hubKeys: [],
onDone: () => {},
requestRender: () => {},
registry: agents,
irc: new IrcBus(agents),
focusAgent: async () => {},
});
const lines = hub.render(120).map(l => Bun.stripANSI(l));
console.error("===HUB RENDER START===");
for (const l of lines) console.error(JSON.stringify(l));
console.error("===HUB RENDER END===");
hub.dispose();
});
});