Imported from qduc/ink-prompt (
AGENTS.md). Install upstream withnpx skills add qduc/ink-prompt. Copyright stays with the author.
This file provides guidance to AI assistants when working with code in this repository.
REMEMBER: This file must be kept up-to-date with every architecture change to the project. It is your job to do it without waiting for user's request.
REMEMBER: README.md must be kept up-to-date whenever adding a user-facing feature, changing component props, changing exported types, or changing documented behavior.
Project Overview
ink-prompt is a React Ink component library for creating interactive CLI prompts. It provides reusable components that integrate with Ink's terminal rendering system to enable user input in CLI applications.
Development Philosophy
This project follows Test-Driven Development (TDD):
- Tests must be written first, before implementation code
- Write the minimum code necessary to make tests pass
- Tests should validate behavior, not implementation details
- Tests must survive large refactoring - they should continue passing even when internals change significantly
Commands
Build & Development
npm run build- Compile TypeScript to JavaScript (outputs todist/)npm run dev- Watch mode for development (continuous compilation)npm run type-check- Run TypeScript type checking without emitting filesnpm run clean- Remove build artifacts
Testing
npm testornpm run test- Run all tests once (Vitest)npm run test:watch- Run tests in watch mode (re-runs on file changes)npm run test:ui- Launch Vitest UI for interactive test debugging
Test environment uses happy-dom for DOM simulation and Vitest globals are enabled.
Architecture
Component Structure:
src/components/- Reusable Ink components- Each component lives in its own directory with an
index.tsxfile - Components export both the component and their prop types
- Each component lives in its own directory with an
src/index.ts- Main entry point that re-exports all public APIs
Hooks:
src/hooks/- Reusable React hooks for Ink componentsuseTerminalWidth.ts- Hook that provides terminal width with resize event handling- Returns
propWidthif provided, otherwise returns terminal width from stdout - Automatically updates on terminal resize events with debouncing (default: 100ms)
- Debounce prevents excessive re-renders during rapid terminal resizing (e.g., when user drags window)
- Debounce delay is configurable via optional second parameter:
useTerminalWidth(width?, debounceMs?) - Properly cleans up event listeners and pending debounce timers on unmount
- Returns
useTerminalHeight.ts- Hook that provides terminal height with resize event handling- Returns
propHeightif provided, otherwise returns terminal height from stdout - Automatically updates on terminal resize events with debouncing (default: 100ms)
- Debounce prevents excessive re-renders during rapid terminal resizing
- Debounce delay is configurable via optional second parameter:
useTerminalHeight(height?, debounceMs?) - Properly cleans up event listeners and pending debounce timers on unmount
- Returns
Utilities:
src/utils/logger.ts- File-based debug loggerlog(message)- Writes timestamped debug messages to a log fileinitLogger()- Clears any existing log file (call once at app start)- Log file location:
$INK_PROMPT_LOG_FILEenv var or./ink-prompt.debug.log
Unified Block System:
-
src/components/MultilineInput/BlockTypes.ts— shared types for both paste placeholder and image blocksBlockKind:'paste' | 'image'BlockEntry: discriminated union (PasteBlockEntry | ImageBlockEntry)BlockState: single registry withentries: Map<string, BlockEntry>, separate counters for paste and image display numbers (both start at 1)
-
src/components/MultilineInput/BlockMarker.ts— unified marker format for both block kinds- Format:
\uE000{kind}:{id}:{displayNumber}\uE001(PUA delimited,p:for paste,i:for image) createBlockMarker(kind, id, displayNumber)— create a marker stringparseBlockMarkers(text)— find all markers in a line (replaces both oldMARKER_REGEXandparseSentinels)findBlockMarkerAt/Before/After— cursor navigation helpersremoveBlockMarker,generateBlockId,getBlockPlaceholderText
- Format:
-
src/components/MultilineInput/BlockRegistry.ts— unified registry managementcreatePasteBlockEntry(state, originalText, displayText)— create a paste block with its markercreateImageBlockEntry(state, imageRef)— create an image block with its markerremoveBlock(state, id)— remove entry from registrygetValue(lines, entries)— expand paste markers to original text, pass image markers throughgetDisplayLine,bufferColToDisplayCol,displayColToBufferCol,getValueCursorOffset,getCursorFromValueOffset— all unified for both block kinds
-
src/components/MultilineInput/AtomicBlocks.ts— single scanner usingparseBlockMarkersfindAtomicBlocks(line, entries?)returns sortedAtomicBlock[]with{ kind: 'paste' | 'image', id, start, end, displayWidth, displayText, dim }dim: truefor image blocks (rendered withdimColor),falsefor paste blocksfindAtomicBlockBefore/After/Spanning(line, offset, entries?)for cursor-side queries- All consumers (
TextBuffer,TextRenderer,useTextInput) use the unified API
Paste Placeholders (pasteThreshold):
- When
pasteThresholdprop is set on MultilineInput, text exceeding this character count (when pasted in a single input event) is replaced with a paste block marker using the unified format formatPastePlaceholderprop customizes display text format, receives 1-baseddisplayNumber- Markers are atomic: backspace/delete remove the entire placeholder, arrow keys skip over them
value/onChange/onSubmitreturn the expanded original text
Image Paste Support:
- Optional feature controlled by
enableImagePasteprop (defaultfalse), backward compatible - Images use the same unified marker format as paste placeholders (
kind: 'i') - Rendered as
[Pasted Image #N]text withdimColorstyling - Image data stored in the same
BlockStateregistry, separate display number counter from paste blocks
Text Wrapping:
- Word-aware wrapping: Text wraps at word boundaries (spaces) when possible
- Long words that exceed the terminal width are hard-wrapped
- Both rendering (
wrapLinesin TextRenderer) and cursor navigation (moveCursorin TextBuffer) use consistent word-aware wrapping logic - Block markers (paste placeholders and image placeholders) are atomic — never split across visual rows
- Visual width of a block marker equals its placeholder text length
Undo/Redo History Management:
useTextInputhook maintains undo/redo stacks for text edits- History is bounded by
historyLimitoption (default: 100 entries) to prevent unbounded memory growth - When undo stack exceeds the limit, oldest entries are discarded
- Each history entry stores a full snapshot of the buffer, cursor, and block state
- Redo stack is cleared whenever a new edit occurs
- Consecutive single-character inserts are batched into one undo step via
undoDebounceMs(default: 200ms); setundoDebounceMs: 0to disable batching - External
valueandcursorOverrideprop sync usesuseTextInput.syncExternalState()so replacement buffer, block state, and cursor are computed atomically; cursor offsets are mapped against the new value, and prop sync is not recorded as undo history
Clipboard Reader Abstraction:
src/components/MultilineInput/clipboard/— platform-specific clipboard readersMacOSClipboardReader— usesosascriptto read«class PNGf»and plain textLinuxX11ClipboardReader— usesxclipwith image/png and text/plain targetsLinuxWaylandClipboardReader— useswl-pastewith --type flagsWindowsClipboardReader— uses PowerShellGet-ClipboardandSystem.Windows.Forms.Clipboard- Factory
createClipboardReader()detects platform viaprocess.platformand$WAYLAND_DISPLAY
ImageValidatorsniffs magic bytes (PNG, JPEG, WebP, GIF) and enforces size/count/mime limitsuseClipboardPastehook wraps async clipboard reading with 1500ms timeout and error mapping
Build System:
- TypeScript compiles from
src/todist/ - Outputs CommonJS modules (
.js) with type definitions (.d.ts) - ESM import available via
dist/index.mjs(dual module support) - Target: ES2020
Dependencies:
- Peer dependencies: React 16.8+ and Ink 4.x/5.x/6.x/7.x
- Components must be compatible with Ink versions from 4.x through 7.x
- Maintain compatibility with both Ink 4.x and 7.x (and versions in-between)
- Development uses Vitest for testing with happy-dom for React component testing
Key Constraints
- This is a library package, not an application - focus on reusable components
- Components must work in terminal environments (Ink limitations apply)
- Maintain compatibility with Ink versions from 4.x through 7.x