@trevonistrevon/pi-loop 0.3.1 → 0.4.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/DIFFERENTIAL_REVIEW_REPORT.md +59 -0
- package/README.md +50 -6
- package/dist/index.js +298 -59
- package/dist/task-store.d.ts +22 -0
- package/dist/task-store.js +181 -0
- package/dist/task-types.d.ts +15 -0
- package/dist/task-types.js +1 -0
- package/dist/trigger-system.d.ts +2 -1
- package/dist/trigger-system.js +14 -16
- package/dist/ui/widget.d.ts +8 -4
- package/dist/ui/widget.js +28 -74
- package/package.json +1 -1
- package/src/index.ts +297 -54
- package/src/task-store.ts +171 -0
- package/src/task-types.ts +17 -0
- package/src/trigger-system.ts +14 -16
- package/src/ui/widget.ts +27 -70
package/src/trigger-system.ts
CHANGED
|
@@ -12,6 +12,7 @@ export class TriggerSystem {
|
|
|
12
12
|
private pi: ExtensionAPI,
|
|
13
13
|
private scheduler: CronScheduler,
|
|
14
14
|
private store: LoopStore,
|
|
15
|
+
private onFire: (entry: LoopEntry) => void,
|
|
15
16
|
) {}
|
|
16
17
|
|
|
17
18
|
start(): void {
|
|
@@ -94,27 +95,24 @@ export class TriggerSystem {
|
|
|
94
95
|
}
|
|
95
96
|
|
|
96
97
|
private fireLoop(entry: LoopEntry): void {
|
|
97
|
-
|
|
98
|
-
|
|
98
|
+
const current = this.store.get(entry.id);
|
|
99
|
+
if (!current || current.status !== "active") {
|
|
99
100
|
this.remove(entry.id);
|
|
100
101
|
return;
|
|
101
102
|
}
|
|
102
|
-
this.store.update(entry.id, { fireCount: (entry.fireCount ?? 0) + 1 });
|
|
103
|
-
|
|
104
|
-
this.lastFireTime.set(entry.id, Date.now());
|
|
105
|
-
this.pi.events.emit("loop:fire", {
|
|
106
|
-
loopId: entry.id,
|
|
107
|
-
prompt: entry.prompt,
|
|
108
|
-
trigger: entry.trigger,
|
|
109
|
-
timestamp: Date.now(),
|
|
110
|
-
readOnly: entry.readOnly,
|
|
111
|
-
recurring: entry.recurring,
|
|
112
|
-
autoTask: entry.autoTask,
|
|
113
|
-
});
|
|
114
103
|
|
|
115
|
-
|
|
104
|
+
this.lastFireTime.set(current.id, Date.now());
|
|
105
|
+
this.onFire(current);
|
|
106
|
+
|
|
107
|
+
const fresh = this.store.get(entry.id);
|
|
108
|
+
if (!fresh) {
|
|
116
109
|
this.remove(entry.id);
|
|
117
|
-
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!fresh.recurring) {
|
|
114
|
+
this.remove(fresh.id);
|
|
115
|
+
this.store.delete(fresh.id);
|
|
118
116
|
}
|
|
119
117
|
}
|
|
120
118
|
|
package/src/ui/widget.ts
CHANGED
|
@@ -2,20 +2,22 @@ import type { ExtensionUIContext, Theme } from "@earendil-works/pi-coding-agent"
|
|
|
2
2
|
import type { Component, TUI } from "@earendil-works/pi-tui";
|
|
3
3
|
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
4
4
|
import type { MonitorManager } from "../monitor-manager.js";
|
|
5
|
-
import type { CronScheduler } from "../scheduler.js";
|
|
6
5
|
import type { LoopStore } from "../store.js";
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
interface TaskSummary {
|
|
8
|
+
count: number;
|
|
9
|
+
focusText?: string;
|
|
10
|
+
}
|
|
9
11
|
|
|
10
12
|
export class LoopWidget {
|
|
11
13
|
private uiCtx: ExtensionUIContext | undefined;
|
|
12
14
|
private tui: TUI | undefined;
|
|
13
15
|
private widgetRegistered = false;
|
|
14
16
|
private interval: ReturnType<typeof setInterval> | undefined;
|
|
17
|
+
private taskSummaryProvider: (() => TaskSummary) | undefined;
|
|
15
18
|
|
|
16
19
|
constructor(
|
|
17
20
|
private store: LoopStore,
|
|
18
|
-
private scheduler: CronScheduler | undefined,
|
|
19
21
|
private monitorManager: MonitorManager,
|
|
20
22
|
) {}
|
|
21
23
|
|
|
@@ -27,32 +29,22 @@ export class LoopWidget {
|
|
|
27
29
|
this.store = store;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
|
-
|
|
31
|
-
this.
|
|
32
|
+
setTaskSummaryProvider(provider: (() => TaskSummary) | undefined) {
|
|
33
|
+
this.taskSummaryProvider = provider;
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
update() {
|
|
35
37
|
if (!this.uiCtx) return;
|
|
36
38
|
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (!hasContent) {
|
|
42
|
-
if (this.widgetRegistered) {
|
|
43
|
-
this.uiCtx.setWidget("loops", undefined);
|
|
44
|
-
this.widgetRegistered = false;
|
|
45
|
-
}
|
|
46
|
-
if (this.interval) {
|
|
47
|
-
clearInterval(this.interval);
|
|
48
|
-
this.interval = undefined;
|
|
49
|
-
}
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (!this.interval) {
|
|
39
|
+
const taskSummary = this.taskSummaryProvider?.() ?? { count: 0 };
|
|
40
|
+
const hasContent = this.store.list().some(l => l.status === "active") || this.monitorManager.list().length > 0 || taskSummary.count > 0;
|
|
41
|
+
if (hasContent && !this.interval) {
|
|
54
42
|
this.interval = setInterval(() => this.update(), 5000);
|
|
55
43
|
}
|
|
44
|
+
if (!hasContent && this.interval) {
|
|
45
|
+
clearInterval(this.interval);
|
|
46
|
+
this.interval = undefined;
|
|
47
|
+
}
|
|
56
48
|
|
|
57
49
|
if (!this.widgetRegistered) {
|
|
58
50
|
this.uiCtx.setWidget("loops", (tui: TUI, theme: Theme) => {
|
|
@@ -67,51 +59,23 @@ export class LoopWidget {
|
|
|
67
59
|
|
|
68
60
|
private renderWidget(tui: TUI, _theme: Theme): string[] {
|
|
69
61
|
const loops = this.store.list().filter(l => l.status === "active");
|
|
70
|
-
const
|
|
71
|
-
const
|
|
72
|
-
const allMonitors = [...runningMonitors, ...completedMonitors];
|
|
62
|
+
const monitors = this.monitorManager.list();
|
|
63
|
+
const taskSummary = this.taskSummaryProvider?.() ?? { count: 0 };
|
|
73
64
|
const w = tui.terminal.columns;
|
|
74
65
|
const trunc = (line: string) => truncateToWidth(line, w);
|
|
75
66
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if (total === 0) return [];
|
|
80
|
-
|
|
81
|
-
const headerParts: string[] = [`⟳ ${loops.length} loops`];
|
|
82
|
-
if (runningMonitors.length > 0) headerParts.push(`${runningMonitors.length} running`);
|
|
83
|
-
if (completedMonitors.length > 0) headerParts.push(`${completedMonitors.length} done`);
|
|
84
|
-
lines.push(trunc(headerParts.join(" · ")));
|
|
85
|
-
|
|
86
|
-
for (const loop of loops.slice(0, MAX_VISIBLE)) {
|
|
87
|
-
const icon = "◷";
|
|
88
|
-
let schedule = "";
|
|
89
|
-
if (loop.trigger.type === "cron") {
|
|
90
|
-
schedule = loop.trigger.schedule;
|
|
91
|
-
} else if (loop.trigger.type === "event") {
|
|
92
|
-
schedule = `event: ${loop.trigger.source}`;
|
|
93
|
-
} else if (loop.trigger.type === "hybrid") {
|
|
94
|
-
schedule = `hybrid: ${loop.trigger.cron}`;
|
|
95
|
-
}
|
|
96
|
-
const nextFire = this.scheduler?.nextFire(loop.id);
|
|
97
|
-
let timing = "";
|
|
98
|
-
if (nextFire) {
|
|
99
|
-
const remaining = Math.max(0, nextFire - Date.now());
|
|
100
|
-
timing = ` (next: ${formatDuration(remaining)})`;
|
|
101
|
-
}
|
|
102
|
-
lines.push(trunc(` ${icon} #${loop.id} ${loop.prompt.slice(0, 50)} → ${schedule}${timing}`));
|
|
67
|
+
if (loops.length === 0 && monitors.length === 0 && taskSummary.count === 0) {
|
|
68
|
+
return [trunc("none")];
|
|
103
69
|
}
|
|
104
70
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
let line = ` ${icon} #${m.id} ${label} ${m.outputLines} lines (${formatDuration(age)})`;
|
|
110
|
-
if (m.exitCode !== undefined && m.status !== "running") line += ` exit=${m.exitCode}`;
|
|
111
|
-
lines.push(trunc(line));
|
|
112
|
-
}
|
|
71
|
+
const parts: string[] = [];
|
|
72
|
+
if (loops.length > 0) parts.push(formatCount(loops.length, "loop"));
|
|
73
|
+
if (monitors.length > 0) parts.push(formatCount(monitors.length, "monitor"));
|
|
74
|
+
if (taskSummary.count > 0) parts.push(formatCount(taskSummary.count, "task"));
|
|
113
75
|
|
|
114
|
-
|
|
76
|
+
let line = parts.join(" · ");
|
|
77
|
+
if (taskSummary.focusText) line += ` | ${taskSummary.focusText}`;
|
|
78
|
+
return [trunc(line)];
|
|
115
79
|
}
|
|
116
80
|
|
|
117
81
|
dispose() {
|
|
@@ -122,13 +86,6 @@ export class LoopWidget {
|
|
|
122
86
|
}
|
|
123
87
|
}
|
|
124
88
|
|
|
125
|
-
function
|
|
126
|
-
|
|
127
|
-
if (totalSec < 60) return `${totalSec}s`;
|
|
128
|
-
const min = Math.floor(totalSec / 60);
|
|
129
|
-
const sec = totalSec % 60;
|
|
130
|
-
if (min < 60) return sec > 0 ? `${min}m ${sec}s` : `${min}m`;
|
|
131
|
-
const hr = Math.floor(min / 60);
|
|
132
|
-
const remMin = min % 60;
|
|
133
|
-
return remMin > 0 ? `${hr}h ${remMin}m` : `${hr}h`;
|
|
89
|
+
function formatCount(count: number, noun: string): string {
|
|
90
|
+
return `${count} ${noun}${count === 1 ? "" : "s"}`;
|
|
134
91
|
}
|