mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-11-06 14:30:13 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8fd224528f |
No files matched your search
@@ -7,7 +7,7 @@ const chromeThemeSource = readFileSync(new URL("./useActiveChromeTheme.ts", impo
|
||||
const storeSource = readFileSync(new URL("./activeTabStore.ts", import.meta.url), "utf8");
|
||||
|
||||
test("active tab changes notify chrome theme before react subscribers", () => {
|
||||
const setActiveTabIdBody = storeSource.match(/setActiveTabId = \(id: string\) => \{[\s\S]*?\n {2}\};/)?.[0] ?? "";
|
||||
const setActiveTabIdBody = storeSource.match(/setActiveTabId = \(id: string(?:, options\?: \w+)?\) => \{[\s\S]*?\n {2}\};/)?.[0] ?? "";
|
||||
assert.match(setActiveTabIdBody, /this\.syncListeners\.forEach\(\(listener\) => listener\(id\)\)/);
|
||||
assert.match(setActiveTabIdBody, /this\.scheduleNotify\(\)/);
|
||||
assert.ok(
|
||||
|
||||
@@ -4,6 +4,37 @@ import test from 'node:test';
|
||||
import { activeTabStore, fromEditorTabId, isEditorTabId, toEditorTabId } from './activeTabStore';
|
||||
import { terminalLayoutSuppressStore } from './terminalLayoutSuppressStore';
|
||||
|
||||
test('active tab store remembers the previous tab for restore-on-close', () => {
|
||||
const previousWindow = (globalThis as { window?: unknown }).window;
|
||||
(globalThis as { window?: unknown }).window ??= globalThis;
|
||||
|
||||
const marker = `prev-tab-${Date.now()}`;
|
||||
const first = `${marker}-a`;
|
||||
const second = `${marker}-b`;
|
||||
const third = `${marker}-c`;
|
||||
const before = activeTabStore.getActiveTabId();
|
||||
|
||||
try {
|
||||
activeTabStore.setActiveTabId(first);
|
||||
activeTabStore.setActiveTabId(second);
|
||||
assert.equal(activeTabStore.getPreviousActiveTabId(), first);
|
||||
|
||||
activeTabStore.setActiveTabId(third);
|
||||
assert.equal(activeTabStore.getPreviousActiveTabId(), second);
|
||||
|
||||
activeTabStore.setActiveTabId(first, { recordPrevious: false });
|
||||
assert.equal(activeTabStore.getActiveTabId(), first);
|
||||
assert.equal(activeTabStore.getPreviousActiveTabId(), second);
|
||||
} finally {
|
||||
activeTabStore.setActiveTabId(before, { recordPrevious: false });
|
||||
if (previousWindow === undefined) {
|
||||
delete (globalThis as { window?: unknown }).window;
|
||||
} else {
|
||||
(globalThis as { window?: unknown }).window = previousWindow;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('editor tab helpers round trip ids', () => {
|
||||
assert.equal(toEditorTabId('file-1'), 'editor:file-1');
|
||||
assert.equal(fromEditorTabId('editor:file-1'), 'file-1');
|
||||
|
||||
@@ -16,14 +16,22 @@ export const toEditorTabId = (editorId: string): string => `${EDITOR_PREFIX}${ed
|
||||
/** Strip the "editor:" prefix to recover the internal editorTab id. */
|
||||
export const fromEditorTabId = (tabId: string): string => tabId.slice(EDITOR_PREFIX.length);
|
||||
|
||||
type SetActiveTabOptions = {
|
||||
/** When false, keep the previous-tab pointer unchanged (used for close restore). */
|
||||
recordPrevious?: boolean;
|
||||
};
|
||||
|
||||
class ActiveTabStore {
|
||||
private activeTabId: string = 'vault';
|
||||
private previousActiveTabId: string | null = null;
|
||||
private listeners = new Set<Listener>();
|
||||
private syncListeners = new Set<SyncListener>();
|
||||
private notifyRafId: number | null = null;
|
||||
|
||||
getActiveTabId = () => this.activeTabId;
|
||||
|
||||
getPreviousActiveTabId = () => this.previousActiveTabId;
|
||||
|
||||
private scheduleNotify = () => {
|
||||
if (this.notifyRafId !== null) return;
|
||||
const schedule = typeof requestAnimationFrame === 'function'
|
||||
@@ -35,8 +43,11 @@ class ActiveTabStore {
|
||||
});
|
||||
};
|
||||
|
||||
setActiveTabId = (id: string) => {
|
||||
setActiveTabId = (id: string, options?: SetActiveTabOptions) => {
|
||||
if (this.activeTabId !== id) {
|
||||
if (options?.recordPrevious !== false) {
|
||||
this.previousActiveTabId = this.activeTabId;
|
||||
}
|
||||
this.activeTabId = id;
|
||||
this.syncListeners.forEach((listener) => listener(id));
|
||||
// Coalesce rapid tab switches into one notification per frame and avoid
|
||||
|
||||
@@ -147,6 +147,7 @@ test("closing one split pane keeps the other terminal as an orphan tab", () => {
|
||||
workspaceId: "ws-1",
|
||||
layoutResult,
|
||||
remainingSessions: nextSessions,
|
||||
preferredTabId: "s-outside",
|
||||
}),
|
||||
"s2",
|
||||
);
|
||||
@@ -197,6 +198,81 @@ test("closing the last visible solo tab does not fall back to a hidden MCP sessi
|
||||
);
|
||||
});
|
||||
|
||||
test("closing a temporary solo tab restores the previously focused tab", () => {
|
||||
const standalone = (id: string, status: TerminalSession["status"] = "connected"): TerminalSession => ({
|
||||
...session(id),
|
||||
workspaceId: undefined,
|
||||
status,
|
||||
});
|
||||
const remainingSessions = [
|
||||
standalone("s1", "disconnected"),
|
||||
standalone("s2"),
|
||||
standalone("s3"),
|
||||
standalone("s4"),
|
||||
standalone("s5", "disconnected"),
|
||||
standalone("s6"),
|
||||
];
|
||||
const layoutResult = closeSessionWorkspaceLayoutState([], undefined, "s7");
|
||||
|
||||
assert.equal(
|
||||
resolveActiveTabAfterCloseSession({
|
||||
currentActiveTabId: "s7",
|
||||
closedSessionId: "s7",
|
||||
workspaceId: undefined,
|
||||
layoutResult,
|
||||
remainingSessions,
|
||||
preferredTabId: "s5",
|
||||
}),
|
||||
"s5",
|
||||
);
|
||||
});
|
||||
|
||||
test("closing a temporary solo tab ignores a preferred tab that no longer exists", () => {
|
||||
const standalone = (id: string): TerminalSession => ({
|
||||
...session(id),
|
||||
workspaceId: undefined,
|
||||
});
|
||||
const remainingSessions = [standalone("s1"), standalone("s2"), standalone("s6")];
|
||||
const layoutResult = closeSessionWorkspaceLayoutState([], undefined, "s7");
|
||||
|
||||
assert.equal(
|
||||
resolveActiveTabAfterCloseSession({
|
||||
currentActiveTabId: "s7",
|
||||
closedSessionId: "s7",
|
||||
workspaceId: undefined,
|
||||
layoutResult,
|
||||
remainingSessions,
|
||||
preferredTabId: "s5",
|
||||
}),
|
||||
"s6",
|
||||
);
|
||||
});
|
||||
|
||||
test("closeSessionsState restores the previous tab instead of the newest remaining tab", () => {
|
||||
const standaloneSession = (id: string): TerminalSession => ({
|
||||
...session(id),
|
||||
workspaceId: undefined,
|
||||
});
|
||||
|
||||
const result = closeSessionsState({
|
||||
sessions: [
|
||||
standaloneSession("s1"),
|
||||
standaloneSession("s2"),
|
||||
standaloneSession("s5"),
|
||||
standaloneSession("s6"),
|
||||
standaloneSession("s7"),
|
||||
],
|
||||
workspaces: [],
|
||||
sessionIds: ["s7"],
|
||||
currentActiveTabId: "s7",
|
||||
preferredTabId: "s5",
|
||||
tabOrder: ["s1", "s2", "s5", "s6", "s7"],
|
||||
});
|
||||
|
||||
assert.equal(result.activeTabId, "s5");
|
||||
assert.deepEqual(result.sessions.map((candidate) => candidate.id), ["s1", "s2", "s5", "s6"]);
|
||||
});
|
||||
|
||||
test("closing several standalone sessions removes the whole batch", () => {
|
||||
const standaloneSession = (id: string): TerminalSession => ({
|
||||
...session(id),
|
||||
|
||||
@@ -129,22 +129,64 @@ export function applyCloseSessionToSessions(
|
||||
));
|
||||
}
|
||||
|
||||
export function isPreferredTabAvailableAfterClose(
|
||||
input: {
|
||||
preferredTabId: string | null | undefined;
|
||||
closedSessionId: string;
|
||||
layoutResult: CloseSessionWorkspaceLayoutResult;
|
||||
remainingSessions: readonly TerminalSession[];
|
||||
},
|
||||
): input is {
|
||||
preferredTabId: string;
|
||||
closedSessionId: string;
|
||||
layoutResult: CloseSessionWorkspaceLayoutResult;
|
||||
remainingSessions: readonly TerminalSession[];
|
||||
} {
|
||||
const { preferredTabId, closedSessionId, layoutResult, remainingSessions } = input;
|
||||
if (!preferredTabId || preferredTabId === closedSessionId) return false;
|
||||
if (
|
||||
preferredTabId === layoutResult.dissolvedWorkspaceId
|
||||
|| preferredTabId === layoutResult.removedWorkspaceId
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (preferredTabId === "vault" || preferredTabId === "sftp") return true;
|
||||
if (layoutResult.workspaces.some((workspace) => workspace.id === preferredTabId)) return true;
|
||||
return remainingSessions.some((session) => (
|
||||
session.id === preferredTabId
|
||||
&& !session.workspaceId
|
||||
&& !session.hiddenFromTabs
|
||||
));
|
||||
}
|
||||
|
||||
export function resolveActiveTabAfterCloseSession({
|
||||
currentActiveTabId,
|
||||
closedSessionId,
|
||||
workspaceId,
|
||||
layoutResult,
|
||||
remainingSessions,
|
||||
preferredTabId,
|
||||
}: {
|
||||
currentActiveTabId: string | null;
|
||||
closedSessionId: string;
|
||||
workspaceId: string | undefined;
|
||||
layoutResult: CloseSessionWorkspaceLayoutResult;
|
||||
remainingSessions: readonly TerminalSession[];
|
||||
preferredTabId?: string | null;
|
||||
}): string | null {
|
||||
const fallbackWorkspace = layoutResult.workspaces[layoutResult.workspaces.length - 1];
|
||||
const fallbackSolo = remainingSessions.filter((session) => !session.workspaceId && !session.hiddenFromTabs).slice(-1)[0];
|
||||
const preferredInput = {
|
||||
preferredTabId,
|
||||
closedSessionId,
|
||||
layoutResult,
|
||||
remainingSessions,
|
||||
};
|
||||
const preferredFallback = isPreferredTabAvailableAfterClose(preferredInput)
|
||||
? preferredInput.preferredTabId
|
||||
: undefined;
|
||||
const fallback = layoutResult.lastRemainingSessionId
|
||||
?? preferredFallback
|
||||
?? fallbackWorkspace?.id
|
||||
?? fallbackSolo?.id
|
||||
?? "vault";
|
||||
@@ -166,12 +208,14 @@ export function closeSessionsState({
|
||||
workspaces,
|
||||
sessionIds,
|
||||
currentActiveTabId,
|
||||
preferredTabId,
|
||||
tabOrder,
|
||||
}: {
|
||||
sessions: readonly TerminalSession[];
|
||||
workspaces: readonly Workspace[];
|
||||
sessionIds: readonly string[];
|
||||
currentActiveTabId: string | null;
|
||||
preferredTabId?: string | null;
|
||||
tabOrder: readonly string[];
|
||||
}): CloseSessionsStateResult {
|
||||
let nextSessions = [...sessions];
|
||||
@@ -207,6 +251,7 @@ export function closeSessionsState({
|
||||
workspaceId,
|
||||
layoutResult,
|
||||
remainingSessions: nextSessions,
|
||||
preferredTabId,
|
||||
});
|
||||
if (activeTabAfterClose) {
|
||||
nextActiveTabId = activeTabAfterClose;
|
||||
|
||||
@@ -542,6 +542,7 @@ export const useSessionState = ({
|
||||
workspaces: workspacesRef.current,
|
||||
sessionIds,
|
||||
currentActiveTabId: activeTabStore.getActiveTabId(),
|
||||
preferredTabId: activeTabStore.getPreviousActiveTabId(),
|
||||
tabOrder: tabOrderRef.current,
|
||||
});
|
||||
|
||||
@@ -549,9 +550,10 @@ export const useSessionState = ({
|
||||
setSessions(result.sessions);
|
||||
setTabOrder(result.tabOrder);
|
||||
if (result.activeTabId) {
|
||||
setActiveTabId(result.activeTabId);
|
||||
// Closing must not rewrite previous-tab history to the closed tab.
|
||||
activeTabStore.setActiveTabId(result.activeTabId, { recordPrevious: false });
|
||||
}
|
||||
}, [setActiveTabId]);
|
||||
}, []);
|
||||
|
||||
const closeSession = useCallback((sessionId: string, e?: MouseEvent) => {
|
||||
e?.stopPropagation();
|
||||
|
||||
Reference in new issue
Block a user