refactor(coding-agent): polish agent hub menu

This commit is contained in:
Kyle McCleary
2026-08-03 12:25:06 -07:00
parent a5090f1f81
commit 4fb3ecd69a
7 changed files with 389 additions and 159 deletions
@@ -110,6 +110,23 @@ const MANUAL_LOGIN_PROMPT = "Paste the authorization code (or full redirect URL)
export class SelectorController {
constructor(private ctx: InteractiveModeContext) {}
/**
* Mount a primary fullscreen menu through the one polished modal path shared
* by Settings, Model Hub, and Agent Hub.
*/
#showFullscreenMenu(component: Component): OverlayHandle {
const handle = this.ctx.ui.showOverlay(component, {
anchor: "bottom-center",
width: "100%",
maxHeight: "100%",
margin: 0,
fullscreen: true,
});
this.ctx.ui.setFocus(component);
this.ctx.ui.requestRender();
return handle;
}
#defaultRoleMutationTail = Promise.resolve();
async #acquireDefaultRoleMutation(): Promise<() => void> {
@@ -242,15 +259,7 @@ export class SelectorController {
},
},
);
overlayHandle = this.ctx.ui.showOverlay(selector, {
anchor: "bottom-center",
width: "100%",
maxHeight: "100%",
margin: 0,
fullscreen: true,
});
this.ctx.ui.setFocus(selector);
this.ctx.ui.requestRender();
overlayHandle = this.#showFullscreenMenu(selector);
});
}
@@ -1023,15 +1032,7 @@ export class SelectorController {
initialProviderId: hubOptions.initialProviderId,
},
);
overlayHandle = this.ctx.ui.showOverlay(hub, {
anchor: "bottom-center",
width: "100%",
maxHeight: "100%",
margin: 0,
fullscreen: true,
});
this.ctx.ui.setFocus(hub);
this.ctx.ui.requestRender();
overlayHandle = this.#showFullscreenMenu(hub);
}
/** /login round-trip for a locked provider; reopen the hub on that provider only after a successful login. */
@@ -2000,26 +2001,19 @@ export class SelectorController {
...this.ctx.keybindings.getKeys("app.agents.hub"),
...this.ctx.keybindings.getKeys("app.session.observe"),
];
let hub: AgentHubOverlayComponent | undefined;
let overlayHandle: OverlayHandle | undefined;
let closed = false;
// 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();
this.ctx.editorContainer.clear();
this.ctx.editorContainer.addChild(this.ctx.editor);
this.ctx.ui.setFocus(this.ctx.editor);
if (closed) return;
closed = true;
hub.dispose();
overlayHandle?.hide();
this.focusActiveEditorArea();
this.ctx.ui.requestRender();
};
hub = new AgentHubOverlayComponent({
const hub = new AgentHubOverlayComponent({
observers,
hubKeys,
expandKeys: this.ctx.keybindings.getKeys("app.tools.expand"),
@@ -2038,30 +2032,24 @@ export class SelectorController {
});
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 (closed) return;
// The double-← gesture stays inert when neither live nor persisted
// subagents are available, so wait for discovery before making the gate.
if (options?.requireContent && hub.isEmpty) {
hub.dispose();
done();
return;
}
this.ctx.editorContainer.clear();
this.ctx.editorContainer.addChild(hub);
this.ctx.ui.setFocus(hub);
// When the hub was raised by the editor's double-← gesture, prime its own
// close detector so the *next* single ← dismisses it — the two taps that
// opened it were consumed by the editor's detector (issue #4780).
// Prime the detector before the first frame when the editor's double-←
// gesture opened the hub, so the next single ← dismisses it.
if (options?.armCloseTap) hub.armCloseTap();
this.ctx.ui.requestRender();
overlayHandle = this.#showFullscreenMenu(hub);
};
if (options?.requireContent && hub.isEmpty) {
void hub.persistedSubagentsReady.then(showReadyHub);
return;
} else {
showReadyHub();
}
showReadyHub();
}
}