@trevonistrevon/pi-loop 0.2.5 → 0.2.6

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/dist/index.js CHANGED
@@ -120,6 +120,7 @@ export default function (pi) {
120
120
  prompt: entry.prompt,
121
121
  trigger: entry.trigger,
122
122
  timestamp: Date.now(),
123
+ readOnly: entry.readOnly,
123
124
  });
124
125
  }
125
126
  // ── Session lifecycle ──
@@ -178,7 +179,9 @@ export default function (pi) {
178
179
  showPersistedLoops(isResume);
179
180
  });
180
181
  // ── Loop fire handler — sends a user message to re-wake the agent ──
181
- pi.events.on("loop:fire", (data) => {
182
+ const pendingFollowUps = new Set();
183
+ pi.events.on("loop:fire", (event) => {
184
+ const data = event;
182
185
  const triggerInfo = typeof data.trigger === "string"
183
186
  ? data.trigger
184
187
  : data.trigger?.type === "cron"
@@ -186,15 +189,23 @@ export default function (pi) {
186
189
  : data.trigger?.type === "event"
187
190
  ? `event: ${data.trigger.source}`
188
191
  : `hybrid`;
192
+ const loopId = data.loopId || "?";
193
+ if (pendingFollowUps.has(loopId)) {
194
+ debug(`loop:fire #${loopId} — follow-up already queued, skipping`);
195
+ return;
196
+ }
197
+ pendingFollowUps.add(loopId);
189
198
  const prompt = data.prompt || "loop fired";
199
+ const constraint = data.readOnly ? "\n\nREAD-ONLY MODE — use only read tools (Read, TaskList, LoopList, MonitorList, LoopCreate, etc.). No file writes, shell execution, or destructive changes." : "";
190
200
  const message = [
191
- `[pi-loop] Loop #${data.loopId || "?"} fired (${triggerInfo}).`,
201
+ `[pi-loop] Loop #${loopId} fired (${triggerInfo}).${constraint}`,
192
202
  prompt,
193
203
  ].join("\n");
194
204
  // deliverAs: "followUp" queues the message when the agent is busy;
195
205
  // it delivers after the current turn finishes.
196
206
  pi.sendUserMessage(message, { deliverAs: "followUp" });
197
207
  });
208
+ pi.on("turn_end", () => { pendingFollowUps.clear(); });
198
209
  // ──────────────────────────────────────────────────
199
210
  // Tool 1: LoopCreate
200
211
  // ──────────────────────────────────────────────────
@@ -230,7 +241,7 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
230
241
  - **prompt**: what to do when the loop fires (e.g., "check if the build passed")
231
242
  - **recurring**: repeat or fire once (default: true)
232
243
  - **autoTask**: if pi-tasks is loaded, auto-create a task on each fire
233
- - **triggerType**: "cron", "event", or "hybrid" (inferred if omitted)`,
244
+ - **readOnly**: restrict the agent to read-only tools when this loop fires (default: false)`,
234
245
  promptGuidelines: [
235
246
  "When the user asks for a loop, repeating task, periodic check, or scheduled reminder, use LoopCreate — not raw Bash for/sleep/while.",
236
247
  "Use LoopCreate for any 'every X seconds/minutes/hours' requests.",
@@ -244,9 +255,10 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
244
255
  autoTask: Type.Optional(Type.Boolean({ description: "Auto-create pi-tasks task on fire", default: false })),
245
256
  triggerType: Type.Optional(Type.String({ description: "cron, event, or hybrid (inferred from trigger string if omitted)", enum: ["cron", "event", "hybrid"] })),
246
257
  debounceMs: Type.Optional(Type.Number({ description: "Debounce for hybrid triggers (default: 30000)", default: 30000 })),
258
+ readOnly: Type.Optional(Type.Boolean({ description: "Restrict the agent to read-only tools when this loop fires (default: false)", default: false })),
247
259
  }),
248
260
  execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
249
- const { trigger: triggerInput, prompt, recurring, autoTask, triggerType, debounceMs } = params;
261
+ const { trigger: triggerInput, prompt, recurring, autoTask, triggerType, debounceMs, readOnly } = params;
250
262
  let trigger;
251
263
  const inferred = triggerType ?? inferTriggerType(triggerInput);
252
264
  if (inferred === "cron") {
@@ -267,10 +279,14 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
267
279
  debounceMs: debounceMs ?? 30000,
268
280
  };
269
281
  }
282
+ const validationError = validateTrigger(trigger);
283
+ if (validationError)
284
+ return Promise.resolve(textResult(validationError));
270
285
  const entry = store.create(trigger, prompt, {
271
286
  recurring: recurring ?? (inferred !== "event"),
272
287
  autoTask,
273
288
  selfPaced: triggerInput === "self-paced" || prompt.toLowerCase().includes("self-paced"),
289
+ readOnly,
274
290
  });
275
291
  triggerSystem.add(entry);
276
292
  widget.update();
@@ -287,6 +303,29 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
287
303
  `ID: ${entry.id} (use LoopDelete to cancel)`));
288
304
  },
289
305
  });
306
+ function validateTrigger(trigger) {
307
+ if (trigger.type === "cron") {
308
+ const parts = trigger.schedule.trim().split(/\s+/);
309
+ if (parts.length !== 5) {
310
+ return `Invalid cron trigger. Expected 5 fields, got ${parts.length}: "${trigger.schedule}". Use formats like "5m", "1h", "0 9 * * 1-5", or set triggerType to "event" for event sources.`;
311
+ }
312
+ }
313
+ else if (trigger.type === "event") {
314
+ if (!trigger.source || trigger.source.trim().length === 0) {
315
+ return `Invalid event trigger. Event source must be non-empty (e.g., "tool_execution_start").`;
316
+ }
317
+ }
318
+ else if (trigger.type === "hybrid") {
319
+ const cronParts = trigger.cron.trim().split(/\s+/);
320
+ if (cronParts.length !== 5) {
321
+ return `Invalid hybrid trigger. Cron part must have 5 fields, got ${cronParts.length}: "${trigger.cron}".`;
322
+ }
323
+ if (!trigger.event.source || trigger.event.source.trim().length === 0) {
324
+ return `Invalid hybrid trigger. Event source must be non-empty (e.g., "tool_execution_start").`;
325
+ }
326
+ }
327
+ return null;
328
+ }
290
329
  function inferTriggerType(input) {
291
330
  if (input.includes("hybrid") || (input.includes("cron") && input.includes("event")))
292
331
  return "hybrid";
package/dist/store.d.ts CHANGED
@@ -12,6 +12,7 @@ export declare class LoopStore {
12
12
  recurring: boolean;
13
13
  autoTask?: boolean;
14
14
  selfPaced?: boolean;
15
+ readOnly?: boolean;
15
16
  }): LoopEntry;
16
17
  get(id: string): LoopEntry | undefined;
17
18
  list(): LoopEntry[];
package/dist/store.js CHANGED
@@ -16,8 +16,11 @@ function acquireLock(lockPath) {
16
16
  if (e.code === "EEXIST") {
17
17
  try {
18
18
  const pid = parseInt(readFileSync(lockPath, "utf-8"), 10);
19
- if (pid && !isProcessRunning(pid)) {
20
- unlinkSync(lockPath);
19
+ if (!pid || !isProcessRunning(pid)) {
20
+ try {
21
+ unlinkSync(lockPath);
22
+ }
23
+ catch { /* ignore */ }
21
24
  continue;
22
25
  }
23
26
  }
@@ -115,6 +118,7 @@ export class LoopStore {
115
118
  recurring: opts.recurring,
116
119
  autoTask: opts.autoTask,
117
120
  selfPaced: opts.selfPaced,
121
+ readOnly: opts.readOnly,
118
122
  createdAt: now,
119
123
  updatedAt: now,
120
124
  expiresAt: now + MAX_EXPIRY_DAYS * 24 * 60 * 60 * 1000,
package/dist/types.d.ts CHANGED
@@ -29,6 +29,7 @@ export interface LoopEntry {
29
29
  expiresAt: number;
30
30
  autoTask?: boolean;
31
31
  selfPaced?: boolean;
32
+ readOnly?: boolean;
32
33
  }
33
34
  export interface LoopStoreData {
34
35
  nextId: number;
@@ -1,15 +1,7 @@
1
+ import type { ExtensionUIContext } from "@earendil-works/pi-coding-agent";
1
2
  import type { MonitorManager } from "../monitor-manager.js";
2
3
  import type { CronScheduler } from "../scheduler.js";
3
4
  import type { LoopStore } from "../store.js";
4
- export type UICtx = {
5
- setStatus(key: string, text: string | undefined): void;
6
- setWidget(key: string, content: undefined | ((tui: any, theme: any) => {
7
- render(): string[];
8
- invalidate(): void;
9
- }), options?: {
10
- placement?: "aboveEditor" | "belowEditor";
11
- }): void;
12
- };
13
5
  export declare class LoopWidget {
14
6
  private store;
15
7
  private scheduler;
@@ -19,7 +11,7 @@ export declare class LoopWidget {
19
11
  private widgetRegistered;
20
12
  private interval;
21
13
  constructor(store: LoopStore, scheduler: CronScheduler | undefined, monitorManager: MonitorManager);
22
- setUICtx(ctx: UICtx): void;
14
+ setUICtx(ctx: ExtensionUIContext): void;
23
15
  setStore(store: LoopStore): void;
24
16
  setScheduler(scheduler: CronScheduler): void;
25
17
  update(): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trevonistrevon/pi-loop",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "description": "A pi extension for cron/event-based agent re-wake loops and background process monitoring.",
5
5
  "author": "trevonistrevon",
6
6
  "license": "MIT",
package/src/index.ts CHANGED
@@ -16,7 +16,7 @@
16
16
 
17
17
  import { randomUUID } from "node:crypto";
18
18
  import { join, resolve } from "node:path";
19
- import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
19
+ import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext, ExtensionUIContext } from "@earendil-works/pi-coding-agent";
20
20
  import { Type } from "typebox";
21
21
  import { parseInterval } from "./loop-parse.js";
22
22
  import { MonitorManager } from "./monitor-manager.js";
@@ -24,7 +24,7 @@ import { CronScheduler } from "./scheduler.js";
24
24
  import { LoopStore } from "./store.js";
25
25
  import { TriggerSystem } from "./trigger-system.js";
26
26
  import type { LoopEntry, Trigger } from "./types.js";
27
- import { LoopWidget, type UICtx } from "./ui/widget.js";
27
+ import { LoopWidget } from "./ui/widget.js";
28
28
 
29
29
  const DEBUG = !!process.env.PI_LOOP_DEBUG;
30
30
  function debug(...args: unknown[]) {
@@ -35,6 +35,18 @@ function textResult(msg: string) {
35
35
  return { content: [{ type: "text" as const, text: msg }], details: undefined as any };
36
36
  }
37
37
 
38
+ interface LoopFireEvent {
39
+ loopId: string;
40
+ prompt: string;
41
+ trigger: Trigger | string;
42
+ timestamp: number;
43
+ readOnly?: boolean;
44
+ }
45
+
46
+ interface SessionSwitchEvent {
47
+ reason?: string;
48
+ }
49
+
38
50
 
39
51
  export default function (pi: ExtensionAPI) {
40
52
  const piLoopEnv = process.env.PI_LOOP;
@@ -123,6 +135,7 @@ export default function (pi: ExtensionAPI) {
123
135
  prompt: entry.prompt,
124
136
  trigger: entry.trigger,
125
137
  timestamp: Date.now(),
138
+ readOnly: entry.readOnly,
126
139
  });
127
140
  }
128
141
 
@@ -161,20 +174,20 @@ export default function (pi: ExtensionAPI) {
161
174
 
162
175
  pi.on("turn_start", async (_event, ctx) => {
163
176
  _latestCtx = ctx;
164
- widget.setUICtx(ctx.ui as UICtx);
177
+ widget.setUICtx(ctx.ui);
165
178
  upgradeStoreIfNeeded(ctx);
166
179
  });
167
180
 
168
181
  pi.on("before_agent_start", async (_event, ctx) => {
169
182
  _latestCtx = ctx;
170
- widget.setUICtx(ctx.ui as UICtx);
183
+ widget.setUICtx(ctx.ui);
171
184
  upgradeStoreIfNeeded(ctx);
172
185
  showPersistedLoops();
173
186
  });
174
187
 
175
- pi.on("session_switch" as any, async (event: any, ctx: ExtensionContext) => {
188
+ pi.on("session_switch" as any, async (event: SessionSwitchEvent, ctx: ExtensionContext) => {
176
189
  _latestCtx = ctx;
177
- widget.setUICtx(ctx.ui as UICtx);
190
+ widget.setUICtx(ctx.ui);
178
191
  triggerSystem.stop();
179
192
 
180
193
  const isResume = event?.reason === "resume";
@@ -191,7 +204,10 @@ export default function (pi: ExtensionAPI) {
191
204
 
192
205
  // ── Loop fire handler — sends a user message to re-wake the agent ──
193
206
 
194
- pi.events.on("loop:fire", (data: any) => {
207
+ const pendingFollowUps = new Set<string>();
208
+
209
+ pi.events.on("loop:fire", (event: unknown) => {
210
+ const data = event as LoopFireEvent;
195
211
  const triggerInfo = typeof data.trigger === "string"
196
212
  ? data.trigger
197
213
  : data.trigger?.type === "cron"
@@ -200,9 +216,17 @@ export default function (pi: ExtensionAPI) {
200
216
  ? `event: ${data.trigger.source}`
201
217
  : `hybrid`;
202
218
 
219
+ const loopId = data.loopId || "?";
220
+ if (pendingFollowUps.has(loopId)) {
221
+ debug(`loop:fire #${loopId} — follow-up already queued, skipping`);
222
+ return;
223
+ }
224
+ pendingFollowUps.add(loopId);
225
+
203
226
  const prompt = data.prompt || "loop fired";
227
+ const constraint = data.readOnly ? "\n\nREAD-ONLY MODE — use only read tools (Read, TaskList, LoopList, MonitorList, LoopCreate, etc.). No file writes, shell execution, or destructive changes." : "";
204
228
  const message = [
205
- `[pi-loop] Loop #${data.loopId || "?"} fired (${triggerInfo}).`,
229
+ `[pi-loop] Loop #${loopId} fired (${triggerInfo}).${constraint}`,
206
230
  prompt,
207
231
  ].join("\n");
208
232
 
@@ -211,6 +235,8 @@ export default function (pi: ExtensionAPI) {
211
235
  pi.sendUserMessage(message, { deliverAs: "followUp" });
212
236
  });
213
237
 
238
+ pi.on("turn_end", () => { pendingFollowUps.clear(); });
239
+
214
240
  // ──────────────────────────────────────────────────
215
241
  // Tool 1: LoopCreate
216
242
  // ──────────────────────────────────────────────────
@@ -247,7 +273,7 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
247
273
  - **prompt**: what to do when the loop fires (e.g., "check if the build passed")
248
274
  - **recurring**: repeat or fire once (default: true)
249
275
  - **autoTask**: if pi-tasks is loaded, auto-create a task on each fire
250
- - **triggerType**: "cron", "event", or "hybrid" (inferred if omitted)`,
276
+ - **readOnly**: restrict the agent to read-only tools when this loop fires (default: false)`,
251
277
  promptGuidelines: [
252
278
  "When the user asks for a loop, repeating task, periodic check, or scheduled reminder, use LoopCreate — not raw Bash for/sleep/while.",
253
279
  "Use LoopCreate for any 'every X seconds/minutes/hours' requests.",
@@ -261,10 +287,11 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
261
287
  autoTask: Type.Optional(Type.Boolean({ description: "Auto-create pi-tasks task on fire", default: false })),
262
288
  triggerType: Type.Optional(Type.String({ description: "cron, event, or hybrid (inferred from trigger string if omitted)", enum: ["cron", "event", "hybrid"] })),
263
289
  debounceMs: Type.Optional(Type.Number({ description: "Debounce for hybrid triggers (default: 30000)", default: 30000 })),
290
+ readOnly: Type.Optional(Type.Boolean({ description: "Restrict the agent to read-only tools when this loop fires (default: false)", default: false })),
264
291
  }),
265
292
 
266
293
  execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
267
- const { trigger: triggerInput, prompt, recurring, autoTask, triggerType, debounceMs } = params;
294
+ const { trigger: triggerInput, prompt, recurring, autoTask, triggerType, debounceMs, readOnly } = params;
268
295
 
269
296
  let trigger: Trigger;
270
297
  const inferred = triggerType ?? inferTriggerType(triggerInput);
@@ -286,10 +313,14 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
286
313
  };
287
314
  }
288
315
 
316
+ const validationError = validateTrigger(trigger);
317
+ if (validationError) return Promise.resolve(textResult(validationError));
318
+
289
319
  const entry = store.create(trigger, prompt, {
290
320
  recurring: recurring ?? (inferred !== "event"),
291
321
  autoTask,
292
322
  selfPaced: triggerInput === "self-paced" || prompt.toLowerCase().includes("self-paced"),
323
+ readOnly,
293
324
  });
294
325
 
295
326
  triggerSystem.add(entry);
@@ -312,6 +343,28 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
312
343
  },
313
344
  });
314
345
 
346
+ function validateTrigger(trigger: Trigger): string | null {
347
+ if (trigger.type === "cron") {
348
+ const parts = trigger.schedule.trim().split(/\s+/);
349
+ if (parts.length !== 5) {
350
+ return `Invalid cron trigger. Expected 5 fields, got ${parts.length}: "${trigger.schedule}". Use formats like "5m", "1h", "0 9 * * 1-5", or set triggerType to "event" for event sources.`;
351
+ }
352
+ } else if (trigger.type === "event") {
353
+ if (!trigger.source || trigger.source.trim().length === 0) {
354
+ return `Invalid event trigger. Event source must be non-empty (e.g., "tool_execution_start").`;
355
+ }
356
+ } else if (trigger.type === "hybrid") {
357
+ const cronParts = trigger.cron.trim().split(/\s+/);
358
+ if (cronParts.length !== 5) {
359
+ return `Invalid hybrid trigger. Cron part must have 5 fields, got ${cronParts.length}: "${trigger.cron}".`;
360
+ }
361
+ if (!trigger.event.source || trigger.event.source.trim().length === 0) {
362
+ return `Invalid hybrid trigger. Event source must be non-empty (e.g., "tool_execution_start").`;
363
+ }
364
+ }
365
+ return null;
366
+ }
367
+
315
368
  function inferTriggerType(input: string): "cron" | "event" | "hybrid" {
316
369
  if (input.includes("hybrid") || (input.includes("cron") && input.includes("event"))) return "hybrid";
317
370
  if (/^\d+\s*[smhd]$/i.test(input.trim())) return "cron";
@@ -554,8 +607,8 @@ Use MonitorList to find the monitor ID, then stop it with this tool.`,
554
607
  triggerSystem.add(entry);
555
608
  widget.update();
556
609
  ui.notify(`Loop #${entry.id} created: every ${parsed.description} — ${prompt.slice(0, 50)}`, "info");
557
- } catch (err: any) {
558
- ui.notify(err.message, "error");
610
+ } catch (err: unknown) {
611
+ ui.notify((err as Error).message, "error");
559
612
  }
560
613
  return;
561
614
  }
@@ -572,7 +625,7 @@ Use MonitorList to find the monitor ID, then stop it with this tool.`,
572
625
  },
573
626
  });
574
627
 
575
- async function scheduleLoop(ui: any, prompt?: string) {
628
+ async function scheduleLoop(ui: ExtensionUIContext, prompt?: string) {
576
629
  const p = prompt || await ui.input("Prompt (what should the agent check?)");
577
630
  if (!p) return;
578
631
 
@@ -586,12 +639,12 @@ Use MonitorList to find the monitor ID, then stop it with this tool.`,
586
639
  triggerSystem.add(entry);
587
640
  widget.update();
588
641
  ui.notify(`Loop #${entry.id} created: every ${parsed.description}`, "info");
589
- } catch (err: any) {
590
- ui.notify(err.message, "error");
642
+ } catch (err: unknown) {
643
+ ui.notify((err as Error).message, "error");
591
644
  }
592
645
  }
593
646
 
594
- async function eventLoop(ui: any, prompt?: string) {
647
+ async function eventLoop(ui: ExtensionUIContext, prompt?: string) {
595
648
  const p = prompt || await ui.input("Prompt");
596
649
  if (!p) return;
597
650
 
@@ -605,7 +658,7 @@ Use MonitorList to find the monitor ID, then stop it with this tool.`,
605
658
  ui.notify(`Event loop #${entry.id} created: fires on "${source}"`, "info");
606
659
  }
607
660
 
608
- async function viewLoops(ui: any) {
661
+ async function viewLoops(ui: ExtensionUIContext) {
609
662
  const loops = store.list();
610
663
  if (loops.length === 0) {
611
664
  await ui.select("No active loops", ["← Back"]);
@@ -658,7 +711,7 @@ Use MonitorList to find the monitor ID, then stop it with this tool.`,
658
711
  return viewLoops(ui);
659
712
  }
660
713
 
661
- async function settings(ui: any) {
714
+ async function settings(ui: ExtensionUIContext) {
662
715
  const loops = store.list();
663
716
  const active = loops.filter(l => l.status === "active").length;
664
717
  ui.notify(`${active}/${loops.length} active loops (max 25)`, "info");
package/src/store.ts CHANGED
@@ -18,8 +18,8 @@ function acquireLock(lockPath: string): void {
18
18
  if (e.code === "EEXIST") {
19
19
  try {
20
20
  const pid = parseInt(readFileSync(lockPath, "utf-8"), 10);
21
- if (pid && !isProcessRunning(pid)) {
22
- unlinkSync(lockPath);
21
+ if (!pid || !isProcessRunning(pid)) {
22
+ try { unlinkSync(lockPath); } catch { /* ignore */ }
23
23
  continue;
24
24
  }
25
25
  } catch { /* ignore read errors */ }
@@ -95,7 +95,7 @@ export class LoopStore {
95
95
  }
96
96
  }
97
97
 
98
- create(trigger: Trigger, prompt: string, opts: { recurring: boolean; autoTask?: boolean; selfPaced?: boolean }): LoopEntry {
98
+ create(trigger: Trigger, prompt: string, opts: { recurring: boolean; autoTask?: boolean; selfPaced?: boolean; readOnly?: boolean }): LoopEntry {
99
99
  return this.withLock(() => {
100
100
  if (this.loops.size >= MAX_LOOPS) {
101
101
  throw new Error(`Maximum of ${MAX_LOOPS} loops reached. Delete some before creating new ones.`);
@@ -109,6 +109,7 @@ export class LoopStore {
109
109
  recurring: opts.recurring,
110
110
  autoTask: opts.autoTask,
111
111
  selfPaced: opts.selfPaced,
112
+ readOnly: opts.readOnly,
112
113
  createdAt: now,
113
114
  updatedAt: now,
114
115
  expiresAt: now + MAX_EXPIRY_DAYS * 24 * 60 * 60 * 1000,
package/src/types.ts CHANGED
@@ -31,6 +31,7 @@ export interface LoopEntry {
31
31
  expiresAt: number;
32
32
  autoTask?: boolean;
33
33
  selfPaced?: boolean;
34
+ readOnly?: boolean;
34
35
  }
35
36
 
36
37
  export interface LoopStoreData {
package/src/ui/widget.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import type { ExtensionUIContext, Theme } from "@earendil-works/pi-coding-agent";
2
+ import type { Component, TUI } from "@earendil-works/pi-tui";
1
3
  import { truncateToWidth } from "@earendil-works/pi-tui";
2
4
  import type { MonitorManager } from "../monitor-manager.js";
3
5
  import type { CronScheduler } from "../scheduler.js";
@@ -5,18 +7,9 @@ import type { LoopStore } from "../store.js";
5
7
 
6
8
  const MAX_VISIBLE = 6;
7
9
 
8
- export type UICtx = {
9
- setStatus(key: string, text: string | undefined): void;
10
- setWidget(
11
- key: string,
12
- content: undefined | ((tui: any, theme: any) => { render(): string[]; invalidate(): void }),
13
- options?: { placement?: "aboveEditor" | "belowEditor" },
14
- ): void;
15
- };
16
-
17
10
  export class LoopWidget {
18
- private uiCtx: UICtx | undefined;
19
- private tui: any | undefined;
11
+ private uiCtx: ExtensionUIContext | undefined;
12
+ private tui: TUI | undefined;
20
13
  private widgetRegistered = false;
21
14
  private interval: ReturnType<typeof setInterval> | undefined;
22
15
 
@@ -26,7 +19,7 @@ export class LoopWidget {
26
19
  private monitorManager: MonitorManager,
27
20
  ) {}
28
21
 
29
- setUICtx(ctx: UICtx) {
22
+ setUICtx(ctx: ExtensionUIContext) {
30
23
  this.uiCtx = ctx;
31
24
  }
32
25
 
@@ -61,17 +54,17 @@ export class LoopWidget {
61
54
  }
62
55
 
63
56
  if (!this.widgetRegistered) {
64
- this.uiCtx.setWidget("loops", (tui, theme) => {
57
+ this.uiCtx.setWidget("loops", (tui: TUI, theme: Theme) => {
65
58
  this.tui = tui;
66
- return { render: () => this.renderWidget(tui, theme), invalidate: () => {} };
59
+ return { render: () => this.renderWidget(tui, theme), invalidate: () => {} } as Component & { dispose?(): void };
67
60
  }, { placement: "aboveEditor" });
68
61
  this.widgetRegistered = true;
69
62
  } else if (this.tui) {
70
- this.tui.requestRender();
63
+ (this.tui as any).requestRender();
71
64
  }
72
65
  }
73
66
 
74
- private renderWidget(tui: any, _theme: any): string[] {
67
+ private renderWidget(tui: TUI, _theme: Theme): string[] {
75
68
  const loops = this.store.list().filter(l => l.status === "active");
76
69
  const monitors = this.monitorManager.list().filter(m => m.status === "running");
77
70
  const w = tui.terminal.columns;