mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-14 16:07:52 +00:00
fix(terminal): hide the tool window or refocus when a session ends (#3290)
With "Close session when it ends" on, a session that exited on its own closed its tab but left an empty Terminal tool window open, and the pane menu's Close Session did the same. Both paths now follow the tab close button: the tool window hides when no tabs remain; otherwise the active session takes the caret, which for a shell exiting on its own happens only when a terminal had the caret, so a background exit doesn't pull focus out of the editor. Co-authored-by: zhzhsx <1379025+zhzhsx@users.noreply.github.com>
This commit is contained in:
@@ -128,7 +128,9 @@ function paneMenu(e: MouseEvent, key: string) {
|
||||
icon: "close",
|
||||
run: () =>
|
||||
void requestCloseSession(key).then((closed) => {
|
||||
if (closed) requestAnimationFrame(() => activeSession()?.focus());
|
||||
if (!closed) return;
|
||||
if (useTerminals.getState().tabs.length === 0) toolWindowService().hide("terminal");
|
||||
else requestAnimationFrame(() => activeSession()?.focus());
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { provideToolWindowService, type ToolWindowService } from "@/workbench/contributions";
|
||||
import { closeAll, getSession, newTab, useTerminals } from "./store";
|
||||
|
||||
const { focused } = vi.hoisted(() => ({ focused: [] as string[] }));
|
||||
|
||||
vi.mock("./session", () => ({
|
||||
TerminalSession: class {
|
||||
key: string;
|
||||
state = { cwd: "/repo" };
|
||||
host = document.createElement("div");
|
||||
private readonly onExit: ((key: string) => void) | undefined;
|
||||
constructor(opts: { key: string; onExit?: (key: string) => void }) {
|
||||
this.key = opts.key;
|
||||
this.onExit = opts.onExit;
|
||||
}
|
||||
exited() {
|
||||
this.onExit?.(this.key);
|
||||
}
|
||||
focus() {
|
||||
focused.push(this.key);
|
||||
}
|
||||
dispose() {}
|
||||
},
|
||||
}));
|
||||
|
||||
const hide = vi.fn();
|
||||
beforeEach(() => {
|
||||
hide.mockClear();
|
||||
focused.length = 0;
|
||||
vi.stubGlobal("requestAnimationFrame", (cb: FrameRequestCallback) => {
|
||||
cb(0);
|
||||
return 0;
|
||||
});
|
||||
provideToolWindowService({ hide } as unknown as ToolWindowService);
|
||||
closeAll();
|
||||
});
|
||||
afterEach(() => vi.unstubAllGlobals());
|
||||
|
||||
function shellExit(key: string) {
|
||||
(getSession(key) as unknown as { exited(): void }).exited();
|
||||
}
|
||||
|
||||
describe("shell exit closes the last tab", () => {
|
||||
it("hides the Terminal tool window", () => {
|
||||
const tab = newTab("/repo");
|
||||
shellExit(tab.session);
|
||||
expect(useTerminals.getState().tabs).toHaveLength(0);
|
||||
expect(hide).toHaveBeenCalledWith("terminal");
|
||||
});
|
||||
|
||||
it("keeps it open while another session is running", () => {
|
||||
const a = newTab("/repo");
|
||||
newTab("/repo");
|
||||
shellExit(a.session);
|
||||
expect(hide).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("shell exit with sessions left", () => {
|
||||
/** Puts the caret in `key`'s terminal, as typing into it would. */
|
||||
function typeInto(key: string) {
|
||||
const host = (getSession(key) as unknown as { host: HTMLDivElement }).host;
|
||||
const input = document.createElement("textarea");
|
||||
host.appendChild(input);
|
||||
document.body.appendChild(host);
|
||||
input.focus();
|
||||
}
|
||||
|
||||
it("focuses the session that takes over the active tab", () => {
|
||||
const a = newTab("/repo");
|
||||
const b = newTab("/repo");
|
||||
typeInto(b.session);
|
||||
shellExit(b.session);
|
||||
expect(useTerminals.getState().activeTab).toBe(a.id);
|
||||
expect(focused).toContain(a.session);
|
||||
});
|
||||
|
||||
it("refocuses the active session when the terminal has the caret and a background shell exits", () => {
|
||||
const a = newTab("/repo");
|
||||
const b = newTab("/repo");
|
||||
typeInto(b.session);
|
||||
shellExit(a.session);
|
||||
expect(focused).toContain(b.session);
|
||||
});
|
||||
|
||||
it("leaves the caret where it is when the terminal does not have it", () => {
|
||||
const a = newTab("/repo");
|
||||
newTab("/repo");
|
||||
const editor = document.createElement("input");
|
||||
document.body.appendChild(editor);
|
||||
editor.focus();
|
||||
shellExit(a.session);
|
||||
expect(focused).toEqual([]);
|
||||
expect(document.activeElement).toBe(editor);
|
||||
});
|
||||
});
|
||||
@@ -8,6 +8,7 @@ import { setContext } from "@/platform/context";
|
||||
import { t } from "@/platform/i18n";
|
||||
import { basename } from "@/platform/project";
|
||||
import { workflowSettings } from "@/platform/workflowSettings";
|
||||
import { toolWindowService } from "@/workbench/contributions";
|
||||
import { confirm } from "@/workbench/messages";
|
||||
import { readStored, writeStored, type StoredTerminals } from "./persist";
|
||||
import { TerminalSession, type SessionOptions, type SessionSnapshot } from "./session";
|
||||
@@ -96,7 +97,15 @@ function createSession(cwd: string, command?: SessionOptions["command"], fallbac
|
||||
},
|
||||
onExit: (k) => {
|
||||
const tab = tabOfSession(k);
|
||||
if (tab && workflowSettings().terminal.closeOnExit) closeTab(tab.id);
|
||||
if (!tab || !workflowSettings().terminal.closeOnExit) return;
|
||||
// A session ending on its own only takes the caret back when the terminal had it: a
|
||||
// background shell exiting must not pull focus out of the editor.
|
||||
const focus = document.activeElement;
|
||||
const hadFocus = !!focus && [...sessions.values()].some((s) => s.host?.contains(focus));
|
||||
closeTab(tab.id);
|
||||
if (useTerminals.getState().tabs.length === 0) toolWindowService().hide("terminal");
|
||||
// Wait for the surviving pane to re-mount before giving xterm the caret.
|
||||
else if (hadFocus) requestAnimationFrame(() => activeSession()?.focus());
|
||||
},
|
||||
});
|
||||
sessions.set(key, session);
|
||||
|
||||
Reference in New Issue
Block a user