Skip to content

v0.8.0

Latest

Choose a tag to compare

@github-actions github-actions released this 26 Aug 13:18
· 69 commits to develop since this release

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. Automatic is itself a row showing
    what the resolution chain currently lands on, so it is never a black box and
    reverting is one click. Installs below piCompatibility.minimum are 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/installs and POST /api/pi/runtime, both behind the same network
    guard as /api/tools. /api/health gains piRuntime, 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/health is 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_list it already sends) on canonicalized real paths, so a card reads
    active, not loaded, or loaded elsewhere — the last of which finally makes
    runtime-registered skills (~/.pi/agent/pi-hermes-memory/skills/, an ancestor
    .agents/skills chain, --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
    labelled pi or repository so 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 via pi-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 a doctor probe. macOS-only; Apple Mail is out of scope
    (use apple-mail-fast-export).

  • paths plugin-requirement category: PluginRequirements.paths declares
    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.redirectBaseUrl field 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
    to http://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 through PUT /api/config without a restart. A base that
    is not an absolute http(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/endpoints still 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) and trustedNetworks (when a
    trusted network is) — in a single PUT /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:
    a http:// 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/diagnostics reports which redirect base actually won and which
    tier produced it, and the same line is written to server.log at every auth
    registration and reload. A new oauth-redirect-base doctor 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/N item 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.notify rows 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 to 2000; 0 restores 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 @role automation shows only a hint naming the role that owns the level.

  • A dedicated @naming role 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 from GET /api/auto-name-outcomes so 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-failed reported 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-provider tunnel.<id>.enabled / .mode, with tunnel.provider as 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 a reachability object on the guarded GET /api/config, and the UI shows an advisory with an inline 0.0.0.0 remediation, a Server-page link and a pending-restart hint. When --host or PI_DASHBOARD_HOST outranks 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 the cost command; the report also folds in measured delivery cost read from your own session history via GET /api/cost-estimator/telemetry. Cost basis defaults to subscription seats rather than metered API pricing. Replaces the former global software-cost-estimator skill 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 blackhole dashboard plugin that reads and writes the pi-blackhole extension'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-production and nano-banana now 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 grammar plugin. 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. Existing config.grammar settings migrate automatically to plugins.grammar. See docs/grammar-checker.md and docs/grammar-model-guidance.md. (#401)

  • Stateless MCP endpoint at POST /mcp — the dashboard can now be driven as an MCP server (protocol revision 2026-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-status payload. configured?: boolean is
    superseded by a per-artifact checklist (settings, agents, prompts,
    openspec, kb; exactly one required: settings), computed on every
    response at the config root and uncached. configured is still emitted but
    deprecated — consumers must read the checklist first. A new
    setupOutdated?: boolean is 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?: ReactNode is removed.
    A plugin can no longer render arbitrary markup into a directory-card slot pill;
    the pill is state-only. Passing actions is 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? }),
    plus useFolderMenuRefresher(scope, refresh) for a callback with no item of
    its own. group is 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 KB Reindex / Index now / Retry controls collapse into
    one state-varying item that keeps its stale badge, + Automation / + Goal
    move to CREATE, and the OpenSpec Archive/Specs shortcuts move to OPEN with
    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.3 to 43.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 (minimumSystemVersion in 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/x64 build 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 pi on
    the shell's PATH.
    buildTmuxCommand returns 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
    pi differs from the dashboard's, those sessions change which binary they run.
    WSL-tmux is unaffected: it still resolves pi inside the WSL namespace.

  • A pre-existing pi-only override now surfaces as divergence. Setting a pi
    path override through Settings → Tools without also setting pi-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/pi and PUT /api/tools/pi-coding-agent now 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 adjacent package.json (a Windows .cmd shim) 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.recommended moves to 0.84.1, the server dependency to
    ^0.84.1, and the Docker image pin to @0.84.1. The broad-support floor
    piCompatibility.minimum deliberately stays at 0.78.0, so pi 0.78–0.84.0
    users see a soft upgrade hint and never a blocking error.

  • Custom models in models.json may now declare arbitrary OpenAI-compatible
    samplingParams (including opt-in vLLM thinking_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 sibling AGENTS.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. extensions and agents remain
    scanner-discovered (pi has no agents resource type).

  • Skill descriptions across the repository were trimmed to the new 400-character
    budget, preserving their trigger phrasing. ship-change,
    frontend-mockup-loop, and anti-slop-frontend are 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.publicBaseUrls is promoted to a top-level publicBaseUrls, 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: Overloaded instead of the raw blob; non-JSON messages pass through unchanged

  • Add-to-workspace affordance is a labelled pill. The cryptic +ws label 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 superseded tool_execution_update events 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/prompt reports transmitted plus a promptId instead of asserting a delivered: true it 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_session message end-to-end instead of a /__dashboard_retry sentinel 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 pi and 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 via PI_GATEWAY_TCP=1 and 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 $HOME no longer collide, and a dashboard that cannot claim the default attaches instead, taking over automatically if the owner dies. A /api/restart that briefly takes /api/health down no longer tears healthy bridges down.

  • Sessions can be moved between dashboard instances: /dashboard-connect hands a running session to another dashboard with an overlap-then-commit handover that completes only once routing has actually transferred, /dashboard-where reports 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
    interpolated cd <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 -c element, and the redundant cd prefix is gone. Values
    interpolated into the pane command are still shell-escaped, because tmux runs
    that argument through a shell of its own. Tracked as fix-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_rejected and stops retrying, and resume is refused when a live bridge already serves that transcript. /api/health reports bridgeContentionCount and contendedSessionIds (#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:9999 default. 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 trusted req.socket.remoteAddress alone, so any locally-terminating relay (zrok, ssh -L, socat, an nginx or Docker userland proxy) let an internet peer present as 127.0.0.1 and 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-file and to the browser-bus resume_session path, 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>.jsonl and 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 mode 0600 instead of following symlinks and inheriting the umask.

  • OAuth redirect-base handling leaked secrets and mis-set the session cookie. A redirectBaseUrl carrying userinfo (https://user:pw@host) was accepted silently and travelled in the provider's redirect_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's Secure flag derived from request.protocol, which is always http behind a reverse proxy — exactly the deployment redirectBaseUrl exists for — and now derives from the resolved redirect base. Fastify's trustProxy was deliberately not enabled: it would rewrite request.ip from X-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 on PUT /api/config. A test now fails if trustProxy is 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.exe syntax (#512).

  • /api/tunnel-status is unauthenticated and was exposing degraded.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-detail instead.

Fixed

  • Live flow and subagent events never reached the dashboard, and every automation
    whose action is flows.run hung until the stale-run reaper wrote
    run exceeded max age — a failure that never happened. The bridge forwarded
    EventBus traffic by monkey-patching pi.events.emit, but pi gives every
    extension its OWN events facade 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 one pi.events.on subscription per declared channel, which observes every
    emitter. A flows.run run 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/health could report the wrong running pi version and raise a spurious
    "upgrade recommended" hint. Several workspaces declare a broad >=0.80.10 pi
    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 be null to
    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 made ModelRegistry.refresh() asynchronous and result-bearing, so the
    dashboard was reading the catalogue before the refresh resolved and
    discarding per-provider errors in a bare catch. 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
    AbortSignal on 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-dashboard works 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
    EBADENGINE before 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.yml Node 26 install legs deliberately run without the
    --config.engine-strict=false override the other legs carry, so the
    EBADENGINE regression is tested rather than assumed. Node 27+ stays refused
    until separately validated. Second, the git-clone path runs
    npm install --omit=dev, which drops devDependencies; the web client's
    prepare Vite build then died with Cannot find module 'vite/package.json'.
    Its direct build-time requirements — vite, @vitejs/plugin-react,
    @tailwindcss/vite, tailwindcss, and tsx — are now runtime
    dependencies, with tsx declared 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 no prepare).
    (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.md were 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/config carrying an auth block 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_user prompts 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 worktree label, 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_modelsend_prompt race (#412, #467).

  • "Needs you" survives a bridge reconnect. A session blocked on ask_user lost 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.notify toast 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 spawnRegisterTimeoutMs no 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 pi process 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. endedAt is 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/models to 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 (native models.json > endpoint-advertised > API floor), keyed on the response shape so an OpenAI-shaped body from an anthropic-messages provider 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-dashboard start 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:8000 silently took over localhost:8000 on 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 8000 was passed explicitly.

  • The dashboard extension loads under Bun-based hosts again. A TypeScript cast defeated jiti's import.meta erasure, so the module was re-imported through a data: URL that Bun rejected with NameTooLong before the agent could start. A repo gate now fails the build if import.meta survives transpilation in any jiti-loaded first-party source, and docs/faq.md documents the JITI_ESM_EVAL_TEMP_FILE=1 escape hatch (#408, #478).

  • The project's .pi/settings.json pinned the kb-extension package 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 leaving npm:, git:, https: and ssh: specifiers alone (#371, #550).

  • A pi session 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 ports 8000/9999, logging the refusal (#480).

  • A keeper-backed session could permanently lose the pi process id it was tracking, degrading every later reattach to a positional match that could terminate the wrong pi. 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 cost command, which renders the same view.

  • convert-pdf accepts an output path again (#507): the documented convert-pdf in.docx out.pdf form never worked — both paths were read as inputs, so every PDF render failed with File not found: <output>.pdf. Output is now an explicit -o/--output flag, 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-bpmn downloads are ~660 KB instead of 164 MB: a nested node_modules tree 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 setTimeout ceiling (~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 install on every monorepo, which corrupts a pnpm hoisted workspace and could leave pi unable to boot (sessions dying with REGISTER_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:

  1. Click More info.
  2. Click Run anyway.

Option B — pre-clear the Mark-of-the-Web:

  1. Right-click the downloaded .exe (or the .zip) → Properties.
  2. At the bottom of the General tab, tick Unblock next to
    "This file came from another computer...".
  3. 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:

  1. Open the DMG and drag PI Dashboard to Applications.
  2. In Applications, right-click (or Control-click) PI Dashboard →
    Open
    .
  3. 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"