@dungle-scrubs/tallow 0.8.21 → 0.8.23
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/cli.js +35 -4
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/interactive-mode-patch.d.ts +2 -0
- package/dist/interactive-mode-patch.d.ts.map +1 -1
- package/dist/interactive-mode-patch.js +82 -0
- package/dist/interactive-mode-patch.js.map +1 -1
- package/dist/sdk.d.ts +17 -0
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +68 -1
- package/dist/sdk.js.map +1 -1
- package/dist/workspace-transition-relay.d.ts +40 -7
- package/dist/workspace-transition-relay.d.ts.map +1 -1
- package/dist/workspace-transition-relay.js +81 -16
- package/dist/workspace-transition-relay.js.map +1 -1
- package/extensions/__integration__/background-task-widget-ownership.test.ts +216 -0
- package/extensions/__integration__/claude-hooks-compat.test.ts +156 -0
- package/extensions/__integration__/slash-command-bridge.test.ts +169 -23
- package/extensions/_shared/atomic-write.ts +1 -1
- package/extensions/_shared/bordered-box.ts +102 -0
- package/extensions/_shared/interop-events.ts +5 -0
- package/extensions/_shared/pid-registry.ts +1 -1
- package/extensions/agent-commands-tool/index.ts +4 -1
- package/extensions/background-task-tool/__tests__/lifecycle.test.ts +50 -25
- package/extensions/background-task-tool/index.ts +139 -221
- package/extensions/bash-tool-enhanced/index.ts +1 -75
- package/extensions/cd-tool/index.ts +2 -2
- package/extensions/context-fork/spawn.ts +4 -1
- package/extensions/health/index.ts +6 -6
- package/extensions/hooks/__tests__/claude-compat.test.ts +35 -0
- package/extensions/hooks/__tests__/subprocess-hardening.test.ts +73 -0
- package/extensions/hooks/index.ts +27 -4
- package/extensions/loop/__tests__/loop.test.ts +168 -4
- package/extensions/loop/extension.json +6 -5
- package/extensions/loop/index.ts +242 -31
- package/extensions/plan-mode-tool/__tests__/agent-end-execution.test.ts +373 -0
- package/extensions/plan-mode-tool/index.ts +103 -41
- package/extensions/prompt-suggestions/__tests__/editor-compatibility.test.ts +42 -0
- package/extensions/prompt-suggestions/index.ts +41 -6
- package/extensions/slash-command-bridge/__tests__/slash-command-bridge.test.ts +267 -671
- package/extensions/slash-command-bridge/extension.json +1 -1
- package/extensions/slash-command-bridge/index.ts +230 -116
- package/extensions/subagent-tool/index.ts +2 -2
- package/extensions/subagent-tool/process.ts +4 -5
- package/extensions/tasks/commands/register-tasks-extension.ts +41 -0
- package/extensions/teams-tool/__tests__/peer-messaging.test.ts +29 -24
- package/extensions/teams-tool/dashboard.ts +3 -5
- package/extensions/teams-tool/dispatch/auto-dispatch.ts +18 -1
- package/extensions/teams-tool/tools/teammate-tools.ts +9 -6
- package/extensions/wezterm-pane-control/__tests__/index.test.ts +88 -4
- package/extensions/wezterm-pane-control/index.ts +113 -8
- package/package.json +6 -4
- package/packages/tallow-tui/README.md +51 -0
- package/packages/tallow-tui/dist/autocomplete.d.ts +48 -0
- package/packages/tallow-tui/dist/autocomplete.d.ts.map +1 -0
- package/packages/tallow-tui/dist/autocomplete.js +564 -0
- package/packages/tallow-tui/dist/autocomplete.js.map +1 -0
- package/packages/tallow-tui/dist/border-styles.d.ts +32 -0
- package/packages/tallow-tui/dist/border-styles.d.ts.map +1 -0
- package/packages/tallow-tui/dist/border-styles.js +46 -0
- package/packages/tallow-tui/dist/border-styles.js.map +1 -0
- package/packages/tallow-tui/dist/components/bordered-box.d.ts +52 -0
- package/packages/tallow-tui/dist/components/bordered-box.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/bordered-box.js +89 -0
- package/packages/tallow-tui/dist/components/bordered-box.js.map +1 -0
- package/packages/tallow-tui/dist/components/box.d.ts +22 -0
- package/packages/tallow-tui/dist/components/box.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/box.js +104 -0
- package/packages/tallow-tui/dist/components/box.js.map +1 -0
- package/packages/tallow-tui/dist/components/cancellable-loader.d.ts +22 -0
- package/packages/tallow-tui/dist/components/cancellable-loader.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/cancellable-loader.js +35 -0
- package/packages/tallow-tui/dist/components/cancellable-loader.js.map +1 -0
- package/packages/tallow-tui/dist/components/editor.d.ts +240 -0
- package/packages/tallow-tui/dist/components/editor.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/editor.js +1766 -0
- package/packages/tallow-tui/dist/components/editor.js.map +1 -0
- package/packages/tallow-tui/dist/components/image.d.ts +126 -0
- package/packages/tallow-tui/dist/components/image.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/image.js +245 -0
- package/packages/tallow-tui/dist/components/image.js.map +1 -0
- package/packages/tallow-tui/dist/components/input.d.ts +37 -0
- package/packages/tallow-tui/dist/components/input.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/input.js +439 -0
- package/packages/tallow-tui/dist/components/input.js.map +1 -0
- package/packages/tallow-tui/dist/components/loader.d.ts +88 -0
- package/packages/tallow-tui/dist/components/loader.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/loader.js +146 -0
- package/packages/tallow-tui/dist/components/loader.js.map +1 -0
- package/packages/tallow-tui/dist/components/markdown.d.ts +95 -0
- package/packages/tallow-tui/dist/components/markdown.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/markdown.js +633 -0
- package/packages/tallow-tui/dist/components/markdown.js.map +1 -0
- package/packages/tallow-tui/dist/components/select-list.d.ts +32 -0
- package/packages/tallow-tui/dist/components/select-list.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/select-list.js +156 -0
- package/packages/tallow-tui/dist/components/select-list.js.map +1 -0
- package/packages/tallow-tui/dist/components/settings-list.d.ts +50 -0
- package/packages/tallow-tui/dist/components/settings-list.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/settings-list.js +189 -0
- package/packages/tallow-tui/dist/components/settings-list.js.map +1 -0
- package/packages/tallow-tui/dist/components/spacer.d.ts +12 -0
- package/packages/tallow-tui/dist/components/spacer.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/spacer.js +23 -0
- package/packages/tallow-tui/dist/components/spacer.js.map +1 -0
- package/packages/tallow-tui/dist/components/text.d.ts +19 -0
- package/packages/tallow-tui/dist/components/text.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/text.js +91 -0
- package/packages/tallow-tui/dist/components/text.js.map +1 -0
- package/packages/tallow-tui/dist/components/truncated-text.d.ts +13 -0
- package/packages/tallow-tui/dist/components/truncated-text.d.ts.map +1 -0
- package/packages/tallow-tui/dist/components/truncated-text.js +51 -0
- package/packages/tallow-tui/dist/components/truncated-text.js.map +1 -0
- package/packages/tallow-tui/dist/editor-component.d.ts +50 -0
- package/packages/tallow-tui/dist/editor-component.d.ts.map +1 -0
- package/packages/tallow-tui/dist/editor-component.js +2 -0
- package/packages/tallow-tui/dist/editor-component.js.map +1 -0
- package/packages/tallow-tui/dist/fuzzy.d.ts +16 -0
- package/packages/tallow-tui/dist/fuzzy.d.ts.map +1 -0
- package/packages/tallow-tui/dist/fuzzy.js +107 -0
- package/packages/tallow-tui/dist/fuzzy.js.map +1 -0
- package/packages/tallow-tui/dist/index.d.ts +25 -0
- package/packages/tallow-tui/dist/index.d.ts.map +1 -0
- package/packages/tallow-tui/dist/index.js +35 -0
- package/packages/tallow-tui/dist/index.js.map +1 -0
- package/packages/tallow-tui/dist/keybindings.d.ts +39 -0
- package/packages/tallow-tui/dist/keybindings.d.ts.map +1 -0
- package/packages/tallow-tui/dist/keybindings.js +114 -0
- package/packages/tallow-tui/dist/keybindings.js.map +1 -0
- package/packages/tallow-tui/dist/keys.d.ts +168 -0
- package/packages/tallow-tui/dist/keys.d.ts.map +1 -0
- package/packages/tallow-tui/dist/keys.js +971 -0
- package/packages/tallow-tui/dist/keys.js.map +1 -0
- package/packages/tallow-tui/dist/kill-ring.d.ts +28 -0
- package/packages/tallow-tui/dist/kill-ring.d.ts.map +1 -0
- package/packages/tallow-tui/dist/kill-ring.js +44 -0
- package/packages/tallow-tui/dist/kill-ring.js.map +1 -0
- package/packages/tallow-tui/dist/stdin-buffer.d.ts +48 -0
- package/packages/tallow-tui/dist/stdin-buffer.d.ts.map +1 -0
- package/packages/tallow-tui/dist/stdin-buffer.js +317 -0
- package/packages/tallow-tui/dist/stdin-buffer.js.map +1 -0
- package/packages/tallow-tui/dist/terminal-image.d.ts +161 -0
- package/packages/tallow-tui/dist/terminal-image.d.ts.map +1 -0
- package/packages/tallow-tui/dist/terminal-image.js +460 -0
- package/packages/tallow-tui/dist/terminal-image.js.map +1 -0
- package/packages/tallow-tui/dist/terminal.d.ts +102 -0
- package/packages/tallow-tui/dist/terminal.d.ts.map +1 -0
- package/packages/tallow-tui/dist/terminal.js +263 -0
- package/packages/tallow-tui/dist/terminal.js.map +1 -0
- package/packages/tallow-tui/dist/test-utils/capability-env.d.ts +14 -0
- package/packages/tallow-tui/dist/test-utils/capability-env.d.ts.map +1 -0
- package/packages/tallow-tui/dist/test-utils/capability-env.js +55 -0
- package/packages/tallow-tui/dist/test-utils/capability-env.js.map +1 -0
- package/packages/tallow-tui/dist/tui.d.ts +239 -0
- package/packages/tallow-tui/dist/tui.d.ts.map +1 -0
- package/packages/tallow-tui/dist/tui.js +1058 -0
- package/packages/tallow-tui/dist/tui.js.map +1 -0
- package/packages/tallow-tui/dist/undo-stack.d.ts +17 -0
- package/packages/tallow-tui/dist/undo-stack.d.ts.map +1 -0
- package/packages/tallow-tui/dist/undo-stack.js +25 -0
- package/packages/tallow-tui/dist/undo-stack.js.map +1 -0
- package/packages/tallow-tui/dist/utils.d.ts +96 -0
- package/packages/tallow-tui/dist/utils.d.ts.map +1 -0
- package/packages/tallow-tui/dist/utils.js +843 -0
- package/packages/tallow-tui/dist/utils.js.map +1 -0
- package/packages/tallow-tui/package.json +24 -0
- package/packages/tallow-tui/src/__tests__/__snapshots__/render.test.ts.snap +121 -0
- package/packages/tallow-tui/src/__tests__/editor-border.test.ts +72 -0
- package/packages/tallow-tui/src/__tests__/editor-change-listener.test.ts +121 -0
- package/packages/tallow-tui/src/__tests__/editor-ghost-text.test.ts +112 -0
- package/packages/tallow-tui/src/__tests__/fuzzy.test.ts +91 -0
- package/packages/tallow-tui/src/__tests__/image-component.test.ts +113 -0
- package/packages/tallow-tui/src/__tests__/keys.test.ts +141 -0
- package/packages/tallow-tui/src/__tests__/render.test.ts +179 -0
- package/packages/tallow-tui/src/__tests__/stdin-buffer.test.ts +82 -0
- package/packages/tallow-tui/src/__tests__/terminal-image.test.ts +363 -0
- package/packages/tallow-tui/src/__tests__/tui-diff-regression.test.ts +454 -0
- package/packages/tallow-tui/src/__tests__/tui-render-scheduling.test.ts +256 -0
- package/packages/tallow-tui/src/__tests__/utils.test.ts +259 -0
- package/packages/tallow-tui/src/autocomplete.ts +716 -0
- package/packages/tallow-tui/src/border-styles.ts +60 -0
- package/packages/tallow-tui/src/components/bordered-box.ts +113 -0
- package/packages/tallow-tui/src/components/box.ts +137 -0
- package/packages/tallow-tui/src/components/cancellable-loader.ts +40 -0
- package/packages/tallow-tui/src/components/editor.ts +2143 -0
- package/packages/tallow-tui/src/components/image.ts +315 -0
- package/packages/tallow-tui/src/components/input.ts +522 -0
- package/packages/tallow-tui/src/components/loader.ts +187 -0
- package/packages/tallow-tui/src/components/markdown.ts +780 -0
- package/packages/tallow-tui/src/components/select-list.ts +197 -0
- package/packages/tallow-tui/src/components/settings-list.ts +264 -0
- package/packages/tallow-tui/src/components/spacer.ts +28 -0
- package/packages/tallow-tui/src/components/text.ts +113 -0
- package/packages/tallow-tui/src/components/truncated-text.ts +65 -0
- package/packages/tallow-tui/src/editor-component.ts +92 -0
- package/packages/tallow-tui/src/fuzzy.ts +133 -0
- package/packages/tallow-tui/src/index.ts +118 -0
- package/packages/tallow-tui/src/keybindings.ts +183 -0
- package/packages/tallow-tui/src/keys.ts +1189 -0
- package/packages/tallow-tui/src/kill-ring.ts +46 -0
- package/packages/tallow-tui/src/stdin-buffer.ts +386 -0
- package/packages/tallow-tui/src/terminal-image.ts +619 -0
- package/packages/tallow-tui/src/terminal.ts +350 -0
- package/packages/tallow-tui/src/test-utils/capability-env.ts +56 -0
- package/packages/tallow-tui/src/tui.ts +1336 -0
- package/packages/tallow-tui/src/undo-stack.ts +28 -0
- package/packages/tallow-tui/src/utils.ts +948 -0
- package/packages/tallow-tui/tsconfig.build.json +21 -0
- package/runtime/agent-runner.ts +20 -0
- package/runtime/atomic-write.ts +8 -0
- package/runtime/otel.ts +12 -0
- package/runtime/resolve-module.ts +23 -0
- package/runtime/runtime-path-provider.ts +12 -0
- package/runtime/runtime-provenance.ts +17 -0
- package/runtime/workspace-transition-relay.ts +21 -0
- package/runtime/workspace-transition.ts +29 -0
package/extensions/loop/index.ts
CHANGED
|
@@ -5,14 +5,23 @@
|
|
|
5
5
|
* current session. The interval timer starts after the previous iteration
|
|
6
6
|
* completes (post-completion delay), preventing overlapping runs.
|
|
7
7
|
*
|
|
8
|
+
* Supports optional limits and stop conditions:
|
|
9
|
+
* - `x<N>` — stop after N iterations
|
|
10
|
+
* - `until "<condition>"` — the model evaluates the condition each
|
|
11
|
+
* iteration and calls the `loop_stop` tool when it's met
|
|
12
|
+
*
|
|
8
13
|
* Usage:
|
|
9
14
|
* /loop 5m check the deploy status
|
|
15
|
+
* /loop 1m x10 run the test suite
|
|
16
|
+
* /loop 2m until "build is done" check fuse index progress
|
|
17
|
+
* /loop 1m x100 until "tests pass" run tests
|
|
10
18
|
* /loop 30s /stats
|
|
11
19
|
* /loop stop
|
|
12
20
|
* /loop status
|
|
13
21
|
*/
|
|
14
22
|
|
|
15
23
|
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
24
|
+
import { type Static, Type } from "@sinclair/typebox";
|
|
16
25
|
|
|
17
26
|
// ── Constants ────────────────────────────────────────────────────────────────
|
|
18
27
|
|
|
@@ -28,8 +37,10 @@ const RESET = "\x1b[0m";
|
|
|
28
37
|
|
|
29
38
|
/** Mutable state for an active loop. */
|
|
30
39
|
interface LoopState {
|
|
31
|
-
/** The prompt text
|
|
40
|
+
/** The base prompt text (without condition suffix). */
|
|
32
41
|
prompt: string;
|
|
42
|
+
/** The full prompt sent each iteration (includes condition instruction). */
|
|
43
|
+
fullPrompt: string;
|
|
33
44
|
/** Interval in milliseconds between iterations. */
|
|
34
45
|
intervalMs: number;
|
|
35
46
|
/** Original interval string (e.g. "5m") for display. */
|
|
@@ -44,6 +55,10 @@ interface LoopState {
|
|
|
44
55
|
awaitingCompletion: boolean;
|
|
45
56
|
/** Number of completed iterations. */
|
|
46
57
|
iterationCount: number;
|
|
58
|
+
/** Maximum iterations before auto-stop, or null for unlimited. */
|
|
59
|
+
maxIterations: number | null;
|
|
60
|
+
/** Stop condition for the model to evaluate, or null for none. */
|
|
61
|
+
untilCondition: string | null;
|
|
47
62
|
}
|
|
48
63
|
|
|
49
64
|
// ── Module State ─────────────────────────────────────────────────────────────
|
|
@@ -79,6 +94,86 @@ export function parseInterval(s: string): number | null {
|
|
|
79
94
|
return value * multiplier;
|
|
80
95
|
}
|
|
81
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Parse an iteration count from a string like "x100" or "x5".
|
|
99
|
+
*
|
|
100
|
+
* @param s - Token to parse
|
|
101
|
+
* @returns Positive integer count, or null if not a count token
|
|
102
|
+
*/
|
|
103
|
+
export function parseMaxIterations(s: string): number | null {
|
|
104
|
+
const match = s.match(/^x(\d+)$/);
|
|
105
|
+
if (!match) return null;
|
|
106
|
+
const value = parseInt(match[1], 10);
|
|
107
|
+
return value > 0 ? value : null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Extract an `until "..."` condition from a token array.
|
|
112
|
+
*
|
|
113
|
+
* Looks for the word "until" followed by a quoted string. Supports
|
|
114
|
+
* both single and double quotes. Returns the condition text and the
|
|
115
|
+
* remaining tokens with the `until "..."` portion removed.
|
|
116
|
+
*
|
|
117
|
+
* @param tokens - Array of whitespace-split tokens
|
|
118
|
+
* @returns Object with condition (or null) and remaining tokens
|
|
119
|
+
*/
|
|
120
|
+
export function extractUntilCondition(tokens: string[]): {
|
|
121
|
+
condition: string | null;
|
|
122
|
+
remaining: string[];
|
|
123
|
+
} {
|
|
124
|
+
const untilIdx = tokens.findIndex((t) => t.toLowerCase() === "until");
|
|
125
|
+
if (untilIdx === -1) {
|
|
126
|
+
return { condition: null, remaining: tokens };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Everything after "until" is the condition + prompt
|
|
130
|
+
const afterUntil = tokens.slice(untilIdx + 1);
|
|
131
|
+
const beforeUntil = tokens.slice(0, untilIdx);
|
|
132
|
+
|
|
133
|
+
if (afterUntil.length === 0) {
|
|
134
|
+
return { condition: null, remaining: tokens };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Check if the condition is quoted
|
|
138
|
+
const first = afterUntil[0];
|
|
139
|
+
const quoteChar = first[0] === '"' || first[0] === "'" ? first[0] : null;
|
|
140
|
+
|
|
141
|
+
if (quoteChar) {
|
|
142
|
+
// Find the closing quote
|
|
143
|
+
const conditionTokens: string[] = [];
|
|
144
|
+
let closingIdx = -1;
|
|
145
|
+
|
|
146
|
+
for (let i = 0; i < afterUntil.length; i++) {
|
|
147
|
+
conditionTokens.push(afterUntil[i]);
|
|
148
|
+
if (i > 0 && afterUntil[i].endsWith(quoteChar)) {
|
|
149
|
+
closingIdx = i;
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
if (i === 0 && afterUntil[i].length > 1 && afterUntil[i].endsWith(quoteChar)) {
|
|
153
|
+
closingIdx = i;
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (closingIdx === -1) {
|
|
159
|
+
// No closing quote — treat everything as condition
|
|
160
|
+
const raw = conditionTokens.join(" ");
|
|
161
|
+
const condition = raw.slice(1); // strip opening quote
|
|
162
|
+
return { condition, remaining: beforeUntil };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const raw = conditionTokens.join(" ");
|
|
166
|
+
const condition = raw.slice(1, -1); // strip both quotes
|
|
167
|
+
const afterCondition = afterUntil.slice(closingIdx + 1);
|
|
168
|
+
return { condition, remaining: [...beforeUntil, ...afterCondition] };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// No quotes — single word is the condition
|
|
172
|
+
const condition = first;
|
|
173
|
+
const afterCondition = afterUntil.slice(1);
|
|
174
|
+
return { condition, remaining: [...beforeUntil, ...afterCondition] };
|
|
175
|
+
}
|
|
176
|
+
|
|
82
177
|
// ── Countdown Formatting ─────────────────────────────────────────────────────
|
|
83
178
|
|
|
84
179
|
/**
|
|
@@ -107,11 +202,20 @@ export function formatCountdown(ms: number): string {
|
|
|
107
202
|
export type LoopArgs =
|
|
108
203
|
| { action: "status" }
|
|
109
204
|
| { action: "stop" }
|
|
110
|
-
| {
|
|
205
|
+
| {
|
|
206
|
+
action: "start";
|
|
207
|
+
intervalMs: number;
|
|
208
|
+
intervalLabel: string;
|
|
209
|
+
prompt: string;
|
|
210
|
+
maxIterations: number | null;
|
|
211
|
+
untilCondition: string | null;
|
|
212
|
+
};
|
|
111
213
|
|
|
112
214
|
/**
|
|
113
215
|
* Parse the argument string passed to `/loop`.
|
|
114
216
|
*
|
|
217
|
+
* Syntax: /loop <interval> [x<N>] [until "<condition>"] <prompt...>
|
|
218
|
+
*
|
|
115
219
|
* @param args - Raw argument text (everything after `/loop `)
|
|
116
220
|
* @returns Parsed action with relevant parameters
|
|
117
221
|
*/
|
|
@@ -130,20 +234,10 @@ export function parseLoopArgs(args: string): LoopArgs | { action: "error"; messa
|
|
|
130
234
|
return { action: "status" };
|
|
131
235
|
}
|
|
132
236
|
|
|
133
|
-
|
|
134
|
-
const spaceIdx = trimmed.indexOf(" ");
|
|
135
|
-
if (spaceIdx === -1) {
|
|
136
|
-
// Could be just an interval with no prompt
|
|
137
|
-
const ms = parseInterval(trimmed);
|
|
138
|
-
if (ms !== null) {
|
|
139
|
-
return { action: "error", message: "Missing prompt. Usage: /loop 5m <prompt>" };
|
|
140
|
-
}
|
|
141
|
-
return { action: "error", message: `Invalid interval "${trimmed}". Use format: 30s, 5m, 1h` };
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const intervalStr = trimmed.slice(0, spaceIdx);
|
|
145
|
-
const prompt = trimmed.slice(spaceIdx + 1).trim();
|
|
237
|
+
const tokens = trimmed.split(/\s+/);
|
|
146
238
|
|
|
239
|
+
// First token must be the interval
|
|
240
|
+
const intervalStr = tokens[0];
|
|
147
241
|
const ms = parseInterval(intervalStr);
|
|
148
242
|
if (ms === null) {
|
|
149
243
|
return {
|
|
@@ -152,15 +246,56 @@ export function parseLoopArgs(args: string): LoopArgs | { action: "error"; messa
|
|
|
152
246
|
};
|
|
153
247
|
}
|
|
154
248
|
|
|
249
|
+
let rest = tokens.slice(1);
|
|
250
|
+
if (rest.length === 0) {
|
|
251
|
+
return { action: "error", message: "Missing prompt. Usage: /loop 5m <prompt>" };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Check for x<N> max iterations (can appear anywhere before the prompt)
|
|
255
|
+
let maxIterations: number | null = null;
|
|
256
|
+
const maxIdx = rest.findIndex((t) => parseMaxIterations(t) !== null);
|
|
257
|
+
if (maxIdx !== -1) {
|
|
258
|
+
maxIterations = parseMaxIterations(rest[maxIdx]);
|
|
259
|
+
rest = [...rest.slice(0, maxIdx), ...rest.slice(maxIdx + 1)];
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Check for until "condition"
|
|
263
|
+
const { condition, remaining } = extractUntilCondition(rest);
|
|
264
|
+
|
|
265
|
+
const prompt = remaining.join(" ").trim();
|
|
155
266
|
if (!prompt) {
|
|
156
267
|
return { action: "error", message: "Missing prompt. Usage: /loop 5m <prompt>" };
|
|
157
268
|
}
|
|
158
269
|
|
|
159
|
-
return {
|
|
270
|
+
return {
|
|
271
|
+
action: "start",
|
|
272
|
+
intervalMs: ms,
|
|
273
|
+
intervalLabel: intervalStr,
|
|
274
|
+
prompt,
|
|
275
|
+
maxIterations,
|
|
276
|
+
untilCondition: condition,
|
|
277
|
+
};
|
|
160
278
|
}
|
|
161
279
|
|
|
162
280
|
// ── Loop Lifecycle ───────────────────────────────────────────────────────────
|
|
163
281
|
|
|
282
|
+
/**
|
|
283
|
+
* Build a display label summarizing the loop configuration.
|
|
284
|
+
*
|
|
285
|
+
* @param loop - Active loop state
|
|
286
|
+
* @returns Human-readable summary like "every 5m x10 until 'build done'"
|
|
287
|
+
*/
|
|
288
|
+
function buildLabel(loop: LoopState): string {
|
|
289
|
+
let label = `every ${loop.intervalLabel}`;
|
|
290
|
+
if (loop.maxIterations !== null) {
|
|
291
|
+
label += ` x${loop.maxIterations}`;
|
|
292
|
+
}
|
|
293
|
+
if (loop.untilCondition) {
|
|
294
|
+
label += ` until "${loop.untilCondition}"`;
|
|
295
|
+
}
|
|
296
|
+
return label;
|
|
297
|
+
}
|
|
298
|
+
|
|
164
299
|
/**
|
|
165
300
|
* Update the status bar with the current loop state.
|
|
166
301
|
*
|
|
@@ -178,9 +313,17 @@ function updateStatus(ctx: ExtensionContext): void {
|
|
|
178
313
|
const promptPreview =
|
|
179
314
|
activeLoop.prompt.length > 30 ? `${activeLoop.prompt.slice(0, 27)}...` : activeLoop.prompt;
|
|
180
315
|
|
|
316
|
+
const iterInfo =
|
|
317
|
+
activeLoop.maxIterations !== null
|
|
318
|
+
? ` ${activeLoop.iterationCount}/${activeLoop.maxIterations}`
|
|
319
|
+
: ` #${activeLoop.iterationCount}`;
|
|
320
|
+
|
|
181
321
|
if (activeLoop.awaitingCompletion) {
|
|
182
322
|
const iter = activeLoop.iterationCount + 1;
|
|
183
|
-
ctx.ui.setStatus(
|
|
323
|
+
ctx.ui.setStatus(
|
|
324
|
+
STATUS_SLOT,
|
|
325
|
+
`${FG_CYAN}🔄 running: "${promptPreview}" (#${iter}${activeLoop.maxIterations ? `/${activeLoop.maxIterations}` : ""})${RESET}`
|
|
326
|
+
);
|
|
184
327
|
return;
|
|
185
328
|
}
|
|
186
329
|
|
|
@@ -188,7 +331,7 @@ function updateStatus(ctx: ExtensionContext): void {
|
|
|
188
331
|
const countdown = formatCountdown(remaining);
|
|
189
332
|
ctx.ui.setStatus(
|
|
190
333
|
STATUS_SLOT,
|
|
191
|
-
`${FG_CYAN}🔄 ${activeLoop.intervalLabel}: ${FG_DIM}"${promptPreview}"${RESET}${FG_CYAN} (next in ${countdown})${RESET}`
|
|
334
|
+
`${FG_CYAN}🔄 ${activeLoop.intervalLabel}${iterInfo}: ${FG_DIM}"${promptPreview}"${RESET}${FG_CYAN} (next in ${countdown})${RESET}`
|
|
192
335
|
);
|
|
193
336
|
}
|
|
194
337
|
|
|
@@ -254,9 +397,12 @@ function scheduleNext(pi: ExtensionAPI, ctx: ExtensionContext): void {
|
|
|
254
397
|
|
|
255
398
|
updateStatus(ctx);
|
|
256
399
|
|
|
257
|
-
|
|
400
|
+
const iterLabel = activeLoop.maxIterations
|
|
401
|
+
? `#${activeLoop.iterationCount + 1}/${activeLoop.maxIterations}`
|
|
402
|
+
: `#${activeLoop.iterationCount + 1}`;
|
|
403
|
+
ctx.ui.notify(`Loop iteration ${iterLabel}: ${activeLoop.prompt}`, "info");
|
|
258
404
|
|
|
259
|
-
pi.sendUserMessage(activeLoop.
|
|
405
|
+
pi.sendUserMessage(activeLoop.fullPrompt, { deliverAs: "followUp" });
|
|
260
406
|
}, activeLoop.intervalMs);
|
|
261
407
|
}
|
|
262
408
|
|
|
@@ -278,19 +424,52 @@ function stopLoop(ctx: ExtensionContext, reason: string = "Loop stopped"): void
|
|
|
278
424
|
|
|
279
425
|
// ── Extension Entry Point ────────────────────────────────────────────────────
|
|
280
426
|
|
|
427
|
+
/** TypeBox schema for the loop_stop tool parameters. */
|
|
428
|
+
const LoopStopParams = Type.Object({
|
|
429
|
+
reason: Type.String({ description: "Why the stop condition was met" }),
|
|
430
|
+
});
|
|
431
|
+
|
|
281
432
|
/**
|
|
282
433
|
* Loop extension factory.
|
|
283
434
|
*
|
|
284
|
-
* Registers the `/loop` command, `agent_end` handler
|
|
285
|
-
* scheduling, and cleanup handlers for session lifecycle
|
|
435
|
+
* Registers the `/loop` command, `loop_stop` tool, `agent_end` handler
|
|
436
|
+
* for iteration scheduling, and cleanup handlers for session lifecycle.
|
|
286
437
|
*
|
|
287
438
|
* @param pi - Extension API
|
|
288
439
|
*/
|
|
289
440
|
export default function loopExtension(pi: ExtensionAPI): void {
|
|
441
|
+
// ── loop_stop tool — lets the model stop the loop when a condition is met
|
|
442
|
+
|
|
443
|
+
pi.registerTool({
|
|
444
|
+
name: "loop_stop",
|
|
445
|
+
label: "Stop Loop",
|
|
446
|
+
description:
|
|
447
|
+
"Stop the active /loop because its stop condition has been met. " +
|
|
448
|
+
"Only call this tool when a /loop is running with an `until` condition " +
|
|
449
|
+
"and you have determined the condition is satisfied.",
|
|
450
|
+
parameters: LoopStopParams,
|
|
451
|
+
async execute(_toolCallId, params: Static<typeof LoopStopParams>, _signal, _onUpdate, ctx) {
|
|
452
|
+
if (!activeLoop) {
|
|
453
|
+
return {
|
|
454
|
+
content: [{ type: "text" as const, text: "No active loop to stop." }],
|
|
455
|
+
details: undefined,
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
const reason = `Loop stopped: condition met — ${params.reason}`;
|
|
459
|
+
stopLoop(ctx, reason);
|
|
460
|
+
return {
|
|
461
|
+
content: [{ type: "text" as const, text: `Loop stopped. Reason: ${params.reason}` }],
|
|
462
|
+
details: undefined,
|
|
463
|
+
};
|
|
464
|
+
},
|
|
465
|
+
});
|
|
466
|
+
|
|
290
467
|
// ── /loop command ────────────────────────────────────────────────────
|
|
291
468
|
|
|
292
469
|
pi.registerCommand("loop", {
|
|
293
|
-
description:
|
|
470
|
+
description:
|
|
471
|
+
"Run a prompt on a recurring interval. " +
|
|
472
|
+
'Syntax: /loop <interval> [x<N>] [until "<condition>"] <prompt>',
|
|
294
473
|
handler: async (args, ctx) => {
|
|
295
474
|
const parsed = parseLoopArgs(args);
|
|
296
475
|
|
|
@@ -309,7 +488,10 @@ export default function loopExtension(pi: ExtensionAPI): void {
|
|
|
309
488
|
|
|
310
489
|
case "status":
|
|
311
490
|
if (!activeLoop) {
|
|
312
|
-
ctx.ui.notify(
|
|
491
|
+
ctx.ui.notify(
|
|
492
|
+
'No active loop. Usage: /loop 5m <prompt>\n Options: x<N> (max iterations), until "<condition>" (auto-stop)',
|
|
493
|
+
"info"
|
|
494
|
+
);
|
|
313
495
|
return;
|
|
314
496
|
}
|
|
315
497
|
{
|
|
@@ -317,10 +499,11 @@ export default function loopExtension(pi: ExtensionAPI): void {
|
|
|
317
499
|
const state = activeLoop.awaitingCompletion
|
|
318
500
|
? `running iteration #${activeLoop.iterationCount + 1}`
|
|
319
501
|
: `next in ${formatCountdown(remaining)}`;
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
502
|
+
let info = `Loop: ${buildLabel(activeLoop)} → "${activeLoop.prompt}" | ${state} | ${activeLoop.iterationCount} completed`;
|
|
503
|
+
if (activeLoop.maxIterations) {
|
|
504
|
+
info += ` of ${activeLoop.maxIterations}`;
|
|
505
|
+
}
|
|
506
|
+
ctx.ui.notify(info, "info");
|
|
324
507
|
}
|
|
325
508
|
return;
|
|
326
509
|
|
|
@@ -329,13 +512,24 @@ export default function loopExtension(pi: ExtensionAPI): void {
|
|
|
329
512
|
if (activeLoop) {
|
|
330
513
|
clearTimers();
|
|
331
514
|
ctx.ui.notify(
|
|
332
|
-
`Replacing active loop (was: ${activeLoop
|
|
515
|
+
`Replacing active loop (was: ${buildLabel(activeLoop)} → "${activeLoop.prompt}")`,
|
|
333
516
|
"info"
|
|
334
517
|
);
|
|
335
518
|
}
|
|
336
519
|
|
|
520
|
+
// Build the full prompt with condition instruction
|
|
521
|
+
let fullPrompt = parsed.prompt;
|
|
522
|
+
if (parsed.untilCondition) {
|
|
523
|
+
fullPrompt +=
|
|
524
|
+
`\n\n---\nLoop stop condition: "${parsed.untilCondition}"\n` +
|
|
525
|
+
`After completing the task above, evaluate whether this condition is now met. ` +
|
|
526
|
+
`If it IS met, call the loop_stop tool with the reason. ` +
|
|
527
|
+
`If it is NOT yet met, do nothing — the loop will continue automatically.`;
|
|
528
|
+
}
|
|
529
|
+
|
|
337
530
|
activeLoop = {
|
|
338
531
|
prompt: parsed.prompt,
|
|
532
|
+
fullPrompt,
|
|
339
533
|
intervalMs: parsed.intervalMs,
|
|
340
534
|
intervalLabel: parsed.intervalLabel,
|
|
341
535
|
timer: null,
|
|
@@ -343,9 +537,11 @@ export default function loopExtension(pi: ExtensionAPI): void {
|
|
|
343
537
|
nextRunAt: 0,
|
|
344
538
|
awaitingCompletion: false,
|
|
345
539
|
iterationCount: 0,
|
|
540
|
+
maxIterations: parsed.maxIterations,
|
|
541
|
+
untilCondition: parsed.untilCondition,
|
|
346
542
|
};
|
|
347
543
|
|
|
348
|
-
ctx.ui.notify(`Loop started:
|
|
544
|
+
ctx.ui.notify(`Loop started: ${buildLabel(activeLoop)} → "${parsed.prompt}"`, "info");
|
|
349
545
|
|
|
350
546
|
scheduleNext(pi, ctx);
|
|
351
547
|
return;
|
|
@@ -366,7 +562,22 @@ export default function loopExtension(pi: ExtensionAPI): void {
|
|
|
366
562
|
activeLoop.awaitingCompletion = false;
|
|
367
563
|
activeLoop.iterationCount++;
|
|
368
564
|
|
|
369
|
-
|
|
565
|
+
// Check max iterations limit
|
|
566
|
+
if (
|
|
567
|
+
activeLoop.maxIterations !== null &&
|
|
568
|
+
activeLoop.iterationCount >= activeLoop.maxIterations
|
|
569
|
+
) {
|
|
570
|
+
stopLoop(
|
|
571
|
+
ctx,
|
|
572
|
+
`Loop complete: ${activeLoop.iterationCount}/${activeLoop.maxIterations} iterations`
|
|
573
|
+
);
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const iterLabel = activeLoop.maxIterations
|
|
578
|
+
? `${activeLoop.iterationCount}/${activeLoop.maxIterations}`
|
|
579
|
+
: `${activeLoop.iterationCount}`;
|
|
580
|
+
ctx.ui.notify(`Loop iteration ${iterLabel} complete`, "info");
|
|
370
581
|
|
|
371
582
|
scheduleNext(pi, ctx);
|
|
372
583
|
});
|