@luan.sh/pi-panels
Generic contributed side-panel host for Pi
Package details
Install @luan.sh/pi-panels from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@luan.sh/pi-panels- Package
@luan.sh/pi-panels- Version
0.3.3- Published
- Sep 12, 2026
- Downloads
- not available
- Author
- cfcluan
- License
- MIT
- Types
- extension
- Size
- 25.6 MB
- Dependencies
- 2 dependencies · 2 peers
Pi manifest JSON
{
"image": "https://pi.luan.sh/media/previews/pi-panels-e69474de1ee3.png",
"video": "https://pi.luan.sh/media/previews/pi-panels-ae3c28bfa509.mp4",
"extensions": [
"./src/extension.ts",
"./node_modules/@luan.sh/pi-libtui/src/extension.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
@luan.sh/pi-panels
@luan.sh/pi-panels is a generic side-panel host for Pi's TUI. It mounts
a right-hand split pane and lets other Pi extensions contribute tabs to it.
The host owns the split layout, focus, pointer resizing, persisted width and
tab order, draggable pill tabs, the empty state, and the top-right show and
zoom controls. It contains no feature content of its own: with nothing else
installed the panel is empty. Tabs come from other packages, for example
@luan.sh/pi-side, @luan.sh/pi-tuicr, @luan.sh/pi-exec-command,
@luan.sh/pi-subagents, and @luan.sh/pi-xsettings.
Preview

Install
pi install npm:@luan.sh/pi-panels
The package ships its internal dependencies bundled and also loads the shared
@luan.sh/pi-libtui extension entry so the registries it relies on exist. It runs only
in interactive TUI sessions; it does nothing in print mode, without a UI, or in
a child process started with PI_EMBEDDED_SIDE_CHAT=1.
Optional companion: pi install npm:@luan.sh/pi-xsettings provides a
shortcut host that binds the keys in your keybindings.json globally; without
it, the panel's shortcuts only work while the panel itself has focus, and the
panel is opened by contributed tabs or the top-right controls.
Use it
The panel appears when a provider adds a tab, or when you run the toggle action. The first mount takes 50% of the terminal width; dragging the divider persists the new width. Clicking a tab activates it, dragging reorders it, and closing the last tab hides the panel until it is reopened. Two icons in the top-right corner of the screen toggle visibility and expand or restore the panel; the expand icon is shown only while the panel is visible. Resizing the divider while expanded returns the panel to its normal width.
The extension registers these actions through @luan.sh/pi-libactions. It adds no
slash commands.
| Action | Effect |
|---|---|
panels.toggle |
Show or hide the side panel |
panels.focus |
Focus the side panel (showing it first if needed) |
panels.main.focus |
Focus the main session |
panels.focus.next |
Move focus to the other split pane |
panels.zoom |
Expand the panel to the full width, or restore it |
panels.tab.previous |
Select the previous tab |
panels.tab.next |
Select the next tab |
Keybindings
None of these actions has a default key. Bindings live in keybindings.json
in Pi's agent directory (normally ~/.pi/agent/keybindings.json). The file is
a JSON object; each property name is an action ID and each value is a key ID
string or an array of key ID strings. The file is read when extensions load,
so reload after editing. An example that binds every action group:
{
"panels.toggle": "ctrl+shift+b",
"panels.zoom": "alt+shift+z",
"panels.focus.next": "alt+o",
"panels.tab.previous": "alt+h",
"panels.tab.next": "alt+l"
}
While panel content has focus, the host checks the pressed key against the
bindings for panels.toggle, panels.zoom, panels.focus.next,
panels.tab.previous, and panels.tab.next, then for contributed
empty-state actions, then for the active tab's inputActions, before
forwarding the key to the tab's component. A matching shortcut runs the action
and is not passed on, so an embedded TUI cannot receive it. Header and
empty-state buttons show the bound shortcut when one exists. Errors thrown by
an action are reported as a notification.
Contribute a tab
Providers register through @luan.sh/pi-libtui, not through this package, so
a contributor has no dependency on @luan.sh/pi-panels and must keep working when
no host is installed. The contract is exported from @luan.sh/pi-libtui:
import { registerSidePanelProvider, type SidePanelSession } from "@luan.sh/pi-libtui";
const dispose = registerSidePanelProvider(
{
id: "my-extension",
session: context.sessionManager,
attach(panel: SidePanelSession) {
panel.addTab({
id: "my-tab",
label: "My tab",
create: (host, theme) => new MyComponent(host, theme),
onClose: () => cleanUp(),
});
return () => panel.removeTab("my-tab");
},
},
globalThis,
);
registerSidePanelProvider stores the provider in a process-wide registry
keyed by Symbol.for("pi-panels/registry/v1"). When this host is present
it calls attach with a SidePanelSession, but only for providers whose
session matches the current Pi session; the returned function runs when the
provider is replaced, unregistered, or the session detaches. Registering a
provider with an ID that is already registered replaces it. When no host is
installed, attach is never called, so a provider that needs a fallback
should check ensureSidePanelRegistry(globalThis).hasHost() and, for
example, open a fullscreen overlay instead. Errors thrown by attach are
reported through context.ui.notify and do not break the host.
A SidePanelTab has id, label, an optional icon, an optional
headerAction ({ label, actionId }, shown as a dropdown button at the right
of the tab bar), optional inputActions (action IDs whose shortcuts the host
intercepts while this tab is focused), create(host, theme), and an optional
onClose called when the user closes the tab. addTab accepts
{ activate, focus } options; by default it activates the new tab, reveals the
panel, and focuses it. The SidePanelSession also offers restoreTab (re-adds
a tab without revealing the panel or persisting state), updateTab,
removeTab, activate, activeTabId, registerEmptyAction (a labelled
button shown when no tabs exist, returning a disposer), show, toggle,
toggleZoom, focus, focusMain, focusNext, activatePrevious,
activateNext, isVisible, isZoomed, and requestRender.
Settings
The package has no settings and does not use @luan.sh/pi-xsettings. Layout is
persisted per session as a custom session entry of type
side-panel:layout-v1 holding visible, width, order, and
activeTabId; the most recent valid entry on the current branch is restored
on session start. Persisted state does not include tab contents, so
providers must re-add their tabs (usually with restoreTab).
Layout
| Responsibility | Owner |
|---|---|
| Contribution protocol, registry, content types | @luan.sh/pi-libtui (panels.ts) |
| Pi registration, host install, top-right controls | src/extension.ts |
| Panel lifecycle, tab bookkeeping, split-pane mount, zoom | src/controller.ts |
| Persisted layout state and its parser | src/state.ts |
| Tab bar, header action, empty state, input routing | src/view.ts |
| Action registration | src/actions.ts |
Develop
Source: https://github.com/luan/agents, directory
harnesses/pi/agent/packages/pi-panels. Run bun run typecheck and
bun test test in that directory.