feat(run): session tab icons and a Debug button in the run panel (#1283)

A finished run session's tab keeps its configuration's own icon instead of the green run arrow (a failed exit still shows the error icon), and the run panel's toolbar gets a Debug button next to Rerun that starts the same configuration under the debugger (in Services, a debug session reruns its debugger launch).

Co-authored-by: qtvbwfn <[email protected]>
Цей коміт міститься в:
Qingтаqtvbwfn authored and GitHub committed 2026-11-05 00:57:54 +08:00
1 джерело 78a3768ec1
коміт 19c2d40528
4 files changed
+115 -2

No files matched your search

+82
Переглянути файл
@@ -0,0 +1,82 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { RunConfig } from "@/platform/ipc";
import { debugSession, resetRuns, useRun, type RunSession } from "./store";
import { sessionIcon } from "./RunView";
const actions = vi.hoisted(() => ({ executeAction: vi.fn() }));
vi.mock("@/platform/actions", async (orig) => ({ ...(await orig<object>()), executeAction: actions.executeAction }));
const config = (extra: Partial<RunConfig> = {}): RunConfig => ({
id: "c",
name: "c",
kind: "spring-boot",
command: "mvn",
args: [],
cwd: null,
env: {},
detected: false,
isTest: false,
debugAdapter: null,
...extra,
});
const session = (extra: Partial<RunSession> = {}): RunSession => ({
id: "s1",
config: config(),
runId: null,
status: "finished",
exitCode: 0,
startedAt: 0,
endedAt: 1,
stdinClosed: false,
...extra,
});
describe("sessionIcon", () => {
it("shows the execution arrow while running, the check for tests", () => {
expect(sessionIcon(session({ status: "running" }))).toBe("run");
expect(sessionIcon(session({ status: "starting" }))).toBe("run");
expect(sessionIcon(session({ status: "starting", config: config({ isTest: true }) }))).toBe("check");
});
it("shows the red error icon for a failed exit", () => {
expect(sessionIcon(session({ status: "failed" }))).toBe("error");
expect(sessionIcon(session({ exitCode: 1 }))).toBe("error");
});
it("shows the configuration's own icon once the process ended (IDEA's third state)", () => {
expect(sessionIcon(session())).toBe("spring-boot");
expect(sessionIcon(session({ config: config({ kind: "program" }) }))).toBe("file-binary");
expect(sessionIcon(session({ config: config({ kind: "npm" }) }))).toBe("file-npm");
});
it("never shows the error icon for a session the user stopped", () => {
expect(sessionIcon(session({ exitCode: 1, stopped: true }))).toBe("spring-boot");
});
});
describe("debugSession", () => {
beforeEach(() => {
resetRuns();
actions.executeAction.mockClear();
});
it("launches the session's configuration through run.debugConfig", () => {
const c = config();
useRun.setState({ sessions: [session({ config: c })] });
debugSession("s1");
expect(actions.executeAction).toHaveBeenCalledWith("run.debugConfig", c);
});
it("prefers the original configuration of a derived run", () => {
const base = config({ id: "base" });
useRun.setState({ sessions: [session({ config: config({ id: "derived" }), baseConfig: base })] });
debugSession("s1");
expect(actions.executeAction).toHaveBeenCalledWith("run.debugConfig", base);
});
it("ignores unknown session ids", () => {
debugSession("nope");
expect(actions.executeAction).not.toHaveBeenCalled();
});
});
+22 -2
Переглянути файл
@@ -14,6 +14,7 @@ import {
clearConsole,
closeSessionTab,
closeStdin,
debugSession,
failedTests,
rerunFailed,
rerunSession,
@@ -31,6 +32,7 @@ import { TestTreeView, formatDuration } from "./TestTreeView";
import { StackTracePanel, closeStackTrace, useStackTraces, type StackTraceTab } from "./stackTrace";
import { showApplicationUpdate, useApplicationUpdates } from "./applicationUpdate";
import { isUpdatable } from "./updatePolicies";
import { configIcon } from "./configs";
import styles from "./Run.module.css";
import { tx } from "@/platform/i18n";
@@ -38,19 +40,28 @@ function failed(s: RunSession): boolean {
return s.status === "failed" || (s.status === "finished" && s.exitCode !== 0 && !s.stopped);
}
function sessionIcon(s: RunSession): string {
/**
* Tab icon, IDEA's three states: the green execution arrow while the process is alive,
* the red error badge for a failed exit, and once the process ended the run
* configuration's own icon (Services keeps the same model, see services.tsx).
*/
export function sessionIcon(s: RunSession): string {
if (s.status === "running" || s.status === "starting") return s.config.isTest ? "check" : "run";
if (failed(s)) return "error";
return s.config.isTest ? "check" : "run";
// Ended (exit or stop): the configuration's own icon — the green arrow stays "still running".
return configIcon(s.config);
}
export function SessionPanel({
session,
onRerun,
onDebug,
onStop,
}: {
session: RunSession;
onRerun?: (() => void) | null;
/** Override the Debug button (Services: rerun the attached debugger's launch); null disables it. */
onDebug?: (() => void) | null;
onStop?: () => void;
}) {
const updating = useApplicationUpdates((s) => s.ids.has(session.id));
@@ -136,6 +147,15 @@ export function SessionPanel({
disabled={onRerun === null}
onClick={onRerun ?? (() => rerunSession(session.id))}
/>
{/* Switch the launch mode: the same configuration started under the debugger. */}
<IconButton
icon="debug"
iconSize={14}
title="Debug"
shortcut={shortcutLabel("run.debug")}
disabled={onDebug === null}
onClick={onDebug ?? (() => void debugSession(session.id))}
/>
{rt.tests && (
<IconButton
icon="rerun"
+1
Переглянути файл
@@ -387,6 +387,7 @@ function RunDetail({ item }: { item: ServiceTreeItem }) {
key={execution.run!.id}
session={execution.run!}
onRerun={d ? d.rerun : undefined}
onDebug={d ? d.rerun : undefined}
onStop={d ? () => void stopServiceSessions([execution]) : undefined}
/>
);
+10
Переглянути файл
@@ -1063,6 +1063,16 @@ export function rerunSession(sessionId: string) {
if (session) void startRun(session.config, sessionId, session.baseConfig);
}
/**
* "Debug": relaunches the session's configuration in debug mode — the same
* `run.debugConfig` entry the Services view and the editor context menu use
* (`baseConfig` keeps the original configuration for derived launches).
*/
export function debugSession(sessionId: string) {
const session = getSession(sessionId);
if (session) void executeAction("run.debugConfig", session.baseConfig ?? session.config);
}
/** Paths of the failed tests of a session's last run. */
export function failedTests(sessionId: string): string[][] {
const tests = runtimes.get(sessionId)?.tests;