@trevonistrevon/pi-loop 0.2.5 → 0.2.7

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,12 @@ 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
+ pi.events.on("loop:fire", (event) => {
183
+ const data = event;
184
+ if (_latestCtx?.hasPendingMessages()) {
185
+ debug(`loop:fire #${data.loopId} — agent has pending messages, skipping`);
186
+ return;
187
+ }
182
188
  const triggerInfo = typeof data.trigger === "string"
183
189
  ? data.trigger
184
190
  : data.trigger?.type === "cron"
@@ -186,13 +192,13 @@ export default function (pi) {
186
192
  : data.trigger?.type === "event"
187
193
  ? `event: ${data.trigger.source}`
188
194
  : `hybrid`;
195
+ const loopId = data.loopId || "?";
189
196
  const prompt = data.prompt || "loop fired";
197
+ 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
198
  const message = [
191
- `[pi-loop] Loop #${data.loopId || "?"} fired (${triggerInfo}).`,
199
+ `[pi-loop] Loop #${loopId} fired (${triggerInfo}).${constraint}`,
192
200
  prompt,
193
201
  ].join("\n");
194
- // deliverAs: "followUp" queues the message when the agent is busy;
195
- // it delivers after the current turn finishes.
196
202
  pi.sendUserMessage(message, { deliverAs: "followUp" });
197
203
  });
198
204
  // ──────────────────────────────────────────────────
@@ -230,7 +236,7 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
230
236
  - **prompt**: what to do when the loop fires (e.g., "check if the build passed")
231
237
  - **recurring**: repeat or fire once (default: true)
232
238
  - **autoTask**: if pi-tasks is loaded, auto-create a task on each fire
233
- - **triggerType**: "cron", "event", or "hybrid" (inferred if omitted)`,
239
+ - **readOnly**: restrict the agent to read-only tools when this loop fires (default: false)`,
234
240
  promptGuidelines: [
235
241
  "When the user asks for a loop, repeating task, periodic check, or scheduled reminder, use LoopCreate — not raw Bash for/sleep/while.",
236
242
  "Use LoopCreate for any 'every X seconds/minutes/hours' requests.",
@@ -244,9 +250,10 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
244
250
  autoTask: Type.Optional(Type.Boolean({ description: "Auto-create pi-tasks task on fire", default: false })),
245
251
  triggerType: Type.Optional(Type.String({ description: "cron, event, or hybrid (inferred from trigger string if omitted)", enum: ["cron", "event", "hybrid"] })),
246
252
  debounceMs: Type.Optional(Type.Number({ description: "Debounce for hybrid triggers (default: 30000)", default: 30000 })),
253
+ readOnly: Type.Optional(Type.Boolean({ description: "Restrict the agent to read-only tools when this loop fires (default: false)", default: false })),
247
254
  }),
248
255
  execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
249
- const { trigger: triggerInput, prompt, recurring, autoTask, triggerType, debounceMs } = params;
256
+ const { trigger: triggerInput, prompt, recurring, autoTask, triggerType, debounceMs, readOnly } = params;
250
257
  let trigger;
251
258
  const inferred = triggerType ?? inferTriggerType(triggerInput);
252
259
  if (inferred === "cron") {
@@ -267,10 +274,14 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
267
274
  debounceMs: debounceMs ?? 30000,
268
275
  };
269
276
  }
277
+ const validationError = validateTrigger(trigger);
278
+ if (validationError)
279
+ return Promise.resolve(textResult(validationError));
270
280
  const entry = store.create(trigger, prompt, {
271
281
  recurring: recurring ?? (inferred !== "event"),
272
282
  autoTask,
273
283
  selfPaced: triggerInput === "self-paced" || prompt.toLowerCase().includes("self-paced"),
284
+ readOnly,
274
285
  });
275
286
  triggerSystem.add(entry);
276
287
  widget.update();
@@ -287,6 +298,29 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
287
298
  `ID: ${entry.id} (use LoopDelete to cancel)`));
288
299
  },
289
300
  });
301
+ function validateTrigger(trigger) {
302
+ if (trigger.type === "cron") {
303
+ const parts = trigger.schedule.trim().split(/\s+/);
304
+ if (parts.length !== 5) {
305
+ 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.`;
306
+ }
307
+ }
308
+ else if (trigger.type === "event") {
309
+ if (!trigger.source || trigger.source.trim().length === 0) {
310
+ return `Invalid event trigger. Event source must be non-empty (e.g., "tool_execution_start").`;
311
+ }
312
+ }
313
+ else if (trigger.type === "hybrid") {
314
+ const cronParts = trigger.cron.trim().split(/\s+/);
315
+ if (cronParts.length !== 5) {
316
+ return `Invalid hybrid trigger. Cron part must have 5 fields, got ${cronParts.length}: "${trigger.cron}".`;
317
+ }
318
+ if (!trigger.event.source || trigger.event.source.trim().length === 0) {
319
+ return `Invalid hybrid trigger. Event source must be non-empty (e.g., "tool_execution_start").`;
320
+ }
321
+ }
322
+ return null;
323
+ }
290
324
  function inferTriggerType(input) {
291
325
  if (input.includes("hybrid") || (input.includes("cron") && input.includes("event")))
292
326
  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.7",
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,14 @@ 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
+ pi.events.on("loop:fire", (event: unknown) => {
208
+ const data = event as LoopFireEvent;
209
+
210
+ if (_latestCtx?.hasPendingMessages()) {
211
+ debug(`loop:fire #${data.loopId} — agent has pending messages, skipping`);
212
+ return;
213
+ }
214
+
195
215
  const triggerInfo = typeof data.trigger === "string"
196
216
  ? data.trigger
197
217
  : data.trigger?.type === "cron"
@@ -200,14 +220,14 @@ export default function (pi: ExtensionAPI) {
200
220
  ? `event: ${data.trigger.source}`
201
221
  : `hybrid`;
202
222
 
223
+ const loopId = data.loopId || "?";
203
224
  const prompt = data.prompt || "loop fired";
225
+ 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
226
  const message = [
205
- `[pi-loop] Loop #${data.loopId || "?"} fired (${triggerInfo}).`,
227
+ `[pi-loop] Loop #${loopId} fired (${triggerInfo}).${constraint}`,
206
228
  prompt,
207
229
  ].join("\n");
208
230
 
209
- // deliverAs: "followUp" queues the message when the agent is busy;
210
- // it delivers after the current turn finishes.
211
231
  pi.sendUserMessage(message, { deliverAs: "followUp" });
212
232
  });
213
233
 
@@ -247,7 +267,7 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
247
267
  - **prompt**: what to do when the loop fires (e.g., "check if the build passed")
248
268
  - **recurring**: repeat or fire once (default: true)
249
269
  - **autoTask**: if pi-tasks is loaded, auto-create a task on each fire
250
- - **triggerType**: "cron", "event", or "hybrid" (inferred if omitted)`,
270
+ - **readOnly**: restrict the agent to read-only tools when this loop fires (default: false)`,
251
271
  promptGuidelines: [
252
272
  "When the user asks for a loop, repeating task, periodic check, or scheduled reminder, use LoopCreate — not raw Bash for/sleep/while.",
253
273
  "Use LoopCreate for any 'every X seconds/minutes/hours' requests.",
@@ -261,10 +281,11 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
261
281
  autoTask: Type.Optional(Type.Boolean({ description: "Auto-create pi-tasks task on fire", default: false })),
262
282
  triggerType: Type.Optional(Type.String({ description: "cron, event, or hybrid (inferred from trigger string if omitted)", enum: ["cron", "event", "hybrid"] })),
263
283
  debounceMs: Type.Optional(Type.Number({ description: "Debounce for hybrid triggers (default: 30000)", default: 30000 })),
284
+ readOnly: Type.Optional(Type.Boolean({ description: "Restrict the agent to read-only tools when this loop fires (default: false)", default: false })),
264
285
  }),
265
286
 
266
287
  execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
267
- const { trigger: triggerInput, prompt, recurring, autoTask, triggerType, debounceMs } = params;
288
+ const { trigger: triggerInput, prompt, recurring, autoTask, triggerType, debounceMs, readOnly } = params;
268
289
 
269
290
  let trigger: Trigger;
270
291
  const inferred = triggerType ?? inferTriggerType(triggerInput);
@@ -286,10 +307,14 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
286
307
  };
287
308
  }
288
309
 
310
+ const validationError = validateTrigger(trigger);
311
+ if (validationError) return Promise.resolve(textResult(validationError));
312
+
289
313
  const entry = store.create(trigger, prompt, {
290
314
  recurring: recurring ?? (inferred !== "event"),
291
315
  autoTask,
292
316
  selfPaced: triggerInput === "self-paced" || prompt.toLowerCase().includes("self-paced"),
317
+ readOnly,
293
318
  });
294
319
 
295
320
  triggerSystem.add(entry);
@@ -312,6 +337,28 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
312
337
  },
313
338
  });
314
339
 
340
+ function validateTrigger(trigger: Trigger): string | null {
341
+ if (trigger.type === "cron") {
342
+ const parts = trigger.schedule.trim().split(/\s+/);
343
+ if (parts.length !== 5) {
344
+ 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.`;
345
+ }
346
+ } else if (trigger.type === "event") {
347
+ if (!trigger.source || trigger.source.trim().length === 0) {
348
+ return `Invalid event trigger. Event source must be non-empty (e.g., "tool_execution_start").`;
349
+ }
350
+ } else if (trigger.type === "hybrid") {
351
+ const cronParts = trigger.cron.trim().split(/\s+/);
352
+ if (cronParts.length !== 5) {
353
+ return `Invalid hybrid trigger. Cron part must have 5 fields, got ${cronParts.length}: "${trigger.cron}".`;
354
+ }
355
+ if (!trigger.event.source || trigger.event.source.trim().length === 0) {
356
+ return `Invalid hybrid trigger. Event source must be non-empty (e.g., "tool_execution_start").`;
357
+ }
358
+ }
359
+ return null;
360
+ }
361
+
315
362
  function inferTriggerType(input: string): "cron" | "event" | "hybrid" {
316
363
  if (input.includes("hybrid") || (input.includes("cron") && input.includes("event"))) return "hybrid";
317
364
  if (/^\d+\s*[smhd]$/i.test(input.trim())) return "cron";
@@ -554,8 +601,8 @@ Use MonitorList to find the monitor ID, then stop it with this tool.`,
554
601
  triggerSystem.add(entry);
555
602
  widget.update();
556
603
  ui.notify(`Loop #${entry.id} created: every ${parsed.description} — ${prompt.slice(0, 50)}`, "info");
557
- } catch (err: any) {
558
- ui.notify(err.message, "error");
604
+ } catch (err: unknown) {
605
+ ui.notify((err as Error).message, "error");
559
606
  }
560
607
  return;
561
608
  }
@@ -572,7 +619,7 @@ Use MonitorList to find the monitor ID, then stop it with this tool.`,
572
619
  },
573
620
  });
574
621
 
575
- async function scheduleLoop(ui: any, prompt?: string) {
622
+ async function scheduleLoop(ui: ExtensionUIContext, prompt?: string) {
576
623
  const p = prompt || await ui.input("Prompt (what should the agent check?)");
577
624
  if (!p) return;
578
625
 
@@ -586,12 +633,12 @@ Use MonitorList to find the monitor ID, then stop it with this tool.`,
586
633
  triggerSystem.add(entry);
587
634
  widget.update();
588
635
  ui.notify(`Loop #${entry.id} created: every ${parsed.description}`, "info");
589
- } catch (err: any) {
590
- ui.notify(err.message, "error");
636
+ } catch (err: unknown) {
637
+ ui.notify((err as Error).message, "error");
591
638
  }
592
639
  }
593
640
 
594
- async function eventLoop(ui: any, prompt?: string) {
641
+ async function eventLoop(ui: ExtensionUIContext, prompt?: string) {
595
642
  const p = prompt || await ui.input("Prompt");
596
643
  if (!p) return;
597
644
 
@@ -605,7 +652,7 @@ Use MonitorList to find the monitor ID, then stop it with this tool.`,
605
652
  ui.notify(`Event loop #${entry.id} created: fires on "${source}"`, "info");
606
653
  }
607
654
 
608
- async function viewLoops(ui: any) {
655
+ async function viewLoops(ui: ExtensionUIContext) {
609
656
  const loops = store.list();
610
657
  if (loops.length === 0) {
611
658
  await ui.select("No active loops", ["← Back"]);
@@ -658,7 +705,7 @@ Use MonitorList to find the monitor ID, then stop it with this tool.`,
658
705
  return viewLoops(ui);
659
706
  }
660
707
 
661
- async function settings(ui: any) {
708
+ async function settings(ui: ExtensionUIContext) {
662
709
  const loops = store.list();
663
710
  const active = loops.filter(l => l.status === "active").length;
664
711
  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;