fix(coding-agent): gated agent hub opening when no subagents are available
- Added an `isEmpty` getter on `AgentHubOverlayComponent` to report whether no subagent rows were loaded. - Extended `showAgentHub` with an optional `requireContent` flag so the overlay is disposed early when empty under the double-<- path. - Added tests for double-<- gating behavior with no subagents, with subagents, and explicit hub-open behavior.
This commit is contained in:
@@ -267,6 +267,15 @@ export class AgentHubOverlayComponent extends Container {
|
||||
this.#refreshRows();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
get isEmpty(): boolean {
|
||||
return this.#rows.length === 0;
|
||||
}
|
||||
|
||||
/** Tear down every subscription and timer. Called by the overlay owner on close. */
|
||||
dispose(): void {
|
||||
for (const unsubscribe of this.#unsubscribers.splice(0)) unsubscribe();
|
||||
|
||||
@@ -332,14 +332,16 @@ export class InputController {
|
||||
|
||||
// Double-tap left arrow on an empty editor: opens the agent hub from the
|
||||
// main session, or returns the focused subagent view to the main session.
|
||||
// Focused ←← intentionally matches Esc.
|
||||
// Focused ←← intentionally matches Esc. From the main session the gesture
|
||||
// stays inert when there are no subagents (requireContent); the explicit
|
||||
// hub key still opens the empty roster.
|
||||
this.ctx.editor.onLeftAtStart = () => {
|
||||
if (this.ctx.focusedAgentId) {
|
||||
this.#handleFocusedLeftTap();
|
||||
return;
|
||||
}
|
||||
if (this.#detectLeftDoubleTap()) {
|
||||
this.ctx.showAgentHub();
|
||||
this.ctx.showAgentHub({ requireContent: true });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1203,7 +1203,7 @@ export class SelectorController {
|
||||
});
|
||||
}
|
||||
|
||||
showAgentHub(observers: SessionObserverRegistry): void {
|
||||
showAgentHub(observers: SessionObserverRegistry, options?: { requireContent?: boolean }): void {
|
||||
const hubKeys = [
|
||||
...this.ctx.keybindings.getKeys("app.agents.hub"),
|
||||
...this.ctx.keybindings.getKeys("app.session.observe"),
|
||||
@@ -1235,6 +1235,15 @@ 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".
|
||||
if (options?.requireContent && hub.isEmpty) {
|
||||
hub.dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
overlayHandle = this.ctx.ui.showOverlay(hub, {
|
||||
anchor: "bottom-center",
|
||||
width: "100%",
|
||||
|
||||
@@ -3466,8 +3466,8 @@ export class InteractiveMode implements InteractiveModeContext {
|
||||
await this.#selectorController.showDebugSelector();
|
||||
}
|
||||
|
||||
showAgentHub(): void {
|
||||
this.#selectorController.showAgentHub(this.#observerRegistry);
|
||||
showAgentHub(options?: { requireContent?: boolean }): void {
|
||||
this.#selectorController.showAgentHub(this.#observerRegistry, options);
|
||||
}
|
||||
|
||||
resetObserverRegistry(): void {
|
||||
|
||||
@@ -310,7 +310,7 @@ export interface InteractiveModeContext {
|
||||
showProviderSetup(): Promise<void>;
|
||||
showHookConfirm(title: string, message: string): Promise<boolean>;
|
||||
showDebugSelector(): Promise<void>;
|
||||
showAgentHub(): void;
|
||||
showAgentHub(options?: { requireContent?: boolean }): void;
|
||||
resetObserverRegistry(): void;
|
||||
|
||||
// Input handling
|
||||
|
||||
@@ -170,3 +170,79 @@ describe("Agent hub Enter activation", () => {
|
||||
capturedHub!.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Agent hub double-← gating", () => {
|
||||
beforeAll(() => {
|
||||
initTheme();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
resetSettingsForTest();
|
||||
});
|
||||
|
||||
function setup(agents: AgentRegistry) {
|
||||
let shown: AgentHubOverlayComponent | undefined;
|
||||
const ctx = {
|
||||
keybindings: { getKeys: () => [] },
|
||||
ui: {
|
||||
showOverlay: (component: AgentHubOverlayComponent) => {
|
||||
shown = component;
|
||||
return { hide: () => {} };
|
||||
},
|
||||
setFocus: () => {},
|
||||
requestRender: () => {},
|
||||
},
|
||||
editor: {},
|
||||
collabGuest: { agentRegistry: agents, hubRemote: undefined },
|
||||
focusAgentSession: async () => {},
|
||||
session: { getToolByName: () => undefined, extensionRunner: undefined },
|
||||
sessionManager: { getCwd: () => "/tmp", getSessionFile: () => null },
|
||||
hideThinkingBlock: false,
|
||||
};
|
||||
const controller = new SelectorController(ctx as unknown as InteractiveModeContext);
|
||||
return { controller, shown: () => shown };
|
||||
}
|
||||
|
||||
function registerWorker(agents: AgentRegistry) {
|
||||
agents.register({
|
||||
id: AGENT_ID,
|
||||
displayName: AGENT_ID,
|
||||
kind: "sub",
|
||||
parentId: "Main",
|
||||
session: { subscribe: () => () => {} } as unknown as AgentSession,
|
||||
sessionFile: null,
|
||||
status: "running",
|
||||
});
|
||||
}
|
||||
|
||||
it("requireContent keeps the hub closed when only Main is registered", () => {
|
||||
const agents = new AgentRegistry();
|
||||
agents.register({ id: "Main", displayName: "Main", kind: "main", session: null, sessionFile: null, status: "running" });
|
||||
const { controller, shown } = setup(agents);
|
||||
|
||||
controller.showAgentHub(new SessionObserverRegistry(), { requireContent: true });
|
||||
|
||||
expect(shown()).toBeUndefined();
|
||||
});
|
||||
|
||||
it("requireContent opens the hub once a subagent exists", () => {
|
||||
const agents = new AgentRegistry();
|
||||
registerWorker(agents);
|
||||
const { controller, shown } = setup(agents);
|
||||
|
||||
controller.showAgentHub(new SessionObserverRegistry(), { requireContent: true });
|
||||
|
||||
expect(shown()).toBeDefined();
|
||||
shown()!.dispose();
|
||||
});
|
||||
|
||||
it("the explicit hub key opens the empty roster even with no subagents", () => {
|
||||
const agents = new AgentRegistry();
|
||||
const { controller, shown } = setup(agents);
|
||||
|
||||
controller.showAgentHub(new SessionObserverRegistry());
|
||||
|
||||
expect(shown()).toBeDefined();
|
||||
shown()!.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user