diff --git a/packages/pi-turn-limit/turn-limit-widget-plan.md b/packages/pi-turn-limit/turn-limit-widget-plan.md new file mode 100644 index 0000000..edf7a0b --- /dev/null +++ b/packages/pi-turn-limit/turn-limit-widget-plan.md @@ -0,0 +1,152 @@ +# Turn Limit Extension — Widget & Confirmation Plan + +## Goal + +Extend the `pi-turn-limit` extension with: +1. A `/turn-limit ` command to change the max turns mid-session +2. A live widget showing `Turns: {current}/{max}` +3. A yes/no confirmation dialog when the turn limit boundary is reached (instead of immediate abort) + +--- + +## Files to Change + +| File | Change | +|------|--------| +| `packages/pi-turn-limit/src/turn-limit.ts` | **Only file to modify.** Add command, widget, and confirmation logic. | + +No other files need changes. No new files, no dependency updates, no config changes. + +--- + +## Design Details + +### Current behavior (preserve these) + +- `DEFAULT_MAX_TURNS = 25` +- `getMaxTurns()` reads `PI_MAX_TURNS` env var, falls back to default +- `checkTurnLimit()` pure function — **do not modify** +- `agent_start` resets `turnCount = 0` +- `turn_start` increments counter, checks limit, aborts if exceeded +- `ctx.hasUI` guard before UI calls + +### New behavior + +#### A. `/turn-limit ` command + +Register via `pi.registerCommand()`. + +- **Name:** `turn-limit` +- **Description:** `Set the maximum number of agent turns for this session` +- **Handler:** + - Parse `args` as integer + - If invalid (not a positive integer): show error via `ctx.ui.notify("Invalid turn limit. Must be a positive integer.", "error")` and return + - Update the in-memory `maxTurns` variable + - Call `ctx.ui.setWidget()` to update the widget display immediately + - Optionally notify: `ctx.ui.notify("Turn limit set to {N}.", "info")` + +#### B. Live widget + +- **Widget name:** `"turn-limit"` (unique string identifier) +- **Content:** `["Turns: {current}/{max}"]` — single line array +- **Placement:** default (above editor) +- **Update timing:** + - Set on every `turn_start` (after incrementing turnCount) + - Clear on `agent_end` (new user prompt starts) +- **Example output:** `Turns: 12/25` + +#### C. Confirmation dialog at boundary + +- **Trigger condition:** `turnIndex === maxTurns` (the boundary turn, NOT `> maxTurns`) +- **Guard:** Only if `ctx.hasUI` is true (print/JSON mode aborts silently) +- **Dialog:** + ``` + Title: "Turn limit reached" + Message: `You've used ${maxTurns} turns. Continue?` + ``` +- **On "Yes" (confirmed):** + - Reset `turnCount = 0` + - Do NOT abort — let the turn proceed +- **On "No" (not confirmed):** + - Call `ctx.ui.notify("Agent aborted by user.", "error")` + - Call `ctx.abort()` +- **After confirmation:** Update the widget with the reset counter (`Turns: 0/{max}`) + +#### D. Exit condition + +The agent still aborts when the user has said "No" to every boundary confirmation. Since the counter resets on "Yes", the user can re-approve multiple times. The only way out is "No" or `PI_MAX_TURNS = 0` (edge case). + +--- + +## Implementation Checklist + +- [ ] **Step 1:** Add `/turn-limit` command registration + - Use `pi.registerCommand("turn-limit", { description, handler })` + - Handler parses args, validates positive integer + - Updates `maxTurns` in-memory variable + - Updates widget via `ctx.ui.setWidget("turn-limit", ["Turns: {current}/{max}"])` + - Shows notify on success or error + +- [ ] **Step 2:** Add live widget updates + - In `turn_start` handler, after incrementing `turnCount`, call `ctx.ui.setWidget("turn-limit", ["Turns: ${turnCount}/${maxTurns}"])` + - In `agent_end` handler, call `ctx.ui.setWidget("turn-limit", undefined)` to clear + +- [ ] **Step 3:** Add boundary confirmation logic + - In `turn_start` handler, after incrementing, check `if (event.turnIndex === maxTurns)` + - If `ctx.hasUI`: + - Show confirmation dialog via `ctx.ui.confirm("Turn limit reached", \`You've used ${maxTurns} turns. Continue?\`)` + - If confirmed: reset `turnCount = 0`, update widget, continue (do not abort) + - If not confirmed: notify user, call `ctx.abort()` + - If `!ctx.hasUI`: abort silently (same as current behavior) + +- [ ] **Step 4:** Verify existing behavior is preserved + - `checkTurnLimit()` function unchanged + - `getMaxTurns()` function unchanged + - `agent_start` still resets counter + - `DEFAULT_MAX_TURNS` still 25 + - Env var `PI_MAX_TURNS` still respected + +--- + +## Code Structure (target file) + +``` +packages/pi-turn-limit/src/turn-limit.ts +├── DEFAULT_MAX_TURNS = 25 (unchanged) +├── getMaxTurns() (unchanged) +├── checkTurnLimit() (unchanged) +└── export default function (pi: ExtensionAPI) + ├── let turnCount = 0 + ├── let maxTurns = getMaxTurns() + │ + ├── pi.on("session_start", ...) (unchanged reset logic) + ├── pi.on("agent_start", ...) (unchanged reset logic) + ├── pi.on("agent_end", ...) (NEW: clear widget) + ├── pi.on("turn_start", ...) (MODIFIED: widget + confirmation) + │ + └── pi.registerCommand("turn-limit", ...) (NEW: set max turns) +``` + +--- + +## Edge Cases & Notes + +- **`PI_MAX_TURNS = 0`**: Every turn hits the boundary immediately. The user gets a confirmation dialog every turn. This is the correct behavior — the user explicitly set 0, so they're asked each time. +- **`PI_MAX_TURNS = 1`**: First turn hits boundary. User confirms → counter resets → second turn hits boundary → user confirms again. Works as expected. +- **Multiple `/turn-limit` calls**: Each call updates `maxTurns` in-memory. Widget reflects the new value. No persistence needed (per-session). +- **`ctx.hasUI` false**: In print/JSON mode, the confirmation dialog can't be shown. Fall back to immediate abort (current behavior). +- **Widget name collision**: Use `"turn-limit"` as the widget name. It's unique to this extension. +- **Import requirements**: No new imports needed. `pi.registerCommand` is available on the `pi` object. `ctx.ui.confirm`, `ctx.ui.notify`, `ctx.ui.setWidget`, `ctx.abort` are all available on `ctx`. + +--- + +## Testing (manual) + +1. Start pi with the extension loaded +2. Run agent tasks and watch the widget update: `Turns: 1/25`, `Turns: 2/25`, ... +3. When turns hit 25, confirm dialog appears → click "Yes" → counter resets to 0, widget shows `Turns: 0/25` +4. Click "No" → agent aborts with error notification +5. Run `/turn-limit 10` → widget immediately shows `Turns: 0/10` +6. Run `/turn-limit abc` → error notification shown +7. Run `/turn-limit 0` → confirmation dialog every turn +8. Run with `PI_MAX_TURNS=5` → boundary at turn 5