A sidebar of spaces and agents inside Emacs, for Pi sessions
run through
Pilish:
one window layout per project space, a sidebar listing every agent with its
status, workers and panels, attention navigation and notifications, all
usable by mouse in graphical and text frames. Built on Emacs 31's side
windows, header lines, (hidden) tab-bar tabs and transient; terminals use
ghostel (libghostty-vt).
◉ 1 blocked · ● 1 done 🎤│ ◐ main · claude-opus-5-5 · 41% [↑] [↓] [⑂] [⤢] [✕]
pi (3) ◐ + │ …chat…
◐ main ✕ │ …input…
◐ review ✕ │ ○ tests (exit 0) [⤢] [✕]
○ tests ✕ │ …terminal…
▸ 2 finished workers ✕ │
swarmy (2) ● + │
│
+ New space │
- A space is a project directory with one window layout. Switching spaces switches the whole layout; there are no visible tabs.
- A pane is a Pilish chat with its input below (one atomic window pair) or a ghostel terminal, each with a title bar.
- An agent is a Pilish session (root, fork or orchestration worker) or a ghostel terminal (a shell or an agent's panel). Agents live on when no pane shows them; the sidebar lists them all.
- Emacs 31.1 or later, built with tree-sitter and dynamic module support.
- Pi CLI on Emacs's
PATH, authenticated with your provider. Pi 1.4.0 is the tested version; use Node.js 22.19 or later. - The Pilish fork's
pi-orchestrationbranch, not stock MELPA Pilish. pi-deck needs its session, dialog and window-pair APIs; thepilish >= 3.1.0package header alone does not identify this fork. - Emacs packages:
ghostel >= 0.59.0,transient >= 0.9.0, and Pilish's dependenciesmagit-section >= 4.0.0,md-ts-mode >= 0.5.0andmarkdown-table-wrap >= 0.2.0. Package.el resolves their further dependencies, includingcompatwhen required, from the archives below. - Ghostel's native module for your platform, plus the
markdownandmarkdown-inlinetree-sitter grammars. Grammar installation needs Git and a C compiler. Other language grammars are optional.
These instructions follow moving branches and package archives, not a locked
or reproducible dependency set. The Emacs daemon restart command additionally
needs a Unix-like system with setsid supporting -f (util-linux on Linux),
sh, kill and sleep on PATH.
Install and authenticate Pi in a terminal first:
npm install -g --ignore-scripts @earendil-works/pi-coding-agent@1.4.0
pi --version
pi
# In Pi, run /login for your provider, then exit.Clone these checkouts (adjust the paths together if you use another location):
mkdir -p ~/.emacs.d/site-lisp
git clone --branch pi-orchestration https://github.com/offline-ant/pilish.git ~/.emacs.d/site-lisp/pilish
git clone https://github.com/offline-ant/pi-deck.git ~/.emacs.d/site-lisp/pi-deckEvaluate this once in Emacs to install the external packages. GNU ELPA is
needed alongside MELPA for dependencies such as compat; allow upgrades of
bundled transient if necessary.
(require 'package)
(add-to-list 'package-archives '("gnu" . "https://elpa.gnu.org/packages/") t)
(add-to-list 'package-archives '("nongnu" . "https://elpa.nongnu.org/nongnu/") t)
(add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/") t)
(setq package-install-upgrade-built-in t)
(package-initialize)
(package-refresh-contents)
(dolist (pkg '(transient magit-section md-ts-mode markdown-table-wrap ghostel))
(package-install pkg))Add this to your init file, after package initialization:
(add-to-list 'load-path (expand-file-name "site-lisp/pilish" user-emacs-directory))
(add-to-list 'load-path (expand-file-name "site-lisp/pi-deck" user-emacs-directory))
(require 'pilish)
(require 'ghostel)Before enabling pi-deck or restoring background sessions, run
M-x pilish-install-grammars and install both Markdown grammars, then run
M-x ghostel-download-module to download Ghostel's prebuilt module. Platforms
without a prebuilt module need Ghostel's Zig build procedure; see the
Ghostel documentation.
Check M-x ghostel opens a working terminal and M-x pilish starts Pi.
If Pi is not on Emacs's PATH, set pilish-executable to its absolute path
(as a list, e.g. '("/path/to/pi")).
Then add:
(require 'pi-deck)
(pi-deck-mode 1)
(keymap-global-set "C-c d" 'pi-deck-command-map)
(xterm-mouse-mode 1) ; mouse support in text framesOrdinary Pilish sessions and your own shells work without pi-ant. For managed workers, agent-started panels and forks, install the public orchestration package and load its Emacs companion before starting any Pi sessions:
git clone https://github.com/offline-ant/pi-ant.git ~/.emacs.d/site-lisp/pi-ant
pi install ~/.emacs.d/site-lisp/pi-ant/orchestrationAfter (require 'pilish) and (require 'ghostel) in your init file, before
enabling pi-deck:
(add-to-list 'load-path
(expand-file-name "site-lisp/pi-ant/orchestration/emacs"
user-emacs-directory))
(require 'pi-orchestration)Run Emacs as a server/daemon so the companion can expose its live socket to
Pi children. No npm install is needed in pi-ant or its orchestration package;
Pi resolves extension imports from its installed CLI. The separate pi-ant
root package (for ask, /tools and other extensions) is optional and has its
own installation instructions.
See the orchestration guide
for host selection and shell prompt integration. Restart Pi sessions after
configuring packages and the companion.
pi-deck-mode starts tracking agents, shows the sidebar and sets
tab-bar-show to nil: each frame keeps a space's layout in a hidden tab, so
the tab bar is never drawn. Run Emacs as a daemon (e.g. emacs --daemon=pi)
and attach with emacsclient -t or -c; a new client frame opens the space
selected last, with its last known layout.
S (or the sidebar's + New space) creates a space for a project directory
(known projects are offered; a typed existing directory is listed first,
marked (typed)) and shows its first pane: a Pi agent, a shell or Dired
(pi-deck-new-space-action). s or clicking a space's name switches to it.
A frame opening a space for the first time starts from the space's last
known layout, else Dired of its directory. Layouts are per frame while open;
leaving a space, closing a frame or saving remembers the layout for the next
frame that opens it.
New agents (a) and shells (e) start in the current space: the first one
takes the layout's pane, later ones split the selected pane beside it.
Outside a space they ask for a space or New space….
An agent's home space is fixed: the space it was started or assigned in, else its parent's, else the closest space whose directory contains it; only an agent none of these place belongs to a space whose layout shows it. Showing an agent in another space's layout never moves it.
A side window (pi-deck-sidebar-side, default left) in every frame wide
enough for it: pi-deck-sidebar-width (28) columns plus
pi-deck-sidebar-min-pane-width (44) for panes. Narrower frames hide it;
navigate there with g and n. b toggles it everywhere and saves the
choice. All frames and spaces share one width; dragging the sidebar's edge
sets it everywhere and saves it.
- The header line shows the status summary (
◉ 2 blocked · ● 1 done, the statuses inpi-deck-summary-statuses; just counts when it does not fit; "all idle" or "no agents" otherwise). Clicking it goes to the next agent needing attention. Buttons frompi-deck-sidebar-header-itemsfollow at the right (see Options). - Each space is a header: name (switches to the space and unfolds it;
on the current space, which is underlined, opens its directory in Dired
beside the current pane), agent count (folds;
TABtoo), rolled-up status, and+(menu: new agent, new shell). - Under it the space's agents, with workers, forks and panels nested
under their parent, each with status, name, detail and a ✕ that stops it
(see Hiding and stopping). Rows of agents the
frame shows are highlighted (
pi-deck-shown-agent). Agents without a space come last, under "other". - Workers are ephemeral: Pi closes them once they return, and their buffers
go. Under each parent a folded "N finished workers" row lists them;
unfold it and click (or
RETon) a worker to read its saved session in Pilish's read-only view. Parent chats link the same views: worker tool results carrydetails.savedConversations, shown by Pilish as "Pi sessions" links. Its right-click menu also has Resume, which reopens the session as a normal editable Pi session in a new pane of the parent's space (or jumps to it when already open). A resumed session is independent: nothing it does returns to the parent. The row's ✕ clears the list; the session files stay on disk. - Terminals whose process ended, panels or your own, fold into one "N finished shells" row at the end of their space. Unfold it to read or close one. The row's ✕ closes them all, which loses their output, and agents can then no longer read those panels.
+ New spaceends the list.
Positions are stable: agents are listed in creation order (new ones at the bottom, workers and panels under their parent), and showing, selecting or renaming an agent never reorders the list. The order is saved with the state and restored after a daemon restart.
Clicking an agent jumps to it: an agent on screen is focused; otherwise its
space is shown, and if that does not show it, a worker or panel whose parent
is on screen opens beside the parent and any other agent replaces the most
recently used pane. Double-clicking shows it alone: its pane is zoomed, so
the other panes are hidden while the sidebar stays (z brings them back).
mouse-2 (or o) shows it beside the current pane and keeps the sidebar
selected. Pane commands (|, -, z, x, l) run from the sidebar act
on the most recently used pane.
| Key | Action |
|---|---|
RET |
jump to agent (answers a pending dialog) / switch to the space at point (its directory when current) / show closed workers / read a closed worker |
o |
show agent beside the current pane, stay in the sidebar |
TAB |
fold or unfold the space or closed workers at point |
n p / j k |
next / previous agent |
A |
answer the dialog of the agent at point |
K |
stop the agent at point (asks when it is working or blocked) |
+ N E |
new space; new agent or shell in the space at point (else read) |
g (gr under Evil) |
refresh |
m ? |
menu |
q |
hide the sidebar (as b) |
Under Evil the sidebar stays in normal state, so leader keys, gg and /
keep working.
Each agent has a base state and a seen flag:
| Icon | Status | Meaning |
|---|---|---|
| ◉ | blocked | a Pi extension dialog (confirm, select, input, editor; e.g. the ask tool, or error-prompt after a run error) awaits you, or an interrupted worker is waiting for you (detail "waiting for you") |
| ● | done | finished work (working/blocked → idle) while not visible; cleared once shown |
| ◐ | working | Pilish reports activity (sending, thinking, replying, running tools, compacting); for terminals, a command runs |
| ○ | idle | waiting for input; for terminals, at the prompt or exited |
| · | unknown | Pi process gone; terminal without prompt marks |
Attention priority is blocked > done > working > idle > unknown. Spaces
show the most urgent status of their agents. Terminal status needs
prompt marks: OSC 133 (A prompt, C command, D done). New shell panes
run ghostel's default shell (ghostel-shell, else pi-deck-shell-program),
whose shell integration emits them for bash, zsh, fish and nushell; other
programs need their own (fish 4 natively, bash via
pi-ant/orchestration/shell/osc133.bash). Exited terminals keep their buffer
and show their exit status.
With pi-deck-dialog-style inline (the default), Pi dialogs never
interrupt you: the agent turns blocked and its question waits at the end of
its chat with clickable choices. Jumping to it (clicking it or RET in the sidebar,
pi-deck-goto, pi-deck-next-attention) or A puts point on the first
choice: click, RET or a digit answers, TAB moves, C-c C-k cancels.
The input header shows
◉ answer: … while a question waits; clicking it goes there. A question that
follows your answer (the next ask question, its editor) takes focus, so
multi-question flows are answered in a row; drafts are never touched.
Notifications follow Herdr: needs attention on becoming blocked and
finished on leaving work for idle, both suppressed while the agent is
visible; orchestration workers and panels never send finished (their
results go to the parent). They use D-Bus desktop notifications (clicking jumps to the agent)
when available, else the echo area (pi-deck-notify-method).
pi-deck-notify-functions run on the same transitions with the kind
(blocked/finished), title, body and agent, also when the method is off;
pi-sound uses it to play Herdr's sounds and show pop-ups on the machine you
sit at.
| Key | Command |
|---|---|
b |
toggle the sidebar in all frames (the choice is saved) |
g |
go to agent (grouped by space; C-u narrows by status) |
n |
next agent needing attention (blocked first, then done) |
N / P |
next / previous agent in sidebar order |
a / e |
new Pi agent / new shell in the current space |
f |
fork the Pi agent in the selected pane (as [⑂]) |
A |
answer the current agent's pending dialog |
S / s |
create / switch space |
R / K |
rename / close space (stops its agents) |
| / - |
split pane right / below |
z |
zoom pane (toggle) |
x |
hide pane (a chat with its input; the agent keeps running) |
= |
repair the layout's panes: every chat over its input again, closed inputs reopened |
l |
grid of the space's Pi agents (pi-deck-grid-include-shells adds shells) |
m / ? |
transient menu (also state and daemon commands) |
Everything works by mouse in graphical and text frames (the config enables
xterm-mouse-mode); pi-deck's buttons and menus act on release.
- Sidebar: see Sidebar.
- Pane title bars (header lines of chats and terminals; Pilish's input
windows keep their own header): status, name, model and context use (Pi)
or exit status (terminals), truncated with
…to keep the buttons visible;[↑]toggles, for that session only, whether its chat shows each final answer from the start, once: when the turn ends if the chat follows the output, else when you open or focus it, moving forward only from above the answer (pilish-jump-to-answer; dimmed when off, the default follows that option),[↓]jumps a Pi chat to its latest output and follows it again (asG),[⑂]forks a Pi session (/fork-here: the fork is listed in the same space, named<session>-fork; the draft is untouched, a busy agent keeps working and its fork ends at the latest finished step),[⤢]zooms,[✕]hides the pane.pi-deck-pane-header-prefix-functioncan put something first, e.g. the window number. - Right-click menus (
mouse-3): agent rows (Jump, Show beside, Show alone, Rename…, Answer question when one waits; for Pi agents Fork, Stop turn while busy, Compact while idle, Copy session file; Stop), closed workers (Open transcript, Resume), space headers and their+(New agent, New shell, Rename…, Close space).
Closing a pane ([✕] in its title bar, x) only hides it: the agent keeps
running and stays in the sidebar. A pane that is the layout's only one
shows the space's directory instead.
Agents stop explicitly: the sidebar's ✕, K in the sidebar, or the agent
menu's Stop stop that agent with its workers and panels. Closing a space
(K, the space menu) stops all its agents and forgets the space. Panes
showing stopped agents go from every layout in every frame.
Stopping asks first only when something it would stop is working or
blocked, naming everything that stops. Mouse commands ask with a popup
dialog (Cancel / Stop N; text terminals draw it as a menu, Cancel first
so a stray click or RET never stops anything); keyboard commands ask in
the minibuffer. Orchestration workers and panels stop through Pi's Emacs
host (pi-orchestration-stop-target), so Pi notices they are gone.
Terminals an agent starts with panel-start (Pi's Emacs orchestration
host) appear at once without taking focus in half of the parent agent's
pane when it is on screen: to the right when the pane is at least twice as
wide as it is high, otherwise below, as in Herdr (pi-deck-show-panels).
A panel whose parent is not on screen, workers (do, delegate) and forks
(/fork-here, in the parent's space) are only listed in the sidebar until
you open them. When Pi closes a worker, fork or panel, the panes showing it
go too.
/switch emacs in a Pi session elsewhere (Herdr, Tau, a terminal) opens that
conversation here through pi-deck-show-session: in the space whose
directory contains the session's cwd (created when missing), revealed and
focused in the most recently used client frame. Without a client frame, the
next frame shows it. An idle view of the same session is reloaded in place
instead.
pi-deck-menu (m, ?) groups space, agent and pane commands, plus:
- state: save / restore;
- daemon: daemon info (server name, socket, PID, uptime, clients, frames,
live Pi sessions, orchestration host, notification method, state file and
whether this Emacs owns it, in a temporary buffer), reload
user-init-file, restart the daemon.
The sidebar toggle and dragged width are saved to ~/.emacs.d/var/pi-deck/preferences.eld
(pi-deck-preferences-file), never to your init files.
pi-deck-save-state (also about 2 s after changes — spaces, window
layouts, agents starting, closing or finishing a turn, a session replaced
in place, also from another frontend sharing it — and at exit, never
periodically; pi-deck-save-delay) writes
~/.emacs.d/var/pi-deck/state.eld: spaces, each space's layout, the current
space, Pi sessions with their session files in sidebar order, terminals
with directory and command, and finished workers. pi-deck-restore-state resumes Pi sessions
with pi --session FILE (history included) and restarts terminals, starting
them pi-deck-restore-interval seconds apart (0.15); once all exist, client
frames show the saved current space with its layout. Orchestration workers
and panels belong to their parent Pi and are not restored; sessions you
stopped stay stopped. Finished workers return under their parent, unless
the parent did not come back (it was a worker itself) or the session file
is gone.
The state file belongs to one Emacs server, recorded by its socket. That
server restores it whenever it starts, however it stopped (restart,
kill-emacs, crash, reboot), like Herdr's agent resume;
pi-deck-auto-restore nil turns this off (the first save then keeps the old
file as state.eld.bak). A daemon claims the file only once its server
runs, after the init file, so restored sessions get the Emacs orchestration
environment. Every other Emacs (no server, another daemon such as a
disposable test daemon) neither restores nor saves the file automatically;
an explicit pi-deck-save-state takes it over. A file recording no owner
is restored by nobody until such a save. Daemon info says when this Emacs
does not own the file.
pi-deck-restart-daemon lists the Pi sessions and terminals that will stop,
asks for confirmation, saves the state, starts a detached helper that waits
for this Emacs to exit and then runs pi-deck-restart-command (default
emacs --daemon=<socket>, the running server's absolute socket file, so the
replacement listens exactly where clients connect), and exits. The new
daemon restores the state like any start. In-flight Pi turns are
interrupted; sessions resume from disk. Reattach clients afterwards.
Each chat and the input window below it form one pane through Pilish's
pilish-window-pairing-mode, which pi-deck-mode turns on: ordinary
window commands (C-x 0 on the chat, C-x 1, splits, switch-to-buffer,
winner-undo) keep chat and input together (C-x 0 on the input closes
just the input), and several agents can sit side by side in one layout,
each with its own input. Pilish sizes the inputs
(pilish-input-window-height; fit grows with the draft like Pi's
editor).
pi-deck-new-space-action (agent/shell/dired), pi-deck-shell-program,
pi-deck-pane-headers, pi-deck-include-shells, pi-deck-dialog-style,
pi-deck-answer-on-jump, pi-deck-notify-method, pi-deck-notify-on,
pi-deck-notify-shells, pi-deck-notify-bell, pi-deck-summary-statuses,
pi-deck-grid-include-shells, pi-deck-sidebar, pi-deck-sidebar-side,
pi-deck-sidebar-width, pi-deck-sidebar-min-pane-width,
pi-deck-sidebar-header-items,
pi-deck-show-panels, pi-deck-finished-workers-limit,
pi-deck-status-icons, pi-deck-save-delay, pi-deck-state-file,
pi-deck-preferences-file, pi-deck-restart-command,
pi-deck-auto-restore, pi-deck-restore-interval,
pi-deck-refresh-interval.
Other packages add buttons at the right of the sidebar's header line
through pi-deck-sidebar-header-items: functions called without arguments
returning a string or nil. Header lines ignore keymap, so a button binds
its clicks with a local-map text property, e.g. <header-line> <mouse-1>
to a command reading (interactive "e"). pi-sound's dictation button 🎤 is
one.
make test # byte-compile (warnings are errors) + ERT, emacs -Q --batch
make smoke # real shell pane, real Pi + faux provider in a disposable daemon, incl. restart and kill+start restoreDependencies (ghostel with its prebuilt native module, Pilish's packages)
install into .cache/elpa/<version>;
Pilish is taken from ../pilish (PILISH_DIR), orchestration from
../pi-ant/orchestration/emacs (ORCHESTRATION_DIR). The smoke test uses a
private server socket and agent directory, only the in-process faux provider
(test/support/faux.ts), and kills its daemons. Install the two Markdown
grammars into ~/.emacs.d/tree-sitter before running it; the current smoke
script looks there rather than bootstrapping grammars. Its panel checks also
need tmux (otherwise they are skipped). The dependency helper does not install
Pilish or pi-ant, lock archive versions, or upgrade packages already present;
check the minimum versions listed above when reusing its cache. For a
terminal-only smoke run with PGTK Emacs, use
env -u DISPLAY -u WAYLAND_DISPLAY GDK_BACKEND=x11 make smoke to keep the
disposable daemon off desktop displays.
- Layouts are per frame while open: two frames showing the same space have separate layouts, and a frame opening a space starts from the layout remembered last.
- Status comes from this Emacs: Pi sessions started elsewhere (terminals, Herdr, Tau) are not shown.
- Emacs is single-threaded; many agents streaming at once share its time.