pi-sticky-prompt
v0.1.3
Published
Always-on-top, full-width macOS prompt bar for pi. A floating native window that survives terminal scrollback and lets you keep typing while you read scrollback history.
Maintainers
Readme
pi-sticky-prompt
Always-on-top, full-width macOS prompt bar for pi.

Pi runs in normal terminal scrollback (not alternate-screen mode), so when you scroll the terminal up to read history, the input prompt scrolls out of view with everything else. pi-sticky-prompt solves that with a tiny native macOS window that sits permanently on top of every other window, on every space, and talks to your live pi sessions over a Unix domain socket.
You can scroll the terminal as much as you want — the prompt stays glued to the bottom of the screen.
Demo
https://github.com/user-attachments/assets/4b8a7e41-6df2-4bf2-98d3-4cd1513aefd9
| Collapsed | Session picker |
| --- | --- |
|
|
|
┌──────────────────────────────────┐ ┌──────────────────────────┐
│ Terminal running pi │ UDS │ PiStickyPrompt.app │
│ (interactive, scrollback intact) │ ◄────── │ floating NSPanel │
│ │ │ always on top │
└──────────────────────────────────┘ └──────────────────────────┘Features
- 🪟 Native floating window —
NSPanelwith.floatinglevel +.canJoinAllSpaces. Visible above every app, every space, even fullscreen Ghostty / Terminal / iTerm. - 🖥️ Auto-docked to the bottom edge of whichever screen has a terminal app open. Plug in a monitor or move Terminal across screens — the bar follows.
- 🔒 Lock / unlock — locked: full-width pinned to bottom; unlocked: free-move, resize, drag between monitors.
- 📜 Multi-session aware — every pi process publishes its own socket; the bar lists all live sessions in a picker (
⌘L) and remembers your selection across launches. - ⌨️ Global hotkey
⌘⌥Pto toggle visibility from anywhere on the system. - 📉 Collapse to a one-line preview of long input (
⌘M); expanding grows upward leaving the toolbar flush with the screen edge. - 🚦 Status echo — model, session name, and a live streaming indicator (green = idle, yellow = streaming, red = disconnected).
- ↩️ Auto-focus the terminal after sending — keystrokes you make right after pressing Enter land in the terminal hosting that pi session, not in the bar.
- 🛑 Abort current pi turn with
Esc; pressEsctwice quickly to hide the bar.
Requirements
- macOS 13+ (Apple Silicon)
- A pi installation (
@mariozechner/pi-coding-agent) - Homebrew (for installing the HUD app)
Tested with Ghostty, Terminal.app, iTerm2, Alacritty, WezTerm, kitty, and Warp.
Building from source additionally requires Xcode command-line tools (
xcode-select --install) and Swift 5.9+ — see Development.
Install
pi-sticky-prompt has two halves and each lives in the registry that fits it best:
| Half | What it does | Where it lives |
| --- | --- | --- |
| Extension | Lets pi sessions expose themselves over a Unix domain socket so the HUD can find them | npm — npm:pi-sticky-prompt |
| HUD app | The native macOS floating window itself | Homebrew cask — pi-sticky-prompt |
1. Install the pi extension
pi install npm:pi-sticky-promptReload pi (/reload) or start a fresh session. Each session will now
publish a socket + descriptor under ~/.pi/agent/sockets/.
2. Install the macOS HUD
Recommended — Homebrew:
brew tap alonmartin2222/pi
brew install --cask pi-sticky-promptFirst launch may show a Gatekeeper prompt because the app is ad-hoc
signed — right-click PiStickyPrompt.app in /Applications and pick
Open once to whitelist it.
Alternative — download the prebuilt zip from a GitHub release:
- Grab
PiStickyPrompt.app.zipfrom https://github.com/alonmartin2222/pi-sticky-prompt/releases/latest - Unzip and drag
PiStickyPrompt.appinto/Applications
Alternative — build from source: see Development below.
3. Launch
Open PiStickyPrompt.app (Spotlight / Launchpad / open -a PiStickyPrompt).
Add it to your Login Items in System Settings if you want it always
running. Press ⌘⌥P to toggle the bar.
Usage
- Start any number of
pisessions in any terminal. Each session writes:- socket:
~/.pi/agent/sockets/pi-<pid>.sock - descriptor:
~/.pi/agent/sockets/pi-<pid>.json
- socket:
- Launch
PiStickyPrompt.app. It scans the descriptor directory and auto-attaches to the most-recent live session (or the one you previously chose). - Type. Enter sends; Shift+Enter inserts a newline.
Keys
While the bar has keyboard focus:
| Key | Action | | ------------------ | --------------------------------------------------- | | ⌘⌥P (global) | Toggle bar visibility from anywhere on the system | | Enter | Send the prompt to the attached pi session | | Shift+Enter | Insert a newline inside the editor | | Esc | Abort current pi turn (twice quickly: hide bar) | | ⌘M | Collapse to one-line preview / expand back | | ⌘L | Open the session picker | | ⌘W | Hide the bar |
A status-bar icon (π▸) also exposes Toggle / Pick Session / Quit.
Toolbar
[● session-name │ model/name] [≡ 🔒 ▲]
│ │ │ │ │ │
│ │ │ │ │ └ collapse / expand
│ │ │ │ └ lock / unlock
│ │ │ └ session picker
│ │ └ provider/model
│ └ session name (or cwd basename if unnamed)
└ status dot: green = idle · yellow = streaming · red = disconnectedWhen idle, sending a prompt triggers a new turn. When pi is mid-turn (yellow dot), sending steers the running turn instead of queueing — same behaviour as typing in the pi TUI itself.
Lock vs unlock
- 🔒 locked (default) — full screen-width, pinned to the bottom of
whichever screen has a terminal app open. Re-snaps automatically on
display changes (
NSApplication.didChangeScreenParametersNotification). - 🔓 unlocked (orange tint) — drag from any background pixel to move, drag from the edges to resize, drag freely between monitors. The last unlocked frame is remembered. Click again to re-dock.
Architecture
Two pieces:
extensions/sticky-prompt.ts ← TypeScript pi extension
PiStickyPrompt/ ← Swift Package for the macOS HUD
├── Package.swift
├── Sources/PiStickyPrompt/
│ ├── main.swift ← entry point, sets accessory activation
│ ├── AppDelegate.swift ← menu-bar item + global hotkey wiring
│ ├── HUDController.swift ← owns the panel, picks a session, locking
│ ├── HUDPanel.swift ← NSPanel subclass; canBecomeKey overrides
│ ├── PromptView.swift ← top toolbar + editor + status row
│ ├── PromptTextView.swift ← NSTextView with Enter/Esc/⌘M handling
│ ├── BridgeClient.swift ← UDS client; line-delimited JSON protocol
│ ├── SessionDiscovery.swift ← scans ~/.pi/agent/sockets for live pids
│ ├── TerminalScreen.swift ← finds which NSScreen hosts a terminal
│ ├── TerminalLocator.swift ← walks parent PIDs to find owning terminal
│ └── Hotkey.swift ← Carbon RegisterEventHotKey wrapper
└── make-app.sh ← bundles the binary into a .appWire protocol
Line-delimited JSON over the Unix domain socket (LF only, both directions):
server -> client
{"type":"hello", pid, cwd, sessionFile, sessionName?, model?, streaming, started}
{"type":"state", streaming, model?, sessionName?}
{"type":"ack", ok, command:"prompt"|"abort", error?}
{"type":"bye"}
client -> server
{"type":"prompt", text}
{"type":"abort"}
{"type":"ping"}You can drive the bridge from the shell to verify it without the HUD:
SOCK=$(ls -t ~/.pi/agent/sockets/pi-*.sock | head -1)
echo '{"type":"prompt","text":"hello from nc"}' | nc -U "$SOCK"Permissions
- No Accessibility permission required. We never use AX APIs.
- No Screen Recording permission required.
CGWindowListCopyWindowInforeturns window owner + bounds without it; we read only those, never pixels or window names. - The global hotkey uses Carbon's
RegisterEventHotKey, which works for accessory (LSUIElement) apps without any permission prompts.
Auto-focus to terminal on send
After a successful prompt ack, the HUD walks the BSD process tree
upward from the pi session's PID using sysctl(KERN_PROC_PID) until it
finds an ancestor whose bundle ID matches a known terminal app (Ghostty,
Terminal, iTerm2, Alacritty, WezTerm, kitty, Warp, Hyper). It then calls
NSRunningApplication.activate(.activateIgnoringOtherApps) on that app.
This brings the terminal to the front so your next keystroke goes to pi
output instead of the now-empty input bar.
Multiple pi sessions
The pi extension publishes one socket + descriptor per pi process. The
HUD scans the directory, hides any whose PID is no longer running, and
shows the rest in the session picker (⌘L). The current selection is
persisted in UserDefaults as pi.preferredPID so re-launches reattach
to the same session if it's still alive.
Heads-up: macOS doesn't expose per-window activation through
NSRunningApplication. If you have multiple windows of the same terminal
app, only the most-recently-focused one of that app comes forward. Per-
window raising would require Accessibility permission, which this project
deliberately avoids.
Disabling
- Hide the bar with
⌘⌥Por quit it from the menu bar. - To remove the extension half:
pi remove npm:pi-sticky-prompt(or whichever spec you used to install it). - To keep the extension but stop the HUD: just don't launch the app. The socket sits unused; pi sessions don't notice.
Limitations
- The bar is viewport-pinned because it is a separate macOS window, not because pi affects terminal scrollback. Terminal scrollback itself is unchanged.
- One HUD process per machine is the intended deployment. Multiple HUDs can connect to the same socket but they will all see each other's state echoes.
- macOS only. The HUD uses AppKit. The pi extension itself is cross-platform Node code, but the only client implementation today is the macOS app.
- Tested only on macOS 13+ on Apple Silicon. Intel builds should work
(the binary is built for
arm64only by default — drop in a universal slice inmake-app.shif needed).
Development
You only need this section if you're hacking on the source. End users should use Homebrew instead.
Prerequisites
- Xcode command-line tools:
xcode-select --install - Swift 5.9+ (ships with current macOS)
- Node.js ≥ 20 (for the extension half)
Build & run
git clone https://github.com/alonmartin2222/pi-sticky-prompt.git
cd pi-sticky-prompt
make debug # fast rebuild for iteration
make release # optimised build + ad-hoc-signed .app bundle
make install # release build copied to ~/Applications
make run # release build + open the .appUse swift build -c debug directly if you don't need the .app bundle.
Releasing a new version
The entire release is automated by
scripts/release.sh — one command from your local
machine, no CI needed:
scripts/release.sh # patch bump (0.1.x → 0.1.x+1)
scripts/release.sh minor # 0.x.y → 0.(x+1).0
scripts/release.sh major # x.y.z → (x+1).0.0
scripts/release.sh 0.4.7 # explicit versionIn order, the script:
- Preflights: clean tree, on
main, in sync with origin, required CLIs (npm,gh,swift,ditto, …) available,gh+npmboth authenticated asalonmartin2222, sibling Homebrew tap cloned. - Runs
npm version <bump>— bumpspackage.json, creates a commit and avX.Y.Zgit tag. - Builds the macOS HUD via
PiStickyPrompt/make-app.sh release. - Stamps
CFBundleShortVersionString/CFBundleVersioninInfo.plistto the new version and re-signs ad-hoc. ditto-zipsPiStickyPrompt.app→PiStickyPrompt.app.zipand computes its SHA-256.- Pushes
mainand the new tag toorigin. gh release create— creates the GitHub release with auto-generated changelog from the commits since the previous tag, and attaches the zip.- Bumps the cask in
~/git/pi-extensions/homebrew-pi(Casks/pi-sticky-prompt.rb—versionandsha256), commits, pushes. - Runs
npm publish— the npm CLI prompts you for your 2FA OTP interactively.
One-time setup before the script will run:
npm login— authenticated asalonmartin2222.gh auth login— authenticated asalonmartin2222. If your defaultghaccount is something else, drop a personal access token (Contents: write on bothpi-sticky-promptandhomebrew-pi) at~/.config/pi-sticky-prompt/github-token; the script picks it up.- The tap repo cloned at
~/git/pi-extensions/homebrew-pi.
If any step fails, the script prints recovery instructions (how to undo
the bump commit / delete the GitHub release / retry npm publish) so
you never end up in a half-released state.
Contributing
Issues and PRs welcome at https://github.com/alonmartin2222/pi-sticky-prompt. The codebase is intentionally small:
extensions/sticky-prompt.ts— ~250 lines TypeScriptPiStickyPrompt/Sources/PiStickyPrompt/*.swift— ~900 lines Swift
The Carbon RegisterEventHotKey symbol signature is 'piPb' (0x70695062)
— historical, but kept stable so config files stay portable.
License
MIT — see LICENSE.
