# 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