@docyrus/docyrus 0.0.19 → 0.0.21

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.
Files changed (111) hide show
  1. package/agent-loader.js +37 -3
  2. package/agent-loader.js.map +2 -2
  3. package/main.js +498 -93
  4. package/main.js.map +4 -4
  5. package/package.json +14 -4
  6. package/resources/chrome-tools/browser-content.js +103 -0
  7. package/resources/chrome-tools/browser-cookies.js +35 -0
  8. package/resources/chrome-tools/browser-eval.js +53 -0
  9. package/resources/chrome-tools/browser-hn-scraper.js +108 -0
  10. package/resources/chrome-tools/browser-nav.js +44 -0
  11. package/resources/chrome-tools/browser-pick.js +162 -0
  12. package/resources/chrome-tools/browser-screenshot.js +34 -0
  13. package/resources/chrome-tools/browser-start.js +86 -0
  14. package/resources/pi-agent/extensions/answer.ts +532 -0
  15. package/resources/pi-agent/extensions/context.ts +578 -0
  16. package/resources/pi-agent/extensions/control.ts +1779 -0
  17. package/resources/pi-agent/extensions/diff.ts +218 -0
  18. package/resources/pi-agent/extensions/files.ts +199 -0
  19. package/resources/pi-agent/extensions/loop.ts +446 -0
  20. package/resources/pi-agent/extensions/multi-edit.ts +835 -0
  21. package/resources/pi-agent/extensions/notify.ts +88 -0
  22. package/resources/pi-agent/extensions/pi-mcp-adapter/CHANGELOG.md +192 -0
  23. package/resources/pi-agent/extensions/pi-mcp-adapter/LICENSE +21 -0
  24. package/resources/pi-agent/extensions/pi-mcp-adapter/README.md +296 -0
  25. package/resources/pi-agent/extensions/pi-mcp-adapter/app-bridge.bundle.js +67 -0
  26. package/resources/pi-agent/extensions/pi-mcp-adapter/cli.js +108 -0
  27. package/resources/pi-agent/extensions/pi-mcp-adapter/commands.ts +211 -0
  28. package/resources/pi-agent/extensions/pi-mcp-adapter/config.ts +227 -0
  29. package/resources/pi-agent/extensions/pi-mcp-adapter/consent-manager.ts +64 -0
  30. package/resources/pi-agent/extensions/pi-mcp-adapter/direct-tools.ts +301 -0
  31. package/resources/pi-agent/extensions/pi-mcp-adapter/errors.ts +219 -0
  32. package/resources/pi-agent/extensions/pi-mcp-adapter/glimpse-ui.ts +80 -0
  33. package/resources/pi-agent/extensions/pi-mcp-adapter/host-html-template.ts +427 -0
  34. package/resources/pi-agent/extensions/pi-mcp-adapter/index.ts +232 -0
  35. package/resources/pi-agent/extensions/pi-mcp-adapter/init.ts +319 -0
  36. package/resources/pi-agent/extensions/pi-mcp-adapter/lifecycle.ts +93 -0
  37. package/resources/pi-agent/extensions/pi-mcp-adapter/logger.ts +169 -0
  38. package/resources/pi-agent/extensions/pi-mcp-adapter/mcp-panel.ts +713 -0
  39. package/resources/pi-agent/extensions/pi-mcp-adapter/metadata-cache.ts +191 -0
  40. package/resources/pi-agent/extensions/pi-mcp-adapter/npx-resolver.ts +419 -0
  41. package/resources/pi-agent/extensions/pi-mcp-adapter/oauth-handler.ts +56 -0
  42. package/resources/pi-agent/extensions/pi-mcp-adapter/package.json +85 -0
  43. package/resources/pi-agent/extensions/pi-mcp-adapter/paths.ts +29 -0
  44. package/resources/pi-agent/extensions/pi-mcp-adapter/proxy-modes.ts +635 -0
  45. package/resources/pi-agent/extensions/pi-mcp-adapter/resource-tools.ts +17 -0
  46. package/resources/pi-agent/extensions/pi-mcp-adapter/server-manager.ts +330 -0
  47. package/resources/pi-agent/extensions/pi-mcp-adapter/state.ts +41 -0
  48. package/resources/pi-agent/extensions/pi-mcp-adapter/tool-metadata.ts +144 -0
  49. package/resources/pi-agent/extensions/pi-mcp-adapter/tool-registrar.ts +46 -0
  50. package/resources/pi-agent/extensions/pi-mcp-adapter/types.ts +367 -0
  51. package/resources/pi-agent/extensions/pi-mcp-adapter/ui-resource-handler.ts +145 -0
  52. package/resources/pi-agent/extensions/pi-mcp-adapter/ui-server.ts +623 -0
  53. package/resources/pi-agent/extensions/pi-mcp-adapter/ui-session.ts +384 -0
  54. package/resources/pi-agent/extensions/pi-mcp-adapter/ui-stream-types.ts +89 -0
  55. package/resources/pi-agent/extensions/pi-mcp-adapter/utils.ts +75 -0
  56. package/resources/pi-agent/extensions/prompt-editor.ts +1315 -0
  57. package/resources/pi-agent/extensions/prompt-url-widget.ts +158 -0
  58. package/resources/pi-agent/extensions/redraws.ts +24 -0
  59. package/resources/pi-agent/extensions/review.ts +2160 -0
  60. package/resources/pi-agent/extensions/todos.ts +2076 -0
  61. package/resources/pi-agent/extensions/tps.ts +47 -0
  62. package/resources/pi-agent/extensions/whimsical.ts +474 -0
  63. package/resources/pi-agent/prompts/coder-system.md +106 -0
  64. package/resources/pi-agent/skills/changelog-generator/SKILL.md +425 -0
  65. package/resources/pi-agent/skills/docyrus-chrome-devtools-cli/SKILL.md +80 -0
  66. package/resources/pi-agent/skills/docyrus-platform/SKILL.md +71 -0
  67. package/resources/pi-agent/skills/docyrus-platform/references/ai-capabilities.md +43 -0
  68. package/resources/pi-agent/skills/docyrus-platform/references/auth-and-multi-tenancy.md +35 -0
  69. package/resources/pi-agent/skills/docyrus-platform/references/automation-and-workflows.md +30 -0
  70. package/resources/pi-agent/skills/docyrus-platform/references/core-building-blocks.md +53 -0
  71. package/resources/pi-agent/skills/{docyrus-api-dev → docyrus-platform}/references/data-source-query-guide.md +32 -28
  72. package/resources/pi-agent/skills/docyrus-platform/references/developer-tools.md +28 -0
  73. package/resources/pi-agent/skills/docyrus-platform/references/docyrus-cli-usage.md +554 -0
  74. package/resources/pi-agent/skills/{docyrus-api-dev → docyrus-platform}/references/formula-design-guide-llm.md +15 -23
  75. package/resources/pi-agent/skills/docyrus-platform/references/integrations-and-events.md +60 -0
  76. package/resources/pi-agent/skills/docyrus-platform/references/platform-services.md +58 -0
  77. package/resources/pi-agent/skills/docyrus-platform/references/querying-and-data-operations.md +27 -0
  78. package/resources/pi-agent/prompts/coder-append-system.md +0 -19
  79. package/resources/pi-agent/skills/docyrus-ai/SKILL.md +0 -28
  80. package/resources/pi-agent/skills/docyrus-api-dev/SKILL.md +0 -161
  81. package/resources/pi-agent/skills/docyrus-api-dev/references/api-client.md +0 -349
  82. package/resources/pi-agent/skills/docyrus-api-dev/references/authentication.md +0 -238
  83. package/resources/pi-agent/skills/docyrus-api-dev/references/query-and-formulas.md +0 -592
  84. package/resources/pi-agent/skills/docyrus-api-doctor/SKILL.md +0 -70
  85. package/resources/pi-agent/skills/docyrus-api-doctor/references/checklist-details.md +0 -588
  86. package/resources/pi-agent/skills/docyrus-app-dev/SKILL.md +0 -159
  87. package/resources/pi-agent/skills/docyrus-app-dev/references/api-client-and-auth.md +0 -275
  88. package/resources/pi-agent/skills/docyrus-app-dev/references/collections-and-patterns.md +0 -352
  89. package/resources/pi-agent/skills/docyrus-app-dev/references/data-source-query-guide.md +0 -2059
  90. package/resources/pi-agent/skills/docyrus-app-dev/references/formula-design-guide-llm.md +0 -320
  91. package/resources/pi-agent/skills/docyrus-app-dev/references/query-guide.md +0 -525
  92. package/resources/pi-agent/skills/docyrus-app-ui-design/SKILL.md +0 -466
  93. package/resources/pi-agent/skills/docyrus-app-ui-design/references/component-selection-guide.md +0 -602
  94. package/resources/pi-agent/skills/docyrus-app-ui-design/references/icon-usage-guide.md +0 -463
  95. package/resources/pi-agent/skills/docyrus-app-ui-design/references/preferred-components-catalog.md +0 -242
  96. package/resources/pi-agent/skills/docyrus-apps/SKILL.md +0 -54
  97. package/resources/pi-agent/skills/docyrus-architect/SKILL.md +0 -174
  98. package/resources/pi-agent/skills/docyrus-architect/references/custom-query-guide.md +0 -410
  99. package/resources/pi-agent/skills/docyrus-architect/references/data-source-query-guide.md +0 -2059
  100. package/resources/pi-agent/skills/docyrus-architect/references/formula-design-guide-llm.md +0 -320
  101. package/resources/pi-agent/skills/docyrus-architect/references/formula-reference.md +0 -145
  102. package/resources/pi-agent/skills/docyrus-auth/SKILL.md +0 -100
  103. package/resources/pi-agent/skills/docyrus-cli-app/SKILL.md +0 -279
  104. package/resources/pi-agent/skills/docyrus-cli-app/references/cli-manifest.md +0 -532
  105. package/resources/pi-agent/skills/docyrus-cli-app/references/list-query-examples.md +0 -248
  106. package/resources/pi-agent/skills/docyrus-curl/SKILL.md +0 -32
  107. package/resources/pi-agent/skills/docyrus-discover/SKILL.md +0 -63
  108. package/resources/pi-agent/skills/docyrus-ds/SKILL.md +0 -95
  109. package/resources/pi-agent/skills/docyrus-env/SKILL.md +0 -21
  110. package/resources/pi-agent/skills/docyrus-studio/SKILL.md +0 -369
  111. package/resources/pi-agent/skills/docyrus-tui/SKILL.md +0 -15
@@ -0,0 +1,446 @@
1
+ /**
2
+ * Loop Extension
3
+ *
4
+ * Provides a /loop command that starts a follow-up loop with a breakout condition.
5
+ * The loop keeps sending a prompt on turn end until the agent calls the
6
+ * signal_loop_success tool.
7
+ */
8
+
9
+ import { Type } from "@sinclair/typebox";
10
+ import { complete, type Api, type Model, type UserMessage } from "@mariozechner/pi-ai";
11
+ import type { ExtensionAPI, ExtensionContext, SessionSwitchEvent } from "@mariozechner/pi-coding-agent";
12
+ import { compact } from "@mariozechner/pi-coding-agent";
13
+ import { Container, type SelectItem, SelectList, Text } from "@mariozechner/pi-tui";
14
+ import { DynamicBorder } from "@mariozechner/pi-coding-agent";
15
+
16
+ type LoopMode = "tests" | "custom" | "self";
17
+
18
+ type LoopStateData = {
19
+ active: boolean;
20
+ mode?: LoopMode;
21
+ condition?: string;
22
+ prompt?: string;
23
+ summary?: string;
24
+ loopCount?: number;
25
+ };
26
+
27
+ const LOOP_PRESETS = [
28
+ { value: "tests", label: "Until tests pass", description: "" },
29
+ { value: "custom", label: "Until custom condition", description: "" },
30
+ { value: "self", label: "Self driven (agent decides)", description: "" },
31
+ ] as const;
32
+
33
+ const LOOP_STATE_ENTRY = "loop-state";
34
+
35
+ const HAIKU_MODEL_ID = "claude-haiku-4-5";
36
+
37
+ const SUMMARY_SYSTEM_PROMPT = `You summarize loop breakout conditions for a status widget.
38
+ Return a concise phrase (max 6 words) that says when the loop should stop.
39
+ Use plain text only, no quotes, no punctuation, no prefix.
40
+
41
+ Form should be "breaks when ...", "loops until ...", "stops on ...", "runs until ...", or similar.
42
+ Use the best form that makes sense for the loop condition.
43
+ `;
44
+
45
+ function buildPrompt(mode: LoopMode, condition?: string): string {
46
+ switch (mode) {
47
+ case "tests":
48
+ return (
49
+ "Run all tests. If they are passing, call the signal_loop_success tool. " +
50
+ "Otherwise continue until the tests pass."
51
+ );
52
+ case "custom": {
53
+ const customCondition = condition?.trim() || "the custom condition is satisfied";
54
+ return (
55
+ `Continue until the following condition is satisfied: ${customCondition}. ` +
56
+ "When it is satisfied, call the signal_loop_success tool."
57
+ );
58
+ }
59
+ case "self":
60
+ return "Continue until you are done. When finished, call the signal_loop_success tool.";
61
+ }
62
+ }
63
+
64
+ function summarizeCondition(mode: LoopMode, condition?: string): string {
65
+ switch (mode) {
66
+ case "tests":
67
+ return "tests pass";
68
+ case "custom": {
69
+ const summary = condition?.trim() || "custom condition";
70
+ return summary.length > 48 ? `${summary.slice(0, 45)}...` : summary;
71
+ }
72
+ case "self":
73
+ return "done";
74
+ }
75
+ }
76
+
77
+ function getConditionText(mode: LoopMode, condition?: string): string {
78
+ switch (mode) {
79
+ case "tests":
80
+ return "tests pass";
81
+ case "custom":
82
+ return condition?.trim() || "custom condition";
83
+ case "self":
84
+ return "you are done";
85
+ }
86
+ }
87
+
88
+ async function selectSummaryModel(
89
+ ctx: ExtensionContext,
90
+ ): Promise<{ model: Model<Api>; apiKey: string } | null> {
91
+ if (!ctx.model) return null;
92
+
93
+ if (ctx.model.provider === "anthropic") {
94
+ const haikuModel = ctx.modelRegistry.find("anthropic", HAIKU_MODEL_ID);
95
+ if (haikuModel) {
96
+ const apiKey = await ctx.modelRegistry.getApiKey(haikuModel);
97
+ if (apiKey) {
98
+ return { model: haikuModel, apiKey };
99
+ }
100
+ }
101
+ }
102
+
103
+ const apiKey = await ctx.modelRegistry.getApiKey(ctx.model);
104
+ if (!apiKey) return null;
105
+ return { model: ctx.model, apiKey };
106
+ }
107
+
108
+ async function summarizeBreakoutCondition(
109
+ ctx: ExtensionContext,
110
+ mode: LoopMode,
111
+ condition?: string,
112
+ ): Promise<string> {
113
+ const fallback = summarizeCondition(mode, condition);
114
+ const selection = await selectSummaryModel(ctx);
115
+ if (!selection) return fallback;
116
+
117
+ const conditionText = getConditionText(mode, condition);
118
+ const userMessage: UserMessage = {
119
+ role: "user",
120
+ content: [{ type: "text", text: conditionText }],
121
+ timestamp: Date.now(),
122
+ };
123
+
124
+ const response = await complete(
125
+ selection.model,
126
+ { systemPrompt: SUMMARY_SYSTEM_PROMPT, messages: [userMessage] },
127
+ { apiKey: selection.apiKey },
128
+ );
129
+
130
+ if (response.stopReason === "aborted" || response.stopReason === "error") {
131
+ return fallback;
132
+ }
133
+
134
+ const summary = response.content
135
+ .filter((c): c is { type: "text"; text: string } => c.type === "text")
136
+ .map((c) => c.text)
137
+ .join(" ")
138
+ .replace(/\s+/g, " ")
139
+ .trim();
140
+
141
+ if (!summary) return fallback;
142
+ return summary.length > 60 ? `${summary.slice(0, 57)}...` : summary;
143
+ }
144
+
145
+ function getCompactionInstructions(mode: LoopMode, condition?: string): string {
146
+ const conditionText = getConditionText(mode, condition);
147
+ return `Loop active. Breakout condition: ${conditionText}. Preserve this loop state and breakout condition in the summary.`;
148
+ }
149
+
150
+ function updateStatus(ctx: ExtensionContext, state: LoopStateData): void {
151
+ if (!ctx.hasUI) return;
152
+ if (!state.active || !state.mode) {
153
+ ctx.ui.setWidget("loop", undefined);
154
+ return;
155
+ }
156
+ const loopCount = state.loopCount ?? 0;
157
+ const turnText = `(turn ${loopCount})`;
158
+ const summary = state.summary?.trim();
159
+ const text = summary
160
+ ? `Loop active: ${summary} ${turnText}`
161
+ : `Loop active ${turnText}`;
162
+ ctx.ui.setWidget("loop", [ctx.ui.theme.fg("accent", text)]);
163
+ }
164
+
165
+ async function loadState(ctx: ExtensionContext): Promise<LoopStateData> {
166
+ const entries = ctx.sessionManager.getEntries();
167
+ for (let i = entries.length - 1; i >= 0; i--) {
168
+ const entry = entries[i] as { type: string; customType?: string; data?: LoopStateData };
169
+ if (entry.type === "custom" && entry.customType === LOOP_STATE_ENTRY && entry.data) {
170
+ return entry.data;
171
+ }
172
+ }
173
+ return { active: false };
174
+ }
175
+
176
+ export default function loopExtension(pi: ExtensionAPI): void {
177
+ let loopState: LoopStateData = { active: false };
178
+
179
+ function persistState(state: LoopStateData): void {
180
+ pi.appendEntry(LOOP_STATE_ENTRY, state);
181
+ }
182
+
183
+ function setLoopState(state: LoopStateData, ctx: ExtensionContext): void {
184
+ loopState = state;
185
+ persistState(state);
186
+ updateStatus(ctx, state);
187
+ }
188
+
189
+ function clearLoopState(ctx: ExtensionContext): void {
190
+ const cleared: LoopStateData = { active: false };
191
+ loopState = cleared;
192
+ persistState(cleared);
193
+ updateStatus(ctx, cleared);
194
+ }
195
+
196
+ function breakLoop(ctx: ExtensionContext): void {
197
+ clearLoopState(ctx);
198
+ ctx.ui.notify("Loop ended", "info");
199
+ }
200
+
201
+ function wasLastAssistantAborted(messages: Array<{ role?: string; stopReason?: string }>): boolean {
202
+ for (let i = messages.length - 1; i >= 0; i--) {
203
+ const message = messages[i];
204
+ if (message?.role === "assistant") {
205
+ return message.stopReason === "aborted";
206
+ }
207
+ }
208
+ return false;
209
+ }
210
+
211
+ function triggerLoopPrompt(ctx: ExtensionContext): void {
212
+ if (!loopState.active || !loopState.mode || !loopState.prompt) return;
213
+ if (ctx.hasPendingMessages()) return;
214
+
215
+ const loopCount = (loopState.loopCount ?? 0) + 1;
216
+ loopState = { ...loopState, loopCount };
217
+ persistState(loopState);
218
+ updateStatus(ctx, loopState);
219
+
220
+ pi.sendMessage({
221
+ customType: "loop",
222
+ content: loopState.prompt,
223
+ display: true
224
+ }, {
225
+ deliverAs: "followUp",
226
+ triggerTurn: true
227
+ });
228
+ }
229
+
230
+ async function showLoopSelector(ctx: ExtensionContext): Promise<LoopStateData | null> {
231
+ const items: SelectItem[] = LOOP_PRESETS.map((preset) => ({
232
+ value: preset.value,
233
+ label: preset.label,
234
+ description: preset.description,
235
+ }));
236
+
237
+ const selection = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
238
+ const container = new Container();
239
+ container.addChild(new DynamicBorder((str) => theme.fg("accent", str)));
240
+ container.addChild(new Text(theme.fg("accent", theme.bold("Select a loop preset"))));
241
+
242
+ const selectList = new SelectList(items, Math.min(items.length, 10), {
243
+ selectedPrefix: (text) => theme.fg("accent", text),
244
+ selectedText: (text) => theme.fg("accent", text),
245
+ description: (text) => theme.fg("muted", text),
246
+ scrollInfo: (text) => theme.fg("dim", text),
247
+ noMatch: (text) => theme.fg("warning", text),
248
+ });
249
+
250
+ selectList.onSelect = (item) => done(item.value);
251
+ selectList.onCancel = () => done(null);
252
+
253
+ container.addChild(selectList);
254
+ container.addChild(new Text(theme.fg("dim", "Press enter to confirm or esc to cancel")));
255
+ container.addChild(new DynamicBorder((str) => theme.fg("accent", str)));
256
+
257
+ return {
258
+ render(width: number) {
259
+ return container.render(width);
260
+ },
261
+ invalidate() {
262
+ container.invalidate();
263
+ },
264
+ handleInput(data: string) {
265
+ selectList.handleInput(data);
266
+ tui.requestRender();
267
+ },
268
+ };
269
+ });
270
+
271
+ if (!selection) return null;
272
+
273
+ switch (selection) {
274
+ case "tests":
275
+ return { active: true, mode: "tests", prompt: buildPrompt("tests") };
276
+ case "self":
277
+ return { active: true, mode: "self", prompt: buildPrompt("self") };
278
+ case "custom": {
279
+ const condition = await ctx.ui.editor("Enter loop breakout condition:", "");
280
+ if (!condition?.trim()) return null;
281
+ return {
282
+ active: true,
283
+ mode: "custom",
284
+ condition: condition.trim(),
285
+ prompt: buildPrompt("custom", condition.trim()),
286
+ };
287
+ }
288
+ default:
289
+ return null;
290
+ }
291
+ }
292
+
293
+ function parseArgs(args: string | undefined): LoopStateData | null {
294
+ if (!args?.trim()) return null;
295
+ const parts = args.trim().split(/\s+/);
296
+ const mode = parts[0]?.toLowerCase();
297
+
298
+ switch (mode) {
299
+ case "tests":
300
+ return { active: true, mode: "tests", prompt: buildPrompt("tests") };
301
+ case "self":
302
+ return { active: true, mode: "self", prompt: buildPrompt("self") };
303
+ case "custom": {
304
+ const condition = parts.slice(1).join(" ").trim();
305
+ if (!condition) return null;
306
+ return {
307
+ active: true,
308
+ mode: "custom",
309
+ condition,
310
+ prompt: buildPrompt("custom", condition),
311
+ };
312
+ }
313
+ default:
314
+ return null;
315
+ }
316
+ }
317
+
318
+ pi.registerTool({
319
+ name: "signal_loop_success",
320
+ label: "Signal Loop Success",
321
+ description: "Stop the active loop when the breakout condition is satisfied. Only call this tool when explicitly instructed to do so by the user, tool or system prompt.",
322
+ parameters: Type.Object({}),
323
+ async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
324
+ if (!loopState.active) {
325
+ return {
326
+ content: [{ type: "text", text: "No active loop is running." }],
327
+ details: { active: false },
328
+ };
329
+ }
330
+
331
+ clearLoopState(ctx);
332
+
333
+ return {
334
+ content: [{ type: "text", text: "Loop ended." }],
335
+ details: { active: false },
336
+ };
337
+ },
338
+ });
339
+
340
+ pi.registerCommand("loop", {
341
+ description: "Start a follow-up loop until a breakout condition is met",
342
+ handler: async (args, ctx) => {
343
+ let nextState = parseArgs(args);
344
+ if (!nextState) {
345
+ if (!ctx.hasUI) {
346
+ ctx.ui.notify("Usage: /loop tests | /loop custom <condition> | /loop self", "warning");
347
+ return;
348
+ }
349
+ nextState = await showLoopSelector(ctx);
350
+ }
351
+
352
+ if (!nextState) {
353
+ ctx.ui.notify("Loop cancelled", "info");
354
+ return;
355
+ }
356
+
357
+ if (loopState.active) {
358
+ const confirm = ctx.hasUI
359
+ ? await ctx.ui.confirm("Replace active loop?", "A loop is already active. Replace it?")
360
+ : true;
361
+ if (!confirm) {
362
+ ctx.ui.notify("Loop unchanged", "info");
363
+ return;
364
+ }
365
+ }
366
+
367
+ const summarizedState: LoopStateData = { ...nextState, summary: undefined, loopCount: 0 };
368
+ setLoopState(summarizedState, ctx);
369
+ ctx.ui.notify("Loop active", "info");
370
+ triggerLoopPrompt(ctx);
371
+
372
+ const mode = nextState.mode!;
373
+ const condition = nextState.condition;
374
+ void (async () => {
375
+ const summary = await summarizeBreakoutCondition(ctx, mode, condition);
376
+ if (!loopState.active || loopState.mode !== mode || loopState.condition !== condition) return;
377
+ loopState = { ...loopState, summary };
378
+ persistState(loopState);
379
+ updateStatus(ctx, loopState);
380
+ })();
381
+ },
382
+ });
383
+
384
+ pi.on("agent_end", async (event, ctx) => {
385
+ if (!loopState.active) return;
386
+
387
+ if (ctx.hasUI && wasLastAssistantAborted(event.messages)) {
388
+ const confirm = await ctx.ui.confirm(
389
+ "Break active loop?",
390
+ "Operation aborted. Break out of the loop?",
391
+ );
392
+ if (confirm) {
393
+ breakLoop(ctx);
394
+ return;
395
+ }
396
+ }
397
+
398
+ triggerLoopPrompt(ctx);
399
+ });
400
+
401
+ pi.on("session_before_compact", async (event, ctx) => {
402
+ if (!loopState.active || !loopState.mode || !ctx.model) return;
403
+ const apiKey = await ctx.modelRegistry.getApiKey(ctx.model);
404
+ if (!apiKey) return;
405
+
406
+ const instructionParts = [event.customInstructions, getCompactionInstructions(loopState.mode, loopState.condition)]
407
+ .filter(Boolean)
408
+ .join("\n\n");
409
+
410
+ try {
411
+ const compaction = await compact(event.preparation, ctx.model, apiKey, instructionParts, event.signal);
412
+ return { compaction };
413
+ } catch (error) {
414
+ if (ctx.hasUI) {
415
+ const message = error instanceof Error ? error.message : String(error);
416
+ ctx.ui.notify(`Loop compaction failed: ${message}`, "warning");
417
+ }
418
+ return;
419
+ }
420
+ });
421
+
422
+ async function restoreLoopState(ctx: ExtensionContext): Promise<void> {
423
+ loopState = await loadState(ctx);
424
+ updateStatus(ctx, loopState);
425
+
426
+ if (loopState.active && loopState.mode && !loopState.summary) {
427
+ const mode = loopState.mode;
428
+ const condition = loopState.condition;
429
+ void (async () => {
430
+ const summary = await summarizeBreakoutCondition(ctx, mode, condition);
431
+ if (!loopState.active || loopState.mode !== mode || loopState.condition !== condition) return;
432
+ loopState = { ...loopState, summary };
433
+ persistState(loopState);
434
+ updateStatus(ctx, loopState);
435
+ })();
436
+ }
437
+ }
438
+
439
+ pi.on("session_start", async (_event, ctx) => {
440
+ await restoreLoopState(ctx);
441
+ });
442
+
443
+ pi.on("session_switch", async (_event: SessionSwitchEvent, ctx) => {
444
+ await restoreLoopState(ctx);
445
+ });
446
+ }