Add setTitle() method to Terminal interface for setting window title.
Uses standard OSC escape sequence \x1b]0;...\x07 for broad terminal
compatibility (macOS Terminal, iTerm2, Kitty, WezTerm, Ghostty, etc.).
Changes:
- Add setTitle(title: string) to Terminal interface
- Implement in ProcessTerminal using OSC sequence
- Implement no-op in VirtualTerminal for testing
- Use in interactive mode to set title as "pi - <dirname>"
Kitty and other smart terminals send a specific CSI u control code for
these shifted special keys, which are interpreted as a no-op by the
editor component. These should send the underlying key instead, matching
the behaviour of other TUI programs (e.g. readline, vim insert mode).
On less smart terminals, these key combinations are the same as the
non-shifted versions, and so already work with the existing code.
The initial render of a session, and any re-draws caused by terminal
resizing are noticeably slow, especially on conversations with 20+
turns and many tool calls.
From profiling with `bun --cpu-prof` (available since bun 1.3.2), the
majority of the rendering (90%) is spent on detection of emojis in the
string-width library, running the expensive `/\p{RGI_Emoji}$/v`
regular expression on every individual grapheme cluster in the entire
scrollback. I believe it essentially expands to a fixed search against
every possible emoji sequence, hence the amount of CPU time spent in it.
This change replaces the `stringWidth` from string-width with a
`graphemeWidth` function that performs a similar check, but avoids
running the `/\p{RGI_Emoji}$/v` regex for emoji detection unless it
contains codepoints that could be emojis.
The `visibleWidth` function also has two more optimisations:
- Short-circuits string length detection for strings that are entirely
printable ASCII characters
- Adds a cache for non-ASCII segments to avoid recomputing string length
when resizing
Strip all Unicode format characters (category Cf) before passing to
string-width. These are invisible control characters that crash
string-width but have no visible width anyway.
Closes#390
- Add setEditorText() and getEditorText() to HookUIContext for prompt generator pattern
- custom() now accepts async factories for fire-and-forget work
- Add CancellableLoader component to tui package
- Add BorderedLoader component for hooks with cancel UI
- Export HookAPI, HookContext, HookFactory from main package
- Update all examples to import from packages instead of relative paths
- Update hooks.md and custom-tools.md documentation
fixes#350
Previously, the Editor component used character-level (grapheme-level)
wrapping, which broke words mid-character at line boundaries. This
created an ugly visual experience when typing or pasting long text.
Now the Editor uses word-aware wrapping:
- Wraps at word boundaries when possible
- Falls back to character-level wrapping for tokens wider than the
available width (e.g., long URLs)
- Strips leading whitespace at line starts
- Preserves multiple spaces within lines
Added wordWrapLine() helper function that tokenizes text into words
and whitespace runs, then builds chunks that fit within the specified
width while respecting word boundaries.
Also updated buildVisualLineMap() to use the same word wrapping logic
for consistent cursor navigation.
Added tests for:
- Word boundary wrapping
- Leading whitespace stripping
- Long token (URL) character-level fallback
- Multiple space preservation
- Edge cases (empty string, exact fit)
- Export wrapTextWithAnsi from tui package
- Add 'Handling Input' section to README with key detection example
- Document wrapTextWithAnsi in utilities section
- Skip trailing whitespace before deleting word (readline behavior)
- Make word navigation grapheme-aware using Intl.Segmenter
- Add Ctrl+Left/Right and Alt+Left/Right word navigation to Input
- Accept full Unicode input while rejecting control characters (C0/C1/DEL)
- Extract shared utilities to utils.ts (getSegmenter, isWhitespaceChar, isPunctuationChar)
- Fix unsafe cast in Editor.forceFileAutocomplete with runtime type check
- Add comprehensive tests for word deletion and navigation
- Fix //tmp issue: distinguish slash commands from file paths by checking if anything precedes the /
- Fix symlinks to directories (like /tmp) not getting trailing slash
- Add new API type 'google-cloud-code-assist' for Gemini CLI / Antigravity auth
- Extract shared Google utilities to google-shared.ts
- Implement streaming provider for Cloud Code Assist endpoint
- Add 7 models: gemini-3-pro-high/low, gemini-3-flash, claude-sonnet/opus, gpt-oss
Models use OAuth authentication and have sh cost (uses Google account quota).
OAuth flow will be implemented in coding-agent in a follow-up.
The cell size query response parser was incorrectly holding back Kitty
keyboard protocol sequences, thinking they might be incomplete cell size
responses. This caused Ctrl+C/Ctrl+D to require multiple presses in iTerm2.
Fixed by only waiting for more data if the buffer doesn't end with a
terminal escape sequence terminator character.
Extended the Kitty protocol parser to handle functional keys with
~ terminator (Delete, Insert, PageUp, PageDown) and Home/End keys.
Updated editor.ts and input.ts to use the new helpers.
Backspace, Delete, and arrow keys now use Intl.Segmenter to operate
on grapheme clusters instead of individual UTF-16 code units. This
fixes deletion of emojis and other multi-codepoint characters.
fixes#240
Fixes keyboard input in Ghostty on Linux when Num Lock is enabled.
The Kitty protocol includes Caps Lock (64) and Num Lock (128) bits
in modifier values. Now masks out lock bits when matching shortcuts.
Added helper functions: isArrowUp/Down/Left/Right, isEnter, isTab,
isBackspace, isShiftEnter, isAltEnter, isAltLeft/Right, isCtrlLeft/Right
fixes#243
Add isEscape() helper that handles both raw (\x1b) and Kitty protocol
(\x1b[27u) Escape sequences. Update all components that check for
Escape key to use the new helper.
These generic helpers aren't used externally and would require
exporting MODIFIERS constants to be ergonomic. The specific
isCtrl* helpers cover all practical use cases.
- Add isCtrlD helper to keys.ts
- CustomEditor intercepts Ctrl+D and only triggers callback when editor is empty
- Single Ctrl+D with empty input exits immediately
- Update CHANGELOG to frame as feature (Kitty protocol support) not fix
- Add isCtrlA/C/E/K/O/P/T/U/W helper functions that check both raw and Kitty formats
- Add isAltBackspace helper function
- Refactor editor.ts, input.ts, select-list.ts to use helper functions
- Refactor custom-editor.ts, session-selector.ts, user-message-selector.ts
- Add CHANGELOG entry for the Shift+Enter fix
Enable the Kitty keyboard protocol on terminal start to receive enhanced
key sequences that include modifier information. This fixes Shift+Enter
not working in Ghostty, Kitty, WezTerm, and other modern terminals.
Changes:
- Enable Kitty protocol on start (\x1b[>1u), disable on stop (\x1b[<u)
- Add centralized key definitions in packages/tui/src/keys.ts
- Support both legacy and Kitty sequences for all modifier+key combos:
- Shift+Enter, Alt+Enter for newlines
- Shift+Tab for thinking level cycling
- Ctrl+C, Ctrl+A, Ctrl+E, Ctrl+K, Ctrl+U, Ctrl+W, Ctrl+O, Ctrl+P, Ctrl+T
- Alt+Backspace for word deletion
- Export Keys constants and helper functions from @mariozechner/pi-tui
- Custom tools: TypeScript modules that extend pi with new tools
- Custom TUI rendering via renderCall/renderResult
- User interaction via pi.ui (select, confirm, input, notify)
- Session lifecycle via onSession callback for state reconstruction
- Examples: todo.ts, question.ts, hello.ts
- Hook examples: permission-gate, git-checkpoint, protected-paths
- Session lifecycle centralized in AgentSession
- Works across all modes (interactive, print, RPC)
- Unified session event for hooks (replaces session_start/session_switch)
- Box component added to pi-tui
- Examples bundled in npm and binary releases
Fixes#190
wrapTextWithAnsi() was processing each line independently after splitting
on newlines, losing ANSI state. When styled text contained embedded newlines
(e.g. from markdown paragraphs), subsequent lines would lose their styling.
Fixed by tracking ANSI state across lines and prepending active codes to
lines after the first.
Fixes#197
- Image component returns correct number of lines (rows) for TUI accounting
- Empty lines rendered first, then cursor moves up and image is drawn
- This clears the space the image occupies before rendering
- Add spacer before inline images in tool output
- Create ShowImagesSelectorComponent with borders like other selectors
- Use showSelector pattern for /show-images command