pi-workflow-engine 0.10.3 → 0.12.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.
Files changed (88) hide show
  1. package/.pi/extensions/pi-workflow-engine/index.ts +418 -213
  2. package/.pi/extensions/pi-workflow-engine/src/advisory-schema.ts +11 -4
  3. package/.pi/extensions/pi-workflow-engine/src/agent-attempt.ts +147 -0
  4. package/.pi/extensions/pi-workflow-engine/src/agent-limits.ts +35 -0
  5. package/.pi/extensions/pi-workflow-engine/src/agent-replay.ts +399 -0
  6. package/.pi/extensions/pi-workflow-engine/src/agent-retry.ts +116 -0
  7. package/.pi/extensions/pi-workflow-engine/src/agent-runner-types.ts +91 -0
  8. package/.pi/extensions/pi-workflow-engine/src/agent-runner.ts +154 -530
  9. package/.pi/extensions/pi-workflow-engine/src/agent-session-identity.ts +256 -0
  10. package/.pi/extensions/pi-workflow-engine/src/agent-session-providers.ts +50 -0
  11. package/.pi/extensions/pi-workflow-engine/src/agent-session.ts +368 -0
  12. package/.pi/extensions/pi-workflow-engine/src/agent-skills.ts +87 -39
  13. package/.pi/extensions/pi-workflow-engine/src/agent-workspace.ts +89 -0
  14. package/.pi/extensions/pi-workflow-engine/src/background-workflow-tool.ts +75 -0
  15. package/.pi/extensions/pi-workflow-engine/src/background-workflows.ts +492 -0
  16. package/.pi/extensions/pi-workflow-engine/src/budget.ts +10 -3
  17. package/.pi/extensions/pi-workflow-engine/src/cancellation.ts +55 -3
  18. package/.pi/extensions/pi-workflow-engine/src/command-completions.ts +36 -0
  19. package/.pi/extensions/pi-workflow-engine/src/concurrency.ts +230 -61
  20. package/.pi/extensions/pi-workflow-engine/src/diff-capture.ts +38 -55
  21. package/.pi/extensions/pi-workflow-engine/src/discovery.ts +70 -17
  22. package/.pi/extensions/pi-workflow-engine/src/dynamax-shortcuts.ts +59 -13
  23. package/.pi/extensions/pi-workflow-engine/src/dynamax.ts +123 -63
  24. package/.pi/extensions/pi-workflow-engine/src/engine.ts +357 -75
  25. package/.pi/extensions/pi-workflow-engine/src/filesystem-error.ts +4 -0
  26. package/.pi/extensions/pi-workflow-engine/src/finalizers.ts +66 -0
  27. package/.pi/extensions/pi-workflow-engine/src/identity-canonicalization.ts +138 -0
  28. package/.pi/extensions/pi-workflow-engine/src/identity-fingerprint.ts +15 -0
  29. package/.pi/extensions/pi-workflow-engine/src/inline-workflow.ts +10 -5
  30. package/.pi/extensions/pi-workflow-engine/src/journal.ts +156 -66
  31. package/.pi/extensions/pi-workflow-engine/src/model-profile-command.ts +167 -0
  32. package/.pi/extensions/pi-workflow-engine/src/model-profiles.ts +310 -0
  33. package/.pi/extensions/pi-workflow-engine/src/options.ts +96 -6
  34. package/.pi/extensions/pi-workflow-engine/src/pi-compat.ts +32 -0
  35. package/.pi/extensions/pi-workflow-engine/src/process-runner.ts +203 -39
  36. package/.pi/extensions/pi-workflow-engine/src/progress-types.ts +50 -0
  37. package/.pi/extensions/pi-workflow-engine/src/progress.ts +74 -122
  38. package/.pi/extensions/pi-workflow-engine/src/provider-usage-limit.ts +189 -0
  39. package/.pi/extensions/pi-workflow-engine/src/replay-path-identity.ts +48 -0
  40. package/.pi/extensions/pi-workflow-engine/src/research-contract.ts +89 -0
  41. package/.pi/extensions/pi-workflow-engine/src/research-evidence.ts +289 -0
  42. package/.pi/extensions/pi-workflow-engine/src/resume-context.ts +701 -0
  43. package/.pi/extensions/pi-workflow-engine/src/review/code-review-orchestration.ts +49 -0
  44. package/.pi/extensions/pi-workflow-engine/src/review/github-pr-comments.ts +75 -16
  45. package/.pi/extensions/pi-workflow-engine/src/review/review-actions.ts +83 -17
  46. package/.pi/extensions/pi-workflow-engine/src/review/review-budget.ts +45 -0
  47. package/.pi/extensions/pi-workflow-engine/src/review/review-fix-workflow.ts +117 -0
  48. package/.pi/extensions/pi-workflow-engine/src/review/review-handoff.ts +14 -66
  49. package/.pi/extensions/pi-workflow-engine/src/review/review-issues.ts +38 -12
  50. package/.pi/extensions/pi-workflow-engine/src/review/review-report.ts +42 -0
  51. package/.pi/extensions/pi-workflow-engine/src/review/review-results-flow.ts +18 -22
  52. package/.pi/extensions/pi-workflow-engine/src/review/review-results-viewer.ts +128 -56
  53. package/.pi/extensions/pi-workflow-engine/src/review/review-session-coordinator.ts +153 -0
  54. package/.pi/extensions/pi-workflow-engine/src/review/review-snapshot.ts +294 -0
  55. package/.pi/extensions/pi-workflow-engine/src/review-diff-target.ts +130 -0
  56. package/.pi/extensions/pi-workflow-engine/src/session-identity.ts +6 -0
  57. package/.pi/extensions/pi-workflow-engine/src/structured-output.ts +32 -0
  58. package/.pi/extensions/pi-workflow-engine/src/tool-capabilities.ts +44 -0
  59. package/.pi/extensions/pi-workflow-engine/src/tool-source-identity.ts +150 -0
  60. package/.pi/extensions/pi-workflow-engine/src/tree-fingerprint.ts +401 -0
  61. package/.pi/extensions/pi-workflow-engine/src/types.ts +87 -25
  62. package/.pi/extensions/pi-workflow-engine/src/ui/dynamax-editor-decoration.ts +220 -0
  63. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-format.ts +26 -6
  64. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-inspector.ts +88 -46
  65. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-result-renderer.ts +7 -70
  66. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-viewer-layout.ts +51 -0
  67. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-widget.ts +12 -67
  68. package/.pi/extensions/pi-workflow-engine/src/unknown-error.ts +8 -0
  69. package/.pi/extensions/pi-workflow-engine/src/usage.ts +124 -16
  70. package/.pi/extensions/pi-workflow-engine/src/workflow-advisory-utils.ts +23 -3
  71. package/.pi/extensions/pi-workflow-engine/src/workflow-execution.ts +121 -0
  72. package/.pi/extensions/pi-workflow-engine/src/workflow-module.ts +19 -1
  73. package/.pi/extensions/pi-workflow-engine/src/workflow-run-background.ts +72 -0
  74. package/.pi/extensions/pi-workflow-engine/src/workflow-run-controller.ts +354 -0
  75. package/.pi/extensions/pi-workflow-engine/src/workflow-run-history.ts +162 -0
  76. package/.pi/extensions/pi-workflow-engine/src/workflow-run-record.ts +632 -0
  77. package/.pi/extensions/pi-workflow-engine/src/workflow-run-store.ts +172 -0
  78. package/.pi/extensions/pi-workflow-engine/src/workflow-usage-limit-scheduler.ts +80 -0
  79. package/.pi/extensions/pi-workflow-engine/src/workflows.ts +30 -3
  80. package/.pi/extensions/pi-workflow-engine/src/worktree.ts +321 -43
  81. package/.pi/extensions/pi-workflow-engine/workflows/code-review.ts +62 -64
  82. package/.pi/extensions/pi-workflow-engine/workflows/diagnose.ts +17 -17
  83. package/.pi/extensions/pi-workflow-engine/workflows/perf-review.ts +15 -16
  84. package/.pi/extensions/pi-workflow-engine/workflows/refactor-scout.ts +15 -15
  85. package/.pi/extensions/pi-workflow-engine/workflows/research.ts +170 -0
  86. package/README.md +82 -207
  87. package/USAGE.md +295 -21
  88. package/package.json +9 -9
@@ -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 { KeyId } from "@earendil-works/pi-tui";
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
- return {
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) || !hasValidShortcutValues(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 hasValidShortcutValues(shortcuts: Record<string, unknown>): boolean {
53
- return isValidShortcutConfigValue(shortcuts.inspector);
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
- function isValidShortcutConfigValue(value: unknown): value is string | null | undefined {
57
- return value === undefined || value === null || (typeof value === "string" && value.trim().length > 0);
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 shortcutFromConfig(configured: unknown, fallback: KeyId): KeyId | null {
61
- if (configured === null) return null;
62
- return typeof configured === "string" ? (configured.trim() as KeyId) : fallback;
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 Dynamax shortcuts.`);
120
+ console.warn(`${reason} pi-workflow-engine config at ${configPath}; using default workflow shortcuts.`);
75
121
  }
@@ -1,7 +1,17 @@
1
1
  import type { AgentMessage } from "@earendil-works/pi-agent-core";
2
- import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
3
- import type { KeyId } from "@earendil-works/pi-tui";
2
+ import { CustomEditor, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
3
+ import type { EditorComponent, KeyId } from "@earendil-works/pi-tui";
4
+ import { completeCurrentArgument } from "./command-completions.ts";
4
5
  import { resolveDynamaxShortcuts, type DynamaxShortcuts } from "./dynamax-shortcuts.ts";
6
+ import { sessionKey } from "./session-identity.ts";
7
+ import { unknownErrorMessage } from "./unknown-error.ts";
8
+ import {
9
+ decorateDynamaxEditor,
10
+ resolveDynamaxEffect,
11
+ type DynamaxAnimationScheduler,
12
+ type DynamaxEditorDecoration,
13
+ type DynamaxEffect,
14
+ } from "./ui/dynamax-editor-decoration.ts";
5
15
 
6
16
  export interface DynamaxState {
7
17
  sticky: boolean;
@@ -15,13 +25,43 @@ export interface DynamaxRuntime {
15
25
  }
16
26
 
17
27
  export type DynamaxRuntimeStore = Map<string, DynamaxRuntime>;
28
+
18
29
  export interface DynamaxRegistrationOptions {
19
30
  openInspector?: (ctx: ExtensionContext) => Promise<void> | void;
31
+ effect?: DynamaxEffect;
32
+ animationScheduler?: DynamaxAnimationScheduler;
20
33
  }
21
34
 
22
35
  export const DYNAMAX_TOKEN_PATTERN = /(^|[^A-Za-z0-9_])dynamax([^A-Za-z0-9_]|$)/i;
23
36
  export const DYNAMAX_STATUS_KEY = "dynamax";
24
- export const DYNAMAX_WIDGET_KEY = "workflow-dynamax";
37
+
38
+ type EditorFactory = NonNullable<ReturnType<ExtensionContext["ui"]["getEditorComponent"]>>;
39
+
40
+ interface DynamaxEditorInstallation {
41
+ factory: EditorFactory;
42
+ previousFactory: EditorFactory | undefined;
43
+ decorations: Set<DynamaxEditorDecoration>;
44
+ }
45
+
46
+ function disposeDynamaxDecorations(decorations: Set<DynamaxEditorDecoration>): void {
47
+ for (const decoration of decorations) decoration.dispose();
48
+ decorations.clear();
49
+ }
50
+
51
+ const DYNAMAX_ACTION_COMPLETIONS = [
52
+ { value: "on", description: "Keep Dynamax enabled for this session" },
53
+ { value: "off", description: "Disable sticky and pending Dynamax modes" },
54
+ { value: "status", description: "Show the current Dynamax state" },
55
+ ] as const;
56
+
57
+ export const ADAPTIVE_WORKFLOW_GUIDANCE = `
58
+ 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:
59
+ - run the bounded first-pass agents;
60
+ - give their surviving results to a structured gap-analysis agent so the LLM decides what needs follow-up;
61
+ - put a hard maxItems bound on LLM-authored task arrays and defensively slice them before fan-out;
62
+ - use ordinary TypeScript conditionals or bounded loops to launch follow-up agents only when gaps exist;
63
+ - synthesize the first-pass and follow-up results together.
64
+ 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.`;
25
65
 
26
66
  export const DYNAMAX_REMINDER = `
27
67
  ## dynamax workflow opt-in
@@ -31,9 +71,11 @@ The user has opted into dynamax multi-agent orchestration. The workflow tool is
31
71
  Inline workflow rules:
32
72
  - Use the injected Type object for schemas, for example Type.Object({ ok: Type.Boolean() }); do not import typebox.
33
73
  - Do not use import statements or dynamic import() in inline workflow scripts.
34
- - Set thinkingLevel explicitly on each agent() call so fan-out remains bounded.
74
+ - Set profile to "small", "medium", or "big" on each agent() call so routing remains explicit; use model/thinkingLevel only for an intentional override.
35
75
  - 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
76
  - Provide exactly one of workflow.name or workflow.script.
77
+
78
+ ${ADAPTIVE_WORKFLOW_GUIDANCE}
37
79
  `;
38
80
 
39
81
  const DYNAMAX_CONTEXT_CUSTOM_TYPE = "workflow-dynamax-reminder";
@@ -46,12 +88,8 @@ export function createDynamaxRuntime(): DynamaxRuntime {
46
88
  return { state: createDynamaxState() };
47
89
  }
48
90
 
49
- export function dynamaxSessionKey(ctx: Pick<ExtensionContext, "sessionManager">): string {
50
- return ctx.sessionManager.getSessionFile() ?? ctx.sessionManager.getSessionId();
51
- }
52
-
53
91
  export function getDynamaxRuntime(store: DynamaxRuntimeStore, ctx: Pick<ExtensionContext, "sessionManager">): DynamaxRuntime {
54
- const key = dynamaxSessionKey(ctx);
92
+ const key = sessionKey(ctx);
55
93
  const existing = store.get(key);
56
94
  if (existing) return existing;
57
95
  const runtime = createDynamaxRuntime();
@@ -67,12 +105,8 @@ export function markDynamaxOneShot(state: DynamaxState): void {
67
105
  state.oneShotPending = true;
68
106
  }
69
107
 
70
- export function setDynamaxSticky(state: DynamaxState, sticky: boolean): void {
71
- state.sticky = sticky;
72
- if (!sticky) {
73
- state.oneShotPending = false;
74
- state.turnActive = false;
75
- }
108
+ export function enableDynamaxSticky(state: DynamaxState): void {
109
+ state.sticky = true;
76
110
  }
77
111
 
78
112
  export function clearDynamax(state: DynamaxState): void {
@@ -81,12 +115,6 @@ export function clearDynamax(state: DynamaxState): void {
81
115
  state.turnActive = false;
82
116
  }
83
117
 
84
- export function consumeDynamaxOneShot(state: DynamaxState): boolean {
85
- const pending = state.oneShotPending;
86
- state.oneShotPending = false;
87
- return pending;
88
- }
89
-
90
118
  export function isDynamaxActive(state: DynamaxState): boolean {
91
119
  return state.sticky || state.oneShotPending || state.turnActive;
92
120
  }
@@ -128,24 +156,85 @@ export function registerDynamax(
128
156
  options: DynamaxRegistrationOptions = {},
129
157
  ): void {
130
158
  const runtimes: DynamaxRuntimeStore = new Map();
159
+ const effect = options.effect ?? resolveDynamaxEffect();
160
+ let editorInstallation: DynamaxEditorInstallation | undefined;
131
161
  const openInspector =
132
162
  options.openInspector ??
133
163
  ((ctx: ExtensionContext): void => {
134
164
  ctx.ui.notify("No workflow inspector is available yet", "warning");
135
165
  });
136
- const refreshIfVisible = (ctx: ExtensionContext): void => {
137
- const runtime = getDynamaxRuntime(runtimes, ctx);
138
- if (isDynamaxActive(runtime.state) || runtime.runningWorkflow) {
139
- updateDynamaxSurfaces(ctx, runtime, shortcuts);
166
+ const installEditor = (ctx: ExtensionContext): void => {
167
+ if (ctx.mode !== "tui" || effect === "off") return;
168
+ const previousFactory = ctx.ui.getEditorComponent();
169
+ if (editorInstallation && editorInstallation.factory === previousFactory) return;
170
+
171
+ const decorations = new Set<DynamaxEditorDecoration>();
172
+ const installation: DynamaxEditorInstallation = {
173
+ previousFactory,
174
+ decorations,
175
+ factory: (tui, theme, keybindings) => {
176
+ const decorate = (editor: EditorComponent): EditorComponent => {
177
+ const decoration = decorateDynamaxEditor(editor, () => tui.requestRender(), {
178
+ effect,
179
+ scheduler: options.animationScheduler,
180
+ isActive: () => editorInstallation === installation && ctx.ui.getEditorComponent() === installation.factory,
181
+ });
182
+ decorations.add(decoration);
183
+ return decoration.editor;
184
+ };
185
+ if (!previousFactory) return decorate(new CustomEditor(tui, theme, keybindings));
186
+
187
+ let editor: EditorComponent;
188
+ try {
189
+ editor = previousFactory(tui, theme, keybindings);
190
+ } catch (error) {
191
+ ctx.ui.notify(
192
+ `Dynamax could not compose the existing custom editor (${unknownErrorMessage(error)}); using pi's stock-compatible CustomEditor so highlighting stays enabled`,
193
+ "warning",
194
+ );
195
+ installation.previousFactory = undefined;
196
+ return decorate(new CustomEditor(tui, theme, keybindings));
197
+ }
198
+
199
+ try {
200
+ return decorate(editor);
201
+ } catch (error) {
202
+ ctx.ui.notify(
203
+ `Dynamax could not decorate the existing custom editor (${unknownErrorMessage(error)}); using pi's stock-compatible CustomEditor so highlighting stays enabled`,
204
+ "warning",
205
+ );
206
+ return decorate(new CustomEditor(tui, theme, keybindings));
207
+ }
208
+ },
209
+ };
210
+
211
+ try {
212
+ ctx.ui.setEditorComponent(installation.factory);
213
+ } catch (error) {
214
+ disposeDynamaxDecorations(decorations);
215
+ ctx.ui.notify(`Dynamax highlighting could not be installed: ${unknownErrorMessage(error)}`, "error");
216
+ throw error;
217
+ }
218
+ editorInstallation = installation;
219
+ };
220
+ const uninstallEditor = (ctx: ExtensionContext): void => {
221
+ const installation = editorInstallation;
222
+ if (!installation) return;
223
+ editorInstallation = undefined;
224
+ disposeDynamaxDecorations(installation.decorations);
225
+ if (ctx.mode === "tui" && ctx.ui.getEditorComponent() === installation.factory) {
226
+ ctx.ui.setEditorComponent(installation.previousFactory);
140
227
  }
141
228
  };
142
229
 
143
230
  pi.on("session_start", (_event, ctx) => {
231
+ installEditor(ctx);
144
232
  updateDynamaxSurfaces(ctx, getDynamaxRuntime(runtimes, ctx), shortcuts);
145
233
  });
146
234
 
147
235
  pi.on("session_shutdown", (_event, ctx) => {
148
- clearDynamaxSurfaces(ctx, getDynamaxRuntime(runtimes, ctx));
236
+ uninstallEditor(ctx);
237
+ clearDynamaxSurfaces(ctx);
149
238
  });
150
239
 
151
240
  pi.on("input", (event, ctx) => {
@@ -166,26 +255,6 @@ export function registerDynamax(
166
255
  return { systemPrompt };
167
256
  });
168
257
 
169
- pi.on("agent_start", (_event, ctx) => {
170
- refreshIfVisible(ctx);
171
- });
172
-
173
- pi.on("message_start", (_event, ctx) => {
174
- refreshIfVisible(ctx);
175
- });
176
-
177
- pi.on("message_update", (_event, ctx) => {
178
- refreshIfVisible(ctx);
179
- });
180
-
181
- pi.on("message_end", (_event, ctx) => {
182
- refreshIfVisible(ctx);
183
- });
184
-
185
- pi.on("turn_end", (_event, ctx) => {
186
- refreshIfVisible(ctx);
187
- });
188
-
189
258
  pi.on("agent_end", (_event, ctx) => {
190
259
  const runtime = getDynamaxRuntime(runtimes, ctx);
191
260
  runtime.runningWorkflow = undefined;
@@ -194,23 +263,17 @@ export function registerDynamax(
194
263
  });
195
264
 
196
265
  pi.on("tool_execution_start", (event, ctx) => {
266
+ if (event.toolName !== "workflow") return undefined;
197
267
  const runtime = getDynamaxRuntime(runtimes, ctx);
198
- if (event.toolName === "workflow") {
199
- runtime.runningWorkflow = workflowLabel(event.args);
200
- }
268
+ runtime.runningWorkflow = workflowLabel(event.args);
201
269
  updateDynamaxSurfaces(ctx, runtime, shortcuts);
202
270
  return undefined;
203
271
  });
204
272
 
205
- pi.on("tool_execution_update", (_event, ctx) => {
206
- refreshIfVisible(ctx);
207
- });
208
-
209
273
  pi.on("tool_execution_end", (event, ctx) => {
274
+ if (event.toolName !== "workflow") return undefined;
210
275
  const runtime = getDynamaxRuntime(runtimes, ctx);
211
- if (event.toolName === "workflow") {
212
- runtime.runningWorkflow = undefined;
213
- }
276
+ runtime.runningWorkflow = undefined;
214
277
  updateDynamaxSurfaces(ctx, runtime, shortcuts);
215
278
  return undefined;
216
279
  });
@@ -233,6 +296,7 @@ export function registerDynamax(
233
296
 
234
297
  pi.registerCommand("workflow:dynamax", {
235
298
  description: "Toggle Dynamax workflow orchestration: /workflow:dynamax [on|off|status]",
299
+ getArgumentCompletions: (argumentPrefix) => completeCurrentArgument(argumentPrefix, DYNAMAX_ACTION_COMPLETIONS),
236
300
  handler: async (args, ctx) => {
237
301
  const runtime = getDynamaxRuntime(runtimes, ctx);
238
302
  const action = args.trim().toLowerCase();
@@ -241,7 +305,7 @@ export function registerDynamax(
241
305
  return;
242
306
  }
243
307
  if (action === "on") {
244
- setDynamaxSticky(runtime.state, true);
308
+ enableDynamaxSticky(runtime.state);
245
309
  updateDynamaxSurfaces(ctx, runtime, shortcuts);
246
310
  ctx.ui.notify("Dynamax workflow orchestration is on for this session", "info");
247
311
  return;
@@ -263,10 +327,8 @@ export function registerDynamax(
263
327
 
264
328
  export function updateDynamaxSurfaces(ctx: Pick<ExtensionContext, "hasUI" | "ui">, runtime: DynamaxRuntime, shortcuts: DynamaxShortcuts): void {
265
329
  if (!ctx.hasUI) return;
266
- ctx.ui.setWidget(DYNAMAX_WIDGET_KEY, undefined);
267
- ctx.ui.setStatus(DYNAMAX_WIDGET_KEY, undefined);
268
330
  if (!isDynamaxActive(runtime.state) && !runtime.runningWorkflow) {
269
- clearDynamaxSurfaces(ctx, runtime);
331
+ clearDynamaxSurfaces(ctx);
270
332
  return;
271
333
  }
272
334
 
@@ -274,11 +336,9 @@ export function updateDynamaxSurfaces(ctx: Pick<ExtensionContext, "hasUI" | "ui"
274
336
  ctx.ui.setStatus(DYNAMAX_STATUS_KEY, status);
275
337
  }
276
338
 
277
- export function clearDynamaxSurfaces(ctx: Pick<ExtensionContext, "hasUI" | "ui">, runtime: DynamaxRuntime): void {
339
+ export function clearDynamaxSurfaces(ctx: Pick<ExtensionContext, "hasUI" | "ui">): void {
278
340
  if (!ctx.hasUI) return;
279
- ctx.ui.setWidget(DYNAMAX_WIDGET_KEY, undefined);
280
341
  ctx.ui.setStatus(DYNAMAX_STATUS_KEY, undefined);
281
- ctx.ui.setStatus(DYNAMAX_WIDGET_KEY, undefined);
282
342
  }
283
343
 
284
344
  function workflowLabel(args: unknown): string {