feat: rewrite and re-scope the pi-processes extension#38
Draft
aliou wants to merge 76 commits into
Draft
Conversation
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.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
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.
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.
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.
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 legacyprocess.jsonand 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:logscommand 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)
process.jsonbefore config load.endTimeon kill timeout so notifications classify correctly.P1 (feature parity / regressions)
blockBackgroundCommandstofalse.onKilledtocontext.cwdstart parameter.write(stdin) tool./pspreview to the newest output page.LogFileViewerper process across/ps:logstab switches./ps:killand/ps:clearcommands, awaiting therequestKillreply so success is reported correctly.Hardening
compileLineMatcher.process writecalls (noinputand noend)./ps:kill+/ps:clearcommand plumbing.Cleanup
timeoutnotification kind (kill path finalizes without emitting).extensions/shared/ui.ts.widget.showStatusWidget).Docs
.pi/extensions/extensions andtests/scenarios/, fix stale doc references.Intentional changes
repeat: falsedefaults./psEnter pins to the dock (pin/unpin toggle) instead of focusing./ps:killdoes not touch the dock; the dock auto-unpins a killed focused process viaprocesses-changed.Config migration
process.jsonon first run.widget.showStatusWidget.Status
409 tests pass; typecheck and lint clean. Not yet version-bumped (changeset pending).
Checklist
assets.aliou.me/pi-extensions/demos/processes/v0.10.0/)Closes #28