@signalridge/pi-workflows 1.8.0 → 1.9.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 +10 -0
- package/README.md +13 -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 +202 -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/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";
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
type WorkflowStrengthTable,
|
|
26
26
|
} from "./strengths.js";
|
|
27
27
|
import { liveWidgetLines, showWorkflowNavigator } from "./ui.js";
|
|
28
|
+
import { buildWorkflowUiSnapshot, openOwnedConversation, WorkflowUiBridge, type WorkflowUiSurface } from "./ui-bus.js";
|
|
28
29
|
import {
|
|
29
30
|
loadProjectWorkflowSettings,
|
|
30
31
|
loadWorkflowSettings,
|
|
@@ -111,6 +112,15 @@ function textResult(text: string, details: unknown = {}) {
|
|
|
111
112
|
};
|
|
112
113
|
}
|
|
113
114
|
|
|
115
|
+
/** Bounded display copies; command notifications never render stored control bytes. */
|
|
116
|
+
function notifyWorkflow(
|
|
117
|
+
ctx: Pick<ExtensionContext, "ui">,
|
|
118
|
+
message: string,
|
|
119
|
+
type: "info" | "warning" | "error" = "info",
|
|
120
|
+
): void {
|
|
121
|
+
ctx.ui.notify(sanitizeDisplayText(message.slice(0, 8_000), { multiline: true }).slice(0, 4_000), type);
|
|
122
|
+
}
|
|
123
|
+
|
|
114
124
|
/**
|
|
115
125
|
* Run one command for diff discovery. argv array, never a shell string — a
|
|
116
126
|
* branch name or path reaches this from user input.
|
|
@@ -366,6 +376,9 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
366
376
|
const PENDING_DELIVERY_TTL_MS = 7 * 24 * 60 * 60 * 1_000;
|
|
367
377
|
let widgetTimer: ReturnType<typeof setInterval> | undefined;
|
|
368
378
|
let widgetUi: ExtensionContext["ui"] | undefined;
|
|
379
|
+
let workflowUiBridge: WorkflowUiBridge | undefined;
|
|
380
|
+
let workflowUiSurfaceId = randomUUID();
|
|
381
|
+
let refreshQueued = false;
|
|
369
382
|
|
|
370
383
|
const currentEngine = (): WorkflowEngine => {
|
|
371
384
|
if (!active || !engine || engine.isDisposed())
|
|
@@ -391,11 +404,13 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
391
404
|
if (!busy) return undefined;
|
|
392
405
|
const notice =
|
|
393
406
|
"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
|
|
407
|
+
if (ctx.hasUI) notifyWorkflow(ctx, notice, "warning");
|
|
395
408
|
else console.warn(`[pi-workflows] ${notice}`);
|
|
396
409
|
return { cancel: true };
|
|
397
410
|
};
|
|
398
411
|
const invalidateDelivery = (): void => {
|
|
412
|
+
workflowUiBridge?.retire(workflowUiSurfaceId);
|
|
413
|
+
workflowUiSurfaceId = randomUUID();
|
|
399
414
|
deliveryGeneration++;
|
|
400
415
|
deliveryObservers.clear();
|
|
401
416
|
turnArming.retire();
|
|
@@ -467,16 +482,55 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
467
482
|
.list()
|
|
468
483
|
.map((summary) => workflowEngine.getRun(String(summary.runId)))
|
|
469
484
|
.filter((run): run is NonNullable<typeof run> => run !== undefined);
|
|
470
|
-
const
|
|
471
|
-
|
|
472
|
-
|
|
485
|
+
const hasActive = runs.some((run) => !["completed", "failed", "stopped"].includes(run.status));
|
|
486
|
+
const surfaceId = workflowUiSurfaceId;
|
|
487
|
+
tui.setWidget(
|
|
488
|
+
"pi-workflows",
|
|
489
|
+
hasActive
|
|
490
|
+
? (host, theme) => ({
|
|
491
|
+
render(width) {
|
|
492
|
+
if (
|
|
493
|
+
!active ||
|
|
494
|
+
engine !== workflowEngine ||
|
|
495
|
+
workflowEngine.isDisposed() ||
|
|
496
|
+
workflowUiSurfaceId !== surfaceId ||
|
|
497
|
+
!Number.isFinite(width) ||
|
|
498
|
+
width < 1
|
|
499
|
+
)
|
|
500
|
+
return [];
|
|
501
|
+
const height = Number.isFinite(host.terminal.rows) ? Math.max(0, host.terminal.rows) : 24;
|
|
502
|
+
const rowBudget = Math.max(1, Math.min(12, Math.floor(height / 4)));
|
|
503
|
+
return liveWidgetLines(
|
|
504
|
+
runs,
|
|
505
|
+
workflowSettings.progressMode,
|
|
506
|
+
workflowSettings.maxAgentsShown,
|
|
507
|
+
rowBudget,
|
|
508
|
+
).map((line, index) =>
|
|
509
|
+
truncateToWidth(theme.fg(index === 0 ? "accent" : "muted", line), Math.floor(width)),
|
|
510
|
+
);
|
|
511
|
+
},
|
|
512
|
+
invalidate() {},
|
|
513
|
+
})
|
|
514
|
+
: undefined,
|
|
515
|
+
);
|
|
516
|
+
workflowUiBridge?.publish();
|
|
517
|
+
if (hasActive && !widgetTimer) {
|
|
473
518
|
widgetTimer = setInterval(refreshWidget, 2_000);
|
|
474
|
-
} else if (
|
|
519
|
+
} else if (!hasActive && widgetTimer) {
|
|
475
520
|
clearInterval(widgetTimer);
|
|
476
521
|
widgetTimer = undefined;
|
|
477
522
|
}
|
|
478
523
|
};
|
|
479
|
-
const notifyRunChanged = (): void =>
|
|
524
|
+
const notifyRunChanged = (): void => {
|
|
525
|
+
if (refreshQueued) return;
|
|
526
|
+
refreshQueued = true;
|
|
527
|
+
const surfaceId = workflowUiSurfaceId;
|
|
528
|
+
queueMicrotask(() => {
|
|
529
|
+
refreshQueued = false;
|
|
530
|
+
if (surfaceId !== workflowUiSurfaceId) return;
|
|
531
|
+
refreshWidget();
|
|
532
|
+
});
|
|
533
|
+
};
|
|
480
534
|
|
|
481
535
|
const DEFAULT_EXCLUDED_TOOLS = ["workflow", "workflow_control"] as const;
|
|
482
536
|
|
|
@@ -524,6 +578,71 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
524
578
|
throw new Error("Provide either `script` or `name`.");
|
|
525
579
|
};
|
|
526
580
|
|
|
581
|
+
const showNavigator = async (ctx: ExtensionContext, initialRunId?: string): Promise<void> => {
|
|
582
|
+
if (ctx.mode !== "tui" || !ctx.hasUI) {
|
|
583
|
+
await showWorkflowNavigator(ctx, currentEngine, { initialRunId });
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
const selectedEngine = currentEngine();
|
|
587
|
+
const surfaceId = workflowUiSurfaceId;
|
|
588
|
+
const manager = ctx.sessionManager;
|
|
589
|
+
const sessionId = manager.getSessionId();
|
|
590
|
+
const resolve = (): WorkflowEngine => {
|
|
591
|
+
if (
|
|
592
|
+
surfaceId !== workflowUiSurfaceId ||
|
|
593
|
+
selectedEngine !== currentEngine() ||
|
|
594
|
+
manager !== deliveryContext?.sessionManager ||
|
|
595
|
+
manager.getSessionId() !== sessionId ||
|
|
596
|
+
!ownsDeliveryBranch()
|
|
597
|
+
)
|
|
598
|
+
throw new Error("Workflow inspector belongs to a retired session or branch.");
|
|
599
|
+
return selectedEngine;
|
|
600
|
+
};
|
|
601
|
+
await showWorkflowNavigator(ctx, resolve, {
|
|
602
|
+
initialRunId,
|
|
603
|
+
getPresentation: (runId) => resolve().getInspection(runId),
|
|
604
|
+
openConversation: async (runId, callIndex) => {
|
|
605
|
+
const current = resolve();
|
|
606
|
+
if (!/^(?:0|[1-9]\d*)$/u.test(callIndex)) throw new Error("This call has no retained root conversation.");
|
|
607
|
+
const state = current.getState(runId);
|
|
608
|
+
const run = state?.run;
|
|
609
|
+
const agentId = run?.agentIds[callIndex];
|
|
610
|
+
const attempt = run?.attempts[callIndex];
|
|
611
|
+
if (!state || !run || !agentId || !Number.isInteger(attempt) || (attempt ?? 0) < 1)
|
|
612
|
+
throw new Error("No retained conversation is available for this call.");
|
|
613
|
+
const executionGeneration = state.executionGeneration;
|
|
614
|
+
await openOwnedConversation(
|
|
615
|
+
pi.events,
|
|
616
|
+
agentId,
|
|
617
|
+
{ extension: "pi-workflows", runId, nodeId: `call-${callIndex}`, attemptId: `attempt-${attempt}` },
|
|
618
|
+
() => {
|
|
619
|
+
try {
|
|
620
|
+
return (
|
|
621
|
+
resolve().getState(runId) === state &&
|
|
622
|
+
state.executionGeneration === executionGeneration &&
|
|
623
|
+
run.agentIds[callIndex] === agentId &&
|
|
624
|
+
run.attempts[callIndex] === attempt
|
|
625
|
+
);
|
|
626
|
+
} catch {
|
|
627
|
+
return false;
|
|
628
|
+
}
|
|
629
|
+
},
|
|
630
|
+
);
|
|
631
|
+
},
|
|
632
|
+
});
|
|
633
|
+
};
|
|
634
|
+
|
|
635
|
+
const workflowUiSurface = (): WorkflowUiSurface | undefined => {
|
|
636
|
+
const ctx = deliveryContext;
|
|
637
|
+
if (!active || !engine || engine.isDisposed() || ctx?.mode !== "tui" || !ctx.hasUI || !ownsDeliveryBranch())
|
|
638
|
+
return undefined;
|
|
639
|
+
return {
|
|
640
|
+
snapshot: buildWorkflowUiSnapshot(engine, workflowUiSurfaceId),
|
|
641
|
+
open: (runId) => showNavigator(ctx, runId),
|
|
642
|
+
notifyError: (message) => notifyWorkflow(ctx, message, "error"),
|
|
643
|
+
};
|
|
644
|
+
};
|
|
645
|
+
|
|
527
646
|
const registerSurface = (): void => {
|
|
528
647
|
if (active) return;
|
|
529
648
|
active = true;
|
|
@@ -531,6 +650,7 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
531
650
|
workflowEngine.onRunSettled = notifyRunChanged;
|
|
532
651
|
workflowEngine.onBackgroundExecution = (runId) => void deliverBackgroundResult(workflowEngine, runId);
|
|
533
652
|
workflowEngine.onRuntimeEvent = notifyRunChanged;
|
|
653
|
+
workflowEngine.onInspectionChanged = notifyRunChanged;
|
|
534
654
|
if (typeof pi.registerMessageRenderer === "function") {
|
|
535
655
|
pi.registerMessageRenderer("pi-workflows:delivery", (message, { expanded }, theme) => {
|
|
536
656
|
const original =
|
|
@@ -549,6 +669,7 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
549
669
|
// never frozen onto a run. Without this they would resume with no table and
|
|
550
670
|
// re-run every finished call untabled.
|
|
551
671
|
workflowEngine.strengths = () => workflowSettings.strengths;
|
|
672
|
+
workflowEngine.loadSavedWorkflow = resolveNestedWorkflow;
|
|
552
673
|
// Command/tool registrations persist across session replacement, but the
|
|
553
674
|
// widget belongs to the newly restored engine and must be refreshed too.
|
|
554
675
|
refreshWidget();
|
|
@@ -712,21 +833,21 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
712
833
|
try {
|
|
713
834
|
return currentEngine();
|
|
714
835
|
} catch (error: unknown) {
|
|
715
|
-
ctx
|
|
836
|
+
notifyWorkflow(ctx, error instanceof Error ? error.message : String(error), "error");
|
|
716
837
|
return undefined;
|
|
717
838
|
}
|
|
718
839
|
};
|
|
719
840
|
if (!subcommand) {
|
|
720
|
-
await
|
|
841
|
+
await showNavigator(ctx);
|
|
721
842
|
return;
|
|
722
843
|
}
|
|
723
844
|
if (subcommand === "run") {
|
|
724
845
|
if (!restArgs) {
|
|
725
|
-
ctx
|
|
846
|
+
notifyWorkflow(ctx, "/workflows run <prompt-or-script>", "warning");
|
|
726
847
|
return;
|
|
727
848
|
}
|
|
728
849
|
const runId = await runWorkflowFromPrompt(ctx, restArgs);
|
|
729
|
-
if (runId) ctx
|
|
850
|
+
if (runId) notifyWorkflow(ctx, `Workflow started in background: ${runId}`, "info");
|
|
730
851
|
return;
|
|
731
852
|
}
|
|
732
853
|
if (subcommand === "status") {
|
|
@@ -735,14 +856,15 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
735
856
|
if (restArgs) {
|
|
736
857
|
const run = workflowEngine.getRun(restArgs);
|
|
737
858
|
if (!run) {
|
|
738
|
-
ctx
|
|
859
|
+
notifyWorkflow(ctx, `Workflow run not found: ${restArgs}`, "warning");
|
|
739
860
|
return;
|
|
740
861
|
}
|
|
741
|
-
ctx
|
|
862
|
+
notifyWorkflow(ctx, `${run.runId} · ${run.status} · ${String(run.meta?.name ?? run.runId)}`, "info");
|
|
742
863
|
return;
|
|
743
864
|
}
|
|
744
865
|
const runs = workflowEngine.list();
|
|
745
|
-
|
|
866
|
+
notifyWorkflow(
|
|
867
|
+
ctx,
|
|
746
868
|
runs.length === 0
|
|
747
869
|
? "No workflow runs."
|
|
748
870
|
: runs.map((r) => `${r.runId} · ${r.status} · ${String(r.name)}`).join("\n"),
|
|
@@ -755,11 +877,16 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
755
877
|
if (!workflowEngine) return;
|
|
756
878
|
const runId = restArgs || String(workflowEngine.list()[0]?.runId ?? "");
|
|
757
879
|
if (!runId) {
|
|
758
|
-
ctx
|
|
880
|
+
notifyWorkflow(ctx, "No workflow runs to watch.", "warning");
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
const run = workflowEngine.getRun(runId);
|
|
884
|
+
if (!run) {
|
|
885
|
+
notifyWorkflow(ctx, `Workflow run not found: ${runId}`, "warning");
|
|
759
886
|
return;
|
|
760
887
|
}
|
|
761
|
-
|
|
762
|
-
|
|
888
|
+
if (ctx.mode === "tui" && ctx.hasUI) await showNavigator(ctx, runId);
|
|
889
|
+
else notifyWorkflow(ctx, `${run.runId} · ${run.status} — use workflow_control outside TUI.`, "info");
|
|
763
890
|
return;
|
|
764
891
|
}
|
|
765
892
|
if (subcommand === "stop" || subcommand === "pause" || subcommand === "resume" || subcommand === "rm") {
|
|
@@ -773,7 +900,8 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
773
900
|
);
|
|
774
901
|
if (!runId) {
|
|
775
902
|
const destructive = subcommand === "stop" || subcommand === "rm";
|
|
776
|
-
|
|
903
|
+
notifyWorkflow(
|
|
904
|
+
ctx,
|
|
777
905
|
destructive
|
|
778
906
|
? `/workflows ${subcommand} <runId> — a run id is required; /workflows status lists them`
|
|
779
907
|
: `/workflows ${subcommand} <runId>`,
|
|
@@ -786,16 +914,16 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
786
914
|
if (!workflowEngine) return;
|
|
787
915
|
await workflowEngine.control(subcommand, runId);
|
|
788
916
|
if (subcommand === "resume") void deliverBackgroundResult(workflowEngine, runId);
|
|
789
|
-
ctx
|
|
917
|
+
notifyWorkflow(ctx, `Workflow ${subcommand} requested: ${runId}`, "info");
|
|
790
918
|
} catch (error: unknown) {
|
|
791
|
-
ctx
|
|
919
|
+
notifyWorkflow(ctx, error instanceof Error ? error.message : String(error), "error");
|
|
792
920
|
}
|
|
793
921
|
return;
|
|
794
922
|
}
|
|
795
923
|
if (subcommand === "save") {
|
|
796
924
|
const name = rest[0] ?? "";
|
|
797
925
|
if (!name) {
|
|
798
|
-
ctx
|
|
926
|
+
notifyWorkflow(ctx, "/workflows save <name> [runId]", "warning");
|
|
799
927
|
return;
|
|
800
928
|
}
|
|
801
929
|
const workflowEngine = engineForCommand();
|
|
@@ -803,13 +931,13 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
803
931
|
// Fallback to most recent run when runId omitted — matches upstream runs.find(r=>r.script)
|
|
804
932
|
const runId = rest[1] ?? String(workflowEngine.list()[0]?.runId ?? "");
|
|
805
933
|
if (!runId) {
|
|
806
|
-
ctx
|
|
934
|
+
notifyWorkflow(ctx, "No workflow runs to save.", "warning");
|
|
807
935
|
return;
|
|
808
936
|
}
|
|
809
937
|
try {
|
|
810
938
|
await saveWorkflowFromRun(ctx, name, runId);
|
|
811
939
|
} catch (error: unknown) {
|
|
812
|
-
ctx
|
|
940
|
+
notifyWorkflow(ctx, error instanceof Error ? error.message : String(error), "error");
|
|
813
941
|
}
|
|
814
942
|
return;
|
|
815
943
|
}
|
|
@@ -881,15 +1009,19 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
881
1009
|
};
|
|
882
1010
|
const [strength, target] = rest;
|
|
883
1011
|
if (strength === undefined) {
|
|
884
|
-
ctx
|
|
1012
|
+
notifyWorkflow(ctx, describe(), "info");
|
|
885
1013
|
return;
|
|
886
1014
|
}
|
|
887
1015
|
if (!isWorkflowStrength(strength)) {
|
|
888
|
-
|
|
1016
|
+
notifyWorkflow(
|
|
1017
|
+
ctx,
|
|
1018
|
+
`"${strength}" is not a workflow strength. Use one of ${WORKFLOW_STRENGTH_LIST}.`,
|
|
1019
|
+
"warning",
|
|
1020
|
+
);
|
|
889
1021
|
return;
|
|
890
1022
|
}
|
|
891
1023
|
if (target === undefined || rest.length !== 2) {
|
|
892
|
-
ctx
|
|
1024
|
+
notifyWorkflow(ctx, `/workflows strength [<${WORKFLOW_STRENGTHS.join("|")}> <tier>|off]`, "warning");
|
|
893
1025
|
return;
|
|
894
1026
|
}
|
|
895
1027
|
// `off` reads as "unset" unless the host really defines a tier by
|
|
@@ -898,7 +1030,8 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
898
1030
|
// to clear one.
|
|
899
1031
|
const unset = target === "off" && catalogue?.includes("off") !== true;
|
|
900
1032
|
if (!unset && catalogue && !catalogue.includes(target)) {
|
|
901
|
-
|
|
1033
|
+
notifyWorkflow(
|
|
1034
|
+
ctx,
|
|
902
1035
|
`"${target}" is not a tier this host defines. Available: ${catalogue.join(", ") || "(none)"}. ` +
|
|
903
1036
|
"Define it in /agents → Model tiers first.",
|
|
904
1037
|
"warning",
|
|
@@ -911,7 +1044,8 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
911
1044
|
// table, and this key replaces rather than merges — so the other two
|
|
912
1045
|
// strengths would go from routed to unmapped as a side effect of
|
|
913
1046
|
// setting this one. Refusing costs a retry; writing costs a run.
|
|
914
|
-
|
|
1047
|
+
notifyWorkflow(
|
|
1048
|
+
ctx,
|
|
915
1049
|
"The tier catalogue is unavailable, so the current table cannot be read. " +
|
|
916
1050
|
"Retry once the managed peer is reachable.",
|
|
917
1051
|
"warning",
|
|
@@ -931,7 +1065,8 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
931
1065
|
try {
|
|
932
1066
|
persistWorkflowSetting({ strengths: unset ? others : { ...others, [strength]: target } });
|
|
933
1067
|
} catch (error: unknown) {
|
|
934
|
-
|
|
1068
|
+
notifyWorkflow(
|
|
1069
|
+
ctx,
|
|
935
1070
|
`Could not save workflow strength routing: ${error instanceof Error ? error.message : String(error)}`,
|
|
936
1071
|
"warning",
|
|
937
1072
|
);
|
|
@@ -958,10 +1093,10 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
958
1093
|
"than as the clear keyword. To leave a strength unmapped here, remove its entry from the " +
|
|
959
1094
|
"`strengths` object in this project's settings.json."
|
|
960
1095
|
: "";
|
|
961
|
-
ctx
|
|
1096
|
+
notifyWorkflow(ctx, `${describe()}${shadowed}`, "info");
|
|
962
1097
|
return;
|
|
963
1098
|
}
|
|
964
|
-
ctx
|
|
1099
|
+
notifyWorkflow(ctx, `Unknown /workflows subcommand: ${subcommand}`, "warning");
|
|
965
1100
|
},
|
|
966
1101
|
});
|
|
967
1102
|
|
|
@@ -972,7 +1107,8 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
972
1107
|
const tokens = args.trim().split(/\s+/).filter(Boolean);
|
|
973
1108
|
const action = tokens[0] ?? "status";
|
|
974
1109
|
if (action === "status") {
|
|
975
|
-
|
|
1110
|
+
notifyWorkflow(
|
|
1111
|
+
ctx,
|
|
976
1112
|
`Workflow progress: ${workflowSettings.progressMode}, max agents shown: ${workflowSettings.maxAgentsShown}`,
|
|
977
1113
|
"info",
|
|
978
1114
|
);
|
|
@@ -984,23 +1120,25 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
984
1120
|
} else if (action === "max") {
|
|
985
1121
|
const max = Number(tokens[1]);
|
|
986
1122
|
if (!Number.isInteger(max) || max < 1 || max > 32) {
|
|
987
|
-
ctx
|
|
1123
|
+
notifyWorkflow(ctx, "Usage: /workflows-progress max <1-32>", "warning");
|
|
988
1124
|
return;
|
|
989
1125
|
}
|
|
990
1126
|
patch = { maxAgentsShown: max };
|
|
991
1127
|
} else {
|
|
992
|
-
ctx
|
|
1128
|
+
notifyWorkflow(ctx, "Usage: /workflows-progress compact|detailed|status|max <N>", "warning");
|
|
993
1129
|
return;
|
|
994
1130
|
}
|
|
995
1131
|
try {
|
|
996
1132
|
persistWorkflowSetting(patch);
|
|
997
1133
|
refreshWidget();
|
|
998
|
-
|
|
1134
|
+
notifyWorkflow(
|
|
1135
|
+
ctx,
|
|
999
1136
|
`Workflow progress set to ${workflowSettings.progressMode}, max ${workflowSettings.maxAgentsShown}.`,
|
|
1000
1137
|
"info",
|
|
1001
1138
|
);
|
|
1002
1139
|
} catch (error: unknown) {
|
|
1003
|
-
|
|
1140
|
+
notifyWorkflow(
|
|
1141
|
+
ctx,
|
|
1004
1142
|
`Could not save workflow progress settings: ${error instanceof Error ? error.message : String(error)}`,
|
|
1005
1143
|
"warning",
|
|
1006
1144
|
);
|
|
@@ -1029,24 +1167,25 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
1029
1167
|
if (token === "off") {
|
|
1030
1168
|
keywordTriggerEnabled = false;
|
|
1031
1169
|
persistTriggerSettings();
|
|
1032
|
-
ctx
|
|
1170
|
+
notifyWorkflow(ctx, "Workflow keyword trigger disabled.", "info");
|
|
1033
1171
|
return;
|
|
1034
1172
|
}
|
|
1035
1173
|
if (token === "on") {
|
|
1036
1174
|
keywordTriggerEnabled = true;
|
|
1037
1175
|
persistTriggerSettings();
|
|
1038
|
-
ctx
|
|
1176
|
+
notifyWorkflow(ctx, `Workflow keyword trigger armed on "${triggerKeyword ?? DEFAULT_TRIGGER_WORD}".`, "info");
|
|
1039
1177
|
return;
|
|
1040
1178
|
}
|
|
1041
1179
|
if (token === "reset") {
|
|
1042
1180
|
triggerKeyword = undefined;
|
|
1043
1181
|
keywordTriggerEnabled = true;
|
|
1044
1182
|
persistTriggerSettings();
|
|
1045
|
-
ctx
|
|
1183
|
+
notifyWorkflow(ctx, `Workflow keyword trigger reset to default "${DEFAULT_TRIGGER_WORD}".`, "info");
|
|
1046
1184
|
return;
|
|
1047
1185
|
}
|
|
1048
1186
|
if (token === "status") {
|
|
1049
|
-
|
|
1187
|
+
notifyWorkflow(
|
|
1188
|
+
ctx,
|
|
1050
1189
|
`Workflow keyword trigger: ${keywordTriggerEnabled ? "armed" : "disabled"} on "${triggerKeyword ?? DEFAULT_TRIGGER_WORD}"`,
|
|
1051
1190
|
"info",
|
|
1052
1191
|
);
|
|
@@ -1054,13 +1193,17 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
1054
1193
|
}
|
|
1055
1194
|
const keyword = token === "set" ? (args.trim().split(/\s+/)[1] ?? "") : token;
|
|
1056
1195
|
if (!TRIGGER_WORD_PATTERN.test(keyword)) {
|
|
1057
|
-
|
|
1196
|
+
notifyWorkflow(
|
|
1197
|
+
ctx,
|
|
1198
|
+
"Usage: /workflows-trigger set <one-word-keyword> | off | on | reset | status",
|
|
1199
|
+
"warning",
|
|
1200
|
+
);
|
|
1058
1201
|
return;
|
|
1059
1202
|
}
|
|
1060
1203
|
triggerKeyword = keyword.toLowerCase();
|
|
1061
1204
|
keywordTriggerEnabled = true;
|
|
1062
1205
|
persistTriggerSettings();
|
|
1063
|
-
ctx
|
|
1206
|
+
notifyWorkflow(ctx, `Workflow keyword trigger armed on "${keyword}".`, "info");
|
|
1064
1207
|
},
|
|
1065
1208
|
});
|
|
1066
1209
|
|
|
@@ -1132,7 +1275,7 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
1132
1275
|
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
1133
1276
|
const level = args.trim().toLowerCase();
|
|
1134
1277
|
if (level !== "off" && level !== "high" && level !== "ultra") {
|
|
1135
|
-
ctx
|
|
1278
|
+
notifyWorkflow(ctx, `Effort is currently "${effortLevel}". Usage: /effort off|high|ultra`, "info");
|
|
1136
1279
|
return;
|
|
1137
1280
|
}
|
|
1138
1281
|
effortLevel = level;
|
|
@@ -1141,7 +1284,8 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
1141
1284
|
} catch {
|
|
1142
1285
|
/* warning is non-fatal */
|
|
1143
1286
|
}
|
|
1144
|
-
|
|
1287
|
+
notifyWorkflow(
|
|
1288
|
+
ctx,
|
|
1145
1289
|
level === "off"
|
|
1146
1290
|
? "Effort off — messages are no longer auto-armed."
|
|
1147
1291
|
: `Effort ${level} enabled for substantive messages.`,
|
|
@@ -1158,7 +1302,8 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
1158
1302
|
} catch {
|
|
1159
1303
|
/* warning is non-fatal */
|
|
1160
1304
|
}
|
|
1161
|
-
|
|
1305
|
+
notifyWorkflow(
|
|
1306
|
+
ctx,
|
|
1162
1307
|
effortLevel === "off"
|
|
1163
1308
|
? "Ultracode off."
|
|
1164
1309
|
: "Ultracode on — substantive messages use exhaustive workflow guidance.",
|
|
@@ -1186,7 +1331,7 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
1186
1331
|
if (name === "code-review") {
|
|
1187
1332
|
const cwd = ctx.cwd;
|
|
1188
1333
|
const scope = resolveCodeReviewScope(text, (file, argv) => runCommand(file, argv, cwd));
|
|
1189
|
-
for (const notice of scope.notices) ctx
|
|
1334
|
+
for (const notice of scope.notices) notifyWorkflow(ctx, notice, "warning");
|
|
1190
1335
|
if (!scope.diff.trim()) return;
|
|
1191
1336
|
scriptArgs = { diff: scope.diff, diffSource: scope.diffSource };
|
|
1192
1337
|
} else {
|
|
@@ -1195,7 +1340,7 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
1195
1340
|
try {
|
|
1196
1341
|
validateBuiltinArgs(name, scriptArgs);
|
|
1197
1342
|
} catch (error: unknown) {
|
|
1198
|
-
ctx
|
|
1343
|
+
notifyWorkflow(ctx, error instanceof Error ? error.message : String(error), "warning");
|
|
1199
1344
|
return;
|
|
1200
1345
|
}
|
|
1201
1346
|
const options: ScriptStartOptions = {
|
|
@@ -1210,9 +1355,9 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
1210
1355
|
const workflowEngine = currentEngine();
|
|
1211
1356
|
const result = await workflowEngine.start(descriptor.script, options);
|
|
1212
1357
|
void deliverBackgroundResult(workflowEngine, result.runId);
|
|
1213
|
-
ctx
|
|
1358
|
+
notifyWorkflow(ctx, `Workflow ${name} started in background: ${result.runId}`, "info");
|
|
1214
1359
|
} catch (error: unknown) {
|
|
1215
|
-
ctx
|
|
1360
|
+
notifyWorkflow(ctx, error instanceof Error ? error.message : String(error), "error");
|
|
1216
1361
|
}
|
|
1217
1362
|
},
|
|
1218
1363
|
});
|
|
@@ -1528,7 +1673,7 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
1528
1673
|
void deliverBackgroundResult(workflowEngine, result.runId);
|
|
1529
1674
|
return result.runId;
|
|
1530
1675
|
} catch (error: unknown) {
|
|
1531
|
-
ctx
|
|
1676
|
+
notifyWorkflow(ctx, error instanceof Error ? error.message : String(error), "error");
|
|
1532
1677
|
return undefined;
|
|
1533
1678
|
}
|
|
1534
1679
|
};
|
|
@@ -1539,7 +1684,7 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
1539
1684
|
throw new Error(`Workflow run not found: ${runId ?? "(none)"}`);
|
|
1540
1685
|
}
|
|
1541
1686
|
saveWorkflow(name, state.run.script, sessionCwd);
|
|
1542
|
-
ctx
|
|
1687
|
+
notifyWorkflow(ctx, `Saved workflow "${name}" (${state.run.script.length} chars).`, "info");
|
|
1543
1688
|
};
|
|
1544
1689
|
|
|
1545
1690
|
pi.on("session_start", async (_event, ctx) => {
|
|
@@ -1654,6 +1799,8 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
1654
1799
|
// ownership, not a captured function value, provide session affinity.
|
|
1655
1800
|
deliveryContext = ctx;
|
|
1656
1801
|
deliveryBranchAnchor = journalBranchAnchor;
|
|
1802
|
+
workflowUiBridge ??= new WorkflowUiBridge(pi.events, workflowUiSurface);
|
|
1803
|
+
workflowUiBridge.publish();
|
|
1657
1804
|
inputPreparing = false;
|
|
1658
1805
|
compacting = false;
|
|
1659
1806
|
navigating = false;
|
|
@@ -1688,9 +1835,9 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
1688
1835
|
};
|
|
1689
1836
|
const result = await workflowEngine.start(script, options);
|
|
1690
1837
|
void deliverBackgroundResult(workflowEngine, result.runId);
|
|
1691
|
-
commandCtx
|
|
1838
|
+
notifyWorkflow(commandCtx, `Workflow ${savedName} started in background: ${result.runId}`, "info");
|
|
1692
1839
|
} catch (error: unknown) {
|
|
1693
|
-
commandCtx
|
|
1840
|
+
notifyWorkflow(commandCtx, error instanceof Error ? error.message : String(error), "error");
|
|
1694
1841
|
}
|
|
1695
1842
|
},
|
|
1696
1843
|
});
|
|
@@ -1701,6 +1848,8 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
1701
1848
|
pi.on("session_shutdown", async () => {
|
|
1702
1849
|
// This synchronous boundary is shared with the bus-first shutdown path.
|
|
1703
1850
|
const quiescence = beginShutdownQuiesce();
|
|
1851
|
+
workflowUiBridge?.dispose();
|
|
1852
|
+
workflowUiBridge = undefined;
|
|
1704
1853
|
clearWidget();
|
|
1705
1854
|
widgetUi = undefined;
|
|
1706
1855
|
if (widgetTimer) {
|