diff --git a/packages/coding-agent/src/modes/components/agent-hub.ts b/packages/coding-agent/src/modes/components/agent-hub.ts index 39dd997e6..fd85bd748 100644 --- a/packages/coding-agent/src/modes/components/agent-hub.ts +++ b/packages/coding-agent/src/modes/components/agent-hub.ts @@ -270,9 +270,9 @@ export class AgentHubOverlayComponent extends Container { } /** - * Whether the table view has no agents to show (every registered agent except - * Main, after the persisted-subagent scan in the constructor). The double-← - * gesture reads this to stay inert when there is nothing to open. + * Whether the current table view has no agents to show (every registered agent + * except Main). Persisted historical rows may arrive later; callers that need + * those included must wait for {@link persistedSubagentsReady} first. */ get isEmpty(): boolean { return this.#rows.length === 0; diff --git a/packages/coding-agent/src/modes/controllers/selector-controller.ts b/packages/coding-agent/src/modes/controllers/selector-controller.ts index b52d72668..92b8a1ac7 100644 --- a/packages/coding-agent/src/modes/controllers/selector-controller.ts +++ b/packages/coding-agent/src/modes/controllers/selector-controller.ts @@ -1426,18 +1426,27 @@ export class SelectorController { sessionFile: this.ctx.sessionManager.getSessionFile() ?? null, }); - // The double-← gesture passes requireContent so it stays inert when there - // are no subagents to show; the explicit hub/observe keys still open the - // empty roster. The freshly built hub already ran the persisted-subagent - // scan, so its row count is the authoritative "is there anything to show". + const showReadyHub = () => { + // The double-← gesture passes requireContent so it stays inert when + // neither live nor persisted subagents are available. Persisted rows now + // load asynchronously, so defer the gate until that scan has refreshed the + // hub instead of treating the initial empty table as authoritative. + if (options?.requireContent && hub.isEmpty) { + hub.dispose(); + return; + } + + this.ctx.editorContainer.clear(); + this.ctx.editorContainer.addChild(hub); + this.ctx.ui.setFocus(hub); + this.ctx.ui.requestRender(); + }; + if (options?.requireContent && hub.isEmpty) { - hub.dispose(); + void hub.persistedSubagentsReady.then(showReadyHub); return; } - this.ctx.editorContainer.clear(); - this.ctx.editorContainer.addChild(hub); - this.ctx.ui.setFocus(hub); - this.ctx.ui.requestRender(); + showReadyHub(); } } diff --git a/packages/coding-agent/test/agent-hub-activate.test.ts b/packages/coding-agent/test/agent-hub-activate.test.ts index 1da36d345..8221e77da 100644 --- a/packages/coding-agent/test/agent-hub-activate.test.ts +++ b/packages/coding-agent/test/agent-hub-activate.test.ts @@ -188,8 +188,9 @@ describe("Agent hub double-← gating", () => { resetSettingsForTest(); }); - function setup(agents: AgentRegistry) { + function setup(agents: AgentRegistry, sessionFile: string | null = null) { let shown: AgentHubOverlayComponent | undefined; + const shownReady = Promise.withResolvers(); const editor = {}; const ctx = { keybindings: { getKeys: () => [] }, @@ -201,17 +202,20 @@ describe("Agent hub double-← gating", () => { editorContainer: { clear: () => {}, addChild: (child: unknown) => { - if (child !== editor) shown = child as AgentHubOverlayComponent; + if (child !== editor) { + shown = child as AgentHubOverlayComponent; + shownReady.resolve(shown); + } }, }, collabGuest: { agentRegistry: agents, hubRemote: undefined }, focusAgentSession: async () => {}, session: { getToolByName: () => undefined, extensionRunner: undefined }, - sessionManager: { getCwd: () => TEST_CWD, getSessionFile: () => null }, + sessionManager: { getCwd: () => TEST_CWD, getSessionFile: () => sessionFile }, hideThinkingBlock: false, }; const controller = new SelectorController(ctx as unknown as InteractiveModeContext); - return { controller, shown: () => shown }; + return { controller, shown: () => shown, shownReady: shownReady.promise }; } function registerWorker(agents: AgentRegistry) { @@ -254,6 +258,24 @@ describe("Agent hub double-← gating", () => { shown()!.dispose(); }); + it("requireContent opens the hub after persisted subagents load", async () => { + using tempDir = TempDir.createSync("@omp-agent-hub-require-content-"); + const sessionFile = path.join(tempDir.path(), "main.jsonl"); + const workerSessionFile = path.join(tempDir.path(), "main", "Worker.jsonl"); + await Bun.write(sessionFile, ""); + await Bun.write(workerSessionFile, ""); + const agents = new AgentRegistry(); + const { controller, shown, shownReady } = setup(agents, sessionFile); + + controller.showAgentHub(new SessionObserverRegistry(), { requireContent: true }); + + expect(shown()).toBeUndefined(); + const shownHub = await shownReady; + expect(shownHub).toBeDefined(); + expect(agents.get("Worker")?.sessionFile).toBe(workerSessionFile); + shownHub!.dispose(); + }); + it("the explicit hub key opens the empty roster even with no subagents", () => { const agents = new AgentRegistry(); const { controller, shown } = setup(agents);