@signalridge/pi-workflows 1.9.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 +5 -0
- package/README.md +18 -0
- package/package.json +1 -1
- 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/index.ts +117 -33
- package/src/status-actions.ts +143 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.10.0
|
|
4
|
+
### Minor Changes
|
|
5
|
+
|
|
6
|
+
- a4dc1de: Expose optional session/branch-owned status navigation to the existing workflow navigator and add compact run-authoritative footer counts without changing execution or widget behavior.
|
|
7
|
+
|
|
3
8
|
## 1.9.0
|
|
4
9
|
### Minor Changes
|
|
5
10
|
|
package/README.md
CHANGED
|
@@ -544,3 +544,21 @@ used it.
|
|
|
544
544
|
- **Schema validation is client-side.** The managed request does not assume a structured-output tool; the runtime asks for JSON, parses, validates (including `additionalProperties`), and repairs across bounded attempts.
|
|
545
545
|
|
|
546
546
|
State is persisted as `pi.appendEntry("pi-workflows:journal", ...)` custom entries (schema v4), including frozen invocation args, resolved tier identity, terminal result previews, script revisions, named nested-workflow result boundaries, and durable removals. Interrupted/provider-limited runs replay from the journal; pre-schema-v4 journals are quarantined rather than replayed. Foreground `AbortSignal`s stop owned children through explicit owner-scoped stop/quiescence, and dispose/branch replacement reject stale waiters. Navigation preflight vetoes while agents or workflows are active or cleaning up, without interrupting them. An accepted tree commit fences old callbacks in either extension load order and restores only the committed branch; preflight itself is not a commit point.
|
|
547
|
+
|
|
548
|
+
## Status navigation
|
|
549
|
+
|
|
550
|
+
With a compatible status selector, **Ctrl+Alt+S**, choose **Workflow runs**, then
|
|
551
|
+
**Enter** opens this package's existing workflow navigator. `/workflows` and its
|
|
552
|
+
watch/detail paths remain unchanged. Querying this optional TUI-only endpoint
|
|
553
|
+
never opens a screen, starts/resumes a run, or sends a model/user message. Each
|
|
554
|
+
selection belongs to one live session/branch surface; navigation and reload
|
|
555
|
+
retire it rather than redirecting an old selection to another owner. Only
|
|
556
|
+
bounded identifiers and labels cross this presentation wire, not execution
|
|
557
|
+
controls, source, logs, or callbacks.
|
|
558
|
+
|
|
559
|
+
The compact `pi-workflows` footer reports `A` active runs (pending, running,
|
|
560
|
+
pausing, stopping) and `P` paused/interrupted runs. With only retained terminal
|
|
561
|
+
runs it reports `C` completed, `F` failed, and `S` stopped counts; an empty run
|
|
562
|
+
list clears it. Counts reflect run status, never inferred child completion.
|
|
563
|
+
This uses the existing progress refresh cadence without adding another widget
|
|
564
|
+
or taking focus.
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: workflow-authoring
|
|
3
3
|
description: Design, write, review, and debug Pi JavaScript workflows that coordinate multiple subagents. Use when a request needs named dependencies, bounded fan-out, per-item pipelines, quality gates, saved or nested workflows, pause/resume, or workflow progress; do not use it merely to run an existing named workflow.
|
|
4
4
|
metadata:
|
|
5
|
-
version: "1.
|
|
5
|
+
version: "1.10.0"
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Workflow authoring
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# Workflow capability index
|
|
3
3
|
|
|
4
4
|
Contract format: `2.0.0`<br>
|
|
5
|
-
Contract content / skill / extension: `1.
|
|
5
|
+
Contract content / skill / extension: `1.10.0`
|
|
6
6
|
|
|
7
7
|
This compact generated index covers supported runtime globals and workflow-tool inputs. For constraints, compatibility behavior, internal boundaries, and dynamic-reference ownership, follow the [exhaustive generated facts](capability-details.md).
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# Exhaustive workflow capability facts
|
|
3
3
|
|
|
4
4
|
Contract format: `2.0.0`<br>
|
|
5
|
-
Contract content / skill / extension: `1.
|
|
5
|
+
Contract content / skill / extension: `1.10.0`
|
|
6
6
|
|
|
7
7
|
Every exact fact below is projected from the installed extension's capability contract. Explanatory judgment belongs in the hand-written references next to this file.
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: workflow-patterns
|
|
3
3
|
description: Choose and run one of Pi's built-in multi-agent workflows for research, adversarial review, code review, multi-perspective analysis, or codebase auditing. Use when the request matches a named pattern; use workflow-authoring instead for a new script or custom topology.
|
|
4
4
|
metadata:
|
|
5
|
-
version: "1.
|
|
5
|
+
version: "1.10.0"
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Built-in workflow patterns
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: workflow-review
|
|
3
3
|
description: Audit an existing Pi JavaScript workflow for topology, prompt/data flow, limits, failure handling, cancellation, resume safety, and publication compatibility. Use when reviewing or debugging a workflow script or saved run; do not use to run a standard named workflow.
|
|
4
4
|
metadata:
|
|
5
|
-
version: "1.
|
|
5
|
+
version: "1.10.0"
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Workflow review
|
package/src/index.ts
CHANGED
|
@@ -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,
|
|
@@ -379,6 +380,16 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
379
380
|
let workflowUiBridge: WorkflowUiBridge | undefined;
|
|
380
381
|
let workflowUiSurfaceId = randomUUID();
|
|
381
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
|
+
};
|
|
382
393
|
|
|
383
394
|
const currentEngine = (): WorkflowEngine => {
|
|
384
395
|
if (!active || !engine || engine.isDisposed())
|
|
@@ -409,6 +420,8 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
409
420
|
return { cancel: true };
|
|
410
421
|
};
|
|
411
422
|
const invalidateDelivery = (): void => {
|
|
423
|
+
navigatorOpen.delete(workflowUiSurfaceId);
|
|
424
|
+
retireStatusActions();
|
|
412
425
|
workflowUiBridge?.retire(workflowUiSurfaceId);
|
|
413
426
|
workflowUiSurfaceId = randomUUID();
|
|
414
427
|
deliveryGeneration++;
|
|
@@ -482,6 +495,7 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
482
495
|
.list()
|
|
483
496
|
.map((summary) => workflowEngine.getRun(String(summary.runId)))
|
|
484
497
|
.filter((run): run is NonNullable<typeof run> => run !== undefined);
|
|
498
|
+
if (statusSource?.()) tui.setStatus("pi-workflows", workflowStatusSummary(runs));
|
|
485
499
|
const hasActive = runs.some((run) => !["completed", "failed", "stopped"].includes(run.status));
|
|
486
500
|
const surfaceId = workflowUiSurfaceId;
|
|
487
501
|
tui.setWidget(
|
|
@@ -578,7 +592,8 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
578
592
|
throw new Error("Provide either `script` or `name`.");
|
|
579
593
|
};
|
|
580
594
|
|
|
581
|
-
const showNavigator = async (ctx: ExtensionContext, initialRunId?: string): Promise<void> => {
|
|
595
|
+
const showNavigator = async (ctx: ExtensionContext, initialRunId?: string, guard?: () => void): Promise<void> => {
|
|
596
|
+
guard?.();
|
|
582
597
|
if (ctx.mode !== "tui" || !ctx.hasUI) {
|
|
583
598
|
await showWorkflowNavigator(ctx, currentEngine, { initialRunId });
|
|
584
599
|
return;
|
|
@@ -588,6 +603,7 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
588
603
|
const manager = ctx.sessionManager;
|
|
589
604
|
const sessionId = manager.getSessionId();
|
|
590
605
|
const resolve = (): WorkflowEngine => {
|
|
606
|
+
guard?.(); // Keep a mounted status navigator bound to its exact source.
|
|
591
607
|
if (
|
|
592
608
|
surfaceId !== workflowUiSurfaceId ||
|
|
593
609
|
selectedEngine !== currentEngine() ||
|
|
@@ -598,38 +614,104 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
598
614
|
throw new Error("Workflow inspector belongs to a retired session or branch.");
|
|
599
615
|
return selectedEngine;
|
|
600
616
|
};
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
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
|
+
);
|
|
633
715
|
};
|
|
634
716
|
|
|
635
717
|
const workflowUiSurface = (): WorkflowUiSurface | undefined => {
|
|
@@ -1452,6 +1534,7 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
1452
1534
|
workflowEngine.resumeLifecycle();
|
|
1453
1535
|
deliveryContext = ctx;
|
|
1454
1536
|
deliveryBranchAnchor = journalBranchAnchor;
|
|
1537
|
+
registerStatusActions();
|
|
1455
1538
|
hydratePendingDeliveries(entries);
|
|
1456
1539
|
schedulePendingDeliveries();
|
|
1457
1540
|
} catch (error: unknown) {
|
|
@@ -1799,6 +1882,7 @@ export default function piWorkflows(pi: ExtensionAPI): void {
|
|
|
1799
1882
|
// ownership, not a captured function value, provide session affinity.
|
|
1800
1883
|
deliveryContext = ctx;
|
|
1801
1884
|
deliveryBranchAnchor = journalBranchAnchor;
|
|
1885
|
+
registerStatusActions();
|
|
1802
1886
|
workflowUiBridge ??= new WorkflowUiBridge(pi.events, workflowUiSurface);
|
|
1803
1887
|
workflowUiBridge.publish();
|
|
1804
1888
|
inputPreparing = false;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
export const STATUS_QUERY = "pi-ui:status-actions:v1:query";
|
|
4
|
+
export const STATUS_OPEN = "pi-ui:status-actions:v1:open";
|
|
5
|
+
const FAILURE = "Status action is unavailable.";
|
|
6
|
+
|
|
7
|
+
export interface StatusSurface {
|
|
8
|
+
surfaceId: string;
|
|
9
|
+
sessionId: string;
|
|
10
|
+
/** Private identity, never placed on the wire. */
|
|
11
|
+
owner: object;
|
|
12
|
+
busy(): boolean;
|
|
13
|
+
open(): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Inspect descriptors, never values on an untrusted object (including getters). */
|
|
17
|
+
function request(raw: unknown, keys: readonly string[]): Record<string, unknown> | undefined {
|
|
18
|
+
try {
|
|
19
|
+
if (!raw || typeof raw !== "object") return;
|
|
20
|
+
const prototype = Object.getPrototypeOf(raw);
|
|
21
|
+
if (prototype !== Object.prototype && prototype !== null) return;
|
|
22
|
+
const descriptors = Object.getOwnPropertyDescriptors(raw);
|
|
23
|
+
if (Reflect.ownKeys(descriptors).length !== keys.length) return;
|
|
24
|
+
const values: Record<string, unknown> = Object.create(null);
|
|
25
|
+
for (const key of keys) {
|
|
26
|
+
const descriptor = descriptors[key];
|
|
27
|
+
if (!descriptor || !("value" in descriptor)) return;
|
|
28
|
+
const value: unknown = descriptor.value;
|
|
29
|
+
if (key === "version" ? value !== 1 : typeof value !== "string" || value.length < 1 || value.length > 128) return;
|
|
30
|
+
values[key] = value;
|
|
31
|
+
}
|
|
32
|
+
return values;
|
|
33
|
+
} catch {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Optional presentation-only endpoint; the source must fail closed on live divergence. */
|
|
39
|
+
export class StatusActions {
|
|
40
|
+
private readonly unsubscribe: Array<() => void>;
|
|
41
|
+
private disposed = false;
|
|
42
|
+
private opening = false;
|
|
43
|
+
private readonly admitted = new Set<string>();
|
|
44
|
+
|
|
45
|
+
constructor(
|
|
46
|
+
private readonly events: ExtensionAPI["events"],
|
|
47
|
+
private readonly descriptor: { providerId: string; statusKey: string; actionId: string; label: string },
|
|
48
|
+
private readonly source: () => StatusSurface | undefined,
|
|
49
|
+
) {
|
|
50
|
+
this.unsubscribe = [
|
|
51
|
+
events.on(STATUS_QUERY, (raw: unknown) => this.query(raw)),
|
|
52
|
+
events.on(STATUS_OPEN, (raw: unknown) => this.open(raw)),
|
|
53
|
+
];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
dispose(): void {
|
|
57
|
+
if (this.disposed) return;
|
|
58
|
+
this.disposed = true;
|
|
59
|
+
for (const unsubscribe of this.unsubscribe) unsubscribe();
|
|
60
|
+
this.admitted.clear();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private query(raw: unknown): void {
|
|
64
|
+
if (this.disposed) return;
|
|
65
|
+
const parsed = request(raw, ["version", "requestId", "sessionId"]);
|
|
66
|
+
if (!parsed) return;
|
|
67
|
+
const surface = this.source();
|
|
68
|
+
if (!surface || parsed.sessionId !== surface.sessionId || this.disposed) return;
|
|
69
|
+
this.events.emit(`${STATUS_QUERY}:reply:${parsed.requestId}`, {
|
|
70
|
+
success: true,
|
|
71
|
+
data: {
|
|
72
|
+
version: 1,
|
|
73
|
+
providerId: this.descriptor.providerId,
|
|
74
|
+
surfaceId: surface.surfaceId,
|
|
75
|
+
sessionId: surface.sessionId,
|
|
76
|
+
entries: [
|
|
77
|
+
{ statusKey: this.descriptor.statusKey, actionId: this.descriptor.actionId, label: this.descriptor.label },
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private open(raw: unknown): void {
|
|
84
|
+
if (this.disposed) return;
|
|
85
|
+
const parsed = request(raw, ["version", "requestId", "providerId", "surfaceId", "sessionId", "actionId"]);
|
|
86
|
+
if (!parsed || parsed.providerId !== this.descriptor.providerId) return;
|
|
87
|
+
const surface = this.source();
|
|
88
|
+
if (!surface || this.disposed || parsed.sessionId !== surface.sessionId || parsed.surfaceId !== surface.surfaceId)
|
|
89
|
+
return; // Other owners, including duplicate providers, may share this bus.
|
|
90
|
+
const channel = `${STATUS_OPEN}:reply:${parsed.requestId}`;
|
|
91
|
+
const id = String(parsed.requestId);
|
|
92
|
+
if (parsed.actionId !== this.descriptor.actionId || this.opening || surface.busy() || this.admitted.has(id)) {
|
|
93
|
+
this.events.emit(channel, { success: false, error: FAILURE });
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
this.admitted.add(id);
|
|
97
|
+
if (this.admitted.size > 256) this.admitted.delete(this.admitted.values().next().value as string);
|
|
98
|
+
this.opening = true;
|
|
99
|
+
// Admission is synchronous; the selector can relinquish focus before mount.
|
|
100
|
+
this.events.emit(channel, { success: true, data: { accepted: true } });
|
|
101
|
+
void Promise.resolve()
|
|
102
|
+
.then(() => {
|
|
103
|
+
if (this.disposed) return;
|
|
104
|
+
const current = this.source();
|
|
105
|
+
if (
|
|
106
|
+
this.disposed ||
|
|
107
|
+
!current ||
|
|
108
|
+
current.owner !== surface.owner ||
|
|
109
|
+
current.surfaceId !== surface.surfaceId ||
|
|
110
|
+
current.sessionId !== surface.sessionId
|
|
111
|
+
)
|
|
112
|
+
return;
|
|
113
|
+
return surface.open();
|
|
114
|
+
})
|
|
115
|
+
.catch(() => {
|
|
116
|
+
// Admission does not promise successful mount. Never redirect to another owner.
|
|
117
|
+
})
|
|
118
|
+
.finally(() => {
|
|
119
|
+
this.opening = false;
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** The host may defer a custom factory after admission; fence that boundary too. */
|
|
125
|
+
export function guardStatusMount(ctx: ExtensionContext, guard: () => void): ExtensionContext {
|
|
126
|
+
const custom: ExtensionContext["ui"]["custom"] = (factory, options) =>
|
|
127
|
+
ctx.ui.custom((...args) => {
|
|
128
|
+
guard();
|
|
129
|
+
return factory(...args);
|
|
130
|
+
}, options);
|
|
131
|
+
return { ...ctx, ui: { ...ctx.ui, custom } };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Count run authority only; child completion cannot establish run completion. */
|
|
135
|
+
export function workflowStatusSummary(runs: ReadonlyArray<{ status: string }>): string | undefined {
|
|
136
|
+
if (!runs.length) return undefined;
|
|
137
|
+
const count = (statuses: string[]) => runs.filter((run) => statuses.includes(run.status)).length;
|
|
138
|
+
const active = count(["pending", "running", "pausing", "stopping"]);
|
|
139
|
+
const paused = count(["paused", "interrupted"]);
|
|
140
|
+
return active || paused
|
|
141
|
+
? `Workflows · A${active} P${paused}`
|
|
142
|
+
: `Workflows · C${count(["completed"])} F${count(["failed"])} S${count(["stopped"])}`;
|
|
143
|
+
}
|