Connection manager in its own native window, modal to main

Single window centered over the main app. Electrobun has no native
modality, so it's approximated: bun bounces the main window's focus back
to the manager, the main webview shows a click-swallowing overlay, and
main-targeted menu/keyboard actions are dropped while open. Editor
windows stay fully usable. Connect/open-stack execute in the main
window via the existing connect: action channel; store edits push a
connectionsChanged refresh. Trigger moved from the panel dropdown to a
tab-bar button.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jokin
2026-07-13 23:11:58 +02:00
co-authored by Claude Fable 5
parent 27d0ac81d6
commit 7dd59a9752
17 changed files with 420 additions and 136 deletions
+37 -4
View File
@@ -6,6 +6,7 @@ import { PanelContextMenu } from "./context-menu";
import { EditManager } from "./edits";
import { EditorWindows } from "./editor-windows";
import { SessionRegistry } from "./fs/registry";
import { ManagerWindow } from "./manager-window";
import { applyApplicationMenu } from "./menu";
import { createAppRPC } from "./rpc-handlers";
import { ConnectionStore } from "./store/connections";
@@ -49,6 +50,7 @@ const events = {
connectionsChanged: () => {},
openEditor: (_ctx: EditorOpenContext) => {},
settingsChanged: (_settings: AppSettings) => {},
openConnectionManager: () => {},
};
const rpc = createAppRPC({ registry, engine, store, settings, edits, contextMenu, events });
const url = await getMainViewUrl();
@@ -83,17 +85,42 @@ const editors = new EditorWindows({
mainFrame: () => win.getFrame(),
notifyMain: { pathRefresh: (info) => rpc.send.pathRefresh(info) },
});
const manager = new ManagerWindow({
store,
settings,
url,
mainFrame: () => win.getFrame(),
notifyMain: {
managerState: (info) => rpc.send.managerState(info),
connectionsChanged: () => rpc.send.connectionsChanged({}),
menuAction: (info) => rpc.send.menuAction(info),
},
rebuildMenu: () => void applyApplicationMenu(store),
});
events.openEditor = (ctx) => editors.open(ctx);
events.settingsChanged = (s) => editors.broadcastSettings(s);
// clean editors close with the main window; dirty ones get to ask first
win.on("close", () => editors.closeAllForAppExit());
events.settingsChanged = (s) => {
editors.broadcastSettings(s);
manager.broadcastSettings(s);
};
events.openConnectionManager = () => manager.open();
// clean editors close with the main window; dirty ones get to ask first,
// and the modal manager has nothing to guard
win.on("close", () => {
manager.close();
editors.closeAllForAppExit();
});
new AppTray(win);
contextMenu.onAction = (action) => rpc.send.menuAction({ action });
// notify on finished transfers while the window is in the background
let windowFocused = true;
win.on("focus", () => (windowFocused = true));
win.on("focus", () => {
windowFocused = true;
// modal bounce: the main window can't take focus while the manager is open
if (manager.isOpen()) manager.activate();
});
win.on("blur", () => (windowFocused = false));
const notifiedStates = new Map<string, string>();
@@ -173,6 +200,12 @@ ApplicationMenu.on("application-menu-clicked", (event) => {
return;
}
}
if (action === "manage-connections") {
manager.open();
return;
}
// the manager is modal to the main window — don't drive main behind it
if (manager.isOpen()) return;
rpc.send.menuAction({ action });
});