mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-10-09 15:58:34 +00:00
8ef91e1266
* feat(ctrl-w): add ps-node + windows-process-tree + tsx deps for close-priority feature * fix(ctrl-w): drop ps-node dep and add windows-process-tree to asarUnpack Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(ctrl-w): add ptyProcessTree bridge with per-platform child-process enumeration Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(ctrl-w): ptyProcessTree uses args= for full command + warns on pid overwrite - Replace `comm=` with `args=` in defaultListPosix so the full command line is captured on both macOS (BSD ps) and Linux (GNU ps), avoiding the 15-char TASK_COMM_LEN truncation. - Add console.warn in registerPid when the same sessionId is overwritten with a different pid, making the race condition visible in logs. - Add test: registerPid warns exactly once on a pid change, not on a same-pid re-registration. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(ctrl-w): register local PTY pid with ptyProcessTree on spawn/exit Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(ctrl-w): unregister pids in cleanupAllSessions to match per-delete invariant Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(ctrl-w): add IPC handlers for pty child processes and confirm-close dialog Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(ctrl-w): guard BrowserWindow.fromWebContents null and document dialog dismiss contract Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(ctrl-w): expose ptyGetChildProcesses and confirmCloseBusy on window.netcatty Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(ctrl-w): add i18n strings for close-busy-terminal dialog Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(ctrl-w): add resolveCloseIntent pure function with 8 unit tests Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(ctrl-w): expose handleCloseSidePanel via ref to App.tsx Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(ctrl-w): wire resolveCloseIntent + local-shell busy confirmation into closeTab hotkey Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(ctrl-w): add re-entrancy guard, aggregate busy count, sync sidebar ref, dedupe intent branches Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(ctrl-w): auto-close workspace when its last session is closed Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(ctrl-w): sidebar close wins over focused terminal in priority chain Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(ctrl-w): sidebar priority applies to single-session tabs too, not just workspaces Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(ctrl-w): compute empty-workspace auto-close outside setSessions updater Addresses Codex P2 on #739: React 18+ does not guarantee updater execution timing under concurrent scheduling. Moving the decision outside the updater makes the microtask queue deterministic. --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
111 lines
3.3 KiB
TypeScript
111 lines
3.3 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import { resolveCloseIntent } from "./resolveCloseIntent.ts";
|
|
|
|
const baseWorkspace = {
|
|
id: "w1",
|
|
focusedSessionId: "s1",
|
|
};
|
|
|
|
const baseSession = { id: "s1" };
|
|
|
|
test("non-workspace tab → closeSingleTab with session id", () => {
|
|
const result = resolveCloseIntent({
|
|
activeTabId: "s1",
|
|
workspace: null,
|
|
sessionForTab: baseSession,
|
|
activeSidePanelTab: null,
|
|
focusIsInsideTerminal: true,
|
|
});
|
|
assert.deepEqual(result, { kind: "closeSingleTab", sessionId: "s1" });
|
|
});
|
|
|
|
test("non-workspace session tab + sidebar open → closeSidePanel (sidebar beats session close)", () => {
|
|
const r = resolveCloseIntent({
|
|
activeTabId: "s1",
|
|
workspace: null,
|
|
sessionForTab: { id: "s1" },
|
|
activeSidePanelTab: "ai",
|
|
focusIsInsideTerminal: true, // focus IS in terminal, but sidebar wins
|
|
});
|
|
assert.deepEqual(r, { kind: "closeSidePanel" });
|
|
});
|
|
|
|
test("vault/sftp tab → noop", () => {
|
|
const r = resolveCloseIntent({
|
|
activeTabId: "vault",
|
|
workspace: null,
|
|
sessionForTab: null,
|
|
activeSidePanelTab: null,
|
|
focusIsInsideTerminal: false,
|
|
});
|
|
assert.deepEqual(r, { kind: "noop" });
|
|
});
|
|
|
|
test("workspace + focus in terminal + sidebar open → closeSidePanel wins (sidebar beats focus)", () => {
|
|
const r = resolveCloseIntent({
|
|
activeTabId: "w1",
|
|
workspace: baseWorkspace,
|
|
sessionForTab: null,
|
|
activeSidePanelTab: "ai",
|
|
focusIsInsideTerminal: true,
|
|
});
|
|
assert.deepEqual(r, { kind: "closeSidePanel" });
|
|
});
|
|
|
|
test("workspace + focus NOT in terminal + sidebar open → closeSidePanel", () => {
|
|
const r = resolveCloseIntent({
|
|
activeTabId: "w1",
|
|
workspace: baseWorkspace,
|
|
sessionForTab: null,
|
|
activeSidePanelTab: "sftp",
|
|
focusIsInsideTerminal: false,
|
|
});
|
|
assert.deepEqual(r, { kind: "closeSidePanel" });
|
|
});
|
|
|
|
test("workspace + sidebar closed + focus in terminal → closeTerminal", () => {
|
|
const r = resolveCloseIntent({
|
|
activeTabId: "w1",
|
|
workspace: baseWorkspace,
|
|
sessionForTab: null,
|
|
activeSidePanelTab: null,
|
|
focusIsInsideTerminal: true,
|
|
});
|
|
assert.deepEqual(r, { kind: "closeTerminal", sessionId: "s1" });
|
|
});
|
|
|
|
test("workspace + sidebar closed + focus NOT in terminal → closeWorkspace", () => {
|
|
const r = resolveCloseIntent({
|
|
activeTabId: "w1",
|
|
workspace: baseWorkspace,
|
|
sessionForTab: null,
|
|
activeSidePanelTab: null,
|
|
focusIsInsideTerminal: false,
|
|
});
|
|
assert.deepEqual(r, { kind: "closeWorkspace", workspaceId: "w1" });
|
|
});
|
|
|
|
test("workspace with no focused session + sidebar closed → closeWorkspace", () => {
|
|
const r = resolveCloseIntent({
|
|
activeTabId: "w1",
|
|
workspace: { id: "w1", focusedSessionId: undefined },
|
|
sessionForTab: null,
|
|
activeSidePanelTab: null,
|
|
focusIsInsideTerminal: true, // even if flag true, no focused id → cannot closeTerminal
|
|
});
|
|
assert.deepEqual(r, { kind: "closeWorkspace", workspaceId: "w1" });
|
|
});
|
|
|
|
test("workspace with no focused session + sidebar open → closeSidePanel", () => {
|
|
const r = resolveCloseIntent({
|
|
activeTabId: "w1",
|
|
workspace: { id: "w1", focusedSessionId: undefined },
|
|
sessionForTab: null,
|
|
activeSidePanelTab: "ai",
|
|
focusIsInsideTerminal: false,
|
|
});
|
|
assert.deepEqual(r, { kind: "closeSidePanel" });
|
|
});
|