The doctor "can find npm/pi" checks wrap the version command in a POSIX
subshell, `(cmd --version 2>&1 || true)`. Fish treats `( ... )` as command
substitution syntax and forbids it in command position, so every fish user saw
false-negative failures:
fish: command substitutions not allowed in command position
command -v npm && (npm --version 2>&1 || true)
Branch on the detected service shell and emit fish's `begin; ...; end`
grouping for fish, mirroring the existing fish-aware quoting in
serviceShellQuote. Bash and zsh keep the POSIX subshell.
Also guard the `main()` invocation with an ESM main-module check so the CLI
helpers can be imported by tests without side effects, and add a regression
test covering the bash, zsh, and fish command shapes.
Split subsession inspection into two tools: check_subsession gives a quick
glance (status + latest assistant output), while read_subsession reads through
a child's transcript with role/content filters, full-content substring search
(including tool-call args), optional per-value maxChars truncation that flags
clipped parts, includeToolArgs, and pagination.
Filtering and search run on full untruncated content; truncation is an explicit,
caller-owned final projection (no default) so a narrow read never silently hides
a match. Empty page-windows are distinguished from zero-match results.
Add spawn_subsession / list_subsessions / read_subsession tools that let an
agent start child sessions it stays attached to: the child records its parent
in the session tree, the parent is notified (as a system-authored custom
message that wakes an idle parent and queues behind in-flight work) when the
child stops working, and the parent can inspect children's status and result.
Gated behind a beta flag, off by default, mirroring spawnSessions: enable via
PI_WEB_SUBSESSIONS, the subsessions config key, or the Settings toggle. Also
requires spawnSessions.
Also fix the release skill so the version step resyncs package-lock.json
(npm install --package-lock-only) and the commit step refuses a release where
package.json and package-lock.json versions disagree.
Attachments can be pasted, dropped, or uploaded, so 'paste' was too
narrow. Rename the default folder and filename prefix accordingly and
update the changeset reference.
The stream catch-up mode was represented by two fields that could drift:
the private catchupStreamSessionId guard (which suppresses live transcript
events while the in-flight message prefix is missing) and the public
isReceivingPartialStream flag (which drives the badge). The socket reconnect
path (refreshSelectedSession) set the flag from status without updating the
guard, so a later idle status.update was skipped by the guard check and the
badge never cleared.
Route every mutation of the mode through a single setStreamCatchup helper so
the guard and badge can't disagree, and clear the mode on any non-streaming
status for the selected session.
The session.created global event (added with spawn_session) could arrive
in the initiating tab before the start request's HTTP response resolved.
applyCreatedSession inserted a plain session row (archive/reload actions)
and then startSession unconditionally prepended the cached version (delete
action), leaving two badges with the same id.
Make the optimistic insert idempotent by filtering out any existing entry
with the same id before prepending the cached session, so the locally
cached session always wins regardless of event ordering.
Add a project-scoped spawn_session tool so agents can dispatch new,
independent sessions (ralph loops, long-plan chaining). Spawned sessions
are constrained to a workspace/worktree of the same registered project,
appear in the session list immediately via a new session.created event,
and the capability is on by default with a Settings -> Session daemon
toggle (spawnSessions / PI_WEB_SPAWN_SESSIONS).
Note: adds a session daemon code path, so pi-web-sessiond.service must be
restarted manually for the server side to take effect.
Builds on marcus's plugin-api-completeness work. Narrows the new plugin
surface to capabilities that expose real, otherwise-unreachable pi-web
functionality, and drops invented/duplicative surfaces:
Kept:
- files.writeFile / deleteFile / moveFile (genuine workspace mutation,
federated, path-safe)
- prompt.insertText / getText / getSelection (editor state access)
Dropped:
- attachments.* (insertFileReference/getAttachedFiles/removeFileReference):
getAttachedFiles invented a structured-attachment notion pi-web does not
have and duplicated prompt.getText() + a regex with a false email-safety
claim; insert/removeFileReference were thin sugar over readFile +
insertText that plugins can compose themselves.
- prompt.onPaste / onKeyDown: an incomplete two-event hook system shaped
around a single use case, overlapping the editor's native image-paste
handling. Deferred until a real editor event/hook surface is designed.
- prompt.focus: redundant and buggier duplicate of the existing
focusPrompt() (silently no-ops when not on the chat view). Focus stays
as focusPrompt().
Security fix:
- deleteWorkspaceFile now resolves the parent via realpath + ensureInside
before lstat/unlink, closing a symlinked-parent-directory escape that
allowed deleting files outside the workspace (write/move already did
this). Final path component is still not resolved, so deleting a symlink
removes the link, not its target. Adds a regression test.
Docs and the registry test mock updated to match the trimmed surface.
- WorkspaceFiles: writeFile, deleteFile, moveFile with path safety
- writeFile: text/binary, auto-create dirs, overwrite option
- deleteFile: idempotent, uses lstat (removes symlinks not targets)
- moveFile: unix mv semantics, overwrite defaults to false
- All mutations auto-refreshFiles() in File Explorer
- Symlink escape prevention via realpath(dirname) check
- PluginPromptEditor: insertText, getText, getSelection, onPaste, onKeyDown, focus
- Uses CM6 EditorView.domEventHandlers() via Compartment (not raw DOM)
- Handlers registered before mount are preserved and applied on mount
- First-to-consume-wins ordering for multi-plugin scenarios
- insertText replaces selection (not inserts after)
- PluginAttachments: insertFileReference, getAttachedFiles, removeFileReference
- insertFileReference validates file exists before inserting @path
- Does not auto-focus editor (unlike prompt.insertText)
- @file regex requires file extension to avoid matching emails
- Server endpoints: PUT /file, DELETE /file, POST /file/move
- All work for local and federated machines
- Tests: 31 unit tests, 9 integration tests, 5 client tests
- Docs: 3 new sections in plugins.md
Add a "Reload Session" core action so reload is keyboard-accessible and can be
assigned a custom shortcut, gated by the same guards as the menu item (writable
session, sessions.reload capability, not currently busy). Disable the Reload
menu entry while the session has active work, mirroring the server guard and the
archived-delete control, so users get a clear reason instead of an error toast.
Co-authored-by: Claude <[email protected]>
Build on the original Reload action with the fixes raised in review:
- Server reload() now refuses to run on archived (read-only) sessions
and when the session has work in progress, mirroring archive(), so a
reload can no longer silently abort an in-flight agent run.
- Add a sessions.reload runtime capability; the client gates both the
reloadSession call and the Reload menu entry on it so the action only
appears for machines whose Pi-Web runtime supports it.
- reloadSession ignores cached-new and archived sessions.
- Add server (PiSessionService + routes) and client (SessionController)
tests covering reload success, the active-work guard, archived
rejection, route forwarding, capability gating, and error mapping.
- Restore alphabetical parser import ordering in clients.ts.
- Add a changeset documenting the feature and the sessiond restart note.
Note: touches a session daemon code path, so pi-web-sessiond.service
must be restarted manually for the server side to take effect.
Depend on @earendil-works/pi-agent-core so the ThinkingLevel union has a single source of truth (re-exported via shared/thinkingLevels). Wire/data fields use string and the parser is lenient, so an unknown level from a newer pi runtime is still listed, selectable, and rendered gracefully instead of throwing. The composer gauge now derives its bar count from the levels available for the current model and fills by rank. Adds compile-time drift guards (satisfies + Exclude check) and unit tests so a changed pi level set fails fast in development.
Replace Send/Queue/Steer/Stop text buttons with compact icons, move Attach into the message box, and show the thinking level as a fill gauge. Keeps mobile layouts uncrowded while preserving accessible labels and the readable model selector.
Add per-role left accent stripes and color-coded header labels across
all themes. Lighten the dark theme user-message background, decouple it
from the generic hover color, and brighten the user border so user and
assistant turns are clearly distinguishable.
isSendingPrompt was a single global flag, so an in-flight upload showed
the "Sending…" dock on whatever session/machine the user switched to.
Replace it with sendingPrompts, a Record<sessionId, true> keyed like
sessionStatuses/sessionActivities. The controller sets/clears the entry
for the originating session (captured before the await), the chat dock
reads only the selected session's entry, and the session list now shows
the activity dot for a session that is uploading so progress is visible
after switching away. Machine switches clear the record like other
per-session state; deselecting a session no longer cancels its indicator.