Skip to content
offline-antPublic

About

Emacs: project spaces and an agent sidebar over Pilish

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

pi-deck

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.

Setup

Requirements

  • 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-orchestration branch, not stock MELPA Pilish. pi-deck needs its session, dialog and window-pair APIs; the pilish >= 3.1.0 package header alone does not identify this fork.
  • Emacs packages: ghostel >= 0.59.0, transient >= 0.9.0, and Pilish's dependencies magit-section >= 4.0.0, md-ts-mode >= 0.5.0 and markdown-table-wrap >= 0.2.0. Package.el resolves their further dependencies, including compat when required, from the archives below.
  • Ghostel's native module for your platform, plus the markdown and markdown-inline tree-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

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-deck

Evaluate 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 frames

Optional orchestration

Ordinary 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/orchestration

After (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.

Spaces

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.

Sidebar

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 in pi-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 from pi-deck-sidebar-header-items follow 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; TAB too), 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 RET on) a worker to read its saved session in Pilish's read-only view. Parent chats link the same views: worker tool results carry details.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 space ends 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.

Status

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.

Keys (pi-deck-command-map)

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)

Mouse

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 (as G), [⑂] 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-function can 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).

Hiding and stopping

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.

Panels, workers and forks

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.

Sessions from other frontends

/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.

Menu and preferences

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.

Persistence and daemon restart

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.

Options

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.

Development

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 restore

Dependencies (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.

Limitations

  • 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.

About

Emacs: project spaces and an agent sidebar over Pilish

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages