Skip to content

feat: rewrite and re-scope the pi-processes extension#38

Draft
aliou wants to merge 76 commits into
mainfrom
feat/rewrite
Draft

feat: rewrite and re-scope the pi-processes extension#38
aliou wants to merge 76 commits into
mainfrom
feat/rewrite

Conversation

@aliou

@aliou aliou commented May 10, 2026

Copy link
Copy Markdown
Owner

Summary

Full rewrite of @aliou/pi-processes. The extension is split into a pi-agnostic core and three thin Pi extensions, with a config migration path that imports the legacy process.json and migrates v0.9.4 settings forward.

Supersedes the single-extension layout and restores feature parity with the original shipping version where the rewrite regressed.

Architecture

  • src/ — pi-agnostic core: ProcessManager, ProcessRegistry, protocol, types, platform utils. Zero Pi imports; unit + e2e tested in isolation.
  • extensions/processes/ — core extension: process tool (start, list, output, update, write, stop, clear), settings, lifecycle hooks, notification service, /ps, /ps:kill, /ps:clear, /ps:settings, config migrations.
  • extensions/processes-logs//ps:logs command and log overlay with per-process viewer cache.
  • extensions/processes-dock//ps:dock, /ps:pin, dock widget, status widget.
  • extensions/shared/ — shared UI helpers (statusDot, processStatusTone, LineComponent, ...).

Parity fixes and hardening

P0 (correctness)

  • Import legacy process.json before config load.
  • Set endTime on kill timeout so notifications classify correctly.

P1 (feature parity / regressions)

  • Default blockBackgroundCommands to false.
  • Default onKilled to context.
  • Restore the Windows platform guard.
  • Restore the cwd start parameter.
  • Reject empty log-match patterns (literal and regex).
  • Add the write (stdin) tool.
  • Default /ps preview to the newest output page.
  • Cache LogFileViewer per process across /ps:logs tab switches.
  • Re-add /ps:kill and /ps:clear commands, awaiting the requestKill reply so success is reported correctly.

Hardening

  • Guard empty regex patterns in compileLineMatcher.
  • Reject no-op process write calls (no input and no end).
  • Simplify /ps:kill + /ps:clear command plumbing.

Cleanup

  • Remove the dead timeout notification kind (kill path finalizes without emitting).
  • Extract shared status/render helpers to extensions/shared/ui.ts.
  • Re-add the status widget below the editor (widget.showStatusWidget).

Docs

  • Sync README, the bundled skill, AGENTS.md, and CONTRIBUTING.md to the final feature set.
  • Drop the dev-only .pi/extensions/ extensions and tests/scenarios/, fix stale doc references.

Intentional changes

  • Watch literal matching by default with one-shot repeat: false defaults.
  • /ps Enter pins to the dock (pin/unpin toggle) instead of focusing.
  • Process IDs are opaque; no numerical ordering assumed. Display concerns live in the UI layer.
  • /ps:kill does not touch the dock; the dock auto-unpins a killed focused process via processes-changed.
  • Status-dot glyphs unified across the three extensions (process tabs and log tabs show strictly more informative states).

Config migration

  • Pre-load step imports the legacy process.json on first run.
  • The v0.9.4 -> v0.10.0 migration migrates settings forward and preserves widget.showStatusWidget.

Status

409 tests pass; typecheck and lint clean. Not yet version-bumped (changeset pending).

Checklist

  • Changeset (version bump to 0.10.0)
  • Demo recordings for README (9 placeholders at assets.aliou.me/pi-extensions/demos/processes/v0.10.0/)

Closes #28

aliou added 11 commits May 9, 2026 18:18
Build the Pi-agnostic process management core under src/.

Add typed process domain models, the inter-extension protocol, and a fresh manager factory owned by the future core extension instance.

Split manager responsibilities into registry, log store, output tracking, throttled notifications, and runtime control so process lifecycle, logs, watches, stdin writes, kill behavior, and cleanup can be tested independently.

Add deterministic Vitest coverage with memfs-backed filesystem mocks, mocked process spawning, typed @golevelup/ts-vitest mocks, literal-by-default log watches, and fast event-driven manager tests.
@aliou

This comment has been minimized.

@aliou

This comment has been minimized.

aliou added 8 commits July 9, 2026 10:44
0.9.4 stored settings under the "process" namespace
(~/.pi/agent/extensions/process.json); the rewrite renamed the
namespace to "processes" (processes.json). ConfigLoader skips
migrations when the target file is missing, so the 001 schema
migration never ran for upgrading users -- they got defaults.

Add a pre-load step that copies process.json -> processes.json when
the new file does not exist, then load() reads real 0.9.4 data and
the 001 migration runs on it. Legacy file left in place as backup.
0.9.4 only used the global scope, so only the global path is imported.
The kill timeout branch set endReason/signal/success but left endTime
null. When the child later fired close, the close handler saw a null
endTime, proceeded anyway, overwrote endReason kill_timeout -> signal,
and transitioned killed again -- a second process_ended. By then the
intentional-stop marker was consumed, so the reclassified event fell
to the killed default (ignore) and the eventual death was swallowed.

Set endTime in the timeout branch. The existing close guard
(if managed.endTime return) then bails, removing the double-emit,
reclassification, and swallowed death. The agent already learns the
stop timed out via the tool result {ok:false, reason:timeout}; no
separate timeout notification is added.

The classify timeout branch is now unreachable dead code (the
terminate_timeout process_ended is swallowed by the intentional-stop
marker); removal is tracked as a follow-up cleanup.
0.9.4 shipped the blocker off. The rewrite flipped the default to true,
hard-rejecting bare `&`/`nohup` calls on upgrade. Revert to 0.9.4 behavior;
the agent should still prefer the process tool via prompt guidance, not
enforcement. Steering mode (allow + nudge) is tracked separately.
Externally-killed (outside this manager) processes were silently ignored
by default. Surface them as context so the agent learns a managed process
disappeared instead of operating on a stale assumption it is still running.
Intentional stops are classified separately and never reach this branch.
The manager spawns detached process groups and kills them by negative pid
(src/utils/process-group.ts) -- POSIX-only operations. Main (0.9.4) early-
returned on win32 with a warning; the rewrite had dropped the guard and
would have misbehaved silently on Windows.

The core extension warns and bails; the logs and dock extensions bail
silently since the core already warned and there is no manager to talk to.
Main accepted an optional cwd and used params.cwd ?? ctx.cwd. The rewrite
always used ctx.cwd, so an agent could not start a process in a different
directory than the current session cwd. Re-add the optional cwd param.
aliou added 8 commits July 10, 2026 14:16
A literal pattern of "" (or whitespace-only) matched every line via
String#includes(""), firing a notification per output line. The schema only
enforced maxLength. Reject empty/whitespace patterns at normalization
(notify.logMatches and update watches share normalizeLogMatch), and guard
the shared compileLineMatcher primitive so an empty literal can never become
a match-all (returns false instead).
manager.writeToStdin was implemented but no tool action exposed it, so the
agent could not send input to a running process's stdin (answer a prompt,
pipe data, or signal EOF). Add a write action with id, input, and end params.

input defaults to an empty string so {action:write, id, end:true} closes
stdin without writing. Wires executor, render, and the tool prompt.
On select/open the preview reset previewOffset to 0 and sliced forward, so
the oldest of the recent window showed first. Default to the newest page
(max(0, total - previewHeight)) so the most recent output is visible
immediately, matching 0.9.4.

refreshPreview is now align-aware: "newest" jumps to the newest page (select
path), "keep" preserves the user's scroll unless they were already at the
bottom (follow-tail), so a deliberate K-scroll-up is not yanked back by the
next streaming output batch.
subscribeToSelected recreated a fresh LogFileViewer on every tab switch,
discarding scroll position, search query, follow toggle, and stream
filter. Keep a per-process viewer Map mirroring the existing notifyMarkers
persistence: the live log connection is unsubscribed on switch (only one
process subscribed at a time) but the viewer is reused when the tab is
revisited, and initialLines is not re-fed into a cached buffer.

Bounded with a MAX_CACHED_VIEWERS cap (LRU-ish, active viewer protected)
and pruned of entries for processes removed from the list.
ps:kill stops a running managed process over the kill protocol channel:
with no arg it picks the sole running process (or offers a picker when
several are live); with an explicit id it stops that one. terminate_timeout
targets are force-killed (SIGKILL, 200ms); others get SIGTERM, 3000ms.

ps:clear removes finished processes via the clear channel and reports the
count.

Neither command touches the dock directly: the dock auto-unpins a killed
focused process on the next processes-changed tick (setup.ts:125).
A kill that times out never reaches process_ended: the kill path sets
endTime and transitions to terminate_timeout without emitting, the close
handler bails on the endTime guard, and the liveness tick skips
terminate_timeout. So classifyProcessEnd never sees terminate_timeout and
the 'timeout' notification kind is unreachable.

The stop tool surfaces the timeout via KillResult.reason = 'timeout'.
Remove the dead classify branch, the protocol kind, the service
shouldForceDisplay/resolveAttention/summary branches, the renderer cases,
and the tests that exercised dead code.
LineComponent/LinesComponent/RuleComponent were copied verbatim in
overview, log-overlay, and log-dock. The status-dot glyphs and the
'formatStatus — command' selection description were duplicated across
tabs, kill, pin, logs completions, and overview.

Add extensions/shared/ui.ts (pi-TUI aware, not pi-agnostic src/) with
the three components, statusDot, processStatusTone, and
formatProcessSelectionDescription. Replace the copies and delete the
unused panel-helpers.ts.
Port main's one-line status widget (process dot + name + state, with
+N more overflow). Add renderStatusWidget in processes-dock/widget/status.ts
and render it from the dock's existing refresh loop via setWidget with
placement belowEditor. Use a width-aware component factory so the line
reflows on resize, not a static process.stdout.columns snapshot.

Add widget.showStatusWidget config (defaults false, matches main), preserve
it through the v0.9.4->v0.10.0 migration (it was dropped when the widget
was removed), and expose it as a boolean settings item. The migration no
longer triggers on showStatusWidget alone.

Override plan #11: live in processes-dock rather than a 4th extension,
since the dock already has the events-only config/process-list pipeline.
aliou added 2 commits July 10, 2026 14:33
Update README, the pi-processes skill, AGENTS.md, and CONTRIBUTING.md to
cover the features added or restored in the rewrite parity pass:

- process write action (stdin), with end:true to signal EOF
- cwd start parameter
- /ps:kill and /ps:clear slash commands (await reply before reporting)
- status widget (showStatusWidget setting) below the editor
- cached /ps:logs viewer across tab switches, newest-first /ps preview
- empty literal and regex logMatches patterns rejected at start/update

Switch README video placeholders from the dead <!-- VIDEO: ... --> HTML
comment convention (the .github/docs-site build was removed during the
rewrite) to the pi-ts-aperture markdown link pattern, with demo asset
paths under https://assets.aliou.me/pi-extensions/demos/processes/v0.10.0/.
Drop the stale .github/docs-site references from CONTRIBUTING.md.
Remove the dev-only extensions under .pi/extensions/:
- dev-processes-scenarios.ts: /dev:processes:scenarios loader for test
  scenario prompts
- dev-pid-widget.ts: below-editor pid/child-pid debug widget

Drop the .gitignore exception that kept dev-processes-scenarios.ts tracked.
Delete tests/scenarios/ (67 manual prompt files across 14 dirs) that were
loaded only by the dev scenarios extension. Unit and e2e tests already cover
behavior.

Fix stale doc references:
- CONTRIBUTING.md: ./test/test-*.sh scripts referenced since before the
  rewrite never existed; point at the real tests/e2e/scripts/ fixtures. Drop
  the fictional Northwind API project demo example.
- pi-processes-testing skill: replace the nonexistent Northwind test
  environment with the actual e2e fixture approach (addScript/addFile, real
  temp dirs). Fix the QA checklist: enter pins to the dock (not 'focuses'),
  document the cached viewer, and cover /ps:kill, /ps:clear, and the status
  widget toggle.
@aliou aliou changed the title feat: rewrite extension feat: rewrite and re-scope the pi-processes extension Jul 10, 2026
After a process ends and its watches are unregistered, a belated
process_output_changed for the cleared id must not emit another
log-match notification.

Also verify unknown/unregistered process ids cannot emit log-match
notifications. The existing manager clearFinished timer test covers the
lower-level guarantee that pending output timers do not emit after clear.

Closes #54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Emit events via pi.events and sunset the dock

1 participant