@luan.sh/pi-copy-mode
Vim-style transcript selection, copying, annotations, and reactions for Pi
Package details
Install @luan.sh/pi-copy-mode from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@luan.sh/pi-copy-mode- Package
@luan.sh/pi-copy-mode- Version
0.3.3- Published
- Sep 12, 2026
- Downloads
- not available
- Author
- cfcluan
- License
- MIT
- Types
- extension
- Size
- 27.6 MB
- Dependencies
- 3 dependencies · 2 peers
Pi manifest JSON
{
"image": "https://pi.luan.sh/media/previews/pi-copy-mode-9b8c74e7ea97.png",
"video": "https://pi.luan.sh/media/previews/pi-copy-mode-7a4bd7d42c6a.mp4",
"extensions": [
"./src/extension.ts",
"./node_modules/@luan.sh/pi-libtui/src/extension.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
@luan.sh/pi-copy-mode
@luan.sh/pi-copy-mode adds Vim-style transcript selection, copying,
annotations, and reactions to Pi. Select characters, lines, or columns, copy
them, or attach feedback to your next request directly from the selection.
It is a Pi extension, not a model-facing tool. It adds no tools and no slash commands.
Preview

Install
pi install npm:@luan.sh/pi-copy-mode
Optional companions:
pi install npm:@luan.sh/pi-xsettingsadds the/xsettingsUI for the settings below and binds thecopy-mode.enteraction to a global key fromkeybindings.json; without it the default setting applies and copy mode can only be entered from a mouse selection (see below).pi install npm:@luan.sh/pi-developer-messagesroutes annotation guidance through developer messages. Without it, the same guidance is appended to the system prompt only when a request contains annotations.
Use it
Copy mode works only in Pi's interactive fullscreen TUI; elsewhere the entry action shows a warning. There are two ways in:
- Run the
copy-mode.enteraction from a key you bound to it. The cursor starts on the bottom visible transcript row. - Select text with the mouse. The selection stays a normal Pi selection, and
an action bar (comment, react, copy) appears next to it. Clicking a bar
action, or pressing any key bound to a copy-mode motion or selection action,
adopts the range as a character selection and enters copy mode. A key bound
to
copy-mode.halfPageDownthat is literallyctrl+ddoes not adopt.
Inside copy mode the status line shows COPY MODE plus the selection kind, a
pending count, or a pending find motion. Unbound keys are swallowed. A focused
overlay (dialog, picker) receives keys as usual; copy mode stays active behind
it.
Behaviour of the actions:
- Motions move the cursor and keep it on screen. Digits typed before a motion
form a count (capped at 9999); a bare
0is not a count, so it can be bound tocopy-mode.lineStart. Find/till motions wait for the next printable character; a non-printable key cancels them. toggleSelection,lineSelection, andcolumnSelectionstart a selection of that kind anchored at the cursor, switch kind if one is active, or return to a bare cursor when pressed again in the same kind.swapEndsmoves the cursor to the other end.clearSelectionfirst cancels a pending count or find, then collapses the selection and stays in copy mode.ctrl+[is treated as the same key asescape.copycopies the selection (or the character under the cursor) and leaves copy mode. Character selections trim trailing whitespace per row; line selections keep trailing spaces and end with a newline; column selections copy the exact rectangle, padding short rows with spaces. Copy uses Pi's clipboard when Pi provides one and falls back to an OSC 52 escape sequence otherwise. A "Copied!" or "Copy failed" pill appears at the cursor.annotateandreactpublishselection.commentandselection.reactionrequests through the@luan.sh/pi-libtui/selectionregistry. The built-in annotation feature handles both requests. While the handler runs, copy-mode input is suspended. A confirmed request collapses the selection to the cursor and keeps copy mode active; a cancelled one keeps the range. When the request was started from a mouse selection, copy mode exits afterwards.cancelclears the selection and leaves copy mode.- Folds:
foldPrefixarms a prefix; the next key is matched againstfoldOpen,foldClose,foldOpenAll, andfoldCloseAll. The suffix keys do nothing without the prefix. Open/close apply to the fold under the cursor, provided by other extensions through the@luan.sh/pi-libtui/foldingregistry.
A left click inside copy mode moves the cursor to that cell and drops the selection; a drag after it is a normal mouse selection.
Annotate a response
- Trigger
copy-mode.enterand select text withcopy-mode.toggleSelectionplus motions, or select transcript text with the mouse. Either way a small action bar with comment, react, and copy appears next to the selection. - Press your
copy-mode.annotatekey (or click "comment") to write a comment, or presscopy-mode.react(or click "react") to pick a reaction. - Enter saves the draft. Escape cancels. The comment dialog also has
Save/Cancel buttons; when editing an existing draft it adds Delete
(
ctrl+d). - Submit the prompt normally. The drafts become part of the request.
A draft appears as a numbered pill in the editor and as a handle in the transcript, and the status line shows how many annotations are pending. Hover a pill to see the selected text and comment; click it to edit or delete the draft. Deleting a draft removes only its editor token and keeps the surrounding prompt unchanged. If you remove a pill's token from the editor text, the draft is dropped. Drafts are cleared once the message is sent.
Reactions are preset comment text. The defaults are:
👍 Looks good🚫 Rejected✅ Approved❓ Clarify🧬 Match existing patterns🔄 Consider alternatives🔍 Verify
The transcript renders a submitted envelope as readable annotation blocks.
Assistant text containing :pi-annotation{index="N"} or the imported
:codex-annotation{index="N"} directive renders the corresponding annotation
as a hoverable pill. Directives inside inline or fenced code remain text.
Keybindings
No action in this package has a default key. Actions are registered through
@luan.sh/pi-libactions, and keys come from keybindings.json in Pi's agent
directory, normally ~/.pi/agent/keybindings.json. The file is a JSON object
mapping action ID to one key ID or an array of key IDs. Key IDs are lowercase:
write shift+v rather than V, escape rather than Escape. Invalid key IDs
are ignored. The file is read when a session starts; run /reload after
editing it.
Global entry (exposed as a shortcut only when @luan.sh/pi-xsettings is
installed):
{ "copy-mode.enter": "alt+z" }
Modal actions, active only inside copy mode or on a completed mouse selection. One example per group; the full ID list follows.
{
"copy-mode.up": ["k", "up"],
"copy-mode.wordForward": "w",
"copy-mode.findForward": "f",
"copy-mode.paragraphForward": "}",
"copy-mode.toggleSelection": ["v", "space"],
"copy-mode.lineSelection": "shift+v",
"copy-mode.columnSelection": "ctrl+v",
"copy-mode.swapEnds": "o",
"copy-mode.clearSelection": "escape",
"copy-mode.copy": "y",
"copy-mode.annotate": "c",
"copy-mode.react": "r",
"copy-mode.cancel": "q",
"copy-mode.foldPrefix": "z",
"copy-mode.foldOpen": "o",
"copy-mode.foldClose": "c",
"copy-mode.foldOpenAll": "shift+r",
"copy-mode.foldCloseAll": "shift+m"
}
| Group | Action IDs (copy-mode. prefix) |
|---|---|
| Basic motion | up, down, left, right, lineStart, lineEnd, top, bottom, halfPageUp, halfPageDown, pageUp, pageDown |
| Word motion | wordForward, wordEnd, wordBackward, bigWordForward, bigWordEnd, bigWordBackward |
| Character find | findForward, findBackward, tillForward, tillBackward, repeatFind, reverseFind |
| Line/paragraph | paragraphForward, paragraphBackward, firstNonblank, firstNonblankDown |
| Selection | toggleSelection, lineSelection, columnSelection, swapEnds, clearSelection |
| Actions | copy, annotate, react, cancel |
| Folds | foldPrefix, foldOpen, foldClose, foldOpenAll, foldCloseAll |
Settings
Settings use the @luan.sh/pi-copy-mode namespace:
| Key | Default | Meaning |
|---|---|---|
reactions |
Seven reactions listed above | Ordered choices in the reaction picker. An empty list disables it until a choice is configured. |
copyOnSelect |
false |
Copy text immediately when a mouse selection is completed. When off, the selection stays available for the action bar and keyboard adoption. |
Edit them under Interaction → Copy mode via /xsettings when
@luan.sh/pi-xsettings is installed; otherwise the compiled defaults apply.
Library API
Importing @luan.sh/pi-copy-mode does not start the extension. It exports:
- Keybinding contracts:
COPY_MODE_ACTIONS,loadCopyModeKeybindings, andmatchCopyModeAction, with theCopyModeActionandCopyModeKeybindingstypes. - Cursor and motion helpers:
clampCursor,moveCursor,moveVirtualCursor,graphemeEnd, andscrollTopForCursor, plus their document and point types.
@luan.sh/pi-copy-mode/annotations exposes annotation helpers without starting Pi:
- Envelope:
serializeEnvelope,parseEnvelope,projectEnvelope,responseAnnotations,annotationText. - Directives:
projectAnnotationDirectives. - Draft state:
AnnotationStore,tokenInsertion,tokenPreview,removeTokenAtom. - Presentation:
plainPill,composerPillContent,responsePillContent,transcriptPillContent,AnnotationPresentationGroups. - Types:
AnnotationSelection,DraftAnnotation,ResponseAnnotation,ParsedResponseAnnotations,ResolvedAnnotationLink.
import { parseEnvelope, projectEnvelope } from "@luan.sh/pi-copy-mode/annotations";
const parsed = parseEnvelope(messageText);
const readable = parsed ? projectEnvelope(messageText) : messageText;
Wire format
When a prompt contains drafts, the editor submits this shape (with the real JSON array in place of the example):
# Response annotations:
Each item contains text selected from an earlier response and may include a user comment.
<response-annotations>
[
{ "text": "selected text", "annotation": "comment" }
]
</response-annotations>
## My request:
ordinary prompt text
Reactions are serialized as ordinary annotation text.
Architecture
The extension registers the copy-mode.enter action and the setting at load
time, and mounts one session-scoped host widget per TUI session. The host
reads the fullscreen transcript through a validated private Pi surface, draws
the cursor and selection as screen decorations, and talks to other extensions
only through the shared @luan.sh/pi-libtui selection, folding, and mouse registries.
Annotations live in their own feature directory and register alongside copy
mode under the same ownership claim. If two copies load, only the owner
registers features; it releases ownership on reload or quit. The annotation
store owns drafts; the annotation composition root owns prompt transforms,
rendering, and optional developer-message contributions. No model-facing tool
or native executable is registered.
Layout
| Responsibility | File |
|---|---|
| Extension entry, ownership claim, session lifecycle | src/extension.ts |
copy-mode.enter action registration |
src/contributions/actions.ts |
| Action IDs, keybinding loading and matching | src/config/keybindings.ts |
| Copy and reaction settings | src/config/settings.ts |
| Modal host: input, selection kinds, copy, selection requests, folds | src/runtime/copy-mode.ts |
| Validated fullscreen surface and clipboard fallback | src/runtime/fullscreen-surface.ts |
| Cursor clamping and basic motions | src/core/cursor.ts |
| Word, find, paragraph motions | src/core/vim-motions.ts |
| Cursor and selection painting | src/ui/screen-decoration.ts |
| Public exports | src/index.ts |
| Annotation hooks, decorators, editor installation | src/annotations/extension.ts |
| Draft store, envelopes, directives, presentation | src/annotations/core/ |
| Selection resolution, compose and edit flows | src/annotations/runtime/annotations.ts |
| Comment/reaction dialogs, editor pills, transcript markers | src/annotations/ui/ |
| Reaction definitions and developer-message contribution | src/config/settings.ts, src/annotations/contributions/developer-prompt.ts |
Troubleshooting
- A key does nothing: there are no built-in keys. Check the action ID and key
ID in
keybindings.jsonand run/reload. Forcopy-mode.enter,@luan.sh/pi-xsettingsmust be installed. - Comment or react requires a completed selection. Both are built in; no separate annotations package is needed. An empty reaction list shows a warning until at least one choice is configured.
- Annotation drafts install a custom editor only when none is already configured. Selection anchors use stable message offsets when uniquely resolvable and best-effort screen positions otherwise.
- "Copy mode requires Pi's fullscreen TUI": the package targets Pi 0.84.2's private fullscreen surface and fails closed when the layout does not match.
Develop
Source: https://github.com/luan/agents, directory
harnesses/pi/agent/packages/pi-copy-mode. Run bun run typecheck and
bun test test in that directory.
The combined gallery video includes selection/copying and annotation workflows. Selection and copying · Comments and reactions.