@getpaseo/server 0.1.96 → 0.1.97-beta.2
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/dist/server/{utils/executable.d.ts → executable-resolution/executable-resolution.d.ts} +2 -2
- package/dist/server/{utils/executable.js → executable-resolution/executable-resolution.js} +16 -14
- package/dist/server/executable-resolution/windows.d.ts +18 -0
- package/dist/server/executable-resolution/windows.js +62 -0
- package/dist/server/server/agent/agent-loading.js +4 -1
- package/dist/server/server/agent/agent-manager.d.ts +10 -2
- package/dist/server/server/agent/agent-manager.js +34 -46
- package/dist/server/server/agent/agent-projections.js +3 -0
- package/dist/server/server/agent/agent-prompt.js +19 -1
- package/dist/server/server/agent/agent-response-loop.js +2 -4
- package/dist/server/server/agent/agent-storage.d.ts +18 -19
- package/dist/server/server/agent/agent-storage.js +6 -23
- package/dist/server/server/agent/create-agent/create.d.ts +2 -12
- package/dist/server/server/agent/create-agent/create.js +28 -30
- package/dist/server/server/agent/create-agent-lifecycle-dispatch.d.ts +4 -2
- package/dist/server/server/agent/create-agent-lifecycle-dispatch.js +31 -22
- package/dist/server/server/agent/create-agent-title.d.ts +2 -0
- package/dist/server/server/agent/create-agent-title.js +5 -0
- package/dist/server/server/agent/import-sessions.d.ts +1 -10
- package/dist/server/server/agent/import-sessions.js +1 -53
- package/dist/server/server/agent/lifecycle-command.js +5 -4
- package/dist/server/server/agent/mcp-server.d.ts +8 -5
- package/dist/server/server/agent/mcp-server.js +41 -14
- package/dist/server/server/agent/mcp-shared.d.ts +6 -3
- package/dist/server/server/agent/mcp-shared.js +3 -0
- package/dist/server/server/agent/provider-launch-config.js +1 -1
- package/dist/server/server/agent/providers/acp-agent.d.ts +5 -0
- package/dist/server/server/agent/providers/acp-agent.js +31 -26
- package/dist/server/server/agent/providers/claude/agent.js +45 -6
- package/dist/server/server/agent/providers/codex-app-server-agent.js +1 -1
- package/dist/server/server/agent/providers/copilot-acp-agent.js +1 -0
- package/dist/server/server/agent/providers/cursor-acp-agent.d.ts +0 -7
- package/dist/server/server/agent/providers/cursor-acp-agent.js +0 -78
- package/dist/server/server/agent/providers/mock-load-test-agent.d.ts +2 -0
- package/dist/server/server/agent/providers/mock-load-test-agent.js +73 -1
- package/dist/server/server/agent/providers/opencode/server-manager.js +1 -1
- package/dist/server/server/agent/structured-generation-providers.js +45 -1
- package/dist/server/server/agent-attention-policy.d.ts +12 -3
- package/dist/server/server/agent-attention-policy.js +15 -3
- package/dist/server/server/auto-archive-on-merge/archive-if-safe.d.ts +7 -6
- package/dist/server/server/auto-archive-on-merge/archive-if-safe.js +21 -16
- package/dist/server/server/bootstrap.d.ts +3 -0
- package/dist/server/server/bootstrap.js +125 -64
- package/dist/server/server/config.js +1 -0
- package/dist/server/server/daemon-config-store.js +1 -0
- package/dist/server/server/exports.d.ts +1 -1
- package/dist/server/server/exports.js +1 -1
- package/dist/server/server/loop-service.d.ts +24 -24
- package/dist/server/server/migrations/backfill-workspace-id.migration.d.ts +9 -0
- package/dist/server/server/migrations/backfill-workspace-id.migration.js +60 -0
- package/dist/server/server/paseo-worktree-service.d.ts +9 -0
- package/dist/server/server/paseo-worktree-service.js +74 -12
- package/dist/server/server/path-utils.d.ts +1 -0
- package/dist/server/server/path-utils.js +6 -1
- package/dist/server/server/persisted-config.d.ts +7 -0
- package/dist/server/server/persisted-config.js +1 -0
- package/dist/server/server/persistence-hooks.d.ts +1 -0
- package/dist/server/server/persistence-hooks.js +13 -5
- package/dist/server/server/resolve-workspace-id-for-path.d.ts +3 -0
- package/dist/server/server/resolve-workspace-id-for-path.js +41 -0
- package/dist/server/server/script-proxy.d.ts +1 -1
- package/dist/server/server/script-proxy.js +1 -1
- package/dist/server/server/service-proxy.js +1 -1
- package/dist/server/server/session.d.ts +33 -6
- package/dist/server/server/session.js +691 -202
- package/dist/server/server/websocket-server.d.ts +5 -0
- package/dist/server/server/websocket-server.js +137 -3
- package/dist/server/server/workspace-archive-service.d.ts +60 -3
- package/dist/server/server/workspace-archive-service.js +217 -4
- package/dist/server/server/workspace-directory.d.ts +20 -2
- package/dist/server/server/workspace-directory.js +148 -70
- package/dist/server/server/workspace-git-service.js +21 -21
- package/dist/server/server/workspace-reconciliation-service.d.ts +1 -1
- package/dist/server/server/workspace-reconciliation-service.js +21 -22
- package/dist/server/server/workspace-registry-bootstrap.js +23 -10
- package/dist/server/server/workspace-registry-model.d.ts +3 -3
- package/dist/server/server/workspace-registry-model.js +9 -10
- package/dist/server/server/workspace-registry.d.ts +17 -4
- package/dist/server/server/workspace-registry.js +27 -0
- package/dist/server/server/worktree/commands.d.ts +7 -5
- package/dist/server/server/worktree/commands.js +38 -18
- package/dist/server/server/worktree-bootstrap.d.ts +1 -0
- package/dist/server/server/worktree-bootstrap.js +4 -1
- package/dist/server/server/worktree-branch-name-generator.d.ts +5 -1
- package/dist/server/server/worktree-branch-name-generator.js +29 -7
- package/dist/server/server/worktree-session.d.ts +4 -5
- package/dist/server/server/worktree-session.js +9 -3
- package/dist/server/services/github-service.js +1 -1
- package/dist/server/terminal/activity/terminal-activity-tracker.d.ts +20 -0
- package/dist/server/terminal/activity/terminal-activity-tracker.js +59 -0
- package/dist/server/terminal/agent-hooks/agent-hook-installer.d.ts +62 -0
- package/dist/server/terminal/agent-hooks/agent-hook-installer.js +117 -0
- package/dist/server/terminal/agent-hooks/claude/claude-settings.d.ts +7 -0
- package/dist/server/terminal/agent-hooks/claude/claude-settings.js +88 -0
- package/dist/server/terminal/agent-hooks/claude/claude.d.ts +4 -0
- package/dist/server/terminal/agent-hooks/claude/claude.js +47 -0
- package/dist/server/terminal/agent-hooks/codex/codex-settings.d.ts +7 -0
- package/dist/server/terminal/agent-hooks/codex/codex-settings.js +99 -0
- package/dist/server/terminal/agent-hooks/codex/codex.d.ts +4 -0
- package/dist/server/terminal/agent-hooks/codex/codex.js +30 -0
- package/dist/server/terminal/agent-hooks/opencode/opencode-plugin.d.ts +4 -0
- package/dist/server/terminal/agent-hooks/opencode/opencode-plugin.js +46 -0
- package/dist/server/terminal/agent-hooks/opencode/opencode.d.ts +3 -0
- package/dist/server/terminal/agent-hooks/opencode/opencode.js +23 -0
- package/dist/server/terminal/agent-hooks/provider-registry.d.ts +24 -0
- package/dist/server/terminal/agent-hooks/provider-registry.js +36 -0
- package/dist/server/terminal/agent-hooks/terminal-agent-hook-setting.d.ts +10 -0
- package/dist/server/terminal/agent-hooks/terminal-agent-hook-setting.js +26 -0
- package/dist/server/terminal/terminal-manager-factory.d.ts +4 -1
- package/dist/server/terminal/terminal-manager-factory.js +2 -2
- package/dist/server/terminal/terminal-manager.d.ts +33 -2
- package/dist/server/terminal/terminal-manager.js +144 -18
- package/dist/server/terminal/terminal-output-coalescer.d.ts +4 -0
- package/dist/server/terminal/terminal-output-coalescer.js +18 -0
- package/dist/server/terminal/terminal-restore.d.ts +1 -0
- package/dist/server/terminal/terminal-restore.js +6 -0
- package/dist/server/terminal/terminal-session-controller.d.ts +4 -2
- package/dist/server/terminal/terminal-session-controller.js +65 -24
- package/dist/server/terminal/terminal-worker-process.js +146 -63
- package/dist/server/terminal/terminal-worker-protocol.d.ts +19 -14
- package/dist/server/terminal/terminal.d.ts +42 -0
- package/dist/server/terminal/terminal.js +235 -16
- package/dist/server/terminal/worker-terminal-manager.d.ts +1 -0
- package/dist/server/terminal/worker-terminal-manager.js +220 -36
- package/dist/server/utils/build-metadata-prompt.d.ts +8 -3
- package/dist/server/utils/build-metadata-prompt.js +10 -9
- package/dist/server/utils/github-remote.js +1 -1
- package/dist/server/utils/tree-kill.d.ts +2 -2
- package/dist/src/{utils/executable.js → executable-resolution/executable-resolution.js} +16 -14
- package/dist/src/executable-resolution/windows.js +62 -0
- package/dist/src/server/agent/provider-launch-config.js +1 -1
- package/dist/src/server/persisted-config.js +1 -0
- package/package.json +10 -5
- package/dist/server/server/agent/agent-metadata-generator.d.ts +0 -36
- package/dist/server/server/agent/agent-metadata-generator.js +0 -112
- package/dist/server/server/paseo-worktree-archive-service.d.ts +0 -41
- package/dist/server/server/paseo-worktree-archive-service.js +0 -144
- package/dist/server/utils/wrap-user-instructions.d.ts +0 -2
- package/dist/server/utils/wrap-user-instructions.js +0 -13
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { killTerminalsForWorkspace as killWorkspaceTerminals } from "../server/workspace-archive-service.js";
|
|
2
2
|
import { TerminalStreamOpcode, decodeTerminalResizePayload, encodeTerminalStreamFrame, } from "@getpaseo/protocol/binary-frames/index";
|
|
3
3
|
import { TerminalOutputCoalescer } from "./terminal-output-coalescer.js";
|
|
4
|
-
import { MAX_TERMINAL_OUTPUT_FRAME_BYTES, encodeLegacyTerminalSnapshotFrame, encodeTerminalRestoreFrame, resolveRestoreAfterOutputOverflow, resolveTerminalRestoreSnapshotOptions, resolveTerminalSubscriptionSnapshotMode, } from "./terminal-restore.js";
|
|
4
|
+
import { MAX_CLIENT_BUFFERED_BYTES, MAX_TERMINAL_OUTPUT_FRAME_BYTES, encodeLegacyTerminalSnapshotFrame, encodeTerminalRestoreFrame, resolveRestoreAfterOutputOverflow, resolveTerminalRestoreSnapshotOptions, resolveTerminalSubscriptionSnapshotMode, } from "./terminal-restore.js";
|
|
5
|
+
import { terminalSubscriptionKey } from "@getpaseo/protocol/terminal-subscription-key";
|
|
5
6
|
const MAX_TERMINAL_STREAM_SLOTS = 256;
|
|
6
7
|
const TERMINAL_MESSAGE_TYPES = new Set([
|
|
7
8
|
"subscribe_terminals_request",
|
|
@@ -17,7 +18,11 @@ const TERMINAL_MESSAGE_TYPES = new Set([
|
|
|
17
18
|
]);
|
|
18
19
|
export class TerminalSessionController {
|
|
19
20
|
constructor(options) {
|
|
20
|
-
|
|
21
|
+
// A subscription is scoped to a (cwd, workspaceId) pair, keyed by
|
|
22
|
+
// terminalSubscriptionKey: two workspaces sharing a cwd subscribe and unsub
|
|
23
|
+
// independently, and each only receives its own workspace's terminals. The
|
|
24
|
+
// workspaceId is absent for old clients, which key to the cwd alone.
|
|
25
|
+
this.subscribedDirectories = new Map();
|
|
21
26
|
this.unsubscribeTerminalsChanged = null;
|
|
22
27
|
this.exitSubscriptions = new Map();
|
|
23
28
|
this.activeStreams = new Map();
|
|
@@ -31,6 +36,7 @@ export class TerminalSessionController {
|
|
|
31
36
|
this.sessionLogger = options.sessionLogger;
|
|
32
37
|
this.listTerminalWorkspaceRoots = options.listTerminalWorkspaceRoots ?? (async () => []);
|
|
33
38
|
this.clientSupportsWrapReflow = options.clientSupportsWrapReflow ?? (() => false);
|
|
39
|
+
this.getClientBufferedAmount = options.getClientBufferedAmount ?? (() => 0);
|
|
34
40
|
}
|
|
35
41
|
start() {
|
|
36
42
|
if (!this.terminalManager) {
|
|
@@ -120,14 +126,12 @@ export class TerminalSessionController {
|
|
|
120
126
|
this.killTracked(terminalId, { emitExit: true });
|
|
121
127
|
return { terminalId, success: true };
|
|
122
128
|
}
|
|
123
|
-
async
|
|
124
|
-
return
|
|
125
|
-
isPathWithinRoot: (pathRoot, candidatePath) => this.isPathWithinRoot(pathRoot, candidatePath),
|
|
126
|
-
killTrackedTerminal: (terminalId, options) => this.killTracked(terminalId, options),
|
|
129
|
+
async killTerminalsForWorkspace(workspaceId) {
|
|
130
|
+
return killWorkspaceTerminals({
|
|
127
131
|
detachTerminalStream: (terminalId, options) => void this.detachStream(terminalId, options),
|
|
128
132
|
sessionLogger: this.sessionLogger,
|
|
129
133
|
terminalManager: this.terminalManager,
|
|
130
|
-
},
|
|
134
|
+
}, workspaceId);
|
|
131
135
|
}
|
|
132
136
|
dispose() {
|
|
133
137
|
if (this.unsubscribeTerminalsChanged) {
|
|
@@ -171,10 +175,13 @@ export class TerminalSessionController {
|
|
|
171
175
|
}
|
|
172
176
|
toTerminalInfo(terminal) {
|
|
173
177
|
const title = terminal.getTitle();
|
|
178
|
+
const activity = terminal.getActivity();
|
|
174
179
|
return {
|
|
175
180
|
id: terminal.id,
|
|
176
181
|
name: terminal.name,
|
|
182
|
+
workspaceId: terminal.workspaceId,
|
|
177
183
|
...(title ? { title } : {}),
|
|
184
|
+
activity,
|
|
178
185
|
};
|
|
179
186
|
}
|
|
180
187
|
async handleTerminalsChanged(event) {
|
|
@@ -183,37 +190,39 @@ export class TerminalSessionController {
|
|
|
183
190
|
// or above the terminal's cwd, keyed by that root, carrying the full
|
|
184
191
|
// aggregated list — so the client's cache replacement doesn't drop the
|
|
185
192
|
// terminals that live directly at the root.
|
|
186
|
-
const
|
|
187
|
-
for (const
|
|
188
|
-
await this.
|
|
193
|
+
const matchingSubscriptions = Array.from(this.subscribedDirectories.values()).filter((subscription) => this.isPathWithinRoot(subscription.cwd, event.cwd));
|
|
194
|
+
for (const subscription of matchingSubscriptions) {
|
|
195
|
+
await this.emitTerminalsSnapshotForSubscription(subscription);
|
|
189
196
|
}
|
|
190
197
|
}
|
|
191
198
|
handleSubscribeTerminalsRequest(msg) {
|
|
192
|
-
|
|
193
|
-
|
|
199
|
+
const subscription = { cwd: msg.cwd, workspaceId: msg.workspaceId };
|
|
200
|
+
this.subscribedDirectories.set(terminalSubscriptionKey(msg.cwd, msg.workspaceId), subscription);
|
|
201
|
+
void this.emitTerminalsSnapshotForSubscription(subscription);
|
|
194
202
|
}
|
|
195
203
|
handleUnsubscribeTerminalsRequest(msg) {
|
|
196
|
-
this.subscribedDirectories.delete(msg.cwd);
|
|
204
|
+
this.subscribedDirectories.delete(terminalSubscriptionKey(msg.cwd, msg.workspaceId));
|
|
197
205
|
}
|
|
198
|
-
async
|
|
199
|
-
|
|
206
|
+
async emitTerminalsSnapshotForSubscription(subscription) {
|
|
207
|
+
const key = terminalSubscriptionKey(subscription.cwd, subscription.workspaceId);
|
|
208
|
+
if (!this.terminalManager || !this.subscribedDirectories.has(key)) {
|
|
200
209
|
return;
|
|
201
210
|
}
|
|
202
211
|
try {
|
|
203
|
-
const terminals = await this.getTerminalsForWorkspaceRoot(cwd);
|
|
212
|
+
const terminals = await this.getTerminalsForWorkspaceRoot(subscription.cwd, subscription.workspaceId);
|
|
204
213
|
for (const terminal of terminals) {
|
|
205
214
|
this.ensureExitSubscription(terminal);
|
|
206
215
|
}
|
|
207
|
-
if (!this.subscribedDirectories.has(
|
|
216
|
+
if (!this.subscribedDirectories.has(key)) {
|
|
208
217
|
return;
|
|
209
218
|
}
|
|
210
219
|
this.emitTerminalsChangedSnapshot({
|
|
211
|
-
cwd,
|
|
220
|
+
cwd: subscription.cwd,
|
|
212
221
|
terminals: terminals.map((terminal) => this.toTerminalInfo(terminal)),
|
|
213
222
|
});
|
|
214
223
|
}
|
|
215
224
|
catch (error) {
|
|
216
|
-
this.sessionLogger.warn({ err: error, cwd }, "Failed to emit initial terminal snapshot");
|
|
225
|
+
this.sessionLogger.warn({ err: error, cwd: subscription.cwd }, "Failed to emit initial terminal snapshot");
|
|
217
226
|
}
|
|
218
227
|
}
|
|
219
228
|
async handleListTerminalsRequest(msg) {
|
|
@@ -230,7 +239,7 @@ export class TerminalSessionController {
|
|
|
230
239
|
}
|
|
231
240
|
try {
|
|
232
241
|
const terminals = typeof msg.cwd === "string"
|
|
233
|
-
? await this.getTerminalsForWorkspaceRoot(msg.cwd)
|
|
242
|
+
? await this.getTerminalsForWorkspaceRoot(msg.cwd, msg.workspaceId)
|
|
234
243
|
: await this.getAllTerminalSessions();
|
|
235
244
|
for (const terminal of terminals) {
|
|
236
245
|
this.ensureExitSubscription(terminal);
|
|
@@ -265,11 +274,11 @@ export class TerminalSessionController {
|
|
|
265
274
|
const terminalsByDirectory = await Promise.all(directories.map((cwd) => manager.getTerminals(cwd)));
|
|
266
275
|
return terminalsByDirectory.flat();
|
|
267
276
|
}
|
|
268
|
-
async getTerminalsForWorkspaceRoot(cwd) {
|
|
277
|
+
async getTerminalsForWorkspaceRoot(cwd, workspaceId) {
|
|
269
278
|
if (!this.terminalManager) {
|
|
270
279
|
return [];
|
|
271
280
|
}
|
|
272
|
-
const terminals = await this.terminalManager.getTerminals(cwd);
|
|
281
|
+
const terminals = await this.terminalManager.getTerminals(cwd, { workspaceId });
|
|
273
282
|
const workspaceRoots = await this.listTerminalWorkspaceRoots();
|
|
274
283
|
if (workspaceRoots.length === 0) {
|
|
275
284
|
return terminals;
|
|
@@ -322,8 +331,20 @@ export class TerminalSessionController {
|
|
|
322
331
|
});
|
|
323
332
|
return;
|
|
324
333
|
}
|
|
334
|
+
if (!msg.workspaceId) {
|
|
335
|
+
this.emit({
|
|
336
|
+
type: "create_terminal_response",
|
|
337
|
+
payload: {
|
|
338
|
+
terminal: null,
|
|
339
|
+
error: "workspaceId is required",
|
|
340
|
+
requestId: msg.requestId,
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
325
345
|
const session = await this.terminalManager.createTerminal({
|
|
326
346
|
cwd: msg.cwd,
|
|
347
|
+
workspaceId: msg.workspaceId,
|
|
327
348
|
name: msg.name,
|
|
328
349
|
command: msg.command,
|
|
329
350
|
args: msg.args,
|
|
@@ -336,7 +357,9 @@ export class TerminalSessionController {
|
|
|
336
357
|
id: session.id,
|
|
337
358
|
name: session.name,
|
|
338
359
|
cwd: session.cwd,
|
|
360
|
+
workspaceId: session.workspaceId,
|
|
339
361
|
...(session.getTitle() ? { title: session.getTitle() } : {}),
|
|
362
|
+
activity: session.getActivity(),
|
|
340
363
|
},
|
|
341
364
|
error: null,
|
|
342
365
|
requestId: msg.requestId,
|
|
@@ -571,7 +594,19 @@ export class TerminalSessionController {
|
|
|
571
594
|
return;
|
|
572
595
|
}
|
|
573
596
|
activeStream.outputBytesSinceSnapshot += payload.byteLength;
|
|
574
|
-
|
|
597
|
+
// Catch up via a snapshot only when the client is BOTH far behind in
|
|
598
|
+
// produced output AND actually backed up on the wire. A client that
|
|
599
|
+
// keeps draining reports ~0 buffered, so it streams continuously even
|
|
600
|
+
// past the byte threshold. outputBytesSinceSnapshot keeps accumulating
|
|
601
|
+
// in that case — it's harmless, it only gates the snapshot decision at
|
|
602
|
+
// the instant backpressure appears, and trySendSnapshot resets it to 0.
|
|
603
|
+
// A null reading means the transport exposes no backpressure signal
|
|
604
|
+
// (e.g. the multiplexed relay socket); there we can't tell a slow client
|
|
605
|
+
// from a fast one, so fall back unconditionally at the byte threshold to
|
|
606
|
+
// keep a slow relay client from falling unboundedly behind.
|
|
607
|
+
const clientBufferedAmount = this.getClientBufferedAmount();
|
|
608
|
+
if (activeStream.outputBytesSinceSnapshot > MAX_TERMINAL_OUTPUT_FRAME_BYTES &&
|
|
609
|
+
(clientBufferedAmount === null || clientBufferedAmount > MAX_CLIENT_BUFFERED_BYTES)) {
|
|
575
610
|
activeStream.restore = resolveRestoreAfterOutputOverflow(activeStream.restore);
|
|
576
611
|
activeStream.needsSnapshot = true;
|
|
577
612
|
void this.trySendSnapshot(activeStream);
|
|
@@ -671,6 +706,9 @@ export class TerminalSessionController {
|
|
|
671
706
|
slot: activeStream.slot,
|
|
672
707
|
snapshot,
|
|
673
708
|
}));
|
|
709
|
+
// The snapshot frame went out-of-band; keep the replay that follows on the
|
|
710
|
+
// coalescer's trailing path so it doesn't flush back-to-back with it.
|
|
711
|
+
activeStream.outputCoalescer.markFlushed();
|
|
674
712
|
return { shouldContinue: true, replayRevision: snapshot.revision };
|
|
675
713
|
}
|
|
676
714
|
async emitRestoreSnapshot(activeStream, terminalManager, restore) {
|
|
@@ -693,6 +731,9 @@ export class TerminalSessionController {
|
|
|
693
731
|
slot: activeStream.slot,
|
|
694
732
|
snapshot,
|
|
695
733
|
}));
|
|
734
|
+
// The restore frame went out-of-band; keep the replay that follows on the
|
|
735
|
+
// coalescer's trailing path so it doesn't flush back-to-back with it.
|
|
736
|
+
activeStream.outputCoalescer.markFlushed();
|
|
696
737
|
return { shouldContinue: true, replayRevision: snapshot.revision };
|
|
697
738
|
}
|
|
698
739
|
replayTerminalOutputAfterSnapshot(activeStream, terminal, replayRevision) {
|
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
import { createTerminalManager } from "./terminal-manager.js";
|
|
2
2
|
import { captureTerminalLines } from "./terminal-capture.js";
|
|
3
|
+
import { TerminalOutputCoalescer } from "./terminal-output-coalescer.js";
|
|
3
4
|
const manager = createTerminalManager();
|
|
4
5
|
const unsubscribeByTerminalId = new Map();
|
|
6
|
+
const outputCoalescerByTerminalId = new Map();
|
|
5
7
|
let ipcClosing = false;
|
|
8
|
+
let inFlightTerminalCreateRequest = null;
|
|
9
|
+
// The conpty failure signal is process-scoped, not request-scoped. Serializing
|
|
10
|
+
// creates keeps an async spawn failure attributable to exactly one request.
|
|
11
|
+
let createTerminalQueue = Promise.resolve();
|
|
12
|
+
// node-pty completes its Windows conpty spawn asynchronously on a separate
|
|
13
|
+
// conout worker thread. When that spawn fails (bad cwd, missing command, etc.)
|
|
14
|
+
// it throws an exception there that cannot be caught at the call site and would
|
|
15
|
+
// otherwise crash this worker process and sever every existing terminal.
|
|
16
|
+
process.on("uncaughtException", (error) => {
|
|
17
|
+
console.error("Terminal worker uncaught exception (kept alive):", error);
|
|
18
|
+
reportInFlightTerminalCreateFailure(error);
|
|
19
|
+
});
|
|
6
20
|
function sendToParent(message) {
|
|
7
21
|
if (ipcClosing || !process.connected || !process.send) {
|
|
8
22
|
return;
|
|
@@ -18,14 +32,37 @@ function sendToParent(message) {
|
|
|
18
32
|
ipcClosing = true;
|
|
19
33
|
}
|
|
20
34
|
}
|
|
35
|
+
function buildTerminalStateResult(session, options) {
|
|
36
|
+
if (!session) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
return { ...session.getStateSnapshot(options), replayPreamble: session.getReplayPreamble() };
|
|
40
|
+
}
|
|
21
41
|
function toTerminalInfo(session) {
|
|
22
42
|
return {
|
|
23
43
|
id: session.id,
|
|
24
44
|
name: session.name,
|
|
25
45
|
cwd: session.cwd,
|
|
46
|
+
workspaceId: session.workspaceId,
|
|
26
47
|
...(session.getTitle() ? { title: session.getTitle() } : {}),
|
|
48
|
+
activity: session.getActivity(),
|
|
27
49
|
};
|
|
28
50
|
}
|
|
51
|
+
function terminalWorkerErrorMessage(error) {
|
|
52
|
+
return error instanceof Error ? error.message : "Terminal worker request failed";
|
|
53
|
+
}
|
|
54
|
+
function reportInFlightTerminalCreateFailure(error) {
|
|
55
|
+
if (!inFlightTerminalCreateRequest || inFlightTerminalCreateRequest.errorReported) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
inFlightTerminalCreateRequest.errorReported = true;
|
|
59
|
+
sendToParent({
|
|
60
|
+
type: "response",
|
|
61
|
+
requestId: inFlightTerminalCreateRequest.requestId,
|
|
62
|
+
ok: false,
|
|
63
|
+
error: terminalWorkerErrorMessage(error),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
29
66
|
function clearTerminalSubscriptions(terminalId) {
|
|
30
67
|
const subscriptions = unsubscribeByTerminalId.get(terminalId);
|
|
31
68
|
if (subscriptions) {
|
|
@@ -39,10 +76,41 @@ function clearTerminalSubscriptions(terminalId) {
|
|
|
39
76
|
}
|
|
40
77
|
}
|
|
41
78
|
unsubscribeByTerminalId.delete(terminalId);
|
|
79
|
+
const coalescer = outputCoalescerByTerminalId.get(terminalId);
|
|
80
|
+
if (coalescer) {
|
|
81
|
+
coalescer.dispose();
|
|
82
|
+
outputCoalescerByTerminalId.delete(terminalId);
|
|
83
|
+
}
|
|
42
84
|
}
|
|
43
85
|
function watchTerminal(session) {
|
|
44
86
|
clearTerminalSubscriptions(session.id);
|
|
87
|
+
// Coalesce pty output chunks into a single IPC message per ~5ms window so a
|
|
88
|
+
// burst of small chunks no longer costs one process.send each. The batch
|
|
89
|
+
// carries the LAST chunk's revision (the highest) so downstream snapshot
|
|
90
|
+
// replay dedup stays correct.
|
|
91
|
+
let pendingOutputRevision;
|
|
92
|
+
const outputCoalescer = new TerminalOutputCoalescer({
|
|
93
|
+
timers: { setTimeout, clearTimeout },
|
|
94
|
+
onFlush: ({ payload }) => {
|
|
95
|
+
const revision = pendingOutputRevision;
|
|
96
|
+
pendingOutputRevision = undefined;
|
|
97
|
+
sendToParent({
|
|
98
|
+
type: "terminalMessage",
|
|
99
|
+
terminalId: session.id,
|
|
100
|
+
message: { type: "output", data: payload.toString("utf8"), revision },
|
|
101
|
+
});
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
outputCoalescerByTerminalId.set(session.id, outputCoalescer);
|
|
45
105
|
const unsubscribeMessage = session.subscribe((message) => {
|
|
106
|
+
if (message.type === "output") {
|
|
107
|
+
pendingOutputRevision = message.revision;
|
|
108
|
+
outputCoalescer.handle(message.data);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
// Non-output messages (snapshot/snapshotReady/titleChange) must not jump
|
|
112
|
+
// ahead of buffered output: flush the coalescer first, then forward.
|
|
113
|
+
outputCoalescer.flush();
|
|
46
114
|
sendToParent({
|
|
47
115
|
type: "terminalMessage",
|
|
48
116
|
terminalId: session.id,
|
|
@@ -50,6 +118,7 @@ function watchTerminal(session) {
|
|
|
50
118
|
});
|
|
51
119
|
});
|
|
52
120
|
const unsubscribeExit = session.onExit((info) => {
|
|
121
|
+
outputCoalescer.flush();
|
|
53
122
|
clearTerminalSubscriptions(session.id);
|
|
54
123
|
sendToParent({
|
|
55
124
|
type: "terminalExit",
|
|
@@ -58,6 +127,7 @@ function watchTerminal(session) {
|
|
|
58
127
|
});
|
|
59
128
|
});
|
|
60
129
|
const unsubscribeTitle = session.onTitleChange((title) => {
|
|
130
|
+
outputCoalescer.flush();
|
|
61
131
|
sendToParent({
|
|
62
132
|
type: "terminalTitleChange",
|
|
63
133
|
terminalId: session.id,
|
|
@@ -65,56 +135,80 @@ function watchTerminal(session) {
|
|
|
65
135
|
});
|
|
66
136
|
});
|
|
67
137
|
const unsubscribeCommandFinished = session.onCommandFinished((info) => {
|
|
138
|
+
outputCoalescer.flush();
|
|
68
139
|
sendToParent({
|
|
69
140
|
type: "terminalCommandFinished",
|
|
70
141
|
terminalId: session.id,
|
|
71
142
|
info,
|
|
72
143
|
});
|
|
73
144
|
});
|
|
145
|
+
const unsubscribeActivity = session.onActivityChange((transition) => {
|
|
146
|
+
sendToParent({
|
|
147
|
+
type: "terminalActivityChange",
|
|
148
|
+
terminalId: session.id,
|
|
149
|
+
activity: transition.activity,
|
|
150
|
+
previous: transition.previous,
|
|
151
|
+
});
|
|
152
|
+
});
|
|
74
153
|
unsubscribeByTerminalId.set(session.id, [
|
|
75
154
|
unsubscribeMessage,
|
|
76
155
|
unsubscribeExit,
|
|
77
156
|
unsubscribeTitle,
|
|
78
157
|
unsubscribeCommandFinished,
|
|
158
|
+
unsubscribeActivity,
|
|
79
159
|
]);
|
|
80
160
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
161
|
+
function enqueueCreateTerminalRequest(message) {
|
|
162
|
+
const nextRequest = createTerminalQueue.then(() => handleCreateTerminalRequest(message));
|
|
163
|
+
createTerminalQueue = nextRequest.catch(() => { });
|
|
164
|
+
return nextRequest;
|
|
165
|
+
}
|
|
166
|
+
async function handleCreateTerminalRequest(message) {
|
|
167
|
+
const request = {
|
|
168
|
+
requestId: message.requestId,
|
|
169
|
+
errorReported: false,
|
|
170
|
+
};
|
|
171
|
+
inFlightTerminalCreateRequest = request;
|
|
172
|
+
try {
|
|
173
|
+
const { workspaceId } = message.options;
|
|
174
|
+
if (!workspaceId) {
|
|
175
|
+
throw new Error("workspaceId is required");
|
|
176
|
+
}
|
|
177
|
+
const session = await manager.createTerminal({ ...message.options, workspaceId });
|
|
178
|
+
if (request.errorReported) {
|
|
179
|
+
session.kill();
|
|
98
180
|
return;
|
|
99
181
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
182
|
+
watchTerminal(session);
|
|
183
|
+
const initialSnapshot = session.getStateSnapshot();
|
|
184
|
+
sendToParent({
|
|
185
|
+
type: "terminalCreated",
|
|
186
|
+
terminal: toTerminalInfo(session),
|
|
187
|
+
state: initialSnapshot.state,
|
|
188
|
+
});
|
|
189
|
+
sendToParent({
|
|
190
|
+
type: "response",
|
|
191
|
+
requestId: message.requestId,
|
|
192
|
+
ok: true,
|
|
193
|
+
result: {
|
|
106
194
|
terminal: toTerminalInfo(session),
|
|
107
195
|
state: initialSnapshot.state,
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
reportInFlightTerminalCreateFailure(error);
|
|
201
|
+
}
|
|
202
|
+
finally {
|
|
203
|
+
if (inFlightTerminalCreateRequest === request) {
|
|
204
|
+
inFlightTerminalCreateRequest = null;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
async function handleRequest(message) {
|
|
209
|
+
switch (message.type) {
|
|
210
|
+
case "createTerminal": {
|
|
211
|
+
await enqueueCreateTerminalRequest(message);
|
|
118
212
|
return;
|
|
119
213
|
}
|
|
120
214
|
case "registerCwdEnv": {
|
|
@@ -122,52 +216,41 @@ async function handleRequest(message) {
|
|
|
122
216
|
sendToParent({ type: "response", requestId: message.requestId, ok: true });
|
|
123
217
|
return;
|
|
124
218
|
}
|
|
219
|
+
case "setActivity": {
|
|
220
|
+
await manager.setTerminalActivity(message.terminalId, message.state);
|
|
221
|
+
sendToParent({ type: "response", requestId: message.requestId, ok: true });
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
case "clearAttention": {
|
|
225
|
+
await manager.clearTerminalAttention(message.terminalId);
|
|
226
|
+
sendToParent({ type: "response", requestId: message.requestId, ok: true });
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
125
229
|
case "killTerminal": {
|
|
126
|
-
const session = manager.getTerminal(message.terminalId);
|
|
127
|
-
const cwd = session?.cwd;
|
|
128
230
|
manager.killTerminal(message.terminalId);
|
|
231
|
+
// Removal is owned by session.onExit -> terminalExit; the parent mirror
|
|
232
|
+
// clears contribution and emits terminalsChanged from that single path.
|
|
129
233
|
clearTerminalSubscriptions(message.terminalId);
|
|
130
|
-
if (cwd) {
|
|
131
|
-
sendToParent({
|
|
132
|
-
type: "terminalRemoved",
|
|
133
|
-
terminalId: message.terminalId,
|
|
134
|
-
cwd,
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
234
|
sendToParent({ type: "response", requestId: message.requestId, ok: true });
|
|
138
235
|
return;
|
|
139
236
|
}
|
|
140
237
|
case "killTerminalAndWait": {
|
|
141
|
-
const session = manager.getTerminal(message.terminalId);
|
|
142
|
-
const cwd = session?.cwd;
|
|
143
238
|
await manager.killTerminalAndWait(message.terminalId, message.options);
|
|
144
239
|
clearTerminalSubscriptions(message.terminalId);
|
|
145
|
-
if (cwd) {
|
|
146
|
-
sendToParent({
|
|
147
|
-
type: "terminalRemoved",
|
|
148
|
-
terminalId: message.terminalId,
|
|
149
|
-
cwd,
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
240
|
sendToParent({ type: "response", requestId: message.requestId, ok: true });
|
|
153
241
|
return;
|
|
154
242
|
}
|
|
155
243
|
case "getTerminalState": {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
result: session?.getStateSnapshot(message.options) ?? null,
|
|
162
|
-
});
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
case "listDirectories": {
|
|
244
|
+
// Flush buffered output before snapshotting: the headless state already includes it,
|
|
245
|
+
// so if the coalescer emitted it afterward (in a batch carrying a revision past the
|
|
246
|
+
// snapshot's) the controller's revision dedup wouldn't drop it and the client would
|
|
247
|
+
// see the bytes twice. Flushing first sends them with a revision <= the snapshot's.
|
|
248
|
+
outputCoalescerByTerminalId.get(message.terminalId)?.flush();
|
|
166
249
|
sendToParent({
|
|
167
250
|
type: "response",
|
|
168
251
|
requestId: message.requestId,
|
|
169
252
|
ok: true,
|
|
170
|
-
result: manager.
|
|
253
|
+
result: buildTerminalStateResult(manager.getTerminal(message.terminalId), message.options),
|
|
171
254
|
});
|
|
172
255
|
return;
|
|
173
256
|
}
|
|
@@ -210,7 +293,7 @@ process.on("message", (message) => {
|
|
|
210
293
|
type: "response",
|
|
211
294
|
requestId: message.requestId,
|
|
212
295
|
ok: false,
|
|
213
|
-
error: error
|
|
296
|
+
error: terminalWorkerErrorMessage(error),
|
|
214
297
|
});
|
|
215
298
|
});
|
|
216
299
|
});
|
|
@@ -1,30 +1,32 @@
|
|
|
1
1
|
import type { TerminalExitInfo, ServerMessage, ClientMessage, TerminalStateSnapshot, TerminalStateSnapshotOptions } from "./terminal.js";
|
|
2
2
|
import type { TerminalState } from "@getpaseo/protocol/messages";
|
|
3
|
+
import type { TerminalActivity, TerminalActivityState } from "@getpaseo/protocol/terminal-activity";
|
|
3
4
|
import type { CaptureTerminalLinesResult } from "./terminal-capture.js";
|
|
4
5
|
export interface WorkerTerminalInfo {
|
|
5
6
|
id: string;
|
|
6
7
|
name: string;
|
|
7
8
|
cwd: string;
|
|
9
|
+
workspaceId?: string;
|
|
8
10
|
title?: string;
|
|
11
|
+
activity: TerminalActivity | null;
|
|
9
12
|
}
|
|
10
13
|
export interface WorkerCreateTerminalOptions {
|
|
11
14
|
id?: string;
|
|
12
15
|
cwd: string;
|
|
16
|
+
workspaceId?: string;
|
|
13
17
|
name?: string;
|
|
14
18
|
title?: string;
|
|
15
19
|
env?: Record<string, string>;
|
|
16
20
|
command?: string;
|
|
17
21
|
args?: string[];
|
|
22
|
+
activityToken?: string;
|
|
23
|
+
activityUrl?: string | null;
|
|
18
24
|
}
|
|
19
25
|
export interface WorkerKillAndWaitOptions {
|
|
20
26
|
gracefulTimeoutMs?: number;
|
|
21
27
|
forceTimeoutMs?: number;
|
|
22
28
|
}
|
|
23
29
|
export type TerminalWorkerRequest = {
|
|
24
|
-
type: "getTerminals";
|
|
25
|
-
requestId: string;
|
|
26
|
-
cwd: string;
|
|
27
|
-
} | {
|
|
28
30
|
type: "createTerminal";
|
|
29
31
|
requestId: string;
|
|
30
32
|
options: WorkerCreateTerminalOptions;
|
|
@@ -33,6 +35,15 @@ export type TerminalWorkerRequest = {
|
|
|
33
35
|
requestId: string;
|
|
34
36
|
cwd: string;
|
|
35
37
|
env: Record<string, string>;
|
|
38
|
+
} | {
|
|
39
|
+
type: "setActivity";
|
|
40
|
+
requestId: string;
|
|
41
|
+
terminalId: string;
|
|
42
|
+
state: TerminalActivityState;
|
|
43
|
+
} | {
|
|
44
|
+
type: "clearAttention";
|
|
45
|
+
requestId: string;
|
|
46
|
+
terminalId: string;
|
|
36
47
|
} | {
|
|
37
48
|
type: "killTerminal";
|
|
38
49
|
requestId: string;
|
|
@@ -54,9 +65,6 @@ export type TerminalWorkerRequest = {
|
|
|
54
65
|
start?: number;
|
|
55
66
|
end?: number;
|
|
56
67
|
stripAnsi?: boolean;
|
|
57
|
-
} | {
|
|
58
|
-
type: "listDirectories";
|
|
59
|
-
requestId: string;
|
|
60
68
|
} | {
|
|
61
69
|
type: "killAll";
|
|
62
70
|
requestId: string;
|
|
@@ -81,10 +89,6 @@ export type TerminalWorkerEvent = {
|
|
|
81
89
|
type: "terminalCreated";
|
|
82
90
|
terminal: WorkerTerminalInfo;
|
|
83
91
|
state: TerminalState;
|
|
84
|
-
} | {
|
|
85
|
-
type: "terminalRemoved";
|
|
86
|
-
terminalId: string;
|
|
87
|
-
cwd: string;
|
|
88
92
|
} | {
|
|
89
93
|
type: "terminalMessage";
|
|
90
94
|
terminalId: string;
|
|
@@ -104,9 +108,10 @@ export type TerminalWorkerEvent = {
|
|
|
104
108
|
exitCode: number | null;
|
|
105
109
|
};
|
|
106
110
|
} | {
|
|
107
|
-
type: "
|
|
108
|
-
|
|
109
|
-
|
|
111
|
+
type: "terminalActivityChange";
|
|
112
|
+
terminalId: string;
|
|
113
|
+
activity: TerminalActivity | null;
|
|
114
|
+
previous: TerminalActivity | null;
|
|
110
115
|
};
|
|
111
116
|
export type TerminalWorkerToParentMessage = TerminalWorkerResponse | TerminalWorkerEvent;
|
|
112
117
|
export type TerminalWorkerCaptureResult = CaptureTerminalLinesResult;
|