6.3 KiB

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