@signalridge/pi-workflows 1.8.0 → 1.10.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/CHANGELOG.md +15 -0
- package/README.md +31 -1
- package/package.json +2 -2
- package/skills/workflow-authoring/SKILL.md +1 -1
- package/skills/workflow-authoring/references/capabilities.md +1 -1
- package/skills/workflow-authoring/references/capability-details.md +1 -1
- package/skills/workflow-patterns/SKILL.md +1 -1
- package/skills/workflow-review/SKILL.md +1 -1
- package/src/engine.ts +1088 -395
- package/src/index.ts +286 -53
- package/src/inspection.ts +544 -0
- package/src/journal.ts +33 -11
- package/src/orchestrator.ts +22 -4
- package/src/runtime.ts +387 -148
- package/src/status-actions.ts +143 -0
- package/src/ui-bus.ts +243 -0
- package/src/ui-command-center.ts +1032 -0
- package/src/ui-inspection.ts +206 -0
- package/src/ui-presentation.ts +196 -0
- package/src/ui.ts +43 -265
package/src/index.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { execFileSync } from "node:child_process";
|
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import { Text } from "@earendil-works/pi-tui";
|
|
5
|
+
import { Text, truncateToWidth } from "@earendil-works/pi-tui";
|
|
6
6
|
import { sanitizeDisplayText } from "@signalridge/pi-ui";
|
|
7
7
|
import { Type } from "@sinclair/typebox";
|
|
8
8
|
import { DEFAULT_TRIGGER_WORD, hasTriggerWord, WORKFLOW_ARMING_MESSAGE_TYPE, WorkflowTurnArming } from "./arming.js";
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
queryChildSessionContextImmediate,
|
|
18
18
|
} from "./rpc-client.js";
|
|
19
19
|
import { listSavedWorkflows, loadSavedWorkflow, saveWorkflow } from "./saved-workflows.js";
|
|
20
|
+
import { guardStatusMount, StatusActions, type StatusSurface, workflowStatusSummary } from "./status-actions.js";
|
|
20
21
|
import {
|
|
21
22
|
defaultStrengthTable,
|
|
22
23
|
isWorkflowStrength,
|
|
@@ -25,6 +26,7 @@ import {
|
|
|
25
26
|
type WorkflowStrengthTable,
|
|
26
27
|
} from "./strengths.js";
|
|
27
28
|
import { liveWidgetLines, showWorkflowNavigator } from "./ui.js";
|
|
29
|
+
import { buildWorkflowUiSnapshot, openOwnedConversation, WorkflowUiBridge, type WorkflowUiSurface } from "./ui-bus.js";
|
|
28
30
|
import {
|
|
29
31
|
loadProjectWorkflowSettings,
|
|
30
32
|
loadWorkflowSettings,
|
|
@@ -111,6 +113,15 @@ function textResult(text: string, details: unknown = {}) {
|
|
|
111
113
|
};
|
|
112
114
|
}
|
|
113
115
|
|
|
116
|
+
/** Bounded display copies; command notifications never render stored control bytes. */
|
|
117
|
+
function notifyWorkflow(
|
|
118
|
+
ctx: Pick<ExtensionContext, "ui">,
|
|
119
|
+
message: string,
|
|
120
|
+
type: "info" | "warning" | "error" = "info",
|
|
121
|
+
): void {
|
|
122
|
+
ctx.ui.notify(sanitizeDisplayText(message.slice(0, 8_000), { multiline: true }).slice(0, 4_000), type);
|
|
123
|
+
}
|
|
124
|
+
|
|
114
125
|
/**
|
|
115
126
|
* Run one command for diff discovery. argv array, never a shell string — a
|
|
116
127
|
* branch name or path reaches this from user input.
|
|
@@ -366,6 +377,19 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
366
377
|
const PENDING_DELIVERY_TTL_MS = 7 * 24 * 60 * 60 * 1_000;
|
|
367
378
|
let widgetTimer: ReturnType<typeof setInterval> | undefined;
|
|
368
379
|
let widgetUi: ExtensionContext["ui"] | undefined;
|
|
380
|
+
let workflowUiBridge: WorkflowUiBridge | undefined;
|
|
381
|
+
let workflowUiSurfaceId = randomUUID();
|
|
382
|
+
let refreshQueued = false;
|
|
383
|
+
let statusActions: StatusActions | undefined;
|
|
384
|
+
let statusSource: (() => StatusSurface | undefined) | undefined;
|
|
385
|
+
const navigatorOpen = new Map<string, number>();
|
|
386
|
+
const retireStatusActions = (): void => {
|
|
387
|
+
const hadStatus = !!statusSource;
|
|
388
|
+
statusActions?.dispose();
|
|
389
|
+
statusActions = undefined;
|
|
390
|
+
statusSource = undefined;
|
|
391
|
+
if (hadStatus && deliveryContext?.mode === "tui") deliveryContext.ui.setStatus("pi-workflows", undefined);
|
|
392
|
+
};
|
|
369
393
|
|
|
370
394
|
const currentEngine = (): WorkflowEngine => {
|
|
371
395
|
if (!active || !engine || engine.isDisposed())
|
|
@@ -391,11 +415,15 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
391
415
|
if (!busy) return undefined;
|
|
392
416
|
const notice =
|
|
393
417
|
"Navigation blocked while agents or workflows are active or cleaning up. Wait for them to finish, or stop them explicitly and retry.";
|
|
394
|
-
if (ctx.hasUI) ctx
|
|
418
|
+
if (ctx.hasUI) notifyWorkflow(ctx, notice, "warning");
|
|
395
419
|
else console.warn(`[pi-workflows] ${notice}`);
|
|
396
420
|
return { cancel: true };
|
|
397
421
|
};
|
|
398
422
|
const invalidateDelivery = (): void => {
|
|
423
|
+
navigatorOpen.delete(workflowUiSurfaceId);
|
|
424
|
+
retireStatusActions();
|
|
425
|
+
workflowUiBridge?.retire(workflowUiSurfaceId);
|
|
426
|
+
workflowUiSurfaceId = randomUUID();
|
|
399
427
|
deliveryGeneration++;
|
|
400
428
|
deliveryObservers.clear();
|
|
401
429
|
turnArming.retire();
|
|
@@ -467,16 +495,56 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
467
495
|
.list()
|
|
468
496
|
.map((summary) => workflowEngine.getRun(String(summary.runId)))
|
|
469
497
|
.filter((run): run is NonNullable<typeof run> => run !== undefined);
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
498
|
+
if (statusSource?.()) tui.setStatus("pi-workflows", workflowStatusSummary(runs));
|
|
499
|
+
const hasActive = runs.some((run) => !["completed", "failed", "stopped"].includes(run.status));
|
|
500
|
+
const surfaceId = workflowUiSurfaceId;
|
|
501
|
+
tui.setWidget(
|
|
502
|
+
"pi-workflows",
|
|
503
|
+
hasActive
|
|
504
|
+
? (host, theme) => ({
|
|
505
|
+
render(width) {
|
|
506
|
+
if (
|
|
507
|
+
!active ||
|
|
508
|
+
engine !== workflowEngine ||
|
|
509
|
+
workflowEngine.isDisposed() ||
|
|
510
|
+
workflowUiSurfaceId !== surfaceId ||
|
|
511
|
+
!Number.isFinite(width) ||
|
|
512
|
+
width < 1
|
|
513
|
+
)
|
|
514
|
+
return [];
|
|
515
|
+
const height = Number.isFinite(host.terminal.rows) ? Math.max(0, host.terminal.rows) : 24;
|
|
516
|
+
const rowBudget = Math.max(1, Math.min(12, Math.floor(height / 4)));
|
|
517
|
+
return liveWidgetLines(
|
|
518
|
+
runs,
|
|
519
|
+
workflowSettings.progressMode,
|
|
520
|
+
workflowSettings.maxAgentsShown,
|
|
521
|
+
rowBudget,
|
|
522
|
+
).map((line, index) =>
|
|
523
|
+
truncateToWidth(theme.fg(index === 0 ? "accent" : "muted", line), Math.floor(width)),
|
|
524
|
+
);
|
|
525
|
+
},
|
|
526
|
+
invalidate() {},
|
|
527
|
+
})
|
|
528
|
+
: undefined,
|
|
529
|
+
);
|
|
530
|
+
workflowUiBridge?.publish();
|
|
531
|
+
if (hasActive && !widgetTimer) {
|
|
473
532
|
widgetTimer = setInterval(refreshWidget, 2_000);
|
|
474
|
-
} else if (
|
|
533
|
+
} else if (!hasActive && widgetTimer) {
|
|
475
534
|
clearInterval(widgetTimer);
|
|
476
535
|
widgetTimer = undefined;
|
|
477
536
|
}
|
|
478
537
|
};
|
|
479
|
-
const notifyRunChanged = (): void =>
|
|
538
|
+
const notifyRunChanged = (): void => {
|
|
539
|
+
if (refreshQueued) return;
|
|
540
|
+
refreshQueued = true;
|
|
541
|
+
const surfaceId = workflowUiSurfaceId;
|
|
542
|
+
queueMicrotask(() => {
|
|
543
|
+
refreshQueued = false;
|
|
544
|
+
if (surfaceId !== workflowUiSurfaceId) return;
|
|
545
|
+
refreshWidget();
|
|
546
|
+
});
|
|
547
|
+
};
|
|
480
548
|
|
|
481
549
|
const DEFAULT_EXCLUDED_TOOLS = ["workflow", "workflow_control"] as const;
|
|
482
550
|
|
|
@@ -524,6 +592,139 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
524
592
|
throw new Error("Provide either `script` or `name`.");
|
|
525
593
|
};
|
|
526
594
|
|
|
595
|
+
const showNavigator = async (ctx: ExtensionContext, initialRunId?: string, guard?: () => void): Promise<void> => {
|
|
596
|
+
guard?.();
|
|
597
|
+
if (ctx.mode !== "tui" || !ctx.hasUI) {
|
|
598
|
+
await showWorkflowNavigator(ctx, currentEngine, { initialRunId });
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
const selectedEngine = currentEngine();
|
|
602
|
+
const surfaceId = workflowUiSurfaceId;
|
|
603
|
+
const manager = ctx.sessionManager;
|
|
604
|
+
const sessionId = manager.getSessionId();
|
|
605
|
+
const resolve = (): WorkflowEngine => {
|
|
606
|
+
guard?.(); // Keep a mounted status navigator bound to its exact source.
|
|
607
|
+
if (
|
|
608
|
+
surfaceId !== workflowUiSurfaceId ||
|
|
609
|
+
selectedEngine !== currentEngine() ||
|
|
610
|
+
manager !== deliveryContext?.sessionManager ||
|
|
611
|
+
manager.getSessionId() !== sessionId ||
|
|
612
|
+
!ownsDeliveryBranch()
|
|
613
|
+
)
|
|
614
|
+
throw new Error("Workflow inspector belongs to a retired session or branch.");
|
|
615
|
+
return selectedEngine;
|
|
616
|
+
};
|
|
617
|
+
navigatorOpen.set(surfaceId, (navigatorOpen.get(surfaceId) ?? 0) + 1);
|
|
618
|
+
try {
|
|
619
|
+
await showWorkflowNavigator(guard ? guardStatusMount(ctx, guard) : ctx, resolve, {
|
|
620
|
+
initialRunId,
|
|
621
|
+
getPresentation: (runId) => resolve().getInspection(runId),
|
|
622
|
+
openConversation: async (runId, callIndex) => {
|
|
623
|
+
const current = resolve();
|
|
624
|
+
if (!/^(?:0|[1-9]\d*)$/u.test(callIndex)) throw new Error("This call has no retained root conversation.");
|
|
625
|
+
const state = current.getState(runId);
|
|
626
|
+
const run = state?.run;
|
|
627
|
+
const agentId = run?.agentIds[callIndex];
|
|
628
|
+
const attempt = run?.attempts[callIndex];
|
|
629
|
+
if (!state || !run || !agentId || !Number.isInteger(attempt) || (attempt ?? 0) < 1)
|
|
630
|
+
throw new Error("No retained conversation is available for this call.");
|
|
631
|
+
const executionGeneration = state.executionGeneration;
|
|
632
|
+
await openOwnedConversation(
|
|
633
|
+
pi.events,
|
|
634
|
+
agentId,
|
|
635
|
+
{ extension: "pi-workflows", runId, nodeId: `call-${callIndex}`, attemptId: `attempt-${attempt}` },
|
|
636
|
+
() => {
|
|
637
|
+
try {
|
|
638
|
+
return (
|
|
639
|
+
resolve().getState(runId) === state &&
|
|
640
|
+
state.executionGeneration === executionGeneration &&
|
|
641
|
+
run.agentIds[callIndex] === agentId &&
|
|
642
|
+
run.attempts[callIndex] === attempt
|
|
643
|
+
);
|
|
644
|
+
} catch {
|
|
645
|
+
return false;
|
|
646
|
+
}
|
|
647
|
+
},
|
|
648
|
+
);
|
|
649
|
+
},
|
|
650
|
+
});
|
|
651
|
+
} finally {
|
|
652
|
+
// An obsolete factory cannot hold or release a replacement surface's slot.
|
|
653
|
+
const remaining = (navigatorOpen.get(surfaceId) ?? 0) - 1;
|
|
654
|
+
if (remaining > 0) navigatorOpen.set(surfaceId, remaining);
|
|
655
|
+
else navigatorOpen.delete(surfaceId);
|
|
656
|
+
}
|
|
657
|
+
};
|
|
658
|
+
|
|
659
|
+
const registerStatusActions = (): void => {
|
|
660
|
+
const ctx = deliveryContext;
|
|
661
|
+
if (!active || !engine || ctx?.mode !== "tui" || !ctx.hasUI) return;
|
|
662
|
+
const selectedEngine = currentEngine();
|
|
663
|
+
const surfaceId = workflowUiSurfaceId;
|
|
664
|
+
const manager = ctx.sessionManager;
|
|
665
|
+
const sessionId = manager.getSessionId();
|
|
666
|
+
let branchIds = manager.getBranch().map((entry) => entry.id);
|
|
667
|
+
let retired = false;
|
|
668
|
+
const source = (): StatusSurface | undefined => {
|
|
669
|
+
if (retired) return;
|
|
670
|
+
let valid = false;
|
|
671
|
+
try {
|
|
672
|
+
const branch = manager.getBranch().map((entry) => entry.id);
|
|
673
|
+
valid =
|
|
674
|
+
active &&
|
|
675
|
+
engine === selectedEngine &&
|
|
676
|
+
!selectedEngine.isDisposed() &&
|
|
677
|
+
workflowUiSurfaceId === surfaceId &&
|
|
678
|
+
deliveryContext?.sessionManager === manager &&
|
|
679
|
+
manager.getSessionId() === sessionId &&
|
|
680
|
+
ctx.mode === "tui" &&
|
|
681
|
+
ctx.hasUI &&
|
|
682
|
+
ownsDeliveryBranch() &&
|
|
683
|
+
branchIds.every((id, index) => branch[index] === id);
|
|
684
|
+
if (valid) branchIds = branch;
|
|
685
|
+
} catch {
|
|
686
|
+
// A disposed context permanently retires this presentation surface.
|
|
687
|
+
}
|
|
688
|
+
if (!valid) {
|
|
689
|
+
retired = true;
|
|
690
|
+
if (statusSource === source) retireStatusActions();
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
return {
|
|
694
|
+
owner: selectedEngine,
|
|
695
|
+
surfaceId,
|
|
696
|
+
sessionId,
|
|
697
|
+
busy: () => (navigatorOpen.get(surfaceId) ?? 0) > 0,
|
|
698
|
+
open: () =>
|
|
699
|
+
showNavigator(ctx, undefined, () => {
|
|
700
|
+
if (!source()) throw new Error("Workflow status surface is retired.");
|
|
701
|
+
}),
|
|
702
|
+
};
|
|
703
|
+
};
|
|
704
|
+
statusSource = source;
|
|
705
|
+
statusActions = new StatusActions(
|
|
706
|
+
pi.events,
|
|
707
|
+
{ providerId: "pi-workflows", statusKey: "pi-workflows", actionId: "runs", label: "Workflow runs" },
|
|
708
|
+
source,
|
|
709
|
+
);
|
|
710
|
+
if (source())
|
|
711
|
+
ctx.ui.setStatus(
|
|
712
|
+
"pi-workflows",
|
|
713
|
+
workflowStatusSummary(selectedEngine.list().map((run) => ({ status: String(run.status) }))),
|
|
714
|
+
);
|
|
715
|
+
};
|
|
716
|
+
|
|
717
|
+
const workflowUiSurface = (): WorkflowUiSurface | undefined => {
|
|
718
|
+
const ctx = deliveryContext;
|
|
719
|
+
if (!active || !engine || engine.isDisposed() || ctx?.mode !== "tui" || !ctx.hasUI || !ownsDeliveryBranch())
|
|
720
|
+
return undefined;
|
|
721
|
+
return {
|
|
722
|
+
snapshot: buildWorkflowUiSnapshot(engine, workflowUiSurfaceId),
|
|
723
|
+
open: (runId) => showNavigator(ctx, runId),
|
|
724
|
+
notifyError: (message) => notifyWorkflow(ctx, message, "error"),
|
|
725
|
+
};
|
|
726
|
+
};
|
|
727
|
+
|
|
527
728
|
const registerSurface = (): void => {
|
|
528
729
|
if (active) return;
|
|
529
730
|
active = true;
|
|
@@ -531,6 +732,7 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
531
732
|
workflowEngine.onRunSettled = notifyRunChanged;
|
|
532
733
|
workflowEngine.onBackgroundExecution = (runId) => void deliverBackgroundResult(workflowEngine, runId);
|
|
533
734
|
workflowEngine.onRuntimeEvent = notifyRunChanged;
|
|
735
|
+
workflowEngine.onInspectionChanged = notifyRunChanged;
|
|
534
736
|
if (typeof pi.registerMessageRenderer === "function") {
|
|
535
737
|
pi.registerMessageRenderer("pi-workflows:delivery", (message, { expanded }, theme) => {
|
|
536
738
|
const original =
|
|
@@ -549,6 +751,7 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
549
751
|
// never frozen onto a run. Without this they would resume with no table and
|
|
550
752
|
// re-run every finished call untabled.
|
|
551
753
|
workflowEngine.strengths = () => workflowSettings.strengths;
|
|
754
|
+
workflowEngine.loadSavedWorkflow = resolveNestedWorkflow;
|
|
552
755
|
// Command/tool registrations persist across session replacement, but the
|
|
553
756
|
// widget belongs to the newly restored engine and must be refreshed too.
|
|
554
757
|
refreshWidget();
|
|
@@ -712,21 +915,21 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
712
915
|
try {
|
|
713
916
|
return currentEngine();
|
|
714
917
|
} catch (error: unknown) {
|
|
715
|
-
ctx
|
|
918
|
+
notifyWorkflow(ctx, error instanceof Error ? error.message : String(error), "error");
|
|
716
919
|
return undefined;
|
|
717
920
|
}
|
|
718
921
|
};
|
|
719
922
|
if (!subcommand) {
|
|
720
|
-
await
|
|
923
|
+
await showNavigator(ctx);
|
|
721
924
|
return;
|
|
722
925
|
}
|
|
723
926
|
if (subcommand === "run") {
|
|
724
927
|
if (!restArgs) {
|
|
725
|
-
ctx
|
|
928
|
+
notifyWorkflow(ctx, "/workflows run <prompt-or-script>", "warning");
|
|
726
929
|
return;
|
|
727
930
|
}
|
|
728
931
|
const runId = await runWorkflowFromPrompt(ctx, restArgs);
|
|
729
|
-
if (runId) ctx
|
|
932
|
+
if (runId) notifyWorkflow(ctx, `Workflow started in background: ${runId}`, "info");
|
|
730
933
|
return;
|
|
731
934
|
}
|
|
732
935
|
if (subcommand === "status") {
|
|
@@ -735,14 +938,15 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
735
938
|
if (restArgs) {
|
|
736
939
|
const run = workflowEngine.getRun(restArgs);
|
|
737
940
|
if (!run) {
|
|
738
|
-
ctx
|
|
941
|
+
notifyWorkflow(ctx, `Workflow run not found: ${restArgs}`, "warning");
|
|
739
942
|
return;
|
|
740
943
|
}
|
|
741
|
-
ctx
|
|
944
|
+
notifyWorkflow(ctx, `${run.runId} · ${run.status} · ${String(run.meta?.name ?? run.runId)}`, "info");
|
|
742
945
|
return;
|
|
743
946
|
}
|
|
744
947
|
const runs = workflowEngine.list();
|
|
745
|
-
|
|
948
|
+
notifyWorkflow(
|
|
949
|
+
ctx,
|
|
746
950
|
runs.length === 0
|
|
747
951
|
? "No workflow runs."
|
|
748
952
|
: runs.map((r) => `${r.runId} · ${r.status} · ${String(r.name)}`).join("\n"),
|
|
@@ -755,11 +959,16 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
755
959
|
if (!workflowEngine) return;
|
|
756
960
|
const runId = restArgs || String(workflowEngine.list()[0]?.runId ?? "");
|
|
757
961
|
if (!runId) {
|
|
758
|
-
ctx
|
|
962
|
+
notifyWorkflow(ctx, "No workflow runs to watch.", "warning");
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
const run = workflowEngine.getRun(runId);
|
|
966
|
+
if (!run) {
|
|
967
|
+
notifyWorkflow(ctx, `Workflow run not found: ${runId}`, "warning");
|
|
759
968
|
return;
|
|
760
969
|
}
|
|
761
|
-
|
|
762
|
-
|
|
970
|
+
if (ctx.mode === "tui" && ctx.hasUI) await showNavigator(ctx, runId);
|
|
971
|
+
else notifyWorkflow(ctx, `${run.runId} · ${run.status} — use workflow_control outside TUI.`, "info");
|
|
763
972
|
return;
|
|
764
973
|
}
|
|
765
974
|
if (subcommand === "stop" || subcommand === "pause" || subcommand === "resume" || subcommand === "rm") {
|
|
@@ -773,7 +982,8 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
773
982
|
);
|
|
774
983
|
if (!runId) {
|
|
775
984
|
const destructive = subcommand === "stop" || subcommand === "rm";
|
|
776
|
-
|
|
985
|
+
notifyWorkflow(
|
|
986
|
+
ctx,
|
|
777
987
|
destructive
|
|
778
988
|
? `/workflows ${subcommand} <runId> — a run id is required; /workflows status lists them`
|
|
779
989
|
: `/workflows ${subcommand} <runId>`,
|
|
@@ -786,16 +996,16 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
786
996
|
if (!workflowEngine) return;
|
|
787
997
|
await workflowEngine.control(subcommand, runId);
|
|
788
998
|
if (subcommand === "resume") void deliverBackgroundResult(workflowEngine, runId);
|
|
789
|
-
ctx
|
|
999
|
+
notifyWorkflow(ctx, `Workflow ${subcommand} requested: ${runId}`, "info");
|
|
790
1000
|
} catch (error: unknown) {
|
|
791
|
-
ctx
|
|
1001
|
+
notifyWorkflow(ctx, error instanceof Error ? error.message : String(error), "error");
|
|
792
1002
|
}
|
|
793
1003
|
return;
|
|
794
1004
|
}
|
|
795
1005
|
if (subcommand === "save") {
|
|
796
1006
|
const name = rest[0] ?? "";
|
|
797
1007
|
if (!name) {
|
|
798
|
-
ctx
|
|
1008
|
+
notifyWorkflow(ctx, "/workflows save <name> [runId]", "warning");
|
|
799
1009
|
return;
|
|
800
1010
|
}
|
|
801
1011
|
const workflowEngine = engineForCommand();
|
|
@@ -803,13 +1013,13 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
803
1013
|
// Fallback to most recent run when runId omitted — matches upstream runs.find(r=>r.script)
|
|
804
1014
|
const runId = rest[1] ?? String(workflowEngine.list()[0]?.runId ?? "");
|
|
805
1015
|
if (!runId) {
|
|
806
|
-
ctx
|
|
1016
|
+
notifyWorkflow(ctx, "No workflow runs to save.", "warning");
|
|
807
1017
|
return;
|
|
808
1018
|
}
|
|
809
1019
|
try {
|
|
810
1020
|
await saveWorkflowFromRun(ctx, name, runId);
|
|
811
1021
|
} catch (error: unknown) {
|
|
812
|
-
ctx
|
|
1022
|
+
notifyWorkflow(ctx, error instanceof Error ? error.message : String(error), "error");
|
|
813
1023
|
}
|
|
814
1024
|
return;
|
|
815
1025
|
}
|
|
@@ -881,15 +1091,19 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
881
1091
|
};
|
|
882
1092
|
const [strength, target] = rest;
|
|
883
1093
|
if (strength === undefined) {
|
|
884
|
-
ctx
|
|
1094
|
+
notifyWorkflow(ctx, describe(), "info");
|
|
885
1095
|
return;
|
|
886
1096
|
}
|
|
887
1097
|
if (!isWorkflowStrength(strength)) {
|
|
888
|
-
|
|
1098
|
+
notifyWorkflow(
|
|
1099
|
+
ctx,
|
|
1100
|
+
`"${strength}" is not a workflow strength. Use one of ${WORKFLOW_STRENGTH_LIST}.`,
|
|
1101
|
+
"warning",
|
|
1102
|
+
);
|
|
889
1103
|
return;
|
|
890
1104
|
}
|
|
891
1105
|
if (target === undefined || rest.length !== 2) {
|
|
892
|
-
ctx
|
|
1106
|
+
notifyWorkflow(ctx, `/workflows strength [<${WORKFLOW_STRENGTHS.join("|")}> <tier>|off]`, "warning");
|
|
893
1107
|
return;
|
|
894
1108
|
}
|
|
895
1109
|
// `off` reads as "unset" unless the host really defines a tier by
|
|
@@ -898,7 +1112,8 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
898
1112
|
// to clear one.
|
|
899
1113
|
const unset = target === "off" && catalogue?.includes("off") !== true;
|
|
900
1114
|
if (!unset && catalogue && !catalogue.includes(target)) {
|
|
901
|
-
|
|
1115
|
+
notifyWorkflow(
|
|
1116
|
+
ctx,
|
|
902
1117
|
`"${target}" is not a tier this host defines. Available: ${catalogue.join(", ") || "(none)"}. ` +
|
|
903
1118
|
"Define it in /agents → Model tiers first.",
|
|
904
1119
|
"warning",
|
|
@@ -911,7 +1126,8 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
911
1126
|
// table, and this key replaces rather than merges — so the other two
|
|
912
1127
|
// strengths would go from routed to unmapped as a side effect of
|
|
913
1128
|
// setting this one. Refusing costs a retry; writing costs a run.
|
|
914
|
-
|
|
1129
|
+
notifyWorkflow(
|
|
1130
|
+
ctx,
|
|
915
1131
|
"The tier catalogue is unavailable, so the current table cannot be read. " +
|
|
916
1132
|
"Retry once the managed peer is reachable.",
|
|
917
1133
|
"warning",
|
|
@@ -931,7 +1147,8 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
931
1147
|
try {
|
|
932
1148
|
persistWorkflowSetting({ strengths: unset ? others : { ...others, [strength]: target } });
|
|
933
1149
|
} catch (error: unknown) {
|
|
934
|
-
|
|
1150
|
+
notifyWorkflow(
|
|
1151
|
+
ctx,
|
|
935
1152
|
`Could not save workflow strength routing: ${error instanceof Error ? error.message : String(error)}`,
|
|
936
1153
|
"warning",
|
|
937
1154
|
);
|
|
@@ -958,10 +1175,10 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
958
1175
|
"than as the clear keyword. To leave a strength unmapped here, remove its entry from the " +
|
|
959
1176
|
"`strengths` object in this project's settings.json."
|
|
960
1177
|
: "";
|
|
961
|
-
ctx
|
|
1178
|
+
notifyWorkflow(ctx, `${describe()}${shadowed}`, "info");
|
|
962
1179
|
return;
|
|
963
1180
|
}
|
|
964
|
-
ctx
|
|
1181
|
+
notifyWorkflow(ctx, `Unknown /workflows subcommand: ${subcommand}`, "warning");
|
|
965
1182
|
},
|
|
966
1183
|
});
|
|
967
1184
|
|
|
@@ -972,7 +1189,8 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
972
1189
|
const tokens = args.trim().split(/\s+/).filter(Boolean);
|
|
973
1190
|
const action = tokens[0] ?? "status";
|
|
974
1191
|
if (action === "status") {
|
|
975
|
-
|
|
1192
|
+
notifyWorkflow(
|
|
1193
|
+
ctx,
|
|
976
1194
|
`Workflow progress: ${workflowSettings.progressMode}, max agents shown: ${workflowSettings.maxAgentsShown}`,
|
|
977
1195
|
"info",
|
|
978
1196
|
);
|
|
@@ -984,23 +1202,25 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
984
1202
|
} else if (action === "max") {
|
|
985
1203
|
const max = Number(tokens[1]);
|
|
986
1204
|
if (!Number.isInteger(max) || max < 1 || max > 32) {
|
|
987
|
-
ctx
|
|
1205
|
+
notifyWorkflow(ctx, "Usage: /workflows-progress max <1-32>", "warning");
|
|
988
1206
|
return;
|
|
989
1207
|
}
|
|
990
1208
|
patch = { maxAgentsShown: max };
|
|
991
1209
|
} else {
|
|
992
|
-
ctx
|
|
1210
|
+
notifyWorkflow(ctx, "Usage: /workflows-progress compact|detailed|status|max <N>", "warning");
|
|
993
1211
|
return;
|
|
994
1212
|
}
|
|
995
1213
|
try {
|
|
996
1214
|
persistWorkflowSetting(patch);
|
|
997
1215
|
refreshWidget();
|
|
998
|
-
|
|
1216
|
+
notifyWorkflow(
|
|
1217
|
+
ctx,
|
|
999
1218
|
`Workflow progress set to ${workflowSettings.progressMode}, max ${workflowSettings.maxAgentsShown}.`,
|
|
1000
1219
|
"info",
|
|
1001
1220
|
);
|
|
1002
1221
|
} catch (error: unknown) {
|
|
1003
|
-
|
|
1222
|
+
notifyWorkflow(
|
|
1223
|
+
ctx,
|
|
1004
1224
|
`Could not save workflow progress settings: ${error instanceof Error ? error.message : String(error)}`,
|
|
1005
1225
|
"warning",
|
|
1006
1226
|
);
|
|
@@ -1029,24 +1249,25 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
1029
1249
|
if (token === "off") {
|
|
1030
1250
|
keywordTriggerEnabled = false;
|
|
1031
1251
|
persistTriggerSettings();
|
|
1032
|
-
ctx
|
|
1252
|
+
notifyWorkflow(ctx, "Workflow keyword trigger disabled.", "info");
|
|
1033
1253
|
return;
|
|
1034
1254
|
}
|
|
1035
1255
|
if (token === "on") {
|
|
1036
1256
|
keywordTriggerEnabled = true;
|
|
1037
1257
|
persistTriggerSettings();
|
|
1038
|
-
ctx
|
|
1258
|
+
notifyWorkflow(ctx, `Workflow keyword trigger armed on "${triggerKeyword ?? DEFAULT_TRIGGER_WORD}".`, "info");
|
|
1039
1259
|
return;
|
|
1040
1260
|
}
|
|
1041
1261
|
if (token === "reset") {
|
|
1042
1262
|
triggerKeyword = undefined;
|
|
1043
1263
|
keywordTriggerEnabled = true;
|
|
1044
1264
|
persistTriggerSettings();
|
|
1045
|
-
ctx
|
|
1265
|
+
notifyWorkflow(ctx, `Workflow keyword trigger reset to default "${DEFAULT_TRIGGER_WORD}".`, "info");
|
|
1046
1266
|
return;
|
|
1047
1267
|
}
|
|
1048
1268
|
if (token === "status") {
|
|
1049
|
-
|
|
1269
|
+
notifyWorkflow(
|
|
1270
|
+
ctx,
|
|
1050
1271
|
`Workflow keyword trigger: ${keywordTriggerEnabled ? "armed" : "disabled"} on "${triggerKeyword ?? DEFAULT_TRIGGER_WORD}"`,
|
|
1051
1272
|
"info",
|
|
1052
1273
|
);
|
|
@@ -1054,13 +1275,17 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
1054
1275
|
}
|
|
1055
1276
|
const keyword = token === "set" ? (args.trim().split(/\s+/)[1] ?? "") : token;
|
|
1056
1277
|
if (!TRIGGER_WORD_PATTERN.test(keyword)) {
|
|
1057
|
-
|
|
1278
|
+
notifyWorkflow(
|
|
1279
|
+
ctx,
|
|
1280
|
+
"Usage: /workflows-trigger set <one-word-keyword> | off | on | reset | status",
|
|
1281
|
+
"warning",
|
|
1282
|
+
);
|
|
1058
1283
|
return;
|
|
1059
1284
|
}
|
|
1060
1285
|
triggerKeyword = keyword.toLowerCase();
|
|
1061
1286
|
keywordTriggerEnabled = true;
|
|
1062
1287
|
persistTriggerSettings();
|
|
1063
|
-
ctx
|
|
1288
|
+
notifyWorkflow(ctx, `Workflow keyword trigger armed on "${keyword}".`, "info");
|
|
1064
1289
|
},
|
|
1065
1290
|
});
|
|
1066
1291
|
|
|
@@ -1132,7 +1357,7 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
1132
1357
|
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
1133
1358
|
const level = args.trim().toLowerCase();
|
|
1134
1359
|
if (level !== "off" && level !== "high" && level !== "ultra") {
|
|
1135
|
-
ctx
|
|
1360
|
+
notifyWorkflow(ctx, `Effort is currently "${effortLevel}". Usage: /effort off|high|ultra`, "info");
|
|
1136
1361
|
return;
|
|
1137
1362
|
}
|
|
1138
1363
|
effortLevel = level;
|
|
@@ -1141,7 +1366,8 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
1141
1366
|
} catch {
|
|
1142
1367
|
/* warning is non-fatal */
|
|
1143
1368
|
}
|
|
1144
|
-
|
|
1369
|
+
notifyWorkflow(
|
|
1370
|
+
ctx,
|
|
1145
1371
|
level === "off"
|
|
1146
1372
|
? "Effort off — messages are no longer auto-armed."
|
|
1147
1373
|
: `Effort ${level} enabled for substantive messages.`,
|
|
@@ -1158,7 +1384,8 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
1158
1384
|
} catch {
|
|
1159
1385
|
/* warning is non-fatal */
|
|
1160
1386
|
}
|
|
1161
|
-
|
|
1387
|
+
notifyWorkflow(
|
|
1388
|
+
ctx,
|
|
1162
1389
|
effortLevel === "off"
|
|
1163
1390
|
? "Ultracode off."
|
|
1164
1391
|
: "Ultracode on — substantive messages use exhaustive workflow guidance.",
|
|
@@ -1186,7 +1413,7 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
1186
1413
|
if (name === "code-review") {
|
|
1187
1414
|
const cwd = ctx.cwd;
|
|
1188
1415
|
const scope = resolveCodeReviewScope(text, (file, argv) => runCommand(file, argv, cwd));
|
|
1189
|
-
for (const notice of scope.notices) ctx
|
|
1416
|
+
for (const notice of scope.notices) notifyWorkflow(ctx, notice, "warning");
|
|
1190
1417
|
if (!scope.diff.trim()) return;
|
|
1191
1418
|
scriptArgs = { diff: scope.diff, diffSource: scope.diffSource };
|
|
1192
1419
|
} else {
|
|
@@ -1195,7 +1422,7 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
1195
1422
|
try {
|
|
1196
1423
|
validateBuiltinArgs(name, scriptArgs);
|
|
1197
1424
|
} catch (error: unknown) {
|
|
1198
|
-
ctx
|
|
1425
|
+
notifyWorkflow(ctx, error instanceof Error ? error.message : String(error), "warning");
|
|
1199
1426
|
return;
|
|
1200
1427
|
}
|
|
1201
1428
|
const options: ScriptStartOptions = {
|
|
@@ -1210,9 +1437,9 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
1210
1437
|
const workflowEngine = currentEngine();
|
|
1211
1438
|
const result = await workflowEngine.start(descriptor.script, options);
|
|
1212
1439
|
void deliverBackgroundResult(workflowEngine, result.runId);
|
|
1213
|
-
ctx
|
|
1440
|
+
notifyWorkflow(ctx, `Workflow ${name} started in background: ${result.runId}`, "info");
|
|
1214
1441
|
} catch (error: unknown) {
|
|
1215
|
-
ctx
|
|
1442
|
+
notifyWorkflow(ctx, error instanceof Error ? error.message : String(error), "error");
|
|
1216
1443
|
}
|
|
1217
1444
|
},
|
|
1218
1445
|
});
|
|
@@ -1307,6 +1534,7 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
1307
1534
|
workflowEngine.resumeLifecycle();
|
|
1308
1535
|
deliveryContext = ctx;
|
|
1309
1536
|
deliveryBranchAnchor = journalBranchAnchor;
|
|
1537
|
+
registerStatusActions();
|
|
1310
1538
|
hydratePendingDeliveries(entries);
|
|
1311
1539
|
schedulePendingDeliveries();
|
|
1312
1540
|
} catch (error: unknown) {
|
|
@@ -1528,7 +1756,7 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
1528
1756
|
void deliverBackgroundResult(workflowEngine, result.runId);
|
|
1529
1757
|
return result.runId;
|
|
1530
1758
|
} catch (error: unknown) {
|
|
1531
|
-
ctx
|
|
1759
|
+
notifyWorkflow(ctx, error instanceof Error ? error.message : String(error), "error");
|
|
1532
1760
|
return undefined;
|
|
1533
1761
|
}
|
|
1534
1762
|
};
|
|
@@ -1539,7 +1767,7 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
1539
1767
|
throw new Error(`Workflow run not found: ${runId ?? "(none)"}`);
|
|
1540
1768
|
}
|
|
1541
1769
|
saveWorkflow(name, state.run.script, sessionCwd);
|
|
1542
|
-
ctx
|
|
1770
|
+
notifyWorkflow(ctx, `Saved workflow "${name}" (${state.run.script.length} chars).`, "info");
|
|
1543
1771
|
};
|
|
1544
1772
|
|
|
1545
1773
|
pi.on("session_start", async (_event, ctx) => {
|
|
@@ -1654,6 +1882,9 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
1654
1882
|
// ownership, not a captured function value, provide session affinity.
|
|
1655
1883
|
deliveryContext = ctx;
|
|
1656
1884
|
deliveryBranchAnchor = journalBranchAnchor;
|
|
1885
|
+
registerStatusActions();
|
|
1886
|
+
workflowUiBridge ??= new WorkflowUiBridge(pi.events, workflowUiSurface);
|
|
1887
|
+
workflowUiBridge.publish();
|
|
1657
1888
|
inputPreparing = false;
|
|
1658
1889
|
compacting = false;
|
|
1659
1890
|
navigating = false;
|
|
@@ -1688,9 +1919,9 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
1688
1919
|
};
|
|
1689
1920
|
const result = await workflowEngine.start(script, options);
|
|
1690
1921
|
void deliverBackgroundResult(workflowEngine, result.runId);
|
|
1691
|
-
commandCtx
|
|
1922
|
+
notifyWorkflow(commandCtx, `Workflow ${savedName} started in background: ${result.runId}`, "info");
|
|
1692
1923
|
} catch (error: unknown) {
|
|
1693
|
-
commandCtx
|
|
1924
|
+
notifyWorkflow(commandCtx, error instanceof Error ? error.message : String(error), "error");
|
|
1694
1925
|
}
|
|
1695
1926
|
},
|
|
1696
1927
|
});
|
|
@@ -1701,6 +1932,8 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
1701
1932
|
pi.on("session_shutdown", async () => {
|
|
1702
1933
|
// This synchronous boundary is shared with the bus-first shutdown path.
|
|
1703
1934
|
const quiescence = beginShutdownQuiesce();
|
|
1935
|
+
workflowUiBridge?.dispose();
|
|
1936
|
+
workflowUiBridge = undefined;
|
|
1704
1937
|
clearWidget();
|
|
1705
1938
|
widgetUi = undefined;
|
|
1706
1939
|
if (widgetTimer) {
|