pi-workflow-engine 0.10.2 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.pi/extensions/pi-workflow-engine/index.ts +199 -120
- package/.pi/extensions/pi-workflow-engine/src/advisory-schema.ts +11 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-attempt.ts +145 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-replay.ts +376 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-runner-types.ts +87 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-runner.ts +84 -529
- package/.pi/extensions/pi-workflow-engine/src/agent-session-identity.ts +256 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-session.ts +413 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-skills.ts +43 -1
- package/.pi/extensions/pi-workflow-engine/src/agent-workspace.ts +81 -0
- package/.pi/extensions/pi-workflow-engine/src/budget.ts +7 -0
- package/.pi/extensions/pi-workflow-engine/src/cancellation.ts +17 -3
- package/.pi/extensions/pi-workflow-engine/src/concurrency.ts +164 -61
- package/.pi/extensions/pi-workflow-engine/src/diff-capture.ts +117 -20
- package/.pi/extensions/pi-workflow-engine/src/discovery.ts +64 -15
- package/.pi/extensions/pi-workflow-engine/src/dynamax-shortcuts.ts +59 -13
- package/.pi/extensions/pi-workflow-engine/src/dynamax.ts +14 -3
- package/.pi/extensions/pi-workflow-engine/src/engine.ts +208 -74
- package/.pi/extensions/pi-workflow-engine/src/finalizers.ts +66 -0
- package/.pi/extensions/pi-workflow-engine/src/identity-canonicalization.ts +138 -0
- package/.pi/extensions/pi-workflow-engine/src/identity-fingerprint.ts +15 -0
- package/.pi/extensions/pi-workflow-engine/src/inline-workflow.ts +8 -3
- package/.pi/extensions/pi-workflow-engine/src/journal.ts +148 -64
- package/.pi/extensions/pi-workflow-engine/src/options.ts +9 -6
- package/.pi/extensions/pi-workflow-engine/src/process-runner.ts +203 -39
- package/.pi/extensions/pi-workflow-engine/src/progress.ts +2 -5
- package/.pi/extensions/pi-workflow-engine/src/replay-path-identity.ts +48 -0
- package/.pi/extensions/pi-workflow-engine/src/resume-context.ts +665 -0
- package/.pi/extensions/pi-workflow-engine/src/review/github-pr-comments.ts +75 -16
- package/.pi/extensions/pi-workflow-engine/src/review/review-actions.ts +83 -17
- package/.pi/extensions/pi-workflow-engine/src/review/review-budget.ts +45 -0
- package/.pi/extensions/pi-workflow-engine/src/review/review-fix-workflow.ts +117 -0
- package/.pi/extensions/pi-workflow-engine/src/review/review-handoff.ts +14 -66
- package/.pi/extensions/pi-workflow-engine/src/review/review-issues.ts +38 -12
- package/.pi/extensions/pi-workflow-engine/src/review/review-report.ts +42 -0
- package/.pi/extensions/pi-workflow-engine/src/review/review-results-flow.ts +17 -22
- package/.pi/extensions/pi-workflow-engine/src/review/review-results-viewer.ts +143 -52
- package/.pi/extensions/pi-workflow-engine/src/review/review-session-coordinator.ts +153 -0
- package/.pi/extensions/pi-workflow-engine/src/review/review-snapshot.ts +295 -0
- package/.pi/extensions/pi-workflow-engine/src/tool-source-identity.ts +153 -0
- package/.pi/extensions/pi-workflow-engine/src/tree-fingerprint.ts +404 -0
- package/.pi/extensions/pi-workflow-engine/src/types.ts +38 -10
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-result-renderer.ts +5 -69
- package/.pi/extensions/pi-workflow-engine/src/unknown-error.ts +8 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-execution.ts +121 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-module.ts +19 -1
- package/.pi/extensions/pi-workflow-engine/src/workflows.ts +28 -3
- package/.pi/extensions/pi-workflow-engine/src/worktree.ts +282 -41
- package/.pi/extensions/pi-workflow-engine/workflows/code-review.ts +48 -26
- package/.pi/extensions/pi-workflow-engine/workflows/diagnose.ts +8 -1
- package/.pi/extensions/pi-workflow-engine/workflows/perf-review.ts +8 -2
- package/.pi/extensions/pi-workflow-engine/workflows/refactor-scout.ts +8 -1
- package/README.md +170 -226
- package/USAGE.md +92 -12
- package/package.json +9 -9
|
@@ -2,10 +2,12 @@ import { readdir } from "node:fs/promises";
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
|
-
import type {
|
|
5
|
+
import type { LoadedWorkflow, WorkflowSourceIdentity } from "./types.ts";
|
|
6
6
|
import type { PerfSink } from "./perf.ts";
|
|
7
|
-
import { parseWorkflowModule } from "./workflow-module.ts";
|
|
8
|
-
import {
|
|
7
|
+
import { loadWorkflow, parseWorkflowModule } from "./workflow-module.ts";
|
|
8
|
+
import { BUILTIN_SOURCE_ROOT, BUILTIN_WORKFLOW_DEFINITIONS, BUILTIN_WORKFLOW_FILES } from "./workflows.ts";
|
|
9
|
+
import { captureTreeFingerprint } from "./tree-fingerprint.ts";
|
|
10
|
+
import { FINGERPRINT_EXCLUDED_RELATIVE_PATHS } from "./resume-context.ts";
|
|
9
11
|
|
|
10
12
|
export interface DiscoverWorkflowsOptions {
|
|
11
13
|
readonly refresh?: boolean;
|
|
@@ -13,10 +15,14 @@ export interface DiscoverWorkflowsOptions {
|
|
|
13
15
|
readonly userWorkflowDir?: string;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
|
-
const discoveryCache = new Map<string, Map<string,
|
|
17
|
-
|
|
18
|
+
const discoveryCache = new Map<string, Map<string, LoadedWorkflow>>();
|
|
18
19
|
/** Best-effort dynamic load of every `*.ts` workflow in a directory. */
|
|
19
|
-
async function loadDir(
|
|
20
|
+
async function loadDir(
|
|
21
|
+
dir: string,
|
|
22
|
+
sourceIdentity: (path: string) => WorkflowSourceIdentity,
|
|
23
|
+
excludeFiles: ReadonlySet<string> = new Set(),
|
|
24
|
+
provenanceRoot?: string,
|
|
25
|
+
): Promise<LoadedWorkflow[]> {
|
|
20
26
|
let entries: string[];
|
|
21
27
|
try {
|
|
22
28
|
entries = await readdir(dir);
|
|
@@ -24,14 +30,18 @@ async function loadDir(dir: string, excludeFiles: ReadonlySet<string> = new Set(
|
|
|
24
30
|
return [];
|
|
25
31
|
}
|
|
26
32
|
|
|
27
|
-
const
|
|
33
|
+
const before = provenanceRoot ? await captureDiscoveryFingerprint(provenanceRoot) : undefined;
|
|
34
|
+
const modules: Array<{ readonly path: string; readonly module: Parameters<typeof loadWorkflow>[0] }> = [];
|
|
28
35
|
for (const name of entries) {
|
|
29
36
|
if (excludeFiles.has(name)) continue;
|
|
30
37
|
if (!name.endsWith(".ts") || name.startsWith("_") || name.startsWith(".")) continue;
|
|
31
38
|
try {
|
|
32
|
-
const
|
|
39
|
+
const path = join(dir, name);
|
|
40
|
+
const importUrl = pathToFileURL(path);
|
|
41
|
+
if (before?.kind === "verified") importUrl.searchParams.set("workflowSource", before.fingerprint);
|
|
42
|
+
const loaded: unknown = await import(importUrl.href);
|
|
33
43
|
const parsed = parseWorkflowModule(loaded);
|
|
34
|
-
if ("module" in parsed) modules.push(parsed.module);
|
|
44
|
+
if ("module" in parsed) modules.push({ path, module: parsed.module });
|
|
35
45
|
else console.error(`[workflow-engine] skipped ${name}: ${parsed.reason}`);
|
|
36
46
|
} catch (error) {
|
|
37
47
|
// Drop-in loading depends on the runtime resolving TS + the bundled typebox.
|
|
@@ -39,14 +49,23 @@ async function loadDir(dir: string, excludeFiles: ReadonlySet<string> = new Set(
|
|
|
39
49
|
console.error(`[workflow-engine] skipped ${name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
40
50
|
}
|
|
41
51
|
}
|
|
42
|
-
return modules;
|
|
52
|
+
return modules.map(({ path, module }) => loadWorkflow(module, sourceIdentity(path)));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function captureDiscoveryFingerprint(root: string) {
|
|
56
|
+
return await captureTreeFingerprint({
|
|
57
|
+
root,
|
|
58
|
+
excludedRelativePaths: FINGERPRINT_EXCLUDED_RELATIVE_PATHS,
|
|
59
|
+
maxBytes: 32 << 20,
|
|
60
|
+
maxFiles: 4096,
|
|
61
|
+
});
|
|
43
62
|
}
|
|
44
63
|
|
|
45
64
|
/**
|
|
46
65
|
* All available workflows by name. Static registry wins on name collisions, so the
|
|
47
66
|
* bundled example is always the verified one even if a same-named file is dropped in.
|
|
48
67
|
*/
|
|
49
|
-
export async function discoverWorkflows(repoDir: string, options: DiscoverWorkflowsOptions = {}): Promise<Map<string,
|
|
68
|
+
export async function discoverWorkflows(repoDir: string, options: DiscoverWorkflowsOptions = {}): Promise<Map<string, LoadedWorkflow>> {
|
|
50
69
|
const userWorkflowDir = options.userWorkflowDir ?? join(homedir(), ".pi", "agent", "workflows");
|
|
51
70
|
const cacheKey = `${repoDir}\0${userWorkflowDir}`;
|
|
52
71
|
const cached = discoveryCache.get(cacheKey);
|
|
@@ -56,13 +75,43 @@ export async function discoverWorkflows(repoDir: string, options: DiscoverWorkfl
|
|
|
56
75
|
}
|
|
57
76
|
|
|
58
77
|
const byName = await timed(options.perf, "discovery.total_ms", async () => {
|
|
59
|
-
const next = new Map<string,
|
|
60
|
-
|
|
78
|
+
const next = new Map<string, LoadedWorkflow>();
|
|
79
|
+
const builtinSource = await captureDiscoveryFingerprint(BUILTIN_SOURCE_ROOT);
|
|
80
|
+
for (const definition of BUILTIN_WORKFLOW_DEFINITIONS) {
|
|
81
|
+
const source: WorkflowSourceIdentity =
|
|
82
|
+
builtinSource.kind === "verified"
|
|
83
|
+
? {
|
|
84
|
+
kind: "file",
|
|
85
|
+
path: definition.path,
|
|
86
|
+
root: definition.root,
|
|
87
|
+
fingerprint: builtinSource.fingerprint,
|
|
88
|
+
}
|
|
89
|
+
: {
|
|
90
|
+
kind: "unverifiable",
|
|
91
|
+
reason: `built-in workflow source capture failed: ${builtinSource.reason}`,
|
|
92
|
+
};
|
|
93
|
+
next.set(definition.module.meta.name, loadWorkflow(definition.module, source));
|
|
94
|
+
}
|
|
61
95
|
|
|
62
96
|
const repoWorkflowDir = join(repoDir, "workflows");
|
|
63
97
|
const [repoDynamic, userDynamic] = await Promise.all([
|
|
64
|
-
timed(options.perf, "discovery.repo_dir_ms", () =>
|
|
65
|
-
|
|
98
|
+
timed(options.perf, "discovery.repo_dir_ms", () =>
|
|
99
|
+
loadDir(
|
|
100
|
+
repoWorkflowDir,
|
|
101
|
+
() => ({
|
|
102
|
+
kind: "unverifiable",
|
|
103
|
+
reason: "dynamic workflow module graphs are not loaded from an immutable source snapshot",
|
|
104
|
+
}),
|
|
105
|
+
BUILTIN_WORKFLOW_FILES,
|
|
106
|
+
repoDir,
|
|
107
|
+
),
|
|
108
|
+
),
|
|
109
|
+
timed(options.perf, "discovery.user_dir_ms", () =>
|
|
110
|
+
loadDir(userWorkflowDir, () => ({
|
|
111
|
+
kind: "unverifiable",
|
|
112
|
+
reason: "dynamic user workflow dependencies do not have a declared trusted source root",
|
|
113
|
+
})),
|
|
114
|
+
),
|
|
66
115
|
]);
|
|
67
116
|
|
|
68
117
|
for (const mod of repoDynamic) {
|
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import type
|
|
4
|
+
import { Key, type KeyId } from "@earendil-works/pi-tui";
|
|
5
5
|
|
|
6
6
|
export const DEFAULT_DYNAMAX_INSPECTOR_SHORTCUT = "ctrl+shift+m" satisfies KeyId;
|
|
7
|
+
export const DEFAULT_REVIEW_RESULTS_SHORTCUT = "ctrl+shift+r" satisfies KeyId;
|
|
7
8
|
|
|
8
9
|
const CONFIG_FILE_NAME = "pi-workflow-engine.json";
|
|
9
10
|
|
|
10
11
|
export interface DynamaxShortcuts {
|
|
11
12
|
inspector: KeyId | null;
|
|
13
|
+
results: KeyId | null;
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
interface DynamaxShortcutConfig {
|
|
15
17
|
inspector?: unknown;
|
|
18
|
+
results?: unknown;
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
export function dynamaxShortcutsConfigPath(agentDir: string = getAgentDir()): string {
|
|
@@ -25,9 +28,15 @@ export function resolveDynamaxShortcuts(configPath: string = dynamaxShortcutsCon
|
|
|
25
28
|
const config = readShortcutConfig(configPath);
|
|
26
29
|
if (!config) return defaultDynamaxShortcuts();
|
|
27
30
|
|
|
28
|
-
|
|
29
|
-
inspector: shortcutFromConfig(config.inspector, DEFAULT_DYNAMAX_INSPECTOR_SHORTCUT),
|
|
31
|
+
const shortcuts = {
|
|
32
|
+
inspector: shortcutFromConfig("inspector", config.inspector, DEFAULT_DYNAMAX_INSPECTOR_SHORTCUT, configPath),
|
|
33
|
+
results: shortcutFromConfig("results", config.results, DEFAULT_REVIEW_RESULTS_SHORTCUT, configPath),
|
|
30
34
|
};
|
|
35
|
+
if (shortcuts.inspector && shortcuts.results && shortcutIdentity(shortcuts.inspector) === shortcutIdentity(shortcuts.results)) {
|
|
36
|
+
console.warn(`Duplicate pi-workflow-engine shortcut ${shortcuts.inspector} in ${configPath}; disabling the results shortcut.`);
|
|
37
|
+
return { inspector: shortcuts.inspector, results: null };
|
|
38
|
+
}
|
|
39
|
+
return shortcuts;
|
|
31
40
|
}
|
|
32
41
|
|
|
33
42
|
function readShortcutConfig(configPath: string): DynamaxShortcutConfig | null {
|
|
@@ -41,7 +50,7 @@ function readShortcutConfig(configPath: string): DynamaxShortcutConfig | null {
|
|
|
41
50
|
|
|
42
51
|
const shortcuts = isRecord(parsed) ? parsed.shortcuts : undefined;
|
|
43
52
|
if (shortcuts === undefined) return {};
|
|
44
|
-
if (!isRecord(shortcuts)
|
|
53
|
+
if (!isRecord(shortcuts)) {
|
|
45
54
|
warnUsingDefaults("Invalid", configPath);
|
|
46
55
|
return null;
|
|
47
56
|
}
|
|
@@ -49,21 +58,58 @@ function readShortcutConfig(configPath: string): DynamaxShortcutConfig | null {
|
|
|
49
58
|
return shortcuts;
|
|
50
59
|
}
|
|
51
60
|
|
|
52
|
-
function
|
|
53
|
-
|
|
61
|
+
function shortcutFromConfig(
|
|
62
|
+
name: keyof DynamaxShortcutConfig,
|
|
63
|
+
configured: unknown,
|
|
64
|
+
fallback: KeyId,
|
|
65
|
+
configPath: string,
|
|
66
|
+
): KeyId | null {
|
|
67
|
+
if (configured === null) return null;
|
|
68
|
+
if (configured === undefined) return fallback;
|
|
69
|
+
if (typeof configured === "string") {
|
|
70
|
+
const normalized = configured.trim();
|
|
71
|
+
if (isKeyId(normalized)) return normalized;
|
|
72
|
+
}
|
|
73
|
+
console.warn(`Invalid ${name} shortcut in ${configPath}; using ${fallback}.`);
|
|
74
|
+
return fallback;
|
|
54
75
|
}
|
|
55
76
|
|
|
56
|
-
|
|
57
|
-
|
|
77
|
+
const MODIFIERS = new Set(["ctrl", "shift", "alt", "super"]);
|
|
78
|
+
const NAMED_BASE_KEYS = new Set<string>(Object.values(Key).flatMap((value) => (typeof value === "string" ? [String(value)] : [])));
|
|
79
|
+
|
|
80
|
+
function isKeyId(value: string): value is KeyId {
|
|
81
|
+
return parseKeyId(value) !== undefined;
|
|
58
82
|
}
|
|
59
83
|
|
|
60
|
-
function
|
|
61
|
-
|
|
62
|
-
|
|
84
|
+
function parseKeyId(value: string): { readonly modifiers: readonly string[]; readonly base: string } | undefined {
|
|
85
|
+
const seen = new Set<string>();
|
|
86
|
+
let base = value;
|
|
87
|
+
while (true) {
|
|
88
|
+
const separator = base.indexOf("+");
|
|
89
|
+
if (separator < 0) break;
|
|
90
|
+
const modifier = base.slice(0, separator);
|
|
91
|
+
if (!MODIFIERS.has(modifier) || seen.has(modifier)) break;
|
|
92
|
+
seen.add(modifier);
|
|
93
|
+
base = base.slice(separator + 1);
|
|
94
|
+
}
|
|
95
|
+
return isBaseKey(base) ? { modifiers: [...seen], base } : undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function isBaseKey(value: string): boolean {
|
|
99
|
+
return /^[a-z0-9]$/.test(value) || NAMED_BASE_KEYS.has(value);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function shortcutIdentity(shortcut: KeyId): string {
|
|
103
|
+
const parsed = parseKeyId(shortcut);
|
|
104
|
+
if (!parsed) return shortcut;
|
|
105
|
+
let base = parsed.base;
|
|
106
|
+
if (base === "esc") base = "escape";
|
|
107
|
+
else if (base === "return") base = "enter";
|
|
108
|
+
return `${[...parsed.modifiers].sort().join("+")}+${base}`;
|
|
63
109
|
}
|
|
64
110
|
|
|
65
111
|
function defaultDynamaxShortcuts(): DynamaxShortcuts {
|
|
66
|
-
return { inspector: DEFAULT_DYNAMAX_INSPECTOR_SHORTCUT };
|
|
112
|
+
return { inspector: DEFAULT_DYNAMAX_INSPECTOR_SHORTCUT, results: DEFAULT_REVIEW_RESULTS_SHORTCUT };
|
|
67
113
|
}
|
|
68
114
|
|
|
69
115
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
@@ -71,5 +117,5 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
71
117
|
}
|
|
72
118
|
|
|
73
119
|
function warnUsingDefaults(reason: "Could not read" | "Invalid", configPath: string): void {
|
|
74
|
-
console.warn(`${reason} pi-workflow-engine config at ${configPath}; using default
|
|
120
|
+
console.warn(`${reason} pi-workflow-engine config at ${configPath}; using default workflow shortcuts.`);
|
|
75
121
|
}
|
|
@@ -23,6 +23,15 @@ export const DYNAMAX_TOKEN_PATTERN = /(^|[^A-Za-z0-9_])dynamax([^A-Za-z0-9_]|$)/
|
|
|
23
23
|
export const DYNAMAX_STATUS_KEY = "dynamax";
|
|
24
24
|
export const DYNAMAX_WIDGET_KEY = "workflow-dynamax";
|
|
25
25
|
|
|
26
|
+
export const ADAPTIVE_WORKFLOW_GUIDANCE = `
|
|
27
|
+
Adaptive multi-pass workflows are optional. Use a simple single-pass fan-out when it is sufficient. When the first pass may expose gaps, conflicts, weak claims, or missing evidence:
|
|
28
|
+
- run the bounded first-pass agents;
|
|
29
|
+
- give their surviving results to a structured gap-analysis agent so the LLM decides what needs follow-up;
|
|
30
|
+
- put a hard maxItems bound on LLM-authored task arrays and defensively slice them before fan-out;
|
|
31
|
+
- use ordinary TypeScript conditionals or bounded loops to launch follow-up agents only when gaps exist;
|
|
32
|
+
- synthesize the first-pass and follow-up results together.
|
|
33
|
+
Do not generate a second pass when the first pass is sufficient, and do not invent iteration, quorum, graph, reduction, or retry primitives for this pattern.`;
|
|
34
|
+
|
|
26
35
|
export const DYNAMAX_REMINDER = `
|
|
27
36
|
## dynamax workflow opt-in
|
|
28
37
|
|
|
@@ -34,6 +43,8 @@ Inline workflow rules:
|
|
|
34
43
|
- Set thinkingLevel explicitly on each agent() call so fan-out remains bounded.
|
|
35
44
|
- Subagents receive no skills by default. Add \`skills: ["skill-name"]\` per agent only when that stage should load that skill; grant the smallest useful set.
|
|
36
45
|
- Provide exactly one of workflow.name or workflow.script.
|
|
46
|
+
|
|
47
|
+
${ADAPTIVE_WORKFLOW_GUIDANCE}
|
|
37
48
|
`;
|
|
38
49
|
|
|
39
50
|
const DYNAMAX_CONTEXT_CUSTOM_TYPE = "workflow-dynamax-reminder";
|
|
@@ -145,7 +156,7 @@ export function registerDynamax(
|
|
|
145
156
|
});
|
|
146
157
|
|
|
147
158
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
148
|
-
clearDynamaxSurfaces(ctx
|
|
159
|
+
clearDynamaxSurfaces(ctx);
|
|
149
160
|
});
|
|
150
161
|
|
|
151
162
|
pi.on("input", (event, ctx) => {
|
|
@@ -266,7 +277,7 @@ export function updateDynamaxSurfaces(ctx: Pick<ExtensionContext, "hasUI" | "ui"
|
|
|
266
277
|
ctx.ui.setWidget(DYNAMAX_WIDGET_KEY, undefined);
|
|
267
278
|
ctx.ui.setStatus(DYNAMAX_WIDGET_KEY, undefined);
|
|
268
279
|
if (!isDynamaxActive(runtime.state) && !runtime.runningWorkflow) {
|
|
269
|
-
clearDynamaxSurfaces(ctx
|
|
280
|
+
clearDynamaxSurfaces(ctx);
|
|
270
281
|
return;
|
|
271
282
|
}
|
|
272
283
|
|
|
@@ -274,7 +285,7 @@ export function updateDynamaxSurfaces(ctx: Pick<ExtensionContext, "hasUI" | "ui"
|
|
|
274
285
|
ctx.ui.setStatus(DYNAMAX_STATUS_KEY, status);
|
|
275
286
|
}
|
|
276
287
|
|
|
277
|
-
export function clearDynamaxSurfaces(ctx: Pick<ExtensionContext, "hasUI" | "ui"
|
|
288
|
+
export function clearDynamaxSurfaces(ctx: Pick<ExtensionContext, "hasUI" | "ui">): void {
|
|
278
289
|
if (!ctx.hasUI) return;
|
|
279
290
|
ctx.ui.setWidget(DYNAMAX_WIDGET_KEY, undefined);
|
|
280
291
|
ctx.ui.setStatus(DYNAMAX_STATUS_KEY, undefined);
|