feat(run): list recently started configurations at the top of the chooser (#1121)

The Run/Debug configuration popup lists the five configurations started last under "Recent", newest first, after the pinned ones and not repeated in their group. Starts are recorded from run and debug sessions (any executor, reruns included). Kept per project.

Refs #997
Цей коміт міститься в:
l0ng-ai authored and GitHub committed 2026-11-04 08:15:13 +08:00
1 джерело 4c5b6de9ad
коміт 192e613830
4 files changed
+112 -13

No files matched your search

+12 -2
Переглянути файл
@@ -38,6 +38,7 @@ export function buildRows(
selected?: RunConfig | null,
temporary: readonly string[] = [],
pinned: readonly string[] = [],
recent: readonly string[] = [],
): Row[] {
const rows: Row[] = [];
// IDEA: "Save '<name>' Configuration" first while a temporary configuration is selected.
@@ -54,7 +55,14 @@ export function buildRows(
rows.push({ type: "section", key: "s:pinned", label: tx("Pinned") });
for (const { c, indices } of pinnedRows) rows.push({ type: "config", key: `c:${c.id}`, config: c, indices });
}
const rest = configs.filter((c) => !pinnedConfigs.includes(c));
// Then the ones started last, newest first.
const recentConfigs = recent.flatMap((id) => configs.find((c) => c.id === id && !pinned.includes(id)) ?? []);
const recentRows = matching(recentConfigs);
if (recentRows.length) {
rows.push({ type: "section", key: "s:recent", label: tx("Recent") });
for (const { c, indices } of recentRows) rows.push({ type: "config", key: `c:${c.id}`, config: c, indices });
}
const rest = configs.filter((c) => !pinnedConfigs.includes(c) && !recentConfigs.includes(c));
for (const group of groupConfigs(rest)) {
const matched = matching(group.configs);
if (matched.length === 0) continue;
@@ -99,6 +107,7 @@ function ConfigChooser() {
const selectedId = useRun((s) => s.selectedId);
const temporary = useRun((s) => s.temporary);
const pinned = useRun((s) => s.pinned);
const recent = useRun((s) => s.recent);
const runTargets = useWorkflowSettings((s) => s.runTargets);
const sessions = useRun((s) => s.sessions);
const debugSessions = useDebugRunSessions((s) => s.sessions);
@@ -111,8 +120,9 @@ function ConfigChooser() {
configs.find((c) => c.id === selectedId),
temporary,
pinned,
recent,
),
[configs, query, selectedId, temporary, pinned],
[configs, query, selectedId, temporary, pinned, recent],
);
const isSection = (r: Row) => r.type === "section";
const initial = Math.max(
+47
Переглянути файл
@@ -82,3 +82,50 @@ it("pins and unpins from the row's button", async () => {
fireEvent.click(screen.getByRole("button", { name: "Unpin" }));
expect(useRun.getState().pinned).toEqual([]);
});
it("lists the configurations started last after the pinned ones, newest first", () => {
expect(names(buildRows(configs, "", null, [], ["serve"], ["App", "serve", "build"]))).toEqual([
"current",
"[Pinned]",
"serve",
"[Recent]",
"App",
"build",
"edit",
]);
});
it("marks a configuration recent when a run or debug session starts", async () => {
const { markRecent, trackRecentConfigurations, MAX_RECENT } = await import("./store");
const { useDebugRunSessions } = await import("@/workbench/contributions");
await loadConfigs();
const off = trackRecentConfigurations();
const session = (id: string, startedAt: number) =>
({ id: `s-${id}`, config: config(id), runId: null, status: "running", startedAt }) as never;
useRun.setState({ sessions: [session("build", 1)] });
useDebugRunSessions.setState({
sessions: [
{
id: 1,
configId: "App",
name: "App",
startedAt: 2,
paused: false,
rerun: null,
stop: () => {},
resume: () => {},
},
],
});
expect(useRun.getState().recent).toEqual(["App", "build"]);
// A rerun reuses the session with a new start time.
useRun.setState({ sessions: [session("build", 3)] });
expect(useRun.getState().recent).toEqual(["build", "App"]);
off();
useDebugRunSessions.setState({ sessions: [] });
for (let i = 0; i < MAX_RECENT + 2; i++) markRecent(`c${i}`);
expect(useRun.getState().recent).toHaveLength(MAX_RECENT);
resetRuns();
await loadConfigs();
expect(useRun.getState().recent[0]).toBe(`c${MAX_RECENT + 1}`);
});
+2
Переглянути файл
@@ -71,6 +71,7 @@ import {
selectedConfig,
startRun,
stopSession,
trackRecentConfigurations,
useRun,
} from "./store";
import { compoundTargets, selectedTarget } from "./sessionTargets";
@@ -296,6 +297,7 @@ export function activate(): () => void {
registerExternalTools(),
activateStackTrace(),
listenForApplicationDeactivation(),
trackRecentConfigurations(),
useDebugRunSessions.subscribe(() => setContext("run.updatableRunning", updatableSessions().length > 0)),
registerEditorContribution({ id: "run.targets", order: 55, extensions: runGutter }),
registerEditorContribution({ id: "run.npmScripts", order: 58, extensions: npmGutter }),
+51 -11
Переглянути файл
@@ -114,6 +114,8 @@ interface RunState {
temporary: string[];
/** Ids of configurations pinned to the top of the chooser, in pin order. */
pinned: string[];
/** Ids of the configurations started last, newest first (at most `MAX_RECENT`). */
recent: string[];
sessions: RunSession[];
activeSession: string | null;
/** Output version per session (re-render trigger for the console). */
@@ -140,6 +142,7 @@ export const useRun = create<RunState>(() => ({
selectedId: null,
temporary: [],
pinned: [],
recent: [],
sessions: [],
activeSession: null,
versions: {},
@@ -205,7 +208,8 @@ export async function loadConfigs(): Promise<void> {
useRun.setState({
configs,
temporary: temps.map((t) => t.id),
pinned: readPinned(root),
pinned: readIds(PINNED_KEY, root),
recent: readIds(RECENT_KEY, root),
loaded: true,
loadError: null,
selectedId,
@@ -235,29 +239,64 @@ export function selectConfig(id: string | null) {
}
const PINNED_KEY = "openidea.run.pinned:";
const RECENT_KEY = "openidea.run.recent:";
export const MAX_RECENT = 5;
function readPinned(root: string): string[] {
function readIds(key: string, root: string): string[] {
try {
const raw = JSON.parse(localStorage.getItem(PINNED_KEY + root) ?? "[]") as unknown;
const raw = JSON.parse(localStorage.getItem(key + root) ?? "[]") as unknown;
return Array.isArray(raw) ? raw.filter((id): id is string => typeof id === "string") : [];
} catch {
return [];
}
}
function writeIds(key: string, ids: readonly string[]) {
const root = useProject.getState().project?.root;
if (!root) return;
try {
if (ids.length) localStorage.setItem(key + root, JSON.stringify(ids));
else localStorage.removeItem(key + root);
} catch {
/* storage unavailable */
}
}
/** Pins a configuration to the top of the chooser, or unpins it. Kept per project. */
export function togglePinned(id: string) {
const { pinned } = useRun.getState();
const next = pinned.includes(id) ? pinned.filter((p) => p !== id) : [...pinned, id];
useRun.setState({ pinned: next });
const root = useProject.getState().project?.root;
if (!root) return;
try {
if (next.length) localStorage.setItem(PINNED_KEY + root, JSON.stringify(next));
else localStorage.removeItem(PINNED_KEY + root);
} catch {
/* storage unavailable */
}
writeIds(PINNED_KEY, next);
}
/** A configuration was started: it leads the chooser's recent configurations. Kept per project. */
export function markRecent(id: string) {
const { recent } = useRun.getState();
if (id === CURRENT_FILE || recent[0] === id) return;
const next = [id, ...recent.filter((r) => r !== id)].slice(0, MAX_RECENT);
useRun.setState({ recent: next });
writeIds(RECENT_KEY, next);
}
/** Marks every configuration whose run or debug session starts (any executor, reruns included). */
export function trackRecentConfigurations(): () => void {
const offRun = useRun.subscribe((s, prev) => {
if (s.sessions === prev.sessions) return;
for (const x of s.sessions) {
if (!prev.sessions.some((p) => p.id === x.id && p.startedAt === x.startedAt)) markRecent(x.config.id);
}
});
const offDebug = useDebugRunSessions.subscribe((s, prev) => {
if (s.sessions === prev.sessions) return;
for (const x of s.sessions) {
if (x.configId && !prev.sessions.some((p) => p.id === x.id)) markRecent(x.configId);
}
});
return () => {
offRun();
offDebug();
};
}
/** Whether the chooser's "Current File" is selected. */
@@ -1131,6 +1170,7 @@ export function resetRuns() {
selectedId: null,
temporary: [],
pinned: [],
recent: [],
sessions: [],
activeSession: null,
versions: {},