@aliou/pi-processes 0.10.9 → 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.
- package/extensions/processes/client.ts +24 -2
- package/extensions/processes/commands/overview.ts +1 -1
- package/extensions/processes/components/overview-component.ts +121 -51
- package/extensions/processes/config/migrations/001-v0-9-4-to-v0-10-0-config.ts +5 -8
- package/extensions/processes/config/types.ts +1 -1
- package/extensions/processes/handlers/commands.ts +21 -41
- package/extensions/processes/handlers/notifications.ts +3 -37
- package/extensions/processes/handlers/requests.ts +1 -54
- package/extensions/processes/handlers/subscriptions.ts +2 -35
- package/extensions/processes/hooks/event-bridge.ts +1 -1
- package/extensions/processes/index.ts +2 -2
- package/extensions/processes/notifications/service.ts +4 -1
- package/extensions/processes/notifications/types.ts +2 -2
- package/extensions/processes/tools/notify.ts +37 -130
- package/extensions/processes/tools/schema.ts +5 -2
- package/extensions/processes/tools/update/index.ts +15 -40
- package/extensions/processes-debug/index.ts +67 -0
- package/extensions/processes-dock/client.ts +2 -2
- package/extensions/processes-dock/widget/setup.ts +12 -37
- package/extensions/processes-logs/client.ts +2 -2
- package/extensions/processes-logs/commands/logs.ts +1 -1
- package/extensions/processes-logs/components/log-file-viewer.ts +135 -8
- package/extensions/processes-logs/components/log-overlay-component.ts +156 -82
- package/extensions/processes-logs/logs-client.ts +2 -23
- package/extensions/shared/log-line.ts +49 -2
- package/{src → extensions/shared}/protocol/broadcasts.ts +1 -1
- package/{src → extensions/shared}/protocol/channels.ts +1 -0
- package/{src → extensions/shared}/protocol/commands.ts +12 -1
- package/{src → extensions/shared}/protocol/index.ts +2 -0
- package/{src → extensions/shared}/protocol/notifications.ts +1 -1
- package/{src → extensions/shared}/protocol/requests.ts +1 -1
- package/extensions/shared/shortcut-hints.ts +150 -0
- package/extensions/shared/shortcuts-overlay.ts +229 -0
- package/extensions/shared/truncate.ts +189 -0
- package/package.json +3 -3
- package/src/utils/command-executor.ts +2 -1
- package/extensions/shared/output-payload.ts +0 -28
- package/src/get-manager.ts +0 -15
- package/src/utils/is-record.ts +0 -3
- /package/{src → extensions/shared}/protocol/logs.ts +0 -0
|
@@ -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 "
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
135
|
-
|
|
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(
|
|
199
|
-
|
|
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
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
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
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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 "
|
|
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
|
-
|
|
9
|
-
|
|
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 "
|
|
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
|
-
(
|
|
87
|
-
|
|
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 "
|
|
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
|
-
}
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import type { EventBus } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
|
|
3
3
|
import type { ProcessManager } from "../../../src/manager";
|
|
4
|
+
import { buildDroppedOutputLine } from "../../shared/line-buffer";
|
|
4
5
|
import {
|
|
5
6
|
CHANNELS,
|
|
6
7
|
type LogsSubscribePayload,
|
|
7
8
|
type LogsUnsubscribePayload,
|
|
8
|
-
} from "
|
|
9
|
-
import { isRecord } from "../../../src/utils/is-record";
|
|
10
|
-
import { buildDroppedOutputLine } from "../../shared/line-buffer";
|
|
9
|
+
} from "../../shared/protocol";
|
|
11
10
|
|
|
12
11
|
interface LogSubscriber {
|
|
13
12
|
subscriberId: string;
|
|
@@ -22,7 +21,6 @@ export function registerLogSubscriptions(
|
|
|
22
21
|
|
|
23
22
|
const disposeSubscribe = events.on(CHANNELS.LOGS_SUBSCRIBE, (payload) => {
|
|
24
23
|
const request = payload as LogsSubscribePayload;
|
|
25
|
-
if (!isLogsSubscribePayload(request)) return;
|
|
26
24
|
|
|
27
25
|
const processInfo = manager.get(request.processId);
|
|
28
26
|
|
|
@@ -51,7 +49,6 @@ export function registerLogSubscriptions(
|
|
|
51
49
|
|
|
52
50
|
const disposeUnsubscribe = events.on(CHANNELS.LOGS_UNSUBSCRIBE, (payload) => {
|
|
53
51
|
const request = payload as LogsUnsubscribePayload;
|
|
54
|
-
if (!isLogsUnsubscribePayload(request)) return;
|
|
55
52
|
|
|
56
53
|
subscribers.delete(request.subscriberId);
|
|
57
54
|
});
|
|
@@ -112,33 +109,3 @@ function removeStaleSubscribers(
|
|
|
112
109
|
if (!manager.get(subscriber.processId)) subscribers.delete(subscriberId);
|
|
113
110
|
}
|
|
114
111
|
}
|
|
115
|
-
|
|
116
|
-
function isLogsSubscribePayload(
|
|
117
|
-
payload: LogsSubscribePayload,
|
|
118
|
-
): payload is LogsSubscribePayload {
|
|
119
|
-
return (
|
|
120
|
-
isRecord(payload) &&
|
|
121
|
-
typeof payload.subscriberId === "string" &&
|
|
122
|
-
typeof payload.processId === "string" &&
|
|
123
|
-
isOptionalNumber(payload.tailLines) &&
|
|
124
|
-
isReply(payload)
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function isLogsUnsubscribePayload(
|
|
129
|
-
payload: LogsUnsubscribePayload,
|
|
130
|
-
): payload is LogsUnsubscribePayload {
|
|
131
|
-
return isRecord(payload) && typeof payload.subscriberId === "string";
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function isReply(
|
|
135
|
-
payload: unknown,
|
|
136
|
-
): payload is { reply: (...args: never[]) => void } {
|
|
137
|
-
return isRecord(payload) && typeof payload.reply === "function";
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function isOptionalNumber(value: unknown): value is number | undefined {
|
|
141
|
-
return (
|
|
142
|
-
value === undefined || (typeof value === "number" && Number.isFinite(value))
|
|
143
|
-
);
|
|
144
|
-
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { EventBus } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
|
|
3
3
|
import type { ProcessManager } from "../../../src/manager";
|
|
4
|
-
import { CHANNELS } from "
|
|
4
|
+
import { CHANNELS } from "../../shared/protocol";
|
|
5
5
|
|
|
6
6
|
export function registerEventBridge(
|
|
7
7
|
events: EventBus,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import {
|
|
2
|
+
import { ProcessManager } from "../../src/manager";
|
|
3
3
|
import { isWindowsPlatform } from "../../src/utils/platform";
|
|
4
4
|
import { registerClearCommand } from "./commands/clear";
|
|
5
5
|
import { registerKillCommand } from "./commands/kill";
|
|
@@ -40,7 +40,7 @@ export default async function processesExtension(
|
|
|
40
40
|
await loadProcessConfig();
|
|
41
41
|
registerMigrationMessageNotifications(pi);
|
|
42
42
|
|
|
43
|
-
const manager =
|
|
43
|
+
const manager = new ProcessManager({
|
|
44
44
|
getConfiguredShellPath: () => configLoader.getConfig().execution.shellPath,
|
|
45
45
|
});
|
|
46
46
|
const notifications = createNotificationRegistry();
|