---
slug: "pi-sticky-prompt"
source_type: "readme"
source_url: "https://cdn.jsdelivr.net/gh/alonmartin2222/pi-sticky-prompt@main/README.md"
repo: "https://github.com/alonmartin2222/pi-sticky-prompt"
source_file: "README.md"
branch: "main"
---
# pi-sticky-prompt

> Always-on-top, full-width macOS prompt bar for [pi](https://github.com/earendil-works/pi).

![sticky bar expanded](https://github.com/alonmartin2222/pi-sticky-prompt/raw/HEAD/assets/sticky-bar-expanded.png)

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 |
| --- | --- |
| ![collapsed](https://github.com/alonmartin2222/pi-sticky-prompt/raw/HEAD/assets/sticky-bar-collapsed.png) | ![session picker](https://github.com/alonmartin2222/pi-sticky-prompt/raw/HEAD/assets/sticky-bar-session-picker.png) |

```
┌──────────────────────────────────┐         ┌──────────────────────────┐
│ Terminal running pi              │  UDS    │ PiStickyPrompt.app       │
│ (interactive, scrollback intact) │ ◄────── │ floating NSPanel         │
│                                  │         │ always on top            │
└──────────────────────────────────┘         └──────────────────────────┘
```

## Features

- 🪟 **Native floating window** — `NSPanel` with `.floating` level + `.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** `⌘⌥P` to 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`; press `Esc` twice quickly to hide the bar.

## Requirements

- macOS 13+ (Apple Silicon)
- A pi installation (`@mariozechner/pi-coding-agent`)
- [Homebrew](https://brew.sh) (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](#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

```bash
pi install npm:pi-sticky-prompt
```

Reload 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:**

```bash
brew tap alonmartin2222/pi
brew install --cask pi-sticky-prompt
```

First 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:**

1. Grab `PiStickyPrompt.app.zip` from <https://github.com/alonmartin2222/pi-sticky-prompt/releases/latest>
2. Unzip and drag `PiStickyPrompt.app` into `/Applications`

**Alternative — build from source:** see [Development](#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

1. Start any number of `pi` sessions in any terminal. Each session writes:
   - **socket**:     `~/.pi/agent/sockets/pi-<pid>.sock`
   - **descriptor**: `~/.pi/agent/sockets/pi-<pid>.json`
2. Launch `PiStickyPrompt.app`. It scans the descriptor directory and
   auto-attaches to the most-recent live session (or the one you previously
   chose).
3. 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 = disconnected
```

When 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 .app
```

### Wire 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:

```bash
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. `CGWindowListCopyWindowInfo`
  returns 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 `⌘⌥P` or 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 `arm64` only by default — drop in a
  universal slice in `make-app.sh` if needed).

## Development

You only need this section if you're hacking on the source. End users
should use [Homebrew](#2-install-the-macos-hud) 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

```bash
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 .app
```

Use `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`](https://github.com/alonmartin2222/pi-sticky-prompt/blob/HEAD/scripts/release.sh) — one command from your local
machine, no CI needed:

```bash
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 version
```

In order, the script:

1. Preflights: clean tree, on `main`, in sync with origin, required CLIs
   (`npm`, `gh`, `swift`, `ditto`, …) available, `gh` + `npm` both
   authenticated as `alonmartin2222`, sibling Homebrew tap cloned.
2. Runs `npm version <bump>` — bumps `package.json`, creates a commit
   and a `vX.Y.Z` git tag.
3. Builds the macOS HUD via `PiStickyPrompt/make-app.sh release`.
4. Stamps `CFBundleShortVersionString` / `CFBundleVersion` in
   `Info.plist` to the new version and re-signs ad-hoc.
5. `ditto`-zips `PiStickyPrompt.app` → `PiStickyPrompt.app.zip` and
   computes its SHA-256.
6. Pushes `main` and the new tag to `origin`.
7. `gh release create` — creates the GitHub release with auto-generated
   changelog from the commits since the previous tag, and attaches the
   zip.
8. Bumps the cask in `~/git/pi-extensions/homebrew-pi`
   (`Casks/pi-sticky-prompt.rb` — `version` and `sha256`), commits, pushes.
9. Runs `npm publish` — the npm CLI prompts you for your 2FA OTP
   interactively.

One-time setup before the script will run:

- `npm login` — authenticated as `alonmartin2222`.
- `gh auth login` — authenticated as `alonmartin2222`. If your default
  `gh` account is something else, drop a personal access token
  (Contents: write on both `pi-sticky-prompt` and `homebrew-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 TypeScript
- `PiStickyPrompt/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](https://github.com/alonmartin2222/pi-sticky-prompt/tree/HEAD/LICENSE).
