@taylorsatula/pi-checkbox-picker
v0.1.0
Published
Interactive checkbox picker tool for Pi — presents a grouped list of items the user cherry-picks with SPACE and submits as one subset. For 'which of these do we act on?' decisions, where single-choice radios are the wrong instrument.
Readme
@taylorsatula/pi-checkbox-picker
Interactive checkbox picker tool for pi.
Presents a grouped list of items that the user cherry-picks with SPACE and submits as one
subset. Use it when the decision is which of these — findings to fix, files to include,
tasks to run, options to keep.
pi's built-in questionnaire tool answers which one (mutually-exclusive radios, one value
per question). This tool answers which ones (independent checkboxes, one submitted subset),
and any row can carry a margin note written with TAB — selected or not. With a checkbox
picker the selection is the strategy: checking every row means "do all of them", so there
is no need to also offer approach-level options.
Install
pi install npm:@taylorsatula/pi-checkbox-pickerOr add "npm:@taylorsatula/pi-checkbox-picker" to the packages array in settings.json.
What the user sees
──────────────────────────────────────────────────────────────────────
CATEGORY C2 — Untrusted-content boundary
MIRA escapes text where it interpolates into prompt structures.
Fifteen sites do not. All fifteen are verified.
TIER 1 — EXTERNAL CONTENT INTO THE SYSTEM PROMPT
Memory text has external provenance: the forage agent holds web_tool
and stores findings verbatim.
> [x] BND1 Memories render unescaped into the persistent prompt [H] ✎
proactive_memory_trinket.py:_format_primary_memory_xml —
f"<text>{text}</text>" interpolates memory text raw. The
composer wraps the block in <mira:hud> with no escaping.
note: escape at the interpolation point, not in the composer
[ ] BND2 peanutgallery renders raw conversation into observer [M]
✓ Actualize selected (1 of 15, 1 noted)
selected 1 of 15 · 1 ✎ · rows 1-18 of 44 [11:02pm - 9/16/26]
SPACE toggle · TAB note · ↑↓ move · a/n section · A/N all · ENTER submit · ESC cancel
──────────────────────────────────────────────────────────────────────The row under the cursor expands its detail; every other row stays one line, so a long brief
stays scannable and scrolls inside the viewport.
Keys
| Key | Action |
|-----|--------|
| SPACE | Toggle the row under the cursor |
| TAB | Write or edit a margin note on that row (Enter saves, Esc discards the note) |
| ↑/↓, k/j | Move |
| PgUp/PgDn, g/G | Page, first, last |
| a / n | Select / clear the cursor's section (a toggles when already complete) |
| A / N or 0 | Select / clear everything |
| ENTER | Submit the subset and all notes |
| ESC | Cancel (notes written so far are still returned) |
Rows carrying a note show ✎ at their end.
Tool parameters
| Field | Type | Notes |
|-------|------|-------|
| title | string | Heading at the top of the picker. |
| summary | string? | Plain statement of what is being decided, 2-5 short lines. |
| sections | array | { heading, context?, items[] }. Sections group rows and scope a/n; they are not selectable. |
| sections[].items[] | array | { id, label, detail?, tag? }. |
| submitLabel | string? | Defaults to Submit. |
| timestamp | string? | Stamp rendered in the footer. |
| preselected | string[]? | Item ids that start checked. |
id must be unique across the whole picker — duplicates and missing ids are rejected with an
explanatory error rather than rendered ambiguously. tag values H/high, M/medium,
L/low are color-coded.
Result
The tool returns the selected rows as text, and details carries the full
CheckboxPickerResult:
{
title: string;
selected: { id, label, tag?, section, note? }[];
unselected: { id, label, tag?, section, note? }[];
cancelled: boolean;
}unselected is populated deliberately: what the user left out is a decision too.
note is a margin note the user wrote with TAB, and it can sit on a selected or an
unselected row. Both are binding instructions for the caller:
- On a selected row — adjusts how to implement it ("fix at the write path, not the renderer", "do this one behind a flag").
- On an unchecked row — usually records why it stays as-is. In an audit workflow that typically means: leave a code comment explaining the intent, so a future pass with no memory of this one reads the reason and does not re-flag it.
Notes survive cancellation, so nothing the user typed is discarded.
Authoring guidance
The schema descriptions carry these rules for the model, and they are what make the picker readable rather than noisy:
- Rows are the unit of decision: a 3-6 char
id, an ~8-wordlabel, and adetailwith the actionable specifics (location, mechanism, evidence) so the reader can act from the row alone. - Shared context goes in the section's
context, stated once — never repeated per item. - State the problem plainly in
summary. No dramatic framing. - Group rows by what makes them similar to decide or fix together.
- Notes are the user's channel, not yours — there is no
noteinput field. Anything you want visible on a row belongs in itsdetail. - Roughly 5-25 rows per picker; split larger sets by category and ask in sequence.
Development
npm install
npm run check # tsc --noEmitLive load probe (non-TUI; exercises loader → registration → schema → execute → guard):
pi -p -nbt -t checkbox_picker \
"Call checkbox_picker with title='t', sections=[{heading:'S', items:[{id:'T1', label:'x'}]}] and print what it returned."
# → Checkbox picker cancelled: interactive TUI mode is required.The interactive surface needs a TTY and cannot be verified headlessly.
