@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
|
@@ -1,8 +1,8 @@
|
|
|
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 "../../../src/protocol";
|
|
5
4
|
import type { ManagerEvent, ProcessInfo } from "../../../src/types";
|
|
5
|
+
import { CHANNELS } from "../../shared/protocol";
|
|
6
6
|
import { classifyProcessEnd } from "./classify";
|
|
7
7
|
import {
|
|
8
8
|
type CompiledLogMatcher,
|
|
@@ -90,6 +90,9 @@ export function createNotificationService(deps: NotificationServiceDeps): {
|
|
|
90
90
|
const kind = classifyProcessEnd(info);
|
|
91
91
|
|
|
92
92
|
if (isIntentionalStop) {
|
|
93
|
+
const details = buildLifecycleDetails(info, kind, "context");
|
|
94
|
+
events.emit(CHANNELS.NOTIFICATION, details);
|
|
95
|
+
|
|
93
96
|
cleanupMatcherState(info.id);
|
|
94
97
|
registry.unregister(info.id);
|
|
95
98
|
return;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Re-export the protocol-safe notification types so the core extension and the
|
|
2
2
|
// protocol layer cannot drift apart. The canonical shape lives in
|
|
3
|
-
// `
|
|
3
|
+
// `extensions/shared/protocol/notifications.ts`; these names keep existing import sites
|
|
4
4
|
// stable while guaranteeing structural compatibility with the events emitted on
|
|
5
5
|
// CHANNELS.NOTIFICATION.
|
|
6
6
|
export type {
|
|
@@ -8,4 +8,4 @@ export type {
|
|
|
8
8
|
ProcessProtocolNotificationKind as ProcessNotificationKind,
|
|
9
9
|
ProcessProtocolNotificationLogMatch as ProcessNotificationLogMatchDetails,
|
|
10
10
|
ProcessProtocolNotificationPayload as ProcessNotificationDetails,
|
|
11
|
-
} from "
|
|
11
|
+
} from "../../shared/protocol";
|
|
@@ -1,17 +1,9 @@
|
|
|
1
|
-
import { isRecord } from "../../../src/utils/is-record";
|
|
2
1
|
import {
|
|
3
2
|
MAX_LOG_MATCH_PATTERN_LENGTH,
|
|
4
3
|
MAX_LOG_MATCHERS_PER_PROCESS,
|
|
5
4
|
} from "../notifications/log-matchers";
|
|
6
5
|
import type { LogMatcherConfig, NotifyConfig } from "../notifications/registry";
|
|
7
|
-
import type {
|
|
8
|
-
|
|
9
|
-
const ATTENTIONS = ["turn", "context", "ignore"] as const;
|
|
10
|
-
const LOG_MATCH_MODES = ["literal", "regex"] as const;
|
|
11
|
-
const LOG_MATCH_STREAMS = ["stdout", "stderr", "both"] as const;
|
|
12
|
-
|
|
13
|
-
export const MAX_NOTIFY_LOG_MATCHERS = MAX_LOG_MATCHERS_PER_PROCESS;
|
|
14
|
-
export const MAX_NOTIFY_PATTERN_LENGTH = MAX_LOG_MATCH_PATTERN_LENGTH;
|
|
6
|
+
import type { NotifyLogMatchParamsType, NotifyParamsType } from "./schema";
|
|
15
7
|
|
|
16
8
|
const DEFAULT_NOTIFY_CONFIG = {
|
|
17
9
|
// A backgrounded process usually outlives the turn that started it, and
|
|
@@ -29,120 +21,66 @@ const DEFAULT_NOTIFY_CONFIG = {
|
|
|
29
21
|
onKilled: "context",
|
|
30
22
|
} as const satisfies Pick<NotifyConfig, "onSuccess" | "onFailure" | "onKilled">;
|
|
31
23
|
|
|
32
|
-
export
|
|
33
|
-
|
|
34
|
-
return { ...DEFAULT_NOTIFY_CONFIG, logMatches: [] };
|
|
35
|
-
}
|
|
24
|
+
export const MAX_NOTIFY_LOG_MATCHERS = MAX_LOG_MATCHERS_PER_PROCESS;
|
|
25
|
+
export const MAX_NOTIFY_PATTERN_LENGTH = MAX_LOG_MATCH_PATTERN_LENGTH;
|
|
36
26
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
27
|
+
// Input arrives already validated against the TypeBox schema in ./schema.ts
|
|
28
|
+
// (Pi validates tool call arguments before execute). Only semantic checks the
|
|
29
|
+
// schema cannot express remain here: whitespace-only patterns (would match
|
|
30
|
+
// every line) and regex validity.
|
|
40
31
|
|
|
41
|
-
|
|
32
|
+
export function normalizeNotifyConfig(
|
|
33
|
+
input: NotifyParamsType | undefined,
|
|
34
|
+
): NotifyConfig {
|
|
35
|
+
if (!input) {
|
|
36
|
+
return { ...DEFAULT_NOTIFY_CONFIG, logMatches: [] };
|
|
37
|
+
}
|
|
42
38
|
|
|
43
39
|
return {
|
|
44
|
-
onSuccess:
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
normalizeAttention(input.onKilled, "notify.onKilled") ??
|
|
52
|
-
DEFAULT_NOTIFY_CONFIG.onKilled,
|
|
53
|
-
logMatches: normalizeLogMatches(logMatches),
|
|
40
|
+
onSuccess: input.onSuccess ?? DEFAULT_NOTIFY_CONFIG.onSuccess,
|
|
41
|
+
onFailure: input.onFailure ?? DEFAULT_NOTIFY_CONFIG.onFailure,
|
|
42
|
+
onKilled: input.onKilled ?? DEFAULT_NOTIFY_CONFIG.onKilled,
|
|
43
|
+
logMatches: normalizeLogMatches(input.logMatches ?? [], {
|
|
44
|
+
actionLabel: "process start",
|
|
45
|
+
pathPrefix: "notify.logMatches",
|
|
46
|
+
}),
|
|
54
47
|
};
|
|
55
48
|
}
|
|
56
49
|
|
|
57
|
-
function normalizeLogMatches(input: unknown): LogMatcherConfig[] {
|
|
58
|
-
if (input === undefined || input === null) return [];
|
|
59
|
-
|
|
60
|
-
if (!Array.isArray(input)) {
|
|
61
|
-
throw new Error("process start notify.logMatches must be an array");
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (input.length > MAX_NOTIFY_LOG_MATCHERS) {
|
|
65
|
-
throw new Error(
|
|
66
|
-
`process start notify.logMatches supports at most ${MAX_NOTIFY_LOG_MATCHERS} matchers`,
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return input.map((entry, index) => normalizeLogMatch(entry, index));
|
|
71
|
-
}
|
|
72
|
-
|
|
73
50
|
export function normalizeLogMatchItems(
|
|
74
|
-
input:
|
|
51
|
+
input: NotifyLogMatchParamsType[],
|
|
75
52
|
options: {
|
|
76
53
|
actionLabel: string;
|
|
77
54
|
pathPrefix: string;
|
|
78
|
-
maxItems?: number;
|
|
79
55
|
},
|
|
80
56
|
): LogMatcherConfig[] {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if (input === undefined || input === null) {
|
|
85
|
-
throw new Error(`${actionLabel} ${pathPrefix} is required`);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (!Array.isArray(input)) {
|
|
89
|
-
throw new Error(`${actionLabel} ${pathPrefix} must be an array`);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if (input.length === 0) {
|
|
93
|
-
throw new Error(`${actionLabel} ${pathPrefix} must not be empty`);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
if (input.length > maxItems) {
|
|
97
|
-
throw new Error(
|
|
98
|
-
`${actionLabel} ${pathPrefix} supports at most ${maxItems} items`,
|
|
99
|
-
);
|
|
100
|
-
}
|
|
57
|
+
return input.map((entry, index) => normalizeLogMatch(entry, index, options));
|
|
58
|
+
}
|
|
101
59
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
60
|
+
function normalizeLogMatches(
|
|
61
|
+
input: NotifyLogMatchParamsType[],
|
|
62
|
+
options: { actionLabel: string; pathPrefix: string },
|
|
63
|
+
): LogMatcherConfig[] {
|
|
64
|
+
return input.map((entry, index) => normalizeLogMatch(entry, index, options));
|
|
105
65
|
}
|
|
106
66
|
|
|
107
|
-
|
|
108
|
-
input:
|
|
67
|
+
function normalizeLogMatch(
|
|
68
|
+
input: NotifyLogMatchParamsType,
|
|
109
69
|
index: number,
|
|
110
|
-
options
|
|
70
|
+
options: { actionLabel: string; pathPrefix: string },
|
|
111
71
|
): LogMatcherConfig {
|
|
112
|
-
const
|
|
113
|
-
const path = `${options?.pathPrefix ?? "notify.logMatches"}[${index}]`;
|
|
114
|
-
|
|
115
|
-
if (!isRecord(input)) {
|
|
116
|
-
throw new Error(`${actionLabel} ${path} must be an object`);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if (typeof input.pattern !== "string") {
|
|
120
|
-
throw new Error(`${actionLabel} ${path}.pattern must be a string`);
|
|
121
|
-
}
|
|
72
|
+
const path = `${options.pathPrefix}[${index}]`;
|
|
122
73
|
|
|
123
74
|
// An empty or whitespace-only literal pattern matches every line
|
|
124
75
|
// (String#includes("")), and an empty regex matches every line too. Reject
|
|
125
76
|
// early so a stray "" from the model does not fire a notification per line.
|
|
126
77
|
if (input.pattern.trim().length === 0) {
|
|
127
78
|
throw new Error(
|
|
128
|
-
`${actionLabel} ${path}.pattern must not be empty or whitespace-only`,
|
|
129
|
-
);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
if (input.pattern.length > MAX_NOTIFY_PATTERN_LENGTH) {
|
|
133
|
-
throw new Error(
|
|
134
|
-
`${actionLabel} ${path}.pattern must be at most ${MAX_NOTIFY_PATTERN_LENGTH} characters`,
|
|
79
|
+
`${options.actionLabel} ${path}.pattern must not be empty or whitespace-only`,
|
|
135
80
|
);
|
|
136
81
|
}
|
|
137
82
|
|
|
138
|
-
const mode =
|
|
139
|
-
normalizeStringEnum(input.mode, LOG_MATCH_MODES, `${path}.mode`) ??
|
|
140
|
-
"literal";
|
|
141
|
-
const stream =
|
|
142
|
-
normalizeStringEnum(input.stream, LOG_MATCH_STREAMS, `${path}.stream`) ??
|
|
143
|
-
"both";
|
|
144
|
-
const repeat = normalizeBoolean(input.repeat, `${path}.repeat`) ?? false;
|
|
145
|
-
const on = normalizeAttention(input.on, `${path}.on`) ?? "turn";
|
|
83
|
+
const mode = input.mode ?? "literal";
|
|
146
84
|
|
|
147
85
|
if (mode === "regex") {
|
|
148
86
|
validateRegex(input.pattern, path);
|
|
@@ -151,43 +89,12 @@ export function normalizeLogMatch(
|
|
|
151
89
|
return {
|
|
152
90
|
pattern: input.pattern,
|
|
153
91
|
mode,
|
|
154
|
-
stream,
|
|
155
|
-
repeat,
|
|
156
|
-
on,
|
|
92
|
+
stream: input.stream ?? "both",
|
|
93
|
+
repeat: input.repeat ?? false,
|
|
94
|
+
on: input.on ?? "turn",
|
|
157
95
|
};
|
|
158
96
|
}
|
|
159
97
|
|
|
160
|
-
function normalizeAttention(
|
|
161
|
-
input: unknown,
|
|
162
|
-
path: string,
|
|
163
|
-
): Attention | undefined {
|
|
164
|
-
return normalizeStringEnum(input, ATTENTIONS, path);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function normalizeStringEnum<const T extends readonly string[]>(
|
|
168
|
-
input: unknown,
|
|
169
|
-
allowed: T,
|
|
170
|
-
path: string,
|
|
171
|
-
): T[number] | undefined {
|
|
172
|
-
if (input === undefined || input === null) return undefined;
|
|
173
|
-
|
|
174
|
-
if (typeof input !== "string" || !allowed.includes(input)) {
|
|
175
|
-
throw new Error(`${path} must be one of: ${allowed.join(", ")}`);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
return input;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function normalizeBoolean(input: unknown, path: string): boolean | undefined {
|
|
182
|
-
if (input === undefined || input === null) return undefined;
|
|
183
|
-
|
|
184
|
-
if (typeof input !== "boolean") {
|
|
185
|
-
throw new Error(`${path} must be a boolean`);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
return input;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
98
|
function validateRegex(pattern: string, path: string): void {
|
|
192
99
|
try {
|
|
193
100
|
new RegExp(pattern);
|
|
@@ -48,7 +48,7 @@ export const PROCESS_NOTIFY_LOG_MATCH_STREAMS = [
|
|
|
48
48
|
"both",
|
|
49
49
|
] as const;
|
|
50
50
|
|
|
51
|
-
const NotifyLogMatchParams = Type.Object({
|
|
51
|
+
export const NotifyLogMatchParams = Type.Object({
|
|
52
52
|
pattern: Type.String({
|
|
53
53
|
maxLength: MAX_NOTIFY_PATTERN_LENGTH,
|
|
54
54
|
description:
|
|
@@ -140,7 +140,7 @@ const NotifyProperties = {
|
|
|
140
140
|
),
|
|
141
141
|
};
|
|
142
142
|
|
|
143
|
-
const NotifyParams = Type.Object(NotifyProperties, {
|
|
143
|
+
export const NotifyParams = Type.Object(NotifyProperties, {
|
|
144
144
|
description:
|
|
145
145
|
"Notify settings. Attention: turn wakes an idle agent, context only reaches an agent still working, ignore never notifies.",
|
|
146
146
|
});
|
|
@@ -248,6 +248,9 @@ export const ProcessesParams = Type.Object({
|
|
|
248
248
|
|
|
249
249
|
export type ProcessesParamsType = Static<typeof ProcessesParams>;
|
|
250
250
|
|
|
251
|
+
export type NotifyParamsType = Static<typeof NotifyParams>;
|
|
252
|
+
export type NotifyLogMatchParamsType = Static<typeof NotifyLogMatchParams>;
|
|
253
|
+
|
|
251
254
|
export type ProcessAction = ProcessesParamsType["action"];
|
|
252
255
|
export type ProcessListStatusFilter =
|
|
253
256
|
(typeof PROCESS_LIST_STATUS_FILTERS)[number];
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { ProcessManager } from "../../../../src/manager";
|
|
2
2
|
import type { ProcessInfo } from "../../../../src/types";
|
|
3
3
|
import { LIVE_STATUSES } from "../../../../src/types";
|
|
4
|
-
import { isRecord } from "../../../../src/utils/is-record";
|
|
5
4
|
import type {
|
|
6
5
|
LogMatcherConfig,
|
|
7
6
|
NotificationRegistry,
|
|
@@ -187,7 +186,7 @@ function applyWatchUpdate(
|
|
|
187
186
|
}
|
|
188
187
|
|
|
189
188
|
if (mode === "append" || mode === "replace") {
|
|
190
|
-
const normalized = normalizeLogMatchItems(items, {
|
|
189
|
+
const normalized = normalizeLogMatchItems(items as never, {
|
|
191
190
|
actionLabel: "process update",
|
|
192
191
|
pathPrefix: "watches.items",
|
|
193
192
|
});
|
|
@@ -205,46 +204,22 @@ function applyWatchUpdate(
|
|
|
205
204
|
throw new Error(`unsupported watch update mode: ${String(mode)}`);
|
|
206
205
|
}
|
|
207
206
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
return items.map((
|
|
219
|
-
if (!isRecord(raw)) {
|
|
220
|
-
throw new Error(
|
|
221
|
-
`process update watches.items[${index}] must be an object`,
|
|
222
|
-
);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
const item = raw as RawRemoveItem;
|
|
226
|
-
|
|
207
|
+
// Watch items arrive already validated against the WatchUpdateItemParams
|
|
208
|
+
// schema (Pi validates tool call arguments before execute). Only semantic
|
|
209
|
+
// checks the schema cannot express remain here.
|
|
210
|
+
type WatchUpdateItem = NonNullable<
|
|
211
|
+
NonNullable<ProcessesParamsType["watches"]>["items"]
|
|
212
|
+
>[number];
|
|
213
|
+
|
|
214
|
+
function normalizeWatchRemoveSpecs(
|
|
215
|
+
items: WatchUpdateItem[],
|
|
216
|
+
): WatchRemoveSpec[] {
|
|
217
|
+
return items.map((item, index) => {
|
|
227
218
|
if (item.index !== undefined) {
|
|
228
|
-
if (
|
|
229
|
-
typeof item.index !== "number" ||
|
|
230
|
-
!Number.isInteger(item.index) ||
|
|
231
|
-
item.index < 0
|
|
232
|
-
) {
|
|
233
|
-
throw new Error(
|
|
234
|
-
`process update watches.items[${index}].index must be a non-negative integer`,
|
|
235
|
-
);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
219
|
return { index: item.index };
|
|
239
220
|
}
|
|
240
221
|
|
|
241
222
|
if (item.pattern !== undefined) {
|
|
242
|
-
if (typeof item.pattern !== "string") {
|
|
243
|
-
throw new Error(
|
|
244
|
-
`process update watches.items[${index}].pattern must be a string`,
|
|
245
|
-
);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
223
|
if (item.pattern.trim().length === 0) {
|
|
249
224
|
throw new Error(
|
|
250
225
|
`process update watches.items[${index}].pattern must not be empty`,
|
|
@@ -265,10 +240,10 @@ function normalizeWatchRemoveSpecs(items: unknown[]): WatchRemoveSpec[] {
|
|
|
265
240
|
|
|
266
241
|
return {
|
|
267
242
|
pattern: item.pattern,
|
|
268
|
-
mode: item.mode
|
|
269
|
-
stream: item.stream
|
|
243
|
+
mode: item.mode,
|
|
244
|
+
stream: item.stream,
|
|
270
245
|
repeat: item.repeat,
|
|
271
|
-
on: item.on
|
|
246
|
+
on: item.on,
|
|
272
247
|
};
|
|
273
248
|
}
|
|
274
249
|
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
import { requestStart } from "../processes/client";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Debug extension for pi-processes.
|
|
7
|
+
*
|
|
8
|
+
* This extension is NOT listed in `package.json` under `pi.extensions`.
|
|
9
|
+
* Load it explicitly via `pi -ne -e ~/pi-processes-debug/`.
|
|
10
|
+
*
|
|
11
|
+
* It provides "programmatic" control over process management by exposing
|
|
12
|
+
* slash commands that go through the core extension's protocol channels,
|
|
13
|
+
* so processes started here are fully visible to the agent's `process` tool.
|
|
14
|
+
* This lets you start a background process during debugging without needing
|
|
15
|
+
* to prompt the agent.
|
|
16
|
+
*/
|
|
17
|
+
export default async function processesDebugExtension(
|
|
18
|
+
pi: ExtensionAPI,
|
|
19
|
+
): Promise<void> {
|
|
20
|
+
const events = pi.events;
|
|
21
|
+
|
|
22
|
+
pi.registerCommand("debug:ps:start", {
|
|
23
|
+
description:
|
|
24
|
+
"Start a background process (debug). Usage: /debug:ps:start <name> <command>",
|
|
25
|
+
handler: async (args: string, ctx) => {
|
|
26
|
+
const parsed = parseStartArgs(args);
|
|
27
|
+
if (!parsed) {
|
|
28
|
+
ctx.ui.notify("Usage: /debug:ps:start <name> <command>", "warning");
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const result = await requestStart(events, {
|
|
33
|
+
name: parsed.name,
|
|
34
|
+
command: parsed.command,
|
|
35
|
+
cwd: ctx.cwd,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
if (result.ok) {
|
|
39
|
+
ctx.ui.notify(
|
|
40
|
+
`Started ${result.process.name} (${result.process.id}) — pid ${result.process.pid}`,
|
|
41
|
+
"info",
|
|
42
|
+
);
|
|
43
|
+
} else {
|
|
44
|
+
ctx.ui.notify(`Failed to start process: ${result.error}`, "warning");
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface ParsedStartArgs {
|
|
51
|
+
name: string;
|
|
52
|
+
command: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function parseStartArgs(args: string): ParsedStartArgs | null {
|
|
56
|
+
const trimmed = args.trim();
|
|
57
|
+
if (!trimmed) return null;
|
|
58
|
+
|
|
59
|
+
const firstSpace = trimmed.indexOf(" ");
|
|
60
|
+
if (firstSpace === -1) return null;
|
|
61
|
+
|
|
62
|
+
const name = trimmed.slice(0, firstSpace);
|
|
63
|
+
const command = trimmed.slice(firstSpace + 1).trim();
|
|
64
|
+
if (!command) return null;
|
|
65
|
+
|
|
66
|
+
return { name, command };
|
|
67
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { EventBus } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { ProcessInfo } from "../../src/types";
|
|
2
3
|
import {
|
|
3
4
|
CHANNELS,
|
|
4
5
|
type ProcessProtocolConfig,
|
|
@@ -6,8 +7,7 @@ import {
|
|
|
6
7
|
type RequestConfigPayload,
|
|
7
8
|
type RequestGetPayload,
|
|
8
9
|
type RequestListPayload,
|
|
9
|
-
} from "
|
|
10
|
-
import type { ProcessInfo } from "../../src/types";
|
|
10
|
+
} from "../shared/protocol";
|
|
11
11
|
|
|
12
12
|
export type ProcessLogLine = { type: "stdout" | "stderr"; text: string };
|
|
13
13
|
|
|
@@ -3,15 +3,14 @@ import type {
|
|
|
3
3
|
ExtensionContext,
|
|
4
4
|
Theme,
|
|
5
5
|
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { LIVE_STATUSES, type ProcessInfo } from "../../../src/types";
|
|
7
|
+
import { buildDroppedOutputLine, trimToBudget } from "../../shared/line-buffer";
|
|
6
8
|
import {
|
|
7
9
|
CHANNELS,
|
|
8
10
|
type CommandPinPayload,
|
|
11
|
+
type ProcessesOutputChangedPayload,
|
|
9
12
|
type ProcessProtocolNotificationPayload,
|
|
10
|
-
} from "
|
|
11
|
-
import { LIVE_STATUSES, type ProcessInfo } from "../../../src/types";
|
|
12
|
-
import { isRecord } from "../../../src/utils/is-record";
|
|
13
|
-
import { buildDroppedOutputLine, trimToBudget } from "../../shared/line-buffer";
|
|
14
|
-
import { isOutputChangedPayload } from "../../shared/output-payload";
|
|
13
|
+
} from "../../shared/protocol";
|
|
15
14
|
import {
|
|
16
15
|
type ProcessLogLine,
|
|
17
16
|
requestCombinedOutput,
|
|
@@ -342,6 +341,9 @@ export function setupDockWidgets(
|
|
|
342
341
|
};
|
|
343
342
|
|
|
344
343
|
const handleStarted = () => {
|
|
344
|
+
// Set synchronously so a process that exits within the scheduleRefresh
|
|
345
|
+
// throttle window still counts as "seen running" for auto-close.
|
|
346
|
+
hasSeenRunningProcess = true;
|
|
345
347
|
if (config.widget.dockDefaultState === "expanded") state.actions.expand();
|
|
346
348
|
else if (config.widget.dockDefaultState === "collapsed") {
|
|
347
349
|
state.actions.collapse();
|
|
@@ -349,11 +351,8 @@ export function setupDockWidgets(
|
|
|
349
351
|
scheduleRefresh();
|
|
350
352
|
};
|
|
351
353
|
|
|
352
|
-
const handleOutputChanged = (
|
|
353
|
-
|
|
354
|
-
scheduleRefresh();
|
|
355
|
-
return;
|
|
356
|
-
}
|
|
354
|
+
const handleOutputChanged = (rawPayload: unknown) => {
|
|
355
|
+
const payload = rawPayload as ProcessesOutputChangedPayload;
|
|
357
356
|
if (
|
|
358
357
|
(!payload.appendedText || payload.appendedText.length === 0) &&
|
|
359
358
|
!payload.droppedLines
|
|
@@ -383,7 +382,6 @@ export function setupDockWidgets(
|
|
|
383
382
|
|
|
384
383
|
const handlePin = (payload: unknown) => {
|
|
385
384
|
const command = payload as CommandPinPayload;
|
|
386
|
-
if (!isCommandPinPayload(command)) return;
|
|
387
385
|
// COMMAND_PIN can arrive before the dock's throttled CHANGED refresh has
|
|
388
386
|
// run. Refresh the local snapshot first so expand/pin renders immediately
|
|
389
387
|
// against the current process list.
|
|
@@ -427,8 +425,9 @@ export function setupDockWidgets(
|
|
|
427
425
|
}),
|
|
428
426
|
);
|
|
429
427
|
disposers.push(
|
|
430
|
-
events.on(CHANNELS.NOTIFICATION, (
|
|
431
|
-
|
|
428
|
+
events.on(CHANNELS.NOTIFICATION, (rawPayload) => {
|
|
429
|
+
const payload = rawPayload as ProcessProtocolNotificationPayload;
|
|
430
|
+
if (payload.kind !== "log_match" || !payload.logMatch) return;
|
|
432
431
|
const list = notifyMarkers.get(payload.processId) ?? [];
|
|
433
432
|
list.push({ line: payload.logMatch.line, timestamp: payload.timestamp });
|
|
434
433
|
if (list.length > MAX_NOTIFY_MARKERS_PER_PROCESS) {
|
|
@@ -486,30 +485,6 @@ function isLogsConnectionError(
|
|
|
486
485
|
return "ok" in connection && connection.ok === false;
|
|
487
486
|
}
|
|
488
487
|
|
|
489
|
-
function isLogMatchNotification(
|
|
490
|
-
payload: unknown,
|
|
491
|
-
): payload is ProcessProtocolNotificationPayload & {
|
|
492
|
-
kind: "log_match";
|
|
493
|
-
logMatch: NonNullable<ProcessProtocolNotificationPayload["logMatch"]>;
|
|
494
|
-
} {
|
|
495
|
-
return (
|
|
496
|
-
isRecord(payload) &&
|
|
497
|
-
payload.kind === "log_match" &&
|
|
498
|
-
typeof payload.processId === "string" &&
|
|
499
|
-
typeof payload.timestamp === "number" &&
|
|
500
|
-
isRecord(payload.logMatch) &&
|
|
501
|
-
typeof payload.logMatch.line === "string"
|
|
502
|
-
);
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
function isCommandPinPayload(payload: unknown): payload is CommandPinPayload {
|
|
506
|
-
return (
|
|
507
|
-
isRecord(payload) &&
|
|
508
|
-
(typeof payload.id === "string" || payload.id === null) &&
|
|
509
|
-
typeof payload.reply === "function"
|
|
510
|
-
);
|
|
511
|
-
}
|
|
512
|
-
|
|
513
488
|
function safeReply<T>(reply: (result: T) => void, result: T): void {
|
|
514
489
|
try {
|
|
515
490
|
reply(result);
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import type { EventBus } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { ProcessInfo } from "../../src/types";
|
|
2
3
|
import {
|
|
3
4
|
CHANNELS,
|
|
4
5
|
type ProcessProtocolConfig,
|
|
5
6
|
type RequestConfigPayload,
|
|
6
7
|
type RequestGetPayload,
|
|
7
8
|
type RequestListPayload,
|
|
8
|
-
} from "
|
|
9
|
-
import type { ProcessInfo } from "../../src/types";
|
|
9
|
+
} from "../shared/protocol";
|
|
10
10
|
|
|
11
11
|
export function requestProcessList(events: EventBus): ProcessInfo[] {
|
|
12
12
|
let processes: ProcessInfo[] = [];
|
|
@@ -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 { allProcessCompletions } from "../completions";
|
|
12
12
|
import { LogOverlayComponent } from "../components/log-overlay-component";
|