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 the Relay method skill (execute a long plan as a chain of independent
sessions handing off via spawn_session) under a top-level skills/ folder that
follows the open agent-skills convention, installable with the skills CLI.
Also clarify the changeset-changelog skill: no changeset is needed for changes
outside the npm release allowlist (package.json files), such as repo-only
agent skills under .agents/ and skills/.
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.
A slash command sent as the first (or any idle) message showed the raw
command text twice until reload: once from the client's optimistic insert
and once from the server's message.append echo, neither of which converges
with the agent's canonical expanded message (e.g. a /skill:* block).
Make commands obey the same source-of-truth contract as prompts:
- client no longer inserts the raw command text optimistically; it shows
the existing per-session sending indicator instead
- forwarded runtime/skill commands return a bare done result rather than a
synthetic "Accepted ..." line
- server suppresses the raw message.append echo for command-forwarded
prompts (threaded through the compaction queue too)
Result: pre-reload state matches reload, with no transient duplicate.
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.
Drop the task-count badge so the Tasks tab no longer draws attention
during normal use. Keep the '!' badge for when the tasks config exists
but cannot be loaded, which is the only state that warrants attention.
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.
Drop pi-web-plugin.test.ts, which relied on a walk() hack to assert
rendered template content and click wiring. Real decision logic is
already covered by updatesLogic.test.ts; the remaining runCommand glue
is trivial inline code.
Move the Updates plugin's pure decision logic (recommended/additional
commands, panel visibility, installation labels, version formatting)
into a sibling updatesLogic.ts module so it can be unit tested directly,
matching the workspace-tasks multi-file plugin layout. The plugin file
is now thin rendering glue that imports those helpers.
Add unit tests for the extracted logic and render smoke tests that
exercise the panel through its public contribution API, including that
Run actions wire to the terminal with the "pi.plugin": "updates"
metadata. No behavior change; the beta label stays.
Ensure prompt.insertText, attachments.insertFileReference, and
attachments.removeFileReference move the cursor to the correct
position after modifying the prompt editor. Previously the cursor
stayed at the start of inserted text, which broke the natural
flow for plugin-driven file attachments like screenshot-paste.
- 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]>