153 lines
6.3 KiB
Markdown

# Turn Limit Extension — Widget & Confirmation Plan
## Goal
Extend the `pi-turn-limit` extension with:
1. A `/turn-limit <N>` 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 <N>` 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