fix(engine-controls): surface a paused engine instead of showing it as idle
A paused engine with nothing running derived executorState "idle", so the footer badge was indistinguishable from a healthy engine waiting for work. That is exactly the state a pause settles into once in-flight tasks drain: triage and planning stall while the board keeps moving, with the pause only visible by opening the Engine Control menu. Pause state now dominates run state; the adjacent counters still report throughput. In the CLI TUI, the global `t` (Git view) branch returned before the Utilities dispatch in the same key handler, so the advertised "[t] Toggle Engine Pause" was unreachable dead UI. It now yields when Utilities owns input, and because the shortcut can stop the board, pausing takes a second `t` within 5s while resuming stays single-press. Tests assert the invariant across the whole state matrix (both pause flags x 0/1/5 running), not just the zero-running repro, plus the TUI routing, the two-press pause, single-press resume, and re-arm behavior. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
7
.changeset/paused-engine-visibility.md
Normal file
7
.changeset/paused-engine-visibility.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
summary: A paused engine now reads "Paused" in the footer instead of "Idle", and pausing from the terminal takes two presses.
|
||||||
|
category: fix
|
||||||
|
dev: `deriveExecutorState` (dashboard `app/hooks/useExecutorStats.ts`) now returns "paused" for any `enginePaused` value regardless of `runningTaskCount`; the previous matrix mapped paused-with-zero-running to "idle". In the CLI TUI, the global `t` (Git view) branch now yields when the Utilities section owns input, making the advertised "[t] Toggle Engine Pause" reachable, and pausing requires a second `t` within `PAUSE_CONFIRM_WINDOW_MS` (5s); resuming stays single-press.
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { describe, it, expect, vi } from "vitest";
|
||||||
|
import { render } from "ink-testing-library";
|
||||||
|
import { I18nextProvider } from "react-i18next";
|
||||||
|
import { DashboardApp } from "../app.js";
|
||||||
|
import { DashboardTUI } from "../controller.js";
|
||||||
|
import { initCliI18n } from "../../../i18n/index.js";
|
||||||
|
import type { SettingsValues, SystemInfo, TUICallbacks } from "../state.js";
|
||||||
|
|
||||||
|
const testI18n = initCliI18n("en");
|
||||||
|
|
||||||
|
function renderNode(controller: DashboardTUI) {
|
||||||
|
return React.createElement(
|
||||||
|
I18nextProvider,
|
||||||
|
{ i18n: testI18n },
|
||||||
|
React.createElement(DashboardApp, { controller }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeSystemInfo(engineMode: "active" | "paused" = "active"): SystemInfo {
|
||||||
|
return {
|
||||||
|
host: "localhost",
|
||||||
|
port: 4040,
|
||||||
|
baseUrl: "http://localhost:4040",
|
||||||
|
authEnabled: false,
|
||||||
|
engineMode,
|
||||||
|
fileWatcher: true,
|
||||||
|
startTimeMs: Date.now(),
|
||||||
|
} as SystemInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeSettings(enginePaused: boolean): SettingsValues {
|
||||||
|
return {
|
||||||
|
maxConcurrent: 1,
|
||||||
|
maxWorktrees: 2,
|
||||||
|
autoMerge: false,
|
||||||
|
mergeStrategy: "direct",
|
||||||
|
pollIntervalMs: 60000,
|
||||||
|
enginePaused,
|
||||||
|
globalPause: false,
|
||||||
|
remoteActiveProvider: null,
|
||||||
|
remoteShortLivedEnabled: false,
|
||||||
|
remoteShortLivedTtlMs: 900000,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wire the minimum callback surface `handleUtilityAction` needs, echoing the
|
||||||
|
* requested pause back through settings the way dashboard.ts's onTogglePause does.
|
||||||
|
*/
|
||||||
|
function attachCallbacks(controller: DashboardTUI) {
|
||||||
|
const onTogglePause = vi.fn(async (paused: boolean) => makeSettings(paused));
|
||||||
|
controller.setCallbacks({
|
||||||
|
onTogglePause,
|
||||||
|
onRefreshStats: async () => {},
|
||||||
|
onClearLogs: () => {},
|
||||||
|
} as unknown as TUICallbacks);
|
||||||
|
return onTogglePause;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function settle() {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 25));
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
FNXC:DashboardTui 2026-07-24-18:40:
|
||||||
|
Two defects covered here, both found while diagnosing an engine that sat paused
|
||||||
|
with no visible cause:
|
||||||
|
|
||||||
|
1. Routing — the global `t` (Git view) branch returned before the Utilities
|
||||||
|
dispatch in the same key handler, so the Utilities panel's advertised
|
||||||
|
"[t] Toggle Engine Pause" was unreachable dead UI.
|
||||||
|
2. Safety — once reachable, one unmodified keystroke would silently stop triage,
|
||||||
|
planning, and dispatch. Pausing now takes two presses inside a short window;
|
||||||
|
resuming is not destructive and stays single-press.
|
||||||
|
*/
|
||||||
|
describe("Utilities [t] engine pause shortcut", () => {
|
||||||
|
it("still opens the Git view when Utilities does not own input", async () => {
|
||||||
|
const controller = new DashboardTUI();
|
||||||
|
controller.setSystemInfo(makeSystemInfo());
|
||||||
|
const onTogglePause = attachCallbacks(controller);
|
||||||
|
controller.setActiveSection("system");
|
||||||
|
|
||||||
|
const { stdin, unmount } = render(renderNode(controller));
|
||||||
|
stdin.write("t");
|
||||||
|
await settle();
|
||||||
|
|
||||||
|
const snapshot = controller.getSnapshot();
|
||||||
|
expect(snapshot.mode).toBe("interactive");
|
||||||
|
expect(snapshot.interactiveView).toBe("git");
|
||||||
|
expect(onTogglePause).not.toHaveBeenCalled();
|
||||||
|
unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("routes to the pause action instead of the Git view when Utilities is focused", async () => {
|
||||||
|
const controller = new DashboardTUI();
|
||||||
|
controller.setSystemInfo(makeSystemInfo());
|
||||||
|
attachCallbacks(controller);
|
||||||
|
controller.setActiveSection("utilities");
|
||||||
|
|
||||||
|
const { stdin, unmount } = render(renderNode(controller));
|
||||||
|
stdin.write("t");
|
||||||
|
await settle();
|
||||||
|
|
||||||
|
// First press arms the confirmation; it must not have escaped to Git view.
|
||||||
|
expect(controller.getSnapshot().mode).toBe("status");
|
||||||
|
expect(controller.getSnapshot().interactiveView).not.toBe("git");
|
||||||
|
unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("requires a second press to pause, and does not pause on the first", async () => {
|
||||||
|
const controller = new DashboardTUI();
|
||||||
|
controller.setSystemInfo(makeSystemInfo());
|
||||||
|
const onTogglePause = attachCallbacks(controller);
|
||||||
|
controller.setActiveSection("utilities");
|
||||||
|
|
||||||
|
const { stdin, unmount } = render(renderNode(controller));
|
||||||
|
|
||||||
|
stdin.write("t");
|
||||||
|
await settle();
|
||||||
|
expect(onTogglePause).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
stdin.write("t");
|
||||||
|
await settle();
|
||||||
|
expect(onTogglePause).toHaveBeenCalledTimes(1);
|
||||||
|
expect(onTogglePause).toHaveBeenCalledWith(true);
|
||||||
|
expect(controller.getSnapshot().systemInfo?.engineMode).toBe("paused");
|
||||||
|
unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resumes on a single press without confirmation", async () => {
|
||||||
|
const controller = new DashboardTUI();
|
||||||
|
controller.setSystemInfo(makeSystemInfo("paused"));
|
||||||
|
const onTogglePause = attachCallbacks(controller);
|
||||||
|
controller.setActiveSection("utilities");
|
||||||
|
|
||||||
|
const { stdin, unmount } = render(renderNode(controller));
|
||||||
|
|
||||||
|
stdin.write("t");
|
||||||
|
await settle();
|
||||||
|
expect(onTogglePause).toHaveBeenCalledTimes(1);
|
||||||
|
expect(onTogglePause).toHaveBeenCalledWith(false);
|
||||||
|
expect(controller.getSnapshot().systemInfo?.engineMode).toBe("active");
|
||||||
|
unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("re-arms rather than pausing twice when pressed a third time", async () => {
|
||||||
|
const controller = new DashboardTUI();
|
||||||
|
controller.setSystemInfo(makeSystemInfo());
|
||||||
|
const onTogglePause = attachCallbacks(controller);
|
||||||
|
controller.setActiveSection("utilities");
|
||||||
|
|
||||||
|
const { stdin, unmount } = render(renderNode(controller));
|
||||||
|
|
||||||
|
stdin.write("t"); // arm
|
||||||
|
await settle();
|
||||||
|
stdin.write("t"); // confirm -> paused
|
||||||
|
await settle();
|
||||||
|
stdin.write("t"); // engine is paused now: resume is single-press
|
||||||
|
await settle();
|
||||||
|
|
||||||
|
expect(onTogglePause).toHaveBeenCalledTimes(2);
|
||||||
|
expect(onTogglePause).toHaveBeenNthCalledWith(1, true);
|
||||||
|
expect(onTogglePause).toHaveBeenNthCalledWith(2, false);
|
||||||
|
unmount();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -4360,7 +4360,16 @@ export function DashboardApp({ controller }: DashboardAppProps) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (input === "t" || input === "T") {
|
/*
|
||||||
|
FNXC:DashboardTui 2026-07-24-18:40:
|
||||||
|
`t` is overloaded: global → Git view, Utilities section → Toggle Engine Pause.
|
||||||
|
The global branch runs first in this same handler and returned unconditionally,
|
||||||
|
so `handleUtilityAction("t")` below was unreachable and the Utilities panel
|
||||||
|
advertised a shortcut that did nothing. Yield to the Utilities dispatch when
|
||||||
|
that section owns focus (matching the `f` / logs-focus precedent above).
|
||||||
|
*/
|
||||||
|
const utilitiesOwnsInput = state.mode === "status" && state.activeSection === "utilities" && !logsFocused;
|
||||||
|
if ((input === "t" || input === "T") && !utilitiesOwnsInput) {
|
||||||
controller.setMode("interactive");
|
controller.setMode("interactive");
|
||||||
controller.setInteractiveView("git");
|
controller.setInteractiveView("git");
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -30,6 +30,14 @@ import type {
|
|||||||
} from "./state.js";
|
} from "./state.js";
|
||||||
import { SECTION_ORDER } from "./state.js";
|
import { SECTION_ORDER } from "./state.js";
|
||||||
|
|
||||||
|
/*
|
||||||
|
FNXC:DashboardTui 2026-07-24-18:40:
|
||||||
|
Window in which the second Utilities [t] press confirms an engine pause. Long
|
||||||
|
enough to read the warning line in the log pane, short enough that an unrelated
|
||||||
|
`t` minutes later cannot land on an armed confirmation.
|
||||||
|
*/
|
||||||
|
export const PAUSE_CONFIRM_WINDOW_MS = 5_000;
|
||||||
|
|
||||||
// ── DashboardTUI ─────────────────────────────────────────────────────────────
|
// ── DashboardTUI ─────────────────────────────────────────────────────────────
|
||||||
//
|
//
|
||||||
// Public API is identical to the old imperative class so dashboard.ts requires
|
// Public API is identical to the old imperative class so dashboard.ts requires
|
||||||
@@ -80,6 +88,15 @@ export class DashboardTUI {
|
|||||||
private lastAutoKillAt = 0;
|
private lastAutoKillAt = 0;
|
||||||
clipboardFlash: { ok: boolean; at: number } | null = null;
|
clipboardFlash: { ok: boolean; at: number } | null = null;
|
||||||
private clipboardFlashTimer: ReturnType<typeof setTimeout> | null = null;
|
private clipboardFlashTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
/*
|
||||||
|
FNXC:DashboardTui 2026-07-24-18:40:
|
||||||
|
Pausing the engine from Utilities [t] is a silent, board-wide stop: triage and
|
||||||
|
planning stall while in-flight tasks drain, so the board keeps moving for
|
||||||
|
minutes and the cause is invisible from the terminal. One unmodified keystroke
|
||||||
|
must not do that — the first `t` arms this deadline and only a second `t`
|
||||||
|
inside the window pauses. Resuming is not destructive and stays single-press.
|
||||||
|
*/
|
||||||
|
private pausePendingConfirmUntil = 0;
|
||||||
interactiveData: InteractiveData | null = null;
|
interactiveData: InteractiveData | null = null;
|
||||||
interactiveView: InteractiveView = "board";
|
interactiveView: InteractiveView = "board";
|
||||||
interactiveInputLocked = false;
|
interactiveInputLocked = false;
|
||||||
@@ -587,6 +604,23 @@ export class DashboardTUI {
|
|||||||
: all.filter((e) => e.level === this.logsSeverityFilter);
|
: all.filter((e) => e.level === this.logsSeverityFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Two-press confirmation for an engine pause initiated from Utilities [t].
|
||||||
|
*
|
||||||
|
* Returns true when the caller may pause: either a prior press armed the
|
||||||
|
* window and it has not expired, or this call arms it and returns false so the
|
||||||
|
* caller can prompt. Any successful confirmation disarms, so a third press
|
||||||
|
* re-arms rather than toggling twice.
|
||||||
|
*/
|
||||||
|
private consumePauseConfirmation(now: number = Date.now()): boolean {
|
||||||
|
if (this.pausePendingConfirmUntil > now) {
|
||||||
|
this.pausePendingConfirmUntil = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
this.pausePendingConfirmUntil = now + PAUSE_CONFIRM_WINDOW_MS;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
async handleUtilityAction(key: string): Promise<void> {
|
async handleUtilityAction(key: string): Promise<void> {
|
||||||
if (!this.callbacks) return;
|
if (!this.callbacks) return;
|
||||||
|
|
||||||
@@ -601,6 +635,13 @@ export class DashboardTUI {
|
|||||||
case "t":
|
case "t":
|
||||||
if (this.systemInfo) {
|
if (this.systemInfo) {
|
||||||
const newPaused = this.systemInfo.engineMode !== "paused";
|
const newPaused = this.systemInfo.engineMode !== "paused";
|
||||||
|
if (newPaused && !this.consumePauseConfirmation()) {
|
||||||
|
this.warn(
|
||||||
|
`Press [t] again within ${Math.round(PAUSE_CONFIRM_WINDOW_MS / 1000)}s to pause the engine (stops triage, planning, and new dispatch).`,
|
||||||
|
"engine",
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
const newSettings = await this.callbacks.onTogglePause(newPaused);
|
const newSettings = await this.callbacks.onTogglePause(newPaused);
|
||||||
const newEngineMode = newSettings.enginePaused ? "paused" : "active";
|
const newEngineMode = newSettings.enginePaused ? "paused" : "active";
|
||||||
this.setSystemInfo({ ...this.systemInfo, engineMode: newEngineMode });
|
this.setSystemInfo({ ...this.systemInfo, engineMode: newEngineMode });
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { deriveExecutorState } from "../hooks/useExecutorStats";
|
||||||
|
|
||||||
|
/*
|
||||||
|
FNXC:EngineControls 2026-07-24-18:35:
|
||||||
|
Regression cover for the pause-reads-as-idle bug: a paused engine with nothing
|
||||||
|
running rendered the same "Idle" footer badge as a healthy engine waiting for
|
||||||
|
work, so operators could not tell why triage/planning had stopped. The invariant
|
||||||
|
under test is pause-dominates-run-state across the whole matrix, not just the
|
||||||
|
reported zero-running repro — asserted at every running count and against both
|
||||||
|
pause flags, since globalPause must keep out-ranking enginePaused.
|
||||||
|
*/
|
||||||
|
describe("deriveExecutorState", () => {
|
||||||
|
it("reports paused at every running count, including zero", () => {
|
||||||
|
for (const runningTaskCount of [0, 1, 5]) {
|
||||||
|
expect(deriveExecutorState(false, true, runningTaskCount)).toBe("paused");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reports stopped whenever globalPause is set, outranking enginePaused", () => {
|
||||||
|
for (const enginePaused of [false, true]) {
|
||||||
|
for (const runningTaskCount of [0, 1, 5]) {
|
||||||
|
expect(deriveExecutorState(true, enginePaused, runningTaskCount)).toBe("stopped");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reports idle only when unpaused with nothing running", () => {
|
||||||
|
expect(deriveExecutorState(false, false, 0)).toBe("idle");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reports running when unpaused with work in flight", () => {
|
||||||
|
expect(deriveExecutorState(false, false, 1)).toBe("running");
|
||||||
|
expect(deriveExecutorState(false, false, 5)).toBe("running");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("never reports idle while either pause flag is set", () => {
|
||||||
|
for (const globalPause of [false, true]) {
|
||||||
|
for (const enginePaused of [false, true]) {
|
||||||
|
if (!globalPause && !enginePaused) continue;
|
||||||
|
for (const runningTaskCount of [0, 1, 5]) {
|
||||||
|
expect(deriveExecutorState(globalPause, enginePaused, runningTaskCount)).not.toBe("idle");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -84,9 +84,13 @@ export type ExecutorState = "idle" | "running" | "paused" | "stopped";
|
|||||||
*
|
*
|
||||||
* The executorState is derived from:
|
* The executorState is derived from:
|
||||||
* - "stopped": globalPause is true
|
* - "stopped": globalPause is true
|
||||||
* - "idle": (enginePaused is true AND runningTaskCount is 0) OR not paused with nothing running
|
* - "paused": enginePaused is true (regardless of runningTaskCount)
|
||||||
* - "paused": enginePaused is true AND runningTaskCount > 0
|
|
||||||
* - "running": globalPause is false AND enginePaused is false AND runningTaskCount > 0
|
* - "running": globalPause is false AND enginePaused is false AND runningTaskCount > 0
|
||||||
|
* - "idle": nothing paused and nothing running
|
||||||
|
*
|
||||||
|
* FNXC:EngineControls 2026-07-24-18:35: pause state dominates run state so a paused
|
||||||
|
* engine never reads "Idle" once in-flight work drains. See deriveExecutorState in
|
||||||
|
* app/hooks/useExecutorStats.ts for the rationale.
|
||||||
*/
|
*/
|
||||||
export interface ExecutorStats {
|
export interface ExecutorStats {
|
||||||
/** Number of tasks currently in "in-progress" column */
|
/** Number of tasks currently in "in-progress" column */
|
||||||
|
|||||||
@@ -28,14 +28,17 @@ export interface UseExecutorStatsResult {
|
|||||||
* Derive the executor state from globalPause, enginePaused, and runningTaskCount.
|
* Derive the executor state from globalPause, enginePaused, and runningTaskCount.
|
||||||
*
|
*
|
||||||
* - "stopped": globalPause is true
|
* - "stopped": globalPause is true
|
||||||
* - "idle": (enginePaused is true AND runningTaskCount is 0) OR not paused with nothing running
|
* - "paused": enginePaused is true (regardless of runningTaskCount)
|
||||||
* - "paused": enginePaused is true AND runningTaskCount > 0
|
|
||||||
* - "running": globalPause is false AND enginePaused is false AND runningTaskCount > 0
|
* - "running": globalPause is false AND enginePaused is false AND runningTaskCount > 0
|
||||||
|
* - "idle": nothing paused and nothing running
|
||||||
*
|
*
|
||||||
* FNXC:EngineControls 2026-06-22-00:00:
|
* FNXC:EngineControls 2026-06-22-00:00:
|
||||||
* `globalPause` dominates the footer state matrix so an operator-stopped engine is distinct from idle even if in-progress tasks still exist.
|
* `globalPause` dominates the footer state matrix so an operator-stopped engine is distinct from idle even if in-progress tasks still exist.
|
||||||
|
*
|
||||||
|
* FNXC:EngineControls 2026-07-24-18:35:
|
||||||
|
* A paused engine must read "Paused" even when nothing is running. The prior matrix mapped (enginePaused && runningTaskCount === 0) to "idle", so the footer was indistinguishable from a healthy engine waiting for work — and that is precisely the state a pause produces once in-flight tasks drain. Operators saw triage tasks sitting untouched with an "Idle" badge and no visible cause; the pause is only otherwise surfaced by the Engine Control menu item label, which requires opening the menu. Pause state now dominates run state: the badge reports the operator-set condition, and the separate running/queued counters already report throughput.
|
||||||
*/
|
*/
|
||||||
function deriveExecutorState(
|
export function deriveExecutorState(
|
||||||
globalPause: boolean,
|
globalPause: boolean,
|
||||||
enginePaused: boolean,
|
enginePaused: boolean,
|
||||||
runningTaskCount: number
|
runningTaskCount: number
|
||||||
@@ -43,10 +46,7 @@ function deriveExecutorState(
|
|||||||
if (globalPause) {
|
if (globalPause) {
|
||||||
return "stopped";
|
return "stopped";
|
||||||
}
|
}
|
||||||
if (enginePaused && runningTaskCount === 0) {
|
if (enginePaused) {
|
||||||
return "idle";
|
|
||||||
}
|
|
||||||
if (enginePaused && runningTaskCount > 0) {
|
|
||||||
return "paused";
|
return "paused";
|
||||||
}
|
}
|
||||||
// globalPause is false and enginePaused is false
|
// globalPause is false and enginePaused is false
|
||||||
@@ -121,7 +121,7 @@ function hasActionableBlockedBy(blockedBy: Task["blockedBy"] | string[] | null):
|
|||||||
* - Derives blockedTaskCount from tasks with blockedBy field set
|
* - Derives blockedTaskCount from tasks with blockedBy field set
|
||||||
* - Derives stuckTaskCount using the project's `taskStuckTimeoutMs` setting;
|
* - Derives stuckTaskCount using the project's `taskStuckTimeoutMs` setting;
|
||||||
* returns 0 when the setting is undefined/disabled
|
* returns 0 when the setting is undefined/disabled
|
||||||
* - Derives executorState from globalPause and enginePaused flags, with globalPause mapping to "stopped"
|
* - Derives executorState from globalPause and enginePaused flags, with globalPause mapping to "stopped" and enginePaused to "paused" at any running count
|
||||||
* - Returns ExecutorStats object with reactive updates
|
* - Returns ExecutorStats object with reactive updates
|
||||||
*/
|
*/
|
||||||
const DEFAULT_API_DATA: Pick<ExecutorStats, "maxConcurrent" | "lastActivityAt"> & {
|
const DEFAULT_API_DATA: Pick<ExecutorStats, "maxConcurrent" | "lastActivityAt"> & {
|
||||||
|
|||||||
Reference in New Issue
Block a user