mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-10-09 15:58:34 +00:00
f99efa8ac1
* fix(ui): open AI silent sessions in an attach terminal popup Tray jumps for hidden MCP sessions attach the existing terminal popup to the live PTY instead of activating a tab-less main-window surface. Output is rebound to the popup and restored when the popup closes. * fix(terminal): register rebindOutput handlers under worker mode Worker-enabled builds returned early from terminalBridge.registerHandlers without registering rebind/restore IPC, so the first AI silent-session attach popup always failed with "No handler registered". Register those handlers in both paths and rebind display routing via terminalWorkerManager. * fix(terminal): harden AI attach popup for worker mode lifecycle Address codex review P1s: resume flow control before restoring output, route exit/events to the rebound renderer, and replay home scrollback via a snapshot request before rebinding display output. * fix(terminal): restore attach routing on popup close and dual-notify exit Hardening for AI observe popups: restore output from BrowserWindow closed lifecycle (not only React cleanup), keep attach home for exit events so main/tray lifecycle updates, and resume paused backend flow on attach. * fix(terminal): resume flow on attach restore and close snapshot gaps Resume backend output when attach popups are force-closed, fan out lifecycle exit events to home+display in non-worker mode, and pause during snapshot→rebind so live bytes are not dropped from the popup. * fix(terminal): harden attach restore and worker lifecycle edges Prefer live display for ZMODEM dialogs after rebind, dual-notify worker crash exits to attach-home, and avoid clearing attach-home when restore targets a dead renderer. * fix(terminal): sync observe-popup state home and fan out SSH exits Push popup serializations home before restore, fall back to hibernate snapshots, settle briefly after pause for worker handoff, and dual-notify SSH exits to attach-home owners in non-worker mode. * fix(terminal): fan out exits for mosh/telnet/et attach owners Use shared fanout for in-process protocol exits so attach-home and display renderers both receive netcatty:exit under non-worker mode. * fix(terminal): restore attach only onto main app windows Avoid routing silent-session output to settings/prewarm windows when the original home renderer is gone; fall back only to registered main windows. * fix(terminal): harden silent session popup handoff * fix(terminal): close observed popup with session * fix(terminal): keep observe popup from reconnecting * fix(terminal): preserve cleared observe snapshots * fix(terminal): route transfer prompts to observer * fix(terminal): keep context after observe restore
131 lines
3.8 KiB
TypeScript
131 lines
3.8 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import type { TerminalSession } from "../domain/models";
|
|
import {
|
|
buildAiSilentSessionPopupPayload,
|
|
handleTrayJumpToSessionImpl,
|
|
} from "./app/AppHandlers";
|
|
|
|
const session = (overrides: Partial<TerminalSession> = {}): TerminalSession => ({
|
|
id: "session-1",
|
|
hostId: "host-1",
|
|
hostLabel: "AI Box",
|
|
hostname: "10.0.0.1",
|
|
username: "root",
|
|
status: "connected",
|
|
protocol: "ssh",
|
|
port: 22,
|
|
...overrides,
|
|
});
|
|
|
|
test("buildAiSilentSessionPopupPayload attaches the same live session PTY", () => {
|
|
const payload = buildAiSilentSessionPopupPayload(
|
|
session({ hiddenFromTabs: true }),
|
|
);
|
|
|
|
assert.equal(payload.parentSessionId, "session-1");
|
|
assert.equal(payload.attachSessionId, "session-1");
|
|
assert.equal(payload.title, "AI Box");
|
|
assert.equal(payload.startupCommand, "");
|
|
assert.equal(payload.sourceSession.hiddenFromTabs, undefined);
|
|
assert.equal(payload.sourceSession.reuseConnectionFromSessionId, undefined);
|
|
});
|
|
|
|
test("handleTrayJumpToSessionImpl opens a terminal popup for AI silent sessions", async () => {
|
|
const opened: unknown[] = [];
|
|
let activeTabId = "session-1";
|
|
|
|
await handleTrayJumpToSessionImpl(
|
|
() => ({
|
|
sessions: [session({ hiddenFromTabs: true })],
|
|
setActiveTabId: (id: string) => {
|
|
activeTabId = id;
|
|
},
|
|
getActiveTabId: () => activeTabId,
|
|
setWorkspaceFocusedSession: () => {
|
|
throw new Error("should not focus workspace for silent sessions");
|
|
},
|
|
netcattyBridge: {
|
|
get: () => ({
|
|
openTerminalPopup: async (payload: unknown) => {
|
|
opened.push(payload);
|
|
return { success: true, popupId: "popup-1" };
|
|
},
|
|
}),
|
|
},
|
|
}),
|
|
"session-1",
|
|
);
|
|
|
|
assert.equal(opened.length, 1);
|
|
assert.equal(activeTabId, "vault");
|
|
assert.deepEqual(opened[0], buildAiSilentSessionPopupPayload(session({ hiddenFromTabs: true })));
|
|
});
|
|
|
|
test("handleTrayJumpToSessionImpl still activates normal solo sessions in the main window", async () => {
|
|
let activeTabId = "vault";
|
|
let openedMain = 0;
|
|
|
|
await handleTrayJumpToSessionImpl(
|
|
() => ({
|
|
sessions: [session()],
|
|
setActiveTabId: (id: string) => {
|
|
activeTabId = id;
|
|
},
|
|
setWorkspaceFocusedSession: () => {
|
|
throw new Error("solo sessions should not use workspace focus");
|
|
},
|
|
netcattyBridge: {
|
|
get: () => ({
|
|
openMainWindow: async () => {
|
|
openedMain += 1;
|
|
return { success: true };
|
|
},
|
|
openTerminalPopup: async () => {
|
|
throw new Error("should not open popup for visible sessions");
|
|
},
|
|
}),
|
|
},
|
|
}),
|
|
"session-1",
|
|
);
|
|
|
|
assert.equal(activeTabId, "session-1");
|
|
assert.equal(openedMain, 1);
|
|
});
|
|
|
|
test("handleTrayJumpToSessionImpl focuses workspace sessions without opening a popup", async () => {
|
|
let activeTabId = "vault";
|
|
let focused: { workspaceId: string; sessionId: string } | null = null;
|
|
let openedMain = 0;
|
|
|
|
await handleTrayJumpToSessionImpl(
|
|
() => ({
|
|
sessions: [session({ workspaceId: "ws-1" })],
|
|
setActiveTabId: (id: string) => {
|
|
activeTabId = id;
|
|
},
|
|
setWorkspaceFocusedSession: (workspaceId: string, sessionId: string) => {
|
|
focused = { workspaceId, sessionId };
|
|
},
|
|
netcattyBridge: {
|
|
get: () => ({
|
|
openMainWindow: async () => {
|
|
openedMain += 1;
|
|
return { success: true };
|
|
},
|
|
openTerminalPopup: async () => {
|
|
throw new Error("should not open popup for workspace sessions");
|
|
},
|
|
}),
|
|
},
|
|
}),
|
|
"session-1",
|
|
);
|
|
|
|
assert.equal(activeTabId, "ws-1");
|
|
assert.equal(openedMain, 1);
|
|
assert.deepEqual(focused, { workspaceId: "ws-1", sessionId: "session-1" });
|
|
});
|