Added
-
Settings → Developer → "Pi runtime": pick which discovered pi install each
of the two consumers uses — the binary sessions spawn (pi) and the module the
server imports (pi-coding-agent). One candidate list, two selection columns,
and a "Keep both in sync" checkbox that is checked by default and DERIVED from
the install both consumers resolve to, never stored. Unchecking is the
deliberate act that permits a mismatch.Automaticis itself a row showing
what the resolution chain currently lands on, so it is never a black box and
reverting is one click. Installs belowpiCompatibility.minimumare disabled
with the required version named; installs whose version cannot be read stay
selectable with an explicit "not floor-checked" warning. Applying writes both
consumers in ONE transaction, so a crash cannot leave the runtime split in half. -
GET /api/pi/installsandPOST /api/pi/runtime, both behind the same network
guard as/api/tools./api/healthgainspiRuntime, reporting CONSUMER
divergence (the two consumers on different installs) and INSTALL-SET divergence
(more than one pi version present) under distinct labels — versions only, no
filesystem paths, since/api/healthis unauthenticated. -
Skill cards in the Resources view now carry provenance. The server joins pi's
resolved skills against what an attached session actually loaded (from the
commands_listit already sends) on canonicalized real paths, so a card reads
active,not loaded, orloaded elsewhere— the last of which finally makes
runtime-registered skills (~/.pi/agent/pi-hermes-memory/skills/, an ancestor
.agents/skillschain,--skill,skillPaths) visible, with the path the
session reported. Provenance is a per-card badge plus a grid filter value: no
new sections, groups, or nesting. When no single session has reported (none, or
more than one), or when pi's resolver was unavailable, the grid says so
explicitly instead of implying a loaded set. -
Themes discovered by pi's resolver now appear on the existing Themes page.
-
scripts/check-skill-frontmatter.mjs, a skill frontmatter guard wired into CI.
It fails only on what pi treats as fatal (missing, empty, or unparseable
description— pi drops such a skill), and warns on pi's 1024/64/charset/hyphen
limits plus a 400-character repository description budget. Every finding is
labelledpiorrepositoryso a house rule is never mistaken for pi's. -
Apple Tools (iMCP) plugin (
@blackbelt-technology/pi-dashboard-apple-tools):
one-command provisioning of iMCP (Apple Calendar, Contacts, Reminders,
Messages, Location, Maps, Weather) for pi viapi-mcp-adapter. Ships a
pi-apple-tools-install [--check]CLI (a pure, macOS-only, nine-state
provisioning machine), a dashboard provisioning panel (status readout,
run-installer, path override, directTools, server enable/disable), an
agent skill, and adoctorprobe. macOS-only; Apple Mail is out of scope
(useapple-mail-fast-export). -
pathsplugin-requirement category:PluginRequirements.pathsdeclares
absolute filesystem paths (e.g..app-bundled binaries) that must exist,
with optional${configKey}interpolation from the plugin's validated
config. Surfaced as a non-actionable warning pill in the Plugins tab. -
OAuth redirect URIs can now be pinned to a fixed public origin with the new
auth.redirectBaseUrlfield in~/.pi/dashboard/config.json. Dashboards
behind a reverse proxy on a stable custom domain (https://pi.example.com→
nginx →:8000) previously had no supported way to state their public origin:
the redirect URI was always derived from the active tunnel URL, falling back
tohttp://localhost:<port>, which every provider rejects with
redirect_uri_mismatch. The configured base now takes precedence over the
tunnel, applies to the authorize redirect and the token exchange alike, and
changes take effect throughPUT /api/configwithout a restart. A base that
is not an absolutehttp(s)origin is still used but logs a warning naming
the field, so a typo surfaces in the log instead of as an unexplained login
loop. The field affects OAuth only — pairing QR codes and
GET /api/tunnel/endpointsstill advertise the tunnel URL. The field is now
editable in Settings ▸ Security instead of by hand-editing the config file. -
One "add gateway URL" action states a public origin once and writes every
key that origin needs —publicBaseUrls,cors.allowedOrigins,
auth.redirectBaseUrl(when OAuth is selected) andtrustedNetworks(when a
trusted network is) — in a singlePUT /api/config, recording exactly what it
wrote so removal reverses that and nothing the operator authored themselves.
The dialog states the scheme rules inline rather than applying them silently:
ahttp://gateway cannot ride a pairing QR or an OAuth callback and is
reachable only through a trusted network, whose CIDR is pre-filled as an exact
/32. Each gateway row carries a computed status (OK / Incomplete /
Conflicting / Ineligible) and a Fix that restores only the missing values.
The same component is used by the first-run setup guide and the Gateway page. -
An OAuth provider can now be removed with
DELETE /api/config/auth/providers/:id, behind the same guard as
PUT /api/config. Deleting the last remaining provider is refused without an
explicit?force=true, because at runtime it produces auth enforced with no
login path — not auth disabled — and can lock out a remote operator until
the server is restarted. -
GET /api/auth/diagnosticsreports which redirect base actually won and which
tier produced it, and the same line is written toserver.logat every auth
registration and reload. A newoauth-redirect-basedoctor module reads it
over loopback, so an operator whose OAuth is broken — and who therefore
cannot obtain a token — can still find out why. -
Folder action banner with a per-artifact setup checklist. A folder that is not yet a pi project — or whose setup is incomplete — now shows a banner and a permanent
Project setup n/Nitem in the folder actions menu, with a per-artifact checklist and a broken-session cleanup action (#551) -
Default Thinking Level setting. Settings pairs a thinking-level control with the Default Model; new sessions start at that level. The control is filtered to the levels the chosen model supports and stays inert until a default model is picked (#516)
-
Fork branch-collision warning. Forking into a branch name that already exists now warns before you submit and points you at the action that works — reuse the branch as a checkout when nothing holds it, or spawn into the existing worktree when one does
-
Replay-in-flight indicator. While an older session is still streaming its history back, the chat shows a "replaying" label above the scroll controls, so a partially loaded transcript is no longer mistaken for the whole conversation. It never covers the scroll-to-bottom button, meets the 3:1 non-text contrast floor, and drops its spinner under
prefers-reduced-motion(#458, #501) -
Notification level filter. Extension
ctx.ui.notifyrows can be muted by level (all / success / warnings / errors); errors are always shown. Notification rows were re-toned onto the shared severity tokens so their level is readable in light themes and no longer carried by colour alone (#453) -
Drag folders across workspaces. Folders can be dragged into, out of, and between workspaces, and the collapse chevron doubles as a drag handle so a collapsed folder can still be reordered (#411)
-
Discord community link next to the settings gear in the sidebar app bar
-
Bounded session replay. Reopening a long session no longer replays its entire event stream: the server serves a head/tail window and the client backfills the middle on demand as you scroll up. The window size is configurable under Settings → Memory Limits (
maxReplayEvents, now defaulting to2000;0restores unlimited replay), and a new tail-only window mode drops the head segment entirely so a session opens straight at its most recent activity (#523, #535, #555). -
Automations can fan out into several concurrent sessions. One trigger fire now expands into N child sessions via a multi-action editor (with a per-action count), bounded by
maxConcurrentSpawns. The board discloses the parent run with its child rows and warns when the list is truncated; stopping the parent cascades to the children, and the parent aggregates their outcomes and findings (#517). -
A thinking level can be picked wherever a model is picked. The Roles panel and the Create Automation dialog now pair a thinking level with the model selector, matching the composer and the OpenSpec run-config row. The level rides the model reference as a
:<level>suffix, so a/roles-written value and the dashboard cannot drift apart. Picking a model that does not support the current level drops it and says so; an@roleautomation shows only a hint naming the role that owns the level. -
A dedicated
@namingrole for automatic session naming, selectable in the Roles panel and falling back to@fast. The auto-name toggle points at it, and Diagnostics now shows why naming stopped for a session (truncated output vs. no topic yet), served fromGET /api/auto-name-outcomesso an outcome reported before the browser connected is still visible. -
Pick your own zrok reserved name in Gateway Setup, validated at set time against your zrok account, with
taken/invalid/write-failedreported as distinct reasons instead of one silent failure. Release and replace are confirm-gated and name the exact URL being destroyed, the old name is released only after the new reservation succeeds, and a banner reports a tunnel serving a different name than the one stored. The Setup tab also gains a live provider-readiness board (installed / enrolled / connected per provider, refreshed while the tab is open) that replaces the previous hardcoded chip list, plus a confirm-gated "Make primary" action and an offer to register a connected provider's URL. Multiple tunnel providers can now run at once via per-providertunnel.<id>.enabled/.mode, withtunnel.provideras the primary that OAuth and QR pairing resolve against; CORS accepts every connected tunnel origin. -
Manage worktrees: a filterable worktree list with a cleanup surface that works even when no session is attached — remove worktrees individually or in a batch, delete the branch, and prune stale entries, with per-entry existence checks, retry on force-retryable failures (including
branch_not_merged), and an escalation to the close dialog when a worktree still has active sessions (#512). -
Trusted-network reachability advisory: binding the dashboard to loopback or a single NIC silently voids any trusted network outside that range — the peer is refused at the TCP layer, so nothing is logged and the Trusted Networks panel stays blank forever. The server now emits a
[bind-reachability]startup warning, reports areachabilityobject on the guardedGET /api/config, and the UI shows an advisory with an inline0.0.0.0remediation, a Server-page link and a pending-restart hint. When--hostorPI_DASHBOARD_HOSToutranks the setting the advisory names the deciding source instead of offering a control that cannot clear it (#483). -
Provider quota plugin (
@blackbelt-technology/pi-dashboard-quota-plugin, #540): a bundled, disabled-by-default plugin that surfaces your provider subscription usage in the dashboard footer. Quotas are fetched server-side — the tokens never leave the server — and each provider is opted in individually behind a terms-of-service acknowledgement. Supports Anthropic, OpenAI Codex, GitHub Copilot, OpenRouter, Z.ai, Kimi Coding and Synthetic with no peer extensions to install: the plugin owns each provider's usage contract itself. Anthropic OAuth tokens (sk-ant-oat01-…) are now recognised instead of being dismissed as plain API keys, a failed refresh keeps the last reading marked "not live" instead of making the provider disappear, and a ticked provider that returns nothing now states why (no credential / no data / unsupported).opencode-go, DeepSeek and MiniMax are deliberately not supported — they expose no resetting usage window. -
Cost estimator (
@blackbelt-technology/pi-dashboard-cost-estimator): a new package and dashboard plugin that estimates developer cost and effort for a set of use cases, requirements and a tech stack — Use Case Points sizing, effort split across 11 roles, four delivery modes compared, Monte Carlo ranges and an NPV/ROI business case. Reached from thecostcommand; the report also folds in measured delivery cost read from your own session history viaGET /api/cost-estimator/telemetry. Cost basis defaults to subscription seats rather than metered API pricing. Replaces the former globalsoftware-cost-estimatorskill install. -
Per-plugin settings pages (#413): every plugin that contributes settings now gets its own page at
/settings/plugins/<id>under host-owned chrome, listed in the settings nav rail. Deep links to a plugin settings page now resolve instead of falling through to the dashboard root, the Save Bar names every page with unsaved changes, and a plugin you just enabled or disabled shows a restart-required banner rather than an unexplained empty page. -
Blackhole plugin settings (#448): a new
blackholedashboard plugin that reads and writes thepi-blackholeextension's on-disk config from Settings — scalar fields plus keyboard-operable ordered fallback-chain editors. It shows a distinct state when the extension is not installed, and refuses to write at all when the config file cannot be parsed, so a malformed file is never silently overwritten with defaults. Keys it does not manage keep their value and position, and it reports when another process changed the file. -
Seven more recommended extensions:
cost-estimator,code-review-toolkit,openspec-workflow,distill-session-knowledge,frontend-patterns,video-productionandnano-banananow appear in the dashboard Packages tab for one-click install. -
Resizable BPMN explorer layout: the diagram canvas and form panel are separated by a drag handle (mouse, touch and arrow keys) with a minimum panel width, and render roots are served by a bundled CORS-enabled static server so the canvas iframe no longer shows a blank diagram with "Failed to fetch".
-
Grammar and spell check in the chat composer — an opt-in, LLM-backed grammar checker for message drafts, shipped as the
grammarplugin. Offers an inline redline view or a before→after list of corrections, one-click apply, a ⌘/Ctrl+G shortcut, a Settings → Grammar section (model picker with recommended models, correction view, accordion-grouped options), and the same panel inside the OpenSpec Explore and New Change dialogs. Existingconfig.grammarsettings migrate automatically toplugins.grammar. Seedocs/grammar-checker.mdanddocs/grammar-model-guidance.md. (#401) -
Stateless MCP endpoint at
POST /mcp— the dashboard can now be driven as an MCP server (protocol revision2026-07-28) with a curated tool surface, per-session bearer tokens minted over the bridge, a self-targeting guard so a session cannot drive itself, and rate limiting. (#470) -
Per-provider health in Settings → Providers — each provider card shows a health pill (connected with model count / HTTP status / unreachable / not tested) plus the verbatim provider error on a monospace line. Health is probed when you save a provider and when you press Test. (#484)
Changed
-
GET /api/git/worktree/init-statuspayload.configured?: booleanis
superseded by a per-artifactchecklist(settings,agents,prompts,
openspec,kb; exactly one required:settings), computed on every
response at the config root and uncached.configuredis still emitted but
deprecated — consumers must read the checklist first. A new
setupOutdated?: booleanis declared (nothing emits it yet). Client and server
ship together; a client that cannot interpret the payload degrades to no
banner, never a false one. -
BREAKING (dashboard plugins):
SlotPill.actions?: ReactNodeis removed.
A plugin can no longer render arbitrary markup into a directory-card slot pill;
the pill is state-only. Passingactionsis now a compile-time error, with no
runtime shim that accepts or converts it.Plugins instead contribute declarative items to the folder actions menu:
useFolderMenuItem(scope, { id, group, label, icon, onSelect, badge?, disabled? }),
plususeFolderMenuRefresher(scope, refresh)for a callback with no item of
its own.groupis one of the host-owned verb taxonomy
workspace · directory · create · open · maintenance. Migration guide:
docs/migration/slot-pill-actions-to-folder-menu.md. -
The directory card's ten slot-pill action buttons became menu items. The
per-slot Automations/Goals/OpenSpec refreshes collapse into one "Refresh
folder" item, the KBReindex/Index now/Retrycontrols collapse into
one state-varying item that keeps its stale badge,+ Automation/+ Goal
move toCREATE, and the OpenSpec Archive/Specs shortcuts move toOPENwith
slot-qualified labels. Every pill is now a single click target that reads a
number; the menu is what changes something. -
BREAKING (macOS): the desktop app now requires macOS 12 (Monterey) or newer.
macOS 10.15 (Catalina) and 11 (Big Sur) are no longer supported.The Electron runtime moved from
32.3.3to43.4.1. The 32.x line is
end-of-life and receives no security backports, and the desktop app ships a
full Chromium to end users and auto-updates it into every install — so staying
on it meant shipping ~22 Chromium majors of known renderer CVEs in a released
artifact. Every currently-supported Electron line requires macOS 12, so the
floor move is not optional: Electron dropped 10.15 at v33 and 11 at v38. Both
are also past Apple's own security window (Catalina EOL 2022-09, Big Sur EOL
2023-09).If you are on macOS 10.15 or 11: you keep the version you have installed
today and will not be offered this or any later update. The update stream
is gated on the minimum supported OS (minimumSystemVersionin the published
latest-mac.yml), specifically so your client does not download a build macOS
would then refuse to launch and retry it forever. There is no action to take,
and nothing breaks — your current install keeps working. To move forward you
need macOS 12 or newer.Intel Macs are not dropped: the
darwin/x64build is retained, and Apple
supports Intel hardware on macOS 12+. Windows and Linux are unaffected. -
tmux sessions now launch the pi the dashboard resolved, not the first
pion
the shell'sPATH.buildTmuxCommandreturns an argv array and the tmux CLI
is invoked without a dashboard-side shell. This aligns tmux with the headless
and Windows Terminal mechanisms, which already resolved through the tool
registry — but it is a real change on the DEFAULT interactive path for macOS and
Linux, and it applies whether or not you ever open the new picker. If your shell
pidiffers from the dashboard's, those sessions change which binary they run.
WSL-tmux is unaffected: it still resolvespiinside the WSL namespace. -
A pre-existing
pi-only override now surfaces as divergence. Setting api
path override through Settings → Tools without also settingpi-coding-agent
has always split the runtime in half; nothing reported it before. After this
release that state opens the new section unchecked with a banner naming both
versions. This is a disclosure of an existing condition, not a regression, and
the banner offers one-click re-linking. Your existing pin is never overwritten
just by opening the section. -
PUT /api/tools/piandPUT /api/tools/pi-coding-agentnow validate the path
before persisting it: it must exist and must not be a directory (a directory is
illegal for both consumers). The 400 names the check that failed. An executable
with no adjacentpackage.json(a Windows.cmdshim) is still accepted, with
an unknown version. No other tool's override behaviour changes. -
Upgraded the pinned pi runtime to
@earendil-works/pi-coding-agent@0.84.1.
piCompatibility.recommendedmoves to0.84.1, the server dependency to
^0.84.1, and the Docker image pin to@0.84.1. The broad-support floor
piCompatibility.minimumdeliberately stays at0.78.0, so pi 0.78–0.84.0
users see a soft upgrade hint and never a blocking error. -
Custom models in
models.jsonmay now declare arbitrary OpenAI-compatible
samplingParams(including opt-in vLLMthinking_token_budget). The field is
forwarded opaquely to pi and omitted entirely when absent; it was previously
dropped by both metadata consumers. -
AGENTS.override.md(pi 0.84.0's per-directory context override) is now
recognised by the kb tooling: it shadows a siblingAGENTS.md/CLAUDE.md
within the same directory when walking the agents chain, and is indexed as a
context file rather than ordinary markdown. Ancestor inheritance is unchanged. -
The Resources view now sources skills, prompts, and themes from pi's own
resolver (PackageManager.resolve()) instead of a parallel filesystem walk,
and applies pi's load gate on top. Scope and package origin come from the
resolver's metadata as per-card attributes.extensionsandagentsremain
scanner-discovered (pi has noagentsresource type). -
Skill descriptions across the repository were trimmed to the new 400-character
budget, preserving their trigger phrasing.ship-change,
frontend-mockup-loop, andanti-slop-frontendare exempt: an existing
requirement locks their wording. -
Package resources excluded by a package's own manifest patterns are now absent
from the Resources view rather than shown as disabled, matching pi, which does
not load them either. There is consequently no activation toggle for them. -
pairing.publicBaseUrlsis promoted to a top-levelpublicBaseUrls, read by
the pairing payload and the endpoint surfaces alike. Existing configurations
keep working untouched: the legacy key is still read when the top-level one is
absent, and no config file is rewritten on read. The publicly-trusted-TLS gate
is unchanged — it stays authoritative at read time, so promoting the key
cannot leak a plain-http address into a QR code. A legacy-sourced value never
feeds OAuth: that value was chosen to answer a different question, and an
OAuth redirect URI must be one origin the operator states explicitly. -
Settings, folder settings and file previews now open as overlay dialogs over the surface you launched them from, instead of replacing the page. The URL still points at the overlay, so the view is linkable and reloadable (#536)
-
Directory rail for session cards. The per-card status gutter is replaced by a folder-level directory rail; session status now rides the inline status chip alone, and each card gains ~16px of content width. Dragging a session uses a hover-revealed bead in the rail
-
One folder status capsule. The folder header's session count, "needs you" pill and collapsed-only status rollup are replaced by a single severity-ordered capsule (needs-you › error › working › idle) that renders in both collapsed and expanded states, with each segment navigating to its first matching session (#446)
-
Model selector stays openable when it has no models. Opening it triggers a provider refresh and, if still empty, offers an "Open provider settings" link, so a provider configured after session start can be recovered in place. Provider refresh failures are surfaced as a count with a link to Settings › Providers (#466, #485)
-
Settings pages reorganised, and every field now carries a description rendered under its control and wired up for screen readers; labels are properly associated with their controls (#430)
-
Retry banner is observe-only. It shows the retry status while pi retries (attempt number plus countdown) and disappears on a clean resume. The redundant "Stop retrying" control and the collapse pill are gone; dismiss, Copy and the settled-error status line remain
-
Provider errors read as
type: message. A raw provider JSON envelope is now rendered as e.g.overloaded_error: Overloadedinstead of the raw blob; non-JSON messages pass through unchanged -
Add-to-workspace affordance is a labelled pill. The cryptic
+wslabel on top-level folder cards becomes an icon-plus-"Workspace" pill, rendered inline in the header action row so it no longer overlaps the open-home and pin icons -
Warm replay no longer ships superseded snapshots. Reopening a large in-memory session replayed every intermediate assistant
message_update(each carrying a full content snapshot); those are now compacted away before send, and replay batches are larger, so a big session reopens dramatically faster with an identical transcript (#399, #402). -
Subagent progress updates got much cheaper. Intermediate ticks no longer carry the whole cumulative subagent timeline — the full detail is fetched on demand when you open the subagent inspector, and the bridge can throttle tick frames (
subagentTickThrottleMs). The server additionally collapses supersededtool_execution_updateevents at retention, so a long subagent run stops crowding out real history in a session's buffer (#460, #502, #511). -
Reload has one honest entry point. An out-of-band reload of a session now routes through a single server-side path that kills and respawns the session process, forwards to the bridge when there is one, refuses while the session is busy, and reports the real outcome instead of always claiming success (#524).
-
POST /api/session/:id/promptreportstransmittedplus apromptIdinstead of asserting adelivered: trueit could not actually prove; the id rides out with the prompt and comes back on acknowledgement so a caller can confirm delivery (#522). -
OpenSpec auto-attach is scoped to the session's own working directory. A command that relocates outside the session cwd (
cd,pushd) no longer triggers auto-attach, and a refused attach now says so instead of failing silently (#461). -
Retry is a first-class protocol message. The Retry button sends a typed
retry_sessionmessage end-to-end instead of a/__dashboard_retrysentinel prompt, and a failed retry returns an explicit error instead of nothing. Manual retry is guarded while an automatic retry chain is already armed (#539). -
The bridge between
piand the dashboard now talks over a per-instance unix socket by default (gateway-<port>.sock, mode 0600), with no bridge TCP port bound at all on macOS and Linux; TCP is opt-in viaPI_GATEWAY_TCP=1and remains the default inside the container. Sessions find their dashboard through a$HOME-rooted rendezvous record with a fixed precedence order (explicit setting > pinned endpoint > record > paired remote; mDNS discovery may only suggest, never override a pin), and verify the dashboard's instance id and Ed25519 identity before registering — a mismatch disconnects and names both ids instead of quietly adopting a substitute. Two dashboards sharing one$HOMEno longer collide, and a dashboard that cannot claim the default attaches instead, taking over automatically if the owner dies. A/api/restartthat briefly takes/api/healthdown no longer tears healthy bridges down. -
Sessions can be moved between dashboard instances:
/dashboard-connecthands a running session to another dashboard with an overlap-then-commit handover that completes only once routing has actually transferred,/dashboard-wherereports the current one, instances can be named and listed, and the origin renders the move rather than a disconnect. A moved session's transcript is retained server-side, so history stays readable from the new dashboard. -
The automation plugin reconciles its config file watchers incrementally instead of closing and reopening every recursive watch handle on a timer, and rescans on a 15s (was 2s) debounce — measured 7× fewer re-arm cycles on a large workspace.
Security
-
Fixed a command-injection vector in tmux session spawning.
buildTmuxCommand
interpolatedcd <cwd> &&into a double-quoted, shell-executed string, so a
workspace path containing$(…)or backticks executed its contents when a
session was spawned into it. The tmux invocation is now an argv array with the
cwd as a literal-celement, and the redundantcdprefix is gone. Values
interpolated into the pane command are still shell-escaped, because tmux runs
that argument through a shell of its own. Tracked asfix-tmux-cwd-command-injection. -
Two live bridges could register the same session id. The gateway's connection map was last-writer-wins, so a newcomer silently displaced the incumbent and every server→extension message — prompts included — went to the loser of that race while diagnostics still reported the session healthy. Ownership is now decided at first-message identity, before any registration side effect; messages from a socket that does not own an id are dropped, contention is resolved by a bounded 5s liveness probe (a busy bridge is never sacrificed), the loser receives
register_rejectedand stops retrying, and resume is refused when a live bridge already serves that transcript./api/healthreportsbridgeContentionCountandcontendedSessionIds(#469). -
The bridge TCP listener accepted any peer that could reach it and let it register an arbitrary session id — harmless while it was loopback-only, exposed by the container's
0.0.0.0:9999default. Remote TCP upgrades now require a single-use bridge-scoped ticket, checked before the socket exists; unix-socket connections stay uncredentialed (the kernel already enforces same-user) and loopback TCP keeps a bounded deprecation grace. That grace previously trustedreq.socket.remoteAddressalone, so any locally-terminating relay (zrok,ssh -L, socat, an nginx or Docker userland proxy) let an internet peer present as127.0.0.1and open an uncredentialed bridge; it now also requires the absence of proxy-forwarding headers. Bridge tickets can no longer be minted by any trusted-network or cookie-session caller — only a paired-device bearer or a genuinely local caller. -
Session move could be used to hijack another session's routing. The commit frame carried both a session id and a token, and the handler honoured the wire id — so a mover could open a provisional registration for a throwaway id, commit naming a victim, and take over the victim's routing; a mismatch is now refused outright. A move also granted more than a plain registration did: it could take a session out of a live socket's hands, bypassing the contention and liveness checks. Both paths are closed, and remote-origin sessions are now genuinely attributed — the origin gate that guards local transcript reads and resume was implemented but never wired, so every session read back as local. Origin is derived at registration from transport, peer and credential, never from what the bridge claims about itself, and an unattributable remote peer is treated as remote rather than local. The same gate is applied to
/api/session-fileand to the browser-busresume_sessionpath, which previously relied only on the client hiding the Resume button. -
Retained remote transcripts were an unbounded, symlink-following write. Content driven by a possibly-remote bridge was appended without a cap (a disk-fill path reachable by a paired device) and written with
writeFileSync, so a same-uid process could pre-plant<sessionId>.jsonland redirect that content to an arbitrary file. Retention is now capped at 64 MB per session and the file is created exclusively; credential, lock and metadata files are likewise created exclusively at mode0600instead of following symlinks and inheriting the umask. -
OAuth redirect-base handling leaked secrets and mis-set the session cookie. A
redirectBaseUrlcarrying userinfo (https://user:pw@host) was accepted silently and travelled in the provider'sredirect_uri, landing credentials in provider request logs and browser history — it is now warned about by name. The invalid-value warning logged the configured URL verbatim, so a value carrying both userinfo and a query printed the password and the query token; redaction now covers every warning path and preserves scheme, host, path and query keys so the warning is still actionable, and it can never reach the minted URI. A bare?or#passed validation while still corrupting the built redirect URI, and is now rejected. The session cookie'sSecureflag derived fromrequest.protocol, which is alwayshttpbehind a reverse proxy — exactly the deploymentredirectBaseUrlexists for — and now derives from the resolved redirect base. Fastify'strustProxywas deliberately not enabled: it would rewriterequest.ipfromX-Forwarded-For, which both the auth bypass-host check and the trusted-network guard read, letting anyone who can reach the port forge a trusted address and pass the gate onPUT /api/config. A test now fails iftrustProxyis ever turned on. -
Worktree operations (
git worktree remove,prune, branch deletion and porcelain reads) are executed in argv form instead of through a shell, so a branch or worktree name can no longer be interpreted as shell/cmd.exesyntax (#512). -
/api/tunnel-statusis unauthenticated and was exposingdegraded.configuredName— a reserved name the operator owns that is by definition not in the served URL, and therefore not already public. The ungated route redacts it and the dialog reads a gated/api/tunnel-status-detailinstead.
Fixed
-
Live flow and subagent events never reached the dashboard, and every automation
whose action isflows.runhung until the stale-run reaper wrote
run exceeded max age— a failure that never happened. The bridge forwarded
EventBus traffic by monkey-patchingpi.events.emit, but pi gives every
extension its OWNeventsfacade over the shared bus, so the patch only ever
observed the bridge's own emissions: pi-flows'flow:complete(and every
flow_*/subagent_*channel) was silently dropped, and flow cards were being
rebuilt from persisted transcript replay rather than live events. Forwarding is
now onepi.events.onsubscription per declared channel, which observes every
emitter. Aflows.runrun now finalizes in seconds from the forwarded
completion event; the reaper is a backstop again. Each finalize now logs the
path it took (completion-event/agent_end/session-death/reaper) so
a delivery outage can no longer masquerade as many independent timeouts. -
/api/healthcould report the wrong running pi version and raise a spurious
"upgrade recommended" hint. Several workspaces declare a broad>=0.80.10pi
range while the server pins an exact one, which under pnpm's hoisted linker
resolved two copies — and the version probe read the hoisted one rather than
the version actually in use. Resolution is now pinned to a single version via
a workspace override; no declared floor changed. -
The session auto-namer no longer mistakes a header-deletion marker for a
credential. pi 0.84.0 returns provider headers whose values may benullto
suppress a header (used to stop a placeholder OpenAI key reaching Cloudflare
AI Gateway); the namer counted those keys as usable credentials and issued a
request with none, and now forwards the markers unchanged instead. -
Model-catalogue refreshes no longer report success when they failed. pi
0.84.0 madeModelRegistry.refresh()asynchronous and result-bearing, so the
dashboard was reading the catalogue before the refresh resolved and
discarding per-provider errors in a barecatch. Failures are now surfaced
with the provider named, a credentials reload refreshes only the providers it
touched, and cancellation is distinguishable from success. -
OAuth token refreshes are now cancellable. pi 0.84.0 requires a concrete
AbortSignalon the refresh callback; the dashboard passed none, so a
provider that never answered would hang every request routed through that
credential. Refreshes are now bounded, and a late answer after the deadline
can no longer persist a credential the caller discarded. -
pi install git:github.com/BlackBeltTechnology/pi-agent-dashboardworks again
(issue #357). It was failing at two independent points. First, the Node engines cap
refused Node 26: pi installs with engine-strict, so the install aborted with
EBADENGINEbefore anything ran. The cap is raised one major,>=22.19.0 <26
→>=22.19.0 <27, and Node 26 is now a CI-validated target — the new
_smoke.ymlNode 26 install legs deliberately run without the
--config.engine-strict=falseoverride the other legs carry, so the
EBADENGINEregression is tested rather than assumed. Node 27+ stays refused
until separately validated. Second, the git-clone path runs
npm install --omit=dev, which dropsdevDependencies; the web client's
prepareVite build then died withCannot find module 'vite/package.json'.
Its direct build-time requirements —vite,@vitejs/plugin-react,
@tailwindcss/vite,tailwindcss, andtsx— are now runtime
dependencies, withtsxdeclared explicitly instead of resolving by accident
through a hoist of the server's copy. A release gate plus repo-lints keep both
fixes from silently regressing. The published npm install path was never
affected (it ships a prebuilt client and runs noprepare).
(change:fix-pi-install-node26-and-omit-dev-build) -
Phantom skills are gone from the Resources view.
UPSTREAM.md,
dox-doctrine.md,AGENTS.md, and*.AGENTS.mdwere being listed as skills
the dashboard's own walk had invented; pi never loaded them. Files beneath
.worktrees/and inside a built Electron bundle are likewise no longer
reported. -
resolveActivation()is now bounded by a 5-second timeout and falls back to
the filesystem walk when pi is unavailable, resolution throws, or the resolver
returns an empty result the walk contradicts. The payload is flagged as a
degraded fallback rather than presented as pi's answer. -
Origins and trusted networks written while the server is running now take
effect immediately instead of at the next restart. The CORS decision and the
network guard read the configuration at request time (mtime-gated, so an
unchanged file is parsed once), which is what makes the gateway action's own
claim true — previously a newly added origin stayed denied and the browser
aborted every module script loaded from it. -
The runtime auth reload no longer drops top-level
trustedNetworks. Any
PUT /api/configcarrying anauthblock silently disabled them until the
next restart; the reload now merges them exactly as boot does. -
Pasted images are no longer silently dropped. Large image attachments used to blow the event size ceiling and collapse the whole message to nothing. Inline images are now fitted to a bounded display copy, rendered in two phases (row first, image after), and clicking one opens the full-resolution original (#419)
-
Inline terminal cards keep their transcript after the shell exits. A terminal whose shell ended before you closed the card used to freeze empty; the transcript is now preserved, closing is idempotent across two browsers, and a terminal you never typed into no longer leaves a blank tombstone card behind (#407)
-
A session no longer renders as a single row after reload. The replay cursor is only persisted when it actually descends from a replay, gaps in the live stream stop the cache being reused, and cached entries are scoped to the server they came from and purged on reset. Already-affected browsers self-heal on upgrade without clearing site data (#445, #482)
-
Concurrent
ask_userprompts both appear. Two prompts sharing a title no longer drop the second one and hang the tool until its 5-minute timeout; concurrently pending prompts are grouped into one panel with independently answerable cards (#538) -
Folder headers no longer show a worktree child's branch as the parent folder's branch, and a browser connecting after the folder was cached now receives the authoritative folder branch instead of a stale one (#525)
-
The Default Model picker works with no live sessions. It now reads the machine-wide model catalogue rather than a union over per-session model pushes, so a fresh dashboard can set the default at all; the last good catalogue is kept when a refresh fails (#518)
-
Back from an overlay returns to the folder you opened it from, not the top-level card list. Adds an explicit
presentation: "page" | "dialog"contract for plugin claims -
Overlay stacking and clipping. A named z-index layer scale plus portalling fixes the folder actions menu being drawn under neighbouring overlays, and the folder header card no longer clips the open menu to its own bounds (#515)
-
Flush dialogs scroll and their close button no longer collides with the dialog's own header controls — five of six overlay surfaces previously clipped their content with no working scrollbar at all (#541)
-
Worktree pill no longer repeats the branch name. It renders a bare
worktreelabel, so a long branch can't wrap inside the pill and overlap the GIT label above it (#546) -
Compaction markers fit the compact context bar. They render as a muted collapse glyph with the detail in the tooltip instead of a text pill that squeezed the bar to a stub; the full view keeps the amber text pill
-
Thinking-level chip matches the model chip's font size in the composer toolbar
-
A session that crashed while streaming can be reopened again. A dashboard-spawned session whose process died (crash, OOM, kill) stayed stuck at "active" with no way back — sending dropped the prompt with "no bridge connection" and resume refused with "already active". Sending or resuming such a session now detects the dead process and reopens it.
-
Images attached to a queued follow-up prompt now reach the model. A prompt sent mid-turn with an attachment silently lost its images between send and delivery: the chip rendered the text, the model never saw the picture. The follow-up queue now carries attachments, shows an image indicator on the queued chip, and every refusal (queue full, size ceiling) reports itself instead of failing quietly (#543).
-
A prompt no longer hangs at "sending" with the composer locked. The bridge's inbound message pump could wedge on a model-catalogue refresh that never settled, stranding every later prompt; the pump is now serialized with a bounded queue and a timeout, control messages bypass the queue, and an unacknowledged prompt after 30s is marked failed with its text preserved instead of silently dropped. This also closes the
set_model→send_promptrace (#412, #467). -
"Needs you" survives a bridge reconnect. A session blocked on
ask_userlost that state on every re-register and rendered "Thinking…" forever while actually waiting on you; it could even be reaped as idle. Also fixes a restarted server rebinding the bridge gateway on the wrong port, which made sessions vanish after a restart (#423). -
A
ctx.ui.notifytoast is no longer treated as an unanswered question. Notifications travel on their own protocol message, so they stop producing a permanent "Needs you", a false unread dot and a sidebar reorder — and they are now retained per session (cap 50), survive a bridge reattach, and replay when you reopen the session (#429). -
Tool-result error cards are readable in light mode and render their output properly. Seven error surfaces shipped dark-tuned red literals that failed the 3:1 contrast floor on light themes; they now derive from severity tokens, with severity on the card chrome rather than a flat red wash over the body. A runtime error body renders as a real command block with an
exit <n>badge and labelled stdout/stderr sections instead of raw markdown, bounded to the last 200 lines like the success path, with links using the theme-aware link colour (#465, #530). -
Raising
spawnRegisterTimeoutMsno longer breaks spawn auto-select. The follow-up timeouts were hardcoded at 60s, so raising the register timeout guaranteed the correlation expired first: the spawn banner cleared but no card appeared until a refresh. Every downstream window is now derived from the timeout that actually armed the watchdog. Also fixes a dashboard-spawned session being filtered out of its own sidebar, and two concurrent spawns in the same directory clobbering each other (#522). -
Shutting down or reaping a session actually kills its process. Shutdown only reached headless-specific kill paths, so a tmux-spawned session was unregistered and removed from the dashboard while its
piprocess kept running; every spawn strategy is now terminated by the PID the server already stores, and the idle reaper uses the same path. Spawns that never register are reclaimed (#462). -
Every ended session carries an end time.
endedAtis derived from evidence — last activity, then transcript mtime, then start time — instead of being left empty or stamped with the moment the server happened to reconstruct the session from disk (#463). -
Custom providers keep the model metadata their endpoint advertises. Discovery reduced
/v1/modelsto bare ids and then re-guessed capabilities from API-type floors, so a 1M-context reasoning model behind a proxy showed as 200k with reasoning disabled. Advertised values are now preserved and merged as their own tier (nativemodels.json> endpoint-advertised > API floor), keyed on the response shape so an OpenAI-shaped body from ananthropic-messagesprovider still maps (#537). -
Clearing a session's display overrides takes effect immediately in every open browser, and the "modified" pill turns off without a page reload (#554).
-
Backfilled history lands in the right place. Scrolling into a history gap could loop on "out of range", double-shrink the gap, splice older events below the tail, or jump the scroll position; tool rows whose result was never loaded now show an explicit "result not loaded" state instead of spinning forever. Editing one Memory Limits field no longer pins a default you never chose (#535).
-
A standalone
pi-dashboardstart now gets the same 8GB heap ceiling the bridge-launched server uses, instead of running at Node's ~4GB default where a large event store could hit a fatal out-of-memory crash with no restart. -
A dashboard started with a temporary HOME can no longer shadow the real one. An isolated/test/worktree server binding
127.0.0.1:8000silently took overlocalhost:8000on macOS, making the live dashboard appear to lose all its sessions; such a server is now remapped to an ephemeral port with a warning, even when--port 8000was passed explicitly. -
The dashboard extension loads under Bun-based hosts again. A TypeScript cast defeated jiti's
import.metaerasure, so the module was re-imported through adata:URL that Bun rejected withNameTooLongbefore the agent could start. A repo gate now fails the build ifimport.metasurvives transpilation in any jiti-loaded first-party source, anddocs/faq.mddocuments theJITI_ESM_EVAL_TEMP_FILE=1escape hatch (#408, #478). -
The project's
.pi/settings.jsonpinned thekb-extensionpackage to an absolute path from one machine, so on every other checkout the extension silently never loaded. Sources are now relative to the settings file, and a CI guard (scripts/check-pi-settings-paths.mjs) rejects absolute or~-rooted local sources while leavingnpm:,git:,https:andssh:specifiers alone (#371, #550). -
A
pisession started inside a git worktree could leave a zombie server holding the shared pi-gateway port (observed: nearly six hours, dashboard port never bound). Server startup now runs under a deadline and tears its listeners down if a later step throws or hangs, auto-start is single-flighted per user and port with an advisory lock, and a worktree-resolved server refuses to auto-start on the shared default ports8000/9999, logging the refusal (#480). -
A keeper-backed session could permanently lose the
piprocess id it was tracking, degrading every later reattach to a positional match that could terminate the wrongpi. The keeper now writes a PID sidecar next to its socket (removed on shutdown) and the registry refills a missing id from it, only ever surfacing a pid that names a live process (#500). -
The cost report no longer hijacks every chat. The cost-estimator plugin claimed the whole content area unconditionally, replacing the chat view in every session on every desktop client with no header, tabs or way to dismiss it. The report is now reached only through its
costcommand, which renders the same view. -
convert-pdfaccepts an output path again (#507): the documentedconvert-pdf in.docx out.pdfform never worked — both paths were read as inputs, so every PDF render failed withFile not found: <output>.pdf. Output is now an explicit-o/--outputflag, with the positional form still recognised where it cannot be confused with a batch conversion. -
Documents with a copied template cover page open in Word again (#506): the cover page carried relationship ids from the template that meant something different (or nothing) in the generated file, so Word and LibreOffice refused the file with "source file could not be loaded". Relationships are now remapped by target rather than by number — which also stops a "first page header" silently pointing at a footer — headers or footers the output cannot satisfy are dropped, and a file with a dangling reference is never written at all.
-
Disabling a global skill for one project now actually disables it. The dashboard previously reported success while pi kept loading the resource, because the exclusion was written in a form that matched nothing for a resource resolved from
~/.pi/agent. The correct form is now written for each origin, re-enabling removes the exclusion cleanly without touching entries you authored yourself, an untrusted folder returns a trust prompt instead of a false success, and an unparseable settings file fails loudly instead of being skipped. (#422) -
pi-forms-bpmndownloads are ~660 KB instead of 164 MB: a nestednode_modulestree and a demo build were being packed into the published tarball. -
Scheduled automations with long gaps no longer fire in a loop — cron delays past the 32-bit
setTimeoutceiling (~24.9 days, i.e. monthly/quarterly/yearly schedules) were clamped to ~1ms and fired immediately and repeatedly. Long waits are now split into bounded hops that recompute against the absolute target. (#552) -
npm:packages installed into pi's managed directory (~/.pi/agent/npm/node_modules) now resolve correctly — previously only the legacy global npm root was checked, leaving modern installs unresolvable. -
Dragging a card on the OpenSpec board drops it where you aimed — drop placement now uses one direction-independent midpoint rule, with a clearer drop indicator. (#438)
-
Text selection in the chat no longer runs backwards when a tool card above the selection grows mid-drag; the drag-origin row is pinned for the duration of the selection. (#439)
-
Crash recovery reinstalls with the repository's own package manager — the recovery server ran
npm installon every monorepo, which corrupts a pnpm hoisted workspace and could leavepiunable to boot (sessions dying withREGISTER_TIMEOUT). It now detects pnpm and uses it. -
Tunnel startup can no longer hang silently — a failure during tunnel setup was swallowed and the operation never settled; errors are now reported. Unhandled promise rejections in the web client and the Electron main process are also logged instead of disappearing. (#432)
First-launch unblocking (unsigned binaries)
The Windows installers and macOS DMGs are not yet code-signed / notarized.
Both OSes will block first-launch with a security warning. These are not
malware — the artifacts are the exact ones produced by
.github/workflows/publish.yml
against this tag. Pick whichever workaround fits your workflow.
Tracking: Authenticode signing → change windows-authenticode-signing;
macOS notarization → change macos-notarization (planned). This section
will shrink and eventually disappear as each lands.
Windows — SmartScreen warning
SmartScreen will show "Windows protected your PC" the first time you
run any .exe artifact (Setup, or any .exe extracted from a ZIP).
Option A — at the SmartScreen dialog:
- Click More info.
- Click Run anyway.
Option B — pre-clear the Mark-of-the-Web:
- Right-click the downloaded
.exe(or the.zip) → Properties. - At the bottom of the General tab, tick Unblock next to
"This file came from another computer...". - Click OK and run as normal.
For ZIP archives, unblock the archive itself before extracting so
the contained .exes inherit the cleared zone.
macOS — Gatekeeper / quarantine
macOS will refuse to launch the app on first run with "PI Dashboard
cannot be opened because the developer cannot be verified" or silently
quarantine it.
Option A — control-click the app:
- Open the DMG and drag PI Dashboard to Applications.
- In Applications, right-click (or Control-click) PI Dashboard →
Open. - Click Open in the confirmation dialog. Subsequent launches are
unrestricted.
Option B — clear the quarantine attribute from the terminal:
xattr -d com.apple.quarantine "/Applications/PI Dashboard.app"If the DMG itself is being blocked, clear it on the mounted volume
before copying:
xattr -d com.apple.quarantine "/Volumes/PI Dashboard/PI Dashboard.app"