@g3un/pi-orchestra 0.2.0 → 0.2.1

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/package.json CHANGED
@@ -1,12 +1,11 @@
1
1
  {
2
2
  "name": "@g3un/pi-orchestra",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Subagent orchestration tools for Pi.",
5
5
  "keywords": [
6
6
  "orchestration",
7
7
  "pi",
8
8
  "pi-extension",
9
- "pi-package",
10
9
  "subagent"
11
10
  ],
12
11
  "homepage": "https://codeberg.org/g3un/pi-orchestra",
@@ -41,6 +41,7 @@ export interface WorkflowRun {
41
41
  id: string;
42
42
  name: string;
43
43
  goal: string;
44
+ startedAtMs: number;
44
45
  state: AgentState;
45
46
  currentStageIndex: number;
46
47
  stages: WorkflowStageRun[];
@@ -2,17 +2,33 @@ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import type { AgentRun } from "../core/subagent.ts";
3
3
  import type { AgentStore } from "../core/store.ts";
4
4
  import type { WorkflowRun, WorkflowStageRun } from "../core/workflow.ts";
5
- import { formatNamedEntityLabel, isTerminalAgentState } from "../utils.ts";
5
+ import { isTerminalAgentState } from "../utils.ts";
6
6
 
7
7
  const WIDGET_KEY = "pi-orchestra.workflow-monitor";
8
8
  const MAX_MONITORED_WORKFLOWS = 2;
9
9
  const MAX_WIDGET_LINES = 10;
10
+ const DEFAULT_TICK_MS = 1_000;
11
+
12
+ export interface WorkflowMonitorControllerOptions {
13
+ now?: () => number;
14
+ /** Defaults to 1000 ms. Use 0 to disable uptime ticks in tests. */
15
+ tickMs?: number;
16
+ }
10
17
 
11
18
  export class WorkflowMonitorController {
19
+ private readonly now: () => number;
20
+ private readonly tickMs: number;
12
21
  private unsubscribe?: () => void;
22
+ private tickTimer?: ReturnType<typeof setInterval>;
13
23
  private ctx?: ExtensionContext;
14
24
 
15
- constructor(private readonly store: AgentStore) {}
25
+ constructor(
26
+ private readonly store: AgentStore,
27
+ options: WorkflowMonitorControllerOptions = {},
28
+ ) {
29
+ this.now = options.now ?? Date.now;
30
+ this.tickMs = options.tickMs ?? DEFAULT_TICK_MS;
31
+ }
16
32
 
17
33
  hasActiveWorkflows(): boolean {
18
34
  return listActiveWorkflows(this.store).length > 0;
@@ -30,6 +46,7 @@ export class WorkflowMonitorController {
30
46
  unsubscribeWorkflows();
31
47
  };
32
48
  }
49
+ this.startTicking();
33
50
 
34
51
  return this.render();
35
52
  }
@@ -37,6 +54,7 @@ export class WorkflowMonitorController {
37
54
  dispose(): void {
38
55
  this.unsubscribe?.();
39
56
  this.unsubscribe = undefined;
57
+ this.stopTicking();
40
58
 
41
59
  if (this.ctx?.hasUI) {
42
60
  this.ctx.ui.setWidget(WIDGET_KEY, undefined);
@@ -48,7 +66,7 @@ export class WorkflowMonitorController {
48
66
  const ctx = this.ctx;
49
67
  if (!ctx?.hasUI) return false;
50
68
 
51
- const lines = buildWorkflowMonitorLines(this.store);
69
+ const lines = buildWorkflowMonitorLines(this.store, this.now());
52
70
  if (lines.length === 0) {
53
71
  this.dispose();
54
72
  return false;
@@ -57,15 +75,28 @@ export class WorkflowMonitorController {
57
75
  ctx.ui.setWidget(WIDGET_KEY, lines, { placement: "belowEditor" });
58
76
  return true;
59
77
  }
78
+
79
+ private startTicking(): void {
80
+ if (this.tickMs <= 0 || this.tickTimer) return;
81
+ const timer = setInterval(() => this.render(), this.tickMs);
82
+ (timer as typeof timer & { unref?: () => void }).unref?.();
83
+ this.tickTimer = timer;
84
+ }
85
+
86
+ private stopTicking(): void {
87
+ if (!this.tickTimer) return;
88
+ clearInterval(this.tickTimer);
89
+ this.tickTimer = undefined;
90
+ }
60
91
  }
61
92
 
62
- export function buildWorkflowMonitorLines(store: AgentStore): string[] {
93
+ export function buildWorkflowMonitorLines(store: AgentStore, nowMs = Date.now()): string[] {
63
94
  const workflows = listActiveWorkflows(store);
64
95
  if (workflows.length === 0) return [];
65
96
 
66
97
  const lines: string[] = [];
67
98
  for (const workflow of workflows.slice(0, MAX_MONITORED_WORKFLOWS)) {
68
- appendWorkflowLines(lines, store, workflow);
99
+ appendWorkflowLines(lines, store, workflow, nowMs);
69
100
  if (lines.length >= MAX_WIDGET_LINES) break;
70
101
  }
71
102
 
@@ -77,12 +108,14 @@ export function buildWorkflowMonitorLines(store: AgentStore): string[] {
77
108
  return lines.slice(0, MAX_WIDGET_LINES);
78
109
  }
79
110
 
80
- function appendWorkflowLines(lines: string[], store: AgentStore, workflow: WorkflowRun): void {
111
+ function appendWorkflowLines(lines: string[], store: AgentStore, workflow: WorkflowRun, nowMs: number): void {
81
112
  if (lines.length >= MAX_WIDGET_LINES) return;
82
113
 
83
114
  const stage = getCurrentStage(workflow);
84
- const stageLabel = stage ? formatStageLabel(store, workflow, stage) : "none · agents 0/0";
85
- lines.push(`${formatNamedEntityLabel(workflow)} | ${stageLabel}`);
115
+ const stageLabel = stage
116
+ ? formatStageLabel(store, workflow, stage)
117
+ : `none (0/${workflow.stages.length}) | agents (0/0)`;
118
+ lines.push(`${workflow.name} | ${stageLabel} | ${formatWorkflowUptime(workflow, nowMs)}`);
86
119
  }
87
120
 
88
121
  function listActiveWorkflows(store: AgentStore): WorkflowRun[] {
@@ -98,7 +131,7 @@ function getCurrentStage(workflow: WorkflowRun): WorkflowStageRun | undefined {
98
131
  function formatStageLabel(store: AgentStore, workflow: WorkflowRun, stage: WorkflowStageRun): string {
99
132
  const stageIndex = workflow.stages.indexOf(stage);
100
133
  const stagePosition = stageIndex >= 0 ? `${stageIndex + 1}/${workflow.stages.length}` : `?/${workflow.stages.length}`;
101
- return `${stage.name} · step ${stagePosition} · agents ${formatStageProgress(store, stage)}`;
134
+ return `${stage.name} (${stagePosition}) | agents (${formatStageProgress(store, stage)})`;
102
135
  }
103
136
 
104
137
  function formatStageProgress(store: AgentStore, stage: WorkflowStageRun): string {
@@ -106,6 +139,22 @@ function formatStageProgress(store: AgentStore, stage: WorkflowStageRun): string
106
139
  return `${progress.completed}/${progress.total}`;
107
140
  }
108
141
 
142
+ function formatWorkflowUptime(workflow: WorkflowRun, nowMs: number): string {
143
+ const elapsedSeconds = Math.max(0, Math.floor((nowMs - workflow.startedAtMs) / 1_000));
144
+ const seconds = elapsedSeconds % 60;
145
+ const totalMinutes = Math.floor(elapsedSeconds / 60);
146
+ const minutes = totalMinutes % 60;
147
+ const hours = Math.floor(totalMinutes / 60);
148
+
149
+ if (hours > 0) return `${hours}h ${pad2(minutes)}m`;
150
+ if (totalMinutes > 0) return `${totalMinutes}m ${pad2(seconds)}s`;
151
+ return `${seconds}s`;
152
+ }
153
+
154
+ function pad2(value: number): string {
155
+ return value.toString().padStart(2, "0");
156
+ }
157
+
109
158
  function calculateStageProgress(store: AgentStore, stage: WorkflowStageRun): { completed: number; total: number } {
110
159
  const runs = collectStageRuns(store, stage);
111
160
  const completed = runs.filter((run) => isTerminalAgentState(run.state)).length;
@@ -312,6 +312,7 @@ function createWorkflowRun(
312
312
  return {
313
313
  ...identity,
314
314
  goal: input.goal,
315
+ startedAtMs: Date.now(),
315
316
  state: "idle",
316
317
  currentStageIndex: 0,
317
318
  stages: input.stages.map((stage) => {