@aliou/pi-processes 0.10.8 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +40 -0
  2. package/extensions/processes/client.ts +24 -2
  3. package/extensions/processes/commands/overview.ts +1 -1
  4. package/extensions/processes/components/overview-component.ts +121 -51
  5. package/extensions/processes/config/migrations/001-v0-9-4-to-v0-10-0-config.ts +5 -8
  6. package/extensions/processes/config/types.ts +1 -1
  7. package/extensions/processes/handlers/commands.ts +21 -41
  8. package/extensions/processes/handlers/notifications.ts +3 -37
  9. package/extensions/processes/handlers/requests.ts +1 -54
  10. package/extensions/processes/handlers/subscriptions.ts +2 -35
  11. package/extensions/processes/hooks/event-bridge.ts +1 -1
  12. package/extensions/processes/index.ts +2 -2
  13. package/extensions/processes/notifications/service.ts +4 -1
  14. package/extensions/processes/notifications/types.ts +2 -2
  15. package/extensions/processes/tools/notify.ts +37 -130
  16. package/extensions/processes/tools/schema.ts +5 -2
  17. package/extensions/processes/tools/update/index.ts +15 -40
  18. package/extensions/processes-debug/index.ts +67 -0
  19. package/extensions/processes-dock/client.ts +2 -2
  20. package/extensions/processes-dock/widget/setup.ts +12 -37
  21. package/extensions/processes-logs/client.ts +2 -2
  22. package/extensions/processes-logs/commands/logs.ts +1 -1
  23. package/extensions/processes-logs/components/log-file-viewer.ts +135 -8
  24. package/extensions/processes-logs/components/log-overlay-component.ts +156 -82
  25. package/extensions/processes-logs/logs-client.ts +2 -23
  26. package/extensions/shared/log-line.ts +49 -2
  27. package/{src → extensions/shared}/protocol/broadcasts.ts +1 -1
  28. package/{src → extensions/shared}/protocol/channels.ts +1 -0
  29. package/{src → extensions/shared}/protocol/commands.ts +12 -1
  30. package/{src → extensions/shared}/protocol/index.ts +2 -0
  31. package/{src → extensions/shared}/protocol/notifications.ts +1 -1
  32. package/{src → extensions/shared}/protocol/requests.ts +1 -1
  33. package/extensions/shared/shortcut-hints.ts +150 -0
  34. package/extensions/shared/shortcuts-overlay.ts +229 -0
  35. package/extensions/shared/truncate.ts +189 -0
  36. package/package.json +4 -4
  37. package/src/utils/command-executor.ts +2 -1
  38. package/src/utils/shell-utils.ts +44 -4
  39. package/extensions/shared/output-payload.ts +0 -28
  40. package/src/get-manager.ts +0 -15
  41. package/src/utils/is-record.ts +0 -3
  42. /package/{src → extensions/shared}/protocol/logs.ts +0 -0
package/README.md CHANGED
@@ -117,6 +117,46 @@ Available settings include:
117
117
  - Linux: supported
118
118
  - Windows: not supported
119
119
 
120
+ ## Similar but different
121
+
122
+ Pi has several process, terminal, and background-task extensions. pi-processes focuses on explicit LLM-managed background processes, log inspection, watches that can wake the agent, and Pi UI surfaces for `/ps`, logs, dock, and status.
123
+
124
+ See [pi.dev/packages](https://pi.dev/packages) for the full registry of Pi extensions.
125
+
126
+ ### Background command managers
127
+
128
+ These packages are closest when you want shell commands, dev servers, watchers, or logs to keep running while Pi continues the conversation.
129
+
130
+ - [pi-background-tasks](https://pi.dev/packages/pi-background-tasks): durable background shell tasks plus delegated child Pi workflows.
131
+ - [@99percentpeople/pi-background-tasks](https://pi.dev/packages/%4099percentpeople/pi-background-tasks): background commands and attachable PTY/TUI sessions, with SSH Remote integration.
132
+ - [@richardgill/pi-background-bash](https://github.com/richardgill/pi-extensions/tree/main/extensions/background-bash): replaces `bash` with session-owned local process groups and adds `bash_process` for list, peek, and kill.
133
+ - [pi-bash-bg](https://pi.dev/packages/pi-bash-bg): minimal `&` support for Pi's bash tool, detaching background processes and keeping output out of the context window.
134
+ - [pi-tian-background-terminals](https://pi.dev/packages/pi-tian-background-terminals): replaces Pi's built-in Bash with automatic background yielding, completion notifications, and a `/ps` viewer.
135
+ - [pi-better-background-tasks](https://pi.dev/packages/pi-better-background-tasks): durable background shell tasks, watchers, logs, and status inspection.
136
+ - [pi-patty-bg-tasks](https://pi.dev/packages/pi-patty-bg-tasks): Claude Code-style background tasks with auto-backgrounding, attach, file-backed output, and cooperative steering.
137
+ - [@mjakl/pi-processes](https://pi.dev/packages/%40mjakl/pi-processes): stripped-down pi-processes fork with the `process` tool, a single `/ps` overlay, a compact status line, and fewer commands/settings.
138
+ - [@haemmid/pi-processes](https://pi.dev/packages/%40haemmid/pi-processes): pi-web-focused pi-processes fork for dev-server automation, with `ensure`, `restart`, and `wait` actions for Astro/Vite-style workflows.
139
+ - [pi-processes-git-bash](https://pi.dev/packages/pi-processes-git-bash): pi-processes fork for Windows users through Git Bash.
140
+
141
+ ### Terminal and shell replacements
142
+
143
+ These packages are a better fit when you want a different shell substrate, PTY behavior, or platform-specific terminal support.
144
+
145
+ - [pi-unified-exec](https://pi.dev/packages/pi-unified-exec): Codex-style long-lived shell sessions with stdin, PTY, REPL, SSH, dev-server, and disk-log support.
146
+ - [pi-live-terminal](https://pi.dev/packages/pi-live-terminal): tmux-backed command runner with a live terminal widget.
147
+ - [@aliaksei-raketski/pi-tmux-bash](https://pi.dev/packages/%40aliaksei-raketski/pi-tmux-bash): runs model-facing shell commands in managed tmux windows.
148
+ - [@4fu/pi-pwsh](https://pi.dev/packages/%404fu/pi-pwsh): persistent PowerShell tasks for Pi on Windows, with ConPTY sessions and user requests.
149
+ - [pi-pwsh-notify](https://pi.dev/packages/pi-pwsh-notify): PowerShell shell with background jobs and completion/server-ready notifications.
150
+
151
+ ### Monitors, schedulers, and visibility
152
+
153
+ These packages are more about watching, waking, or surfacing process state than replacing pi-processes directly.
154
+
155
+ - [pi-event-monitor](https://pi.dev/packages/pi-event-monitor): event-driven shell-stream and file watchers that wake the session on process exit, matching output, or file writes.
156
+ - [pi-monitor-plugin](https://pi.dev/packages/pi-monitor-plugin): background jobs, monitors, loops, schedules, and idle-aware notifications.
157
+ - [pi-tripwire](https://pi.dev/packages/pi-tripwire): footer visibility for agent-spawned localhost servers; not a process runner itself.
158
+ - [@cortexkit/aft-pi](https://pi.dev/packages/%40cortexkit/aft-pi): broader Agent File Tools package that includes background bash tasks, PTY sessions, and output compression alongside code-analysis tools.
159
+
120
160
  ## Troubleshooting
121
161
 
122
162
  ### Pi started something and I want to see more output
@@ -8,22 +8,44 @@
8
8
  */
9
9
 
10
10
  import type { EventBus } from "@earendil-works/pi-coding-agent";
11
+ import type { KillResult, ProcessInfo } from "../../src/types";
11
12
  import {
12
13
  CHANNELS,
13
14
  type CommandClearPayload,
14
15
  type CommandKillPayload,
15
16
  type CommandPinPayload,
16
17
  type CommandPinResult,
18
+ type CommandStartPayload,
19
+ type CommandStartResult,
17
20
  type ProcessProtocolConfig,
18
21
  type RequestCombinedOutputPayload,
19
22
  type RequestConfigPayload,
20
23
  type RequestGetPayload,
21
24
  type RequestListPayload,
22
- } from "../../src/protocol";
23
- import type { KillResult, ProcessInfo } from "../../src/types";
25
+ } from "../shared/protocol";
24
26
 
25
27
  export type ProcessLogLine = { type: "stdout" | "stderr"; text: string };
26
28
 
29
+ /**
30
+ * Start a managed process via the core extension's protocol channel. Resolves
31
+ * when the core handler replies. The started process is fully visible to the
32
+ * agent via the `process` tool (list, output, stop, etc.).
33
+ */
34
+ export function requestStart(
35
+ events: EventBus,
36
+ options: { name: string; command: string; cwd?: string },
37
+ ): Promise<CommandStartResult> {
38
+ return new Promise((resolve) => {
39
+ const payload: CommandStartPayload = {
40
+ name: options.name,
41
+ command: options.command,
42
+ cwd: options.cwd,
43
+ reply: (result) => resolve(result),
44
+ };
45
+ events.emit(CHANNELS.COMMAND_START, payload);
46
+ });
47
+ }
48
+
27
49
  export function requestProcessList(events: EventBus): ProcessInfo[] {
28
50
  let processes: ProcessInfo[] = [];
29
51
  const payload: RequestListPayload = {
@@ -4,9 +4,9 @@ import type {
4
4
  ExtensionCommandContext,
5
5
  Theme,
6
6
  } from "@earendil-works/pi-coding-agent";
7
- import type { ProcessProtocolConfig } from "../../../src/protocol";
8
7
  import type { ProcessInfo } from "../../../src/types";
9
8
  import { sanitizeForDisplay } from "../../shared/display-text";
9
+ import type { ProcessProtocolConfig } from "../../shared/protocol";
10
10
  import { requestConfig, requestProcess, requestProcessList } from "../client";
11
11
  import { OverviewComponent } from "../components/overview-component";
12
12
 
@@ -4,11 +4,10 @@ import {
4
4
  type Component,
5
5
  Input,
6
6
  Key,
7
- matchesKey,
7
+ parseKey,
8
8
  type TUI,
9
9
  visibleWidth,
10
10
  } from "@earendil-works/pi-tui";
11
- import { CHANNELS, type ProcessProtocolConfig } from "../../../src/protocol";
12
11
  import { LIVE_STATUSES, type ProcessInfo } from "../../../src/types";
13
12
  import { formatRuntime } from "../../../src/utils/format";
14
13
  import {
@@ -17,7 +16,20 @@ import {
17
16
  } from "../../shared/display-text";
18
17
  import { buildDroppedOutputLine, trimToBudget } from "../../shared/line-buffer";
19
18
  import { renderLogLine } from "../../shared/log-line";
20
- import { isOutputChangedPayload } from "../../shared/output-payload";
19
+ import {
20
+ CHANNELS,
21
+ type ProcessesOutputChangedPayload,
22
+ type ProcessProtocolConfig,
23
+ } from "../../shared/protocol";
24
+ import {
25
+ SHORTCUTS_KEY,
26
+ type ShortcutHint,
27
+ ShortcutHintsComponent,
28
+ } from "../../shared/shortcut-hints";
29
+ import {
30
+ type ShortcutGroup,
31
+ showShortcutsOverlay,
32
+ } from "../../shared/shortcuts-overlay";
21
33
  import { truncateToWidth } from "../../shared/truncate";
22
34
  import { LineComponent, LinesComponent, statusColor } from "../../shared/ui";
23
35
  import {
@@ -89,6 +101,8 @@ export class OverviewComponent implements Component {
89
101
  private runningProcessCount = 0;
90
102
  private readonly disposers: Array<() => void> = [];
91
103
  private disposed = false;
104
+ /** Disposer for the "?" shortcuts overlay, when open. */
105
+ private shortcutsHelp: (() => void) | null = null;
92
106
 
93
107
  constructor(private readonly opts: OverviewOptions) {
94
108
  this.configureFilterInput();
@@ -120,6 +134,34 @@ export class OverviewComponent implements Component {
120
134
  return panel.render(width);
121
135
  }
122
136
 
137
+ /**
138
+ * Key dispatch for normal mode, keyed by parsed key id. Close keys are
139
+ * handled in `handleInput` before the table is consulted.
140
+ */
141
+ private readonly keyActions: Record<string, () => void> = {
142
+ [Key.down]: () => this.moveSelection(1),
143
+ [Key.up]: () => this.moveSelection(-1),
144
+ [Key.enter]: () => void this.pinSelected(),
145
+ j: () => this.moveSelection(1),
146
+ k: () => this.moveSelection(-1),
147
+ J: () => this.scrollPreview(1),
148
+ K: () => this.scrollPreview(-1),
149
+ g: () => {
150
+ this.previewOffset = 0;
151
+ this.requestRender();
152
+ },
153
+ G: () => {
154
+ this.previewOffset = Math.max(0, this.previewLines.length - 1);
155
+ this.requestRender();
156
+ },
157
+ x: () => this.killSelected(),
158
+ c: () => this.clearFinished(),
159
+ s: () => this.cycleSort(),
160
+ f: () => this.cycleFilter(),
161
+ "/": () => this.startQuickFilter(),
162
+ [SHORTCUTS_KEY]: () => this.openShortcutsHelp(),
163
+ };
164
+
123
165
  handleInput(data: string): void {
124
166
  if (this.mode === "filter-typing") {
125
167
  this.filterInput.handleInput?.(data);
@@ -127,30 +169,13 @@ export class OverviewComponent implements Component {
127
169
  return;
128
170
  }
129
171
 
130
- if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) {
172
+ const key = parseKey(data);
173
+ if (key === "escape" || key === "ctrl+c" || key === "q" || key === "Q") {
131
174
  this.close();
132
175
  return;
133
176
  }
134
- if (data === "q" || data === "Q") {
135
- this.close();
136
- return;
137
- }
138
- if (matchesKey(data, Key.down) || data === "j") this.moveSelection(1);
139
- else if (matchesKey(data, Key.up) || data === "k") this.moveSelection(-1);
140
- else if (data === "J") this.scrollPreview(1);
141
- else if (data === "K") this.scrollPreview(-1);
142
- else if (data === "g") {
143
- this.previewOffset = 0;
144
- this.requestRender();
145
- } else if (data === "G") {
146
- this.previewOffset = Math.max(0, this.previewLines.length - 1);
147
- this.requestRender();
148
- } else if (data === "x") this.killSelected();
149
- else if (data === "c") this.clearFinished();
150
- else if (data === "s") this.cycleSort();
151
- else if (data === "f") this.cycleFilter();
152
- else if (data === "/") this.startQuickFilter();
153
- else if (matchesKey(data, Key.enter)) void this.pinSelected();
177
+
178
+ this.keyActions[key ?? ""]?.();
154
179
  }
155
180
 
156
181
  invalidate(): void {}
@@ -158,6 +183,8 @@ export class OverviewComponent implements Component {
158
183
  dispose(): void {
159
184
  if (this.disposed) return;
160
185
  this.disposed = true;
186
+ this.shortcutsHelp?.();
187
+ this.shortcutsHelp = null;
161
188
  for (const dispose of this.disposers.splice(0)) dispose();
162
189
  }
163
190
 
@@ -195,8 +222,8 @@ export class OverviewComponent implements Component {
195
222
  this.requestRender();
196
223
  }
197
224
 
198
- private handleOutputChanged(payload: unknown): void {
199
- if (!isOutputChangedPayload(payload)) return;
225
+ private handleOutputChanged(rawPayload: unknown): void {
226
+ const payload = rawPayload as ProcessesOutputChangedPayload;
200
227
  const selected = this.selectedProcess();
201
228
  if (!selected || selected.id !== payload.id) return;
202
229
  const appended = [
@@ -447,6 +474,19 @@ export class OverviewComponent implements Component {
447
474
  this.opts.onClose();
448
475
  }
449
476
 
477
+ /**
478
+ * Open the "?" shortcuts overlay on top of the overview. While open it
479
+ * captures input; closing it restores focus here. Disposed with the
480
+ * overview so a background close cannot leave it behind.
481
+ */
482
+ private openShortcutsHelp(): void {
483
+ if (this.shortcutsHelp) return;
484
+ this.shortcutsHelp = showShortcutsOverlay(this.opts.tui, {
485
+ theme: this.opts.theme,
486
+ groups: this.shortcutGroups(),
487
+ });
488
+ }
489
+
450
490
  private requestRender(): void {
451
491
  this.opts.tui.requestRender();
452
492
  }
@@ -658,35 +698,65 @@ export class OverviewComponent implements Component {
658
698
  }
659
699
 
660
700
  private buildFooter(): Component {
661
- return new LineComponent((width) => this.renderFooterLine(width));
662
- }
663
-
664
- private renderFooterLine(width: number): string {
665
- const t = this.opts.theme;
666
- const dim = (value: string) => t.fg("dim", value);
667
-
668
701
  if (this.mode === "filter-typing") {
669
- const rendered = this.filterInput.render(60)[0] ?? "";
670
- return truncateToWidth(
671
- `${dim("/")}${rendered} ${dim("enter")} apply ${dim("esc")} cancel`,
672
- width,
673
- "",
674
- true,
675
- );
702
+ return new LineComponent((width) => {
703
+ const dim = (value: string) => this.opts.theme.fg("dim", value);
704
+ const rendered = this.filterInput.render(60)[0] ?? "";
705
+ return truncateToWidth(
706
+ `${dim("/")}${rendered} ${dim("enter")} apply ${dim("esc")} cancel`,
707
+ width,
708
+ "",
709
+ true,
710
+ );
711
+ });
676
712
  }
713
+ return new ShortcutHintsComponent(
714
+ () => this.overviewHints(),
715
+ this.opts.theme,
716
+ );
717
+ }
718
+
719
+ /** Footer hint list for the overview. */
720
+ private overviewHints(): ShortcutHint[] {
721
+ return [
722
+ { key: "j/k", label: "move" },
723
+ { key: "J/K", label: "scroll" },
724
+ { key: "enter", label: this.renderPinHint() },
725
+ { key: "x", label: "kill" },
726
+ { key: "c", label: "clear" },
727
+ { key: "s", label: "sort" },
728
+ { key: "f", label: "filter" },
729
+ { key: "/", label: "find" },
730
+ { key: "g/G", label: "top/bot" },
731
+ { key: "q", label: "close" },
732
+ ];
733
+ }
677
734
 
678
- const keys = [
679
- `${dim("j/k")} move`,
680
- `${dim("J/K")} scroll`,
681
- `${dim("enter")} ${this.renderPinHint()}`,
682
- `${dim("x")} kill`,
683
- `${dim("c")} clear`,
684
- `${dim("s")} sort`,
685
- `${dim("f")} filter`,
686
- `${dim("/")} find`,
687
- `${dim("q")} close`,
735
+ /** Groups for the "?" shortcuts overlay. */
736
+ private shortcutGroups(): ShortcutGroup[] {
737
+ return [
738
+ {
739
+ title: "list",
740
+ rows: [
741
+ { keys: "j / k", description: "move selection" },
742
+ { keys: "g / G", description: "preview top / bottom" },
743
+ { keys: "enter", description: this.renderPinHint() },
744
+ { keys: "x", description: "kill" },
745
+ { keys: "c", description: "clear finished" },
746
+ { keys: "s", description: "sort" },
747
+ { keys: "f", description: "filter" },
748
+ { keys: "/", description: "find by name" },
749
+ ],
750
+ },
751
+ {
752
+ title: "preview",
753
+ rows: [{ keys: "J / K", description: "scroll output" }],
754
+ },
755
+ {
756
+ title: "general",
757
+ rows: [{ keys: "q", description: "close" }],
758
+ },
688
759
  ];
689
- return truncateToWidth(keys.join(" "), width, "", true);
690
760
  }
691
761
  }
692
762
 
@@ -73,10 +73,6 @@ export interface ConfigV0100 {
73
73
  * - widget.showStatusWidget is preserved (still controls the status widget).
74
74
  */
75
75
 
76
- function isRecord(value: unknown): value is Record<string, unknown> {
77
- return typeof value === "object" && value !== null && !Array.isArray(value);
78
- }
79
-
80
76
  function setIfDefined<T extends object, K extends keyof T>(
81
77
  target: T,
82
78
  key: K,
@@ -120,10 +116,11 @@ export function needsConfigV094ToV0100Migration(
120
116
  return false;
121
117
  }
122
118
 
123
- const root = config as Record<string, unknown>;
124
- const widget = isRecord(root.widget) ? root.widget : undefined;
125
-
126
- return widget?.dockDefaultState === "hidden";
119
+ // Widen to string | undefined: the incoming config predates the migration,
120
+ // so its dockDefaultState can be "hidden" even though the static type says
121
+ // it cannot.
122
+ const dockDefaultState: string | undefined = config.widget?.dockDefaultState;
123
+ return dockDefaultState === "hidden";
127
124
  }
128
125
 
129
126
  export function migrateConfigV094ToV0100(config: ConfigV094): ConfigV0100 {
@@ -5,7 +5,7 @@
5
5
  * Runtime config uses the shared protocol shape returned by REQUEST_CONFIG.
6
6
  */
7
7
 
8
- import type { ProcessProtocolConfig } from "../../../src/protocol";
8
+ import type { ProcessProtocolConfig } from "../../shared/protocol";
9
9
 
10
10
  export interface ExecutionConfig {
11
11
  /** Absolute shell executable used to run process commands. */
@@ -1,13 +1,13 @@
1
1
  import type { EventBus } from "@earendil-works/pi-coding-agent";
2
2
 
3
3
  import type { ProcessManager } from "../../../src/manager";
4
+ import type { KillResult } from "../../../src/types";
4
5
  import {
5
6
  CHANNELS,
6
7
  type CommandClearPayload,
7
8
  type CommandKillPayload,
8
- } from "../../../src/protocol";
9
- import type { KillResult } from "../../../src/types";
10
- import { isRecord } from "../../../src/utils/is-record";
9
+ type CommandStartPayload,
10
+ } from "../../shared/protocol";
11
11
  import type { NotificationRegistry } from "../notifications/registry";
12
12
  import { killIntentionally } from "./kill-process";
13
13
 
@@ -17,9 +17,26 @@ export function registerCommandHandlers(
17
17
  notifications: NotificationRegistry,
18
18
  ): () => void {
19
19
  const disposers = [
20
+ events.on(CHANNELS.COMMAND_START, (payload) => {
21
+ const command = payload as CommandStartPayload;
22
+
23
+ try {
24
+ const info = manager.start(
25
+ command.name,
26
+ command.command,
27
+ command.cwd ?? process.cwd(),
28
+ );
29
+ notifications.register(info.id, {});
30
+ safeReply(command.reply, { ok: true, process: info });
31
+ } catch (err) {
32
+ safeReply(command.reply, {
33
+ ok: false,
34
+ error: err instanceof Error ? err.message : String(err),
35
+ });
36
+ }
37
+ }),
20
38
  events.on(CHANNELS.COMMAND_KILL, (payload) => {
21
39
  const command = payload as CommandKillPayload;
22
- if (!isCommandKillPayload(command)) return;
23
40
 
24
41
  void killIntentionally(manager, notifications, command.id, {
25
42
  signal: command.signal,
@@ -31,7 +48,6 @@ export function registerCommandHandlers(
31
48
  }),
32
49
  events.on(CHANNELS.COMMAND_CLEAR, (payload) => {
33
50
  const command = payload as CommandClearPayload;
34
- if (!isCommandClearPayload(command)) return;
35
51
 
36
52
  safeReply(command.reply, manager.clearFinished());
37
53
  }),
@@ -42,42 +58,6 @@ export function registerCommandHandlers(
42
58
  };
43
59
  }
44
60
 
45
- function isCommandKillPayload(
46
- payload: CommandKillPayload,
47
- ): payload is CommandKillPayload {
48
- return (
49
- isRecord(payload) &&
50
- typeof payload.id === "string" &&
51
- isOptionalSignal(payload.signal) &&
52
- isOptionalNumber(payload.timeoutMs) &&
53
- isReply(payload)
54
- );
55
- }
56
-
57
- function isCommandClearPayload(
58
- payload: CommandClearPayload,
59
- ): payload is CommandClearPayload {
60
- return isRecord(payload) && isReply(payload);
61
- }
62
-
63
- function isReply(
64
- payload: unknown,
65
- ): payload is { reply: (...args: never[]) => void } {
66
- return isRecord(payload) && typeof payload.reply === "function";
67
- }
68
-
69
- function isOptionalSignal(
70
- signal: unknown,
71
- ): signal is NodeJS.Signals | undefined {
72
- return signal === undefined || typeof signal === "string";
73
- }
74
-
75
- function isOptionalNumber(value: unknown): value is number | undefined {
76
- return (
77
- value === undefined || (typeof value === "number" && Number.isFinite(value))
78
- );
79
- }
80
-
81
61
  function safeReply<T>(reply: (result: T) => void, result: T): void {
82
62
  try {
83
63
  reply(result);
@@ -3,8 +3,7 @@ import type { EventBus, ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
3
  import {
4
4
  CHANNELS,
5
5
  type ProcessProtocolNotificationPayload,
6
- } from "../../../src/protocol";
7
- import { isRecord } from "../../../src/utils/is-record";
6
+ } from "../../shared/protocol";
8
7
  import {
9
8
  attentionToSendOptions,
10
9
  sendProcessNotificationMessage,
@@ -83,8 +82,8 @@ export function registerNotificationDelivery(
83
82
 
84
83
  const disposeListener = events.on(
85
84
  CHANNELS.NOTIFICATION,
86
- (payload: unknown) => {
87
- if (!isNotificationPayload(payload)) return;
85
+ (rawPayload: unknown) => {
86
+ const payload = rawPayload as ProcessProtocolNotificationPayload;
88
87
 
89
88
  if (payload.kind === "log_match") {
90
89
  const now = performance.now();
@@ -113,36 +112,3 @@ export function registerNotificationDelivery(
113
112
  resetWindow();
114
113
  };
115
114
  }
116
-
117
- function isNotificationPayload(
118
- payload: unknown,
119
- ): payload is ProcessProtocolNotificationPayload {
120
- if (!isRecord(payload)) return false;
121
- if (!isNotificationKind(payload.kind)) return false;
122
- if (typeof payload.processId !== "string") return false;
123
- if (typeof payload.processName !== "string") return false;
124
- if (typeof payload.command !== "string") return false;
125
- if (typeof payload.timestamp !== "number") return false;
126
- if (typeof payload.summary !== "string") return false;
127
- if (
128
- payload.attention !== "turn" &&
129
- payload.attention !== "context" &&
130
- payload.attention !== "ignore"
131
- ) {
132
- return false;
133
- }
134
- return true;
135
- }
136
-
137
- function isNotificationKind(
138
- value: unknown,
139
- ): value is ProcessProtocolNotificationPayload["kind"] {
140
- return (
141
- value === "success" ||
142
- value === "failure" ||
143
- value === "crash" ||
144
- value === "killed" ||
145
- value === "log_match" ||
146
- value === "log_match_suppressed"
147
- );
148
- }
@@ -9,8 +9,7 @@ import {
9
9
  type RequestListPayload,
10
10
  type RequestLogFilesPayload,
11
11
  type RequestOutputPayload,
12
- } from "../../../src/protocol";
13
- import { isRecord } from "../../../src/utils/is-record";
12
+ } from "../../shared/protocol";
14
13
  import type { ProcessProtocolConfig } from "../config";
15
14
 
16
15
  export function registerRequestHandlers(
@@ -21,43 +20,36 @@ export function registerRequestHandlers(
21
20
  const disposers = [
22
21
  events.on(CHANNELS.REQUEST_LIST, (payload) => {
23
22
  const request = payload as RequestListPayload;
24
- if (!isRequestListPayload(request)) return;
25
23
 
26
24
  request.reply(manager.list());
27
25
  }),
28
26
  events.on(CHANNELS.REQUEST_GET, (payload) => {
29
27
  const request = payload as RequestGetPayload;
30
- if (!isIdRequest(request)) return;
31
28
 
32
29
  request.reply(manager.get(request.id));
33
30
  }),
34
31
  events.on(CHANNELS.REQUEST_OUTPUT, (payload) => {
35
32
  const request = payload as RequestOutputPayload;
36
- if (!isIdRequest(request)) return;
37
33
 
38
34
  request.reply(manager.getOutput(request.id, request.tailLines));
39
35
  }),
40
36
  events.on(CHANNELS.REQUEST_COMBINED_OUTPUT, (payload) => {
41
37
  const request = payload as RequestCombinedOutputPayload;
42
- if (!isIdRequest(request)) return;
43
38
 
44
39
  request.reply(manager.getCombinedOutput(request.id, request.tailLines));
45
40
  }),
46
41
  events.on(CHANNELS.REQUEST_LOG_FILES, (payload) => {
47
42
  const request = payload as RequestLogFilesPayload;
48
- if (!isIdRequest(request)) return;
49
43
 
50
44
  request.reply(manager.getLogFiles(request.id));
51
45
  }),
52
46
  events.on(CHANNELS.REQUEST_FILE_SIZE, (payload) => {
53
47
  const request = payload as RequestFileSizePayload;
54
- if (!isIdRequest(request)) return;
55
48
 
56
49
  request.reply(manager.getFileSize(request.id));
57
50
  }),
58
51
  events.on(CHANNELS.REQUEST_CONFIG, (payload) => {
59
52
  const request = payload as RequestConfigPayload;
60
- if (!isRequestConfigPayload(request)) return;
61
53
 
62
54
  request.reply(getConfig());
63
55
  }),
@@ -67,48 +59,3 @@ export function registerRequestHandlers(
67
59
  for (const dispose of disposers) dispose();
68
60
  };
69
61
  }
70
-
71
- function isRequestListPayload(
72
- payload: RequestListPayload,
73
- ): payload is RequestListPayload {
74
- return isRecord(payload) && isReply(payload);
75
- }
76
-
77
- function isRequestConfigPayload(
78
- payload: RequestConfigPayload,
79
- ): payload is RequestConfigPayload {
80
- return isRecord(payload) && isReply(payload);
81
- }
82
-
83
- function isIdRequest(
84
- payload:
85
- | RequestGetPayload
86
- | RequestOutputPayload
87
- | RequestCombinedOutputPayload
88
- | RequestLogFilesPayload
89
- | RequestFileSizePayload,
90
- ): payload is
91
- | RequestGetPayload
92
- | RequestOutputPayload
93
- | RequestCombinedOutputPayload
94
- | RequestLogFilesPayload
95
- | RequestFileSizePayload {
96
- return (
97
- isRecord(payload) &&
98
- typeof payload.id === "string" &&
99
- isOptionalNumber(payload.tailLines) &&
100
- isReply(payload)
101
- );
102
- }
103
-
104
- function isReply(
105
- payload: unknown,
106
- ): payload is { reply: (...args: never[]) => void } {
107
- return isRecord(payload) && typeof payload.reply === "function";
108
- }
109
-
110
- function isOptionalNumber(value: unknown): value is number | undefined {
111
- return (
112
- value === undefined || (typeof value === "number" && Number.isFinite(value))
113
- );
114
- }