feat(app): ask before closing with running Run/Debug processes (#1445)

Closing a window, closing or switching a project, or quitting while Run/Debug sessions are still running now asks before terminating them (Terminate / Cancel / Don't ask again, sharing run.confirmTerminate with the Run tab prompt). Run publishes its live sessions through a RunningProcessesProvider contribution, so the close guard collects them without importing the run feature; the prompt comes before the unsaved-changes step so cancelling never discards edits.

Closes #1433

Co-authored-by: ppxb <[email protected]>
Цей коміт міститься в:
ppxbтаppxb authored and GitHub committed 2026-11-05 12:34:35 +08:00
1 джерело 3e88fa1f39
коміт 43af99d3ec
7 files changed
+223 -7

No files matched your search

+36
Переглянути файл
@@ -1459,6 +1459,42 @@ export const useDebugRunSessions = create<{
Console: ComponentType<{ sessionId: number }> | null;
}>(() => ({ sessions: [], serviceSessions: [], Console: null }));
// ---------------------------------------------------------------------------
// Processes a closing window or project would terminate. Features that own
// child processes (Run/Debug sessions) publish them here, so the close guard can
// ask first (IDEA's "Terminate processes?").
export interface RunningProcess {
/** Configuration or session name shown in the prompt. */
name: string;
}
export interface RunningProcessesProvider {
id: string;
/** Processes this feature is running in the window right now. */
running(): RunningProcess[] | Promise<RunningProcess[]>;
}
const runningProcessesProviders = new Map<string, RunningProcessesProvider>();
export function registerRunningProcessesProvider(p: RunningProcessesProvider): () => void {
runningProcessesProviders.set(p.id, p);
return () => {
if (runningProcessesProviders.get(p.id) === p) runningProcessesProviders.delete(p.id);
};
}
/** Live processes of every provider, one entry per name. */
export async function runningProcesses(): Promise<RunningProcess[]> {
const all = (await Promise.all([...runningProcessesProviders.values()].map((p) => p.running()))).flat();
const seen = new Set<string>();
return all.filter((p) => {
if (!p.name || seen.has(p.name)) return false;
seen.add(p.name);
return true;
});
}
// ---------------------------------------------------------------------------
// "Get from Version Control" sources besides a plain URL (IDEA: GitHub, GitLab…),
// listed in the Clone Repository dialog's sidebar.
+35
Переглянути файл
@@ -0,0 +1,35 @@
import { expect, it, vi } from "vitest";
import { registerRunningProcessesProvider, runningProcesses } from "./contributions";
it("collects live processes from every provider, one entry per name", async () => {
const first = vi.fn(async () => [{ name: "app" }, { name: "worker" }]);
const second = vi.fn(() => [{ name: "app" }, { name: "docs" }]);
const disposers = [
registerRunningProcessesProvider({ id: "test.first", running: first }),
registerRunningProcessesProvider({ id: "test.second", running: second }),
];
try {
expect(await runningProcesses()).toEqual([{ name: "app" }, { name: "worker" }, { name: "docs" }]);
expect(first).toHaveBeenCalledOnce();
expect(second).toHaveBeenCalledOnce();
} finally {
disposers.forEach((dispose) => dispose());
}
expect(await runningProcesses()).toEqual([]);
});
it("keeps a replacement provider registered when its predecessor is disposed", async () => {
const disposeOld = registerRunningProcessesProvider({ id: "test.replaced", running: async () => [] });
const disposeNew = registerRunningProcessesProvider({
id: "test.replaced",
running: async () => [{ name: "app" }],
});
try {
disposeOld();
expect(await runningProcesses()).toEqual([{ name: "app" }]);
} finally {
disposeOld();
disposeNew();
}
expect(await runningProcesses()).toEqual([]);
});