@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.
Files changed (139) hide show
  1. package/dist/server/{utils/executable.d.ts → executable-resolution/executable-resolution.d.ts} +2 -2
  2. package/dist/server/{utils/executable.js → executable-resolution/executable-resolution.js} +16 -14
  3. package/dist/server/executable-resolution/windows.d.ts +18 -0
  4. package/dist/server/executable-resolution/windows.js +62 -0
  5. package/dist/server/server/agent/agent-loading.js +4 -1
  6. package/dist/server/server/agent/agent-manager.d.ts +10 -2
  7. package/dist/server/server/agent/agent-manager.js +34 -46
  8. package/dist/server/server/agent/agent-projections.js +3 -0
  9. package/dist/server/server/agent/agent-prompt.js +19 -1
  10. package/dist/server/server/agent/agent-response-loop.js +2 -4
  11. package/dist/server/server/agent/agent-storage.d.ts +18 -19
  12. package/dist/server/server/agent/agent-storage.js +6 -23
  13. package/dist/server/server/agent/create-agent/create.d.ts +2 -12
  14. package/dist/server/server/agent/create-agent/create.js +28 -30
  15. package/dist/server/server/agent/create-agent-lifecycle-dispatch.d.ts +4 -2
  16. package/dist/server/server/agent/create-agent-lifecycle-dispatch.js +31 -22
  17. package/dist/server/server/agent/create-agent-title.d.ts +2 -0
  18. package/dist/server/server/agent/create-agent-title.js +5 -0
  19. package/dist/server/server/agent/import-sessions.d.ts +1 -10
  20. package/dist/server/server/agent/import-sessions.js +1 -53
  21. package/dist/server/server/agent/lifecycle-command.js +5 -4
  22. package/dist/server/server/agent/mcp-server.d.ts +8 -5
  23. package/dist/server/server/agent/mcp-server.js +41 -14
  24. package/dist/server/server/agent/mcp-shared.d.ts +6 -3
  25. package/dist/server/server/agent/mcp-shared.js +3 -0
  26. package/dist/server/server/agent/provider-launch-config.js +1 -1
  27. package/dist/server/server/agent/providers/acp-agent.d.ts +5 -0
  28. package/dist/server/server/agent/providers/acp-agent.js +31 -26
  29. package/dist/server/server/agent/providers/claude/agent.js +45 -6
  30. package/dist/server/server/agent/providers/codex-app-server-agent.js +1 -1
  31. package/dist/server/server/agent/providers/copilot-acp-agent.js +1 -0
  32. package/dist/server/server/agent/providers/cursor-acp-agent.d.ts +0 -7
  33. package/dist/server/server/agent/providers/cursor-acp-agent.js +0 -78
  34. package/dist/server/server/agent/providers/mock-load-test-agent.d.ts +2 -0
  35. package/dist/server/server/agent/providers/mock-load-test-agent.js +73 -1
  36. package/dist/server/server/agent/providers/opencode/server-manager.js +1 -1
  37. package/dist/server/server/agent/structured-generation-providers.js +45 -1
  38. package/dist/server/server/agent-attention-policy.d.ts +12 -3
  39. package/dist/server/server/agent-attention-policy.js +15 -3
  40. package/dist/server/server/auto-archive-on-merge/archive-if-safe.d.ts +7 -6
  41. package/dist/server/server/auto-archive-on-merge/archive-if-safe.js +21 -16
  42. package/dist/server/server/bootstrap.d.ts +3 -0
  43. package/dist/server/server/bootstrap.js +125 -64
  44. package/dist/server/server/config.js +1 -0
  45. package/dist/server/server/daemon-config-store.js +1 -0
  46. package/dist/server/server/exports.d.ts +1 -1
  47. package/dist/server/server/exports.js +1 -1
  48. package/dist/server/server/loop-service.d.ts +24 -24
  49. package/dist/server/server/migrations/backfill-workspace-id.migration.d.ts +9 -0
  50. package/dist/server/server/migrations/backfill-workspace-id.migration.js +60 -0
  51. package/dist/server/server/paseo-worktree-service.d.ts +9 -0
  52. package/dist/server/server/paseo-worktree-service.js +74 -12
  53. package/dist/server/server/path-utils.d.ts +1 -0
  54. package/dist/server/server/path-utils.js +6 -1
  55. package/dist/server/server/persisted-config.d.ts +7 -0
  56. package/dist/server/server/persisted-config.js +1 -0
  57. package/dist/server/server/persistence-hooks.d.ts +1 -0
  58. package/dist/server/server/persistence-hooks.js +13 -5
  59. package/dist/server/server/resolve-workspace-id-for-path.d.ts +3 -0
  60. package/dist/server/server/resolve-workspace-id-for-path.js +41 -0
  61. package/dist/server/server/script-proxy.d.ts +1 -1
  62. package/dist/server/server/script-proxy.js +1 -1
  63. package/dist/server/server/service-proxy.js +1 -1
  64. package/dist/server/server/session.d.ts +33 -6
  65. package/dist/server/server/session.js +691 -202
  66. package/dist/server/server/websocket-server.d.ts +5 -0
  67. package/dist/server/server/websocket-server.js +137 -3
  68. package/dist/server/server/workspace-archive-service.d.ts +60 -3
  69. package/dist/server/server/workspace-archive-service.js +217 -4
  70. package/dist/server/server/workspace-directory.d.ts +20 -2
  71. package/dist/server/server/workspace-directory.js +148 -70
  72. package/dist/server/server/workspace-git-service.js +21 -21
  73. package/dist/server/server/workspace-reconciliation-service.d.ts +1 -1
  74. package/dist/server/server/workspace-reconciliation-service.js +21 -22
  75. package/dist/server/server/workspace-registry-bootstrap.js +23 -10
  76. package/dist/server/server/workspace-registry-model.d.ts +3 -3
  77. package/dist/server/server/workspace-registry-model.js +9 -10
  78. package/dist/server/server/workspace-registry.d.ts +17 -4
  79. package/dist/server/server/workspace-registry.js +27 -0
  80. package/dist/server/server/worktree/commands.d.ts +7 -5
  81. package/dist/server/server/worktree/commands.js +38 -18
  82. package/dist/server/server/worktree-bootstrap.d.ts +1 -0
  83. package/dist/server/server/worktree-bootstrap.js +4 -1
  84. package/dist/server/server/worktree-branch-name-generator.d.ts +5 -1
  85. package/dist/server/server/worktree-branch-name-generator.js +29 -7
  86. package/dist/server/server/worktree-session.d.ts +4 -5
  87. package/dist/server/server/worktree-session.js +9 -3
  88. package/dist/server/services/github-service.js +1 -1
  89. package/dist/server/terminal/activity/terminal-activity-tracker.d.ts +20 -0
  90. package/dist/server/terminal/activity/terminal-activity-tracker.js +59 -0
  91. package/dist/server/terminal/agent-hooks/agent-hook-installer.d.ts +62 -0
  92. package/dist/server/terminal/agent-hooks/agent-hook-installer.js +117 -0
  93. package/dist/server/terminal/agent-hooks/claude/claude-settings.d.ts +7 -0
  94. package/dist/server/terminal/agent-hooks/claude/claude-settings.js +88 -0
  95. package/dist/server/terminal/agent-hooks/claude/claude.d.ts +4 -0
  96. package/dist/server/terminal/agent-hooks/claude/claude.js +47 -0
  97. package/dist/server/terminal/agent-hooks/codex/codex-settings.d.ts +7 -0
  98. package/dist/server/terminal/agent-hooks/codex/codex-settings.js +99 -0
  99. package/dist/server/terminal/agent-hooks/codex/codex.d.ts +4 -0
  100. package/dist/server/terminal/agent-hooks/codex/codex.js +30 -0
  101. package/dist/server/terminal/agent-hooks/opencode/opencode-plugin.d.ts +4 -0
  102. package/dist/server/terminal/agent-hooks/opencode/opencode-plugin.js +46 -0
  103. package/dist/server/terminal/agent-hooks/opencode/opencode.d.ts +3 -0
  104. package/dist/server/terminal/agent-hooks/opencode/opencode.js +23 -0
  105. package/dist/server/terminal/agent-hooks/provider-registry.d.ts +24 -0
  106. package/dist/server/terminal/agent-hooks/provider-registry.js +36 -0
  107. package/dist/server/terminal/agent-hooks/terminal-agent-hook-setting.d.ts +10 -0
  108. package/dist/server/terminal/agent-hooks/terminal-agent-hook-setting.js +26 -0
  109. package/dist/server/terminal/terminal-manager-factory.d.ts +4 -1
  110. package/dist/server/terminal/terminal-manager-factory.js +2 -2
  111. package/dist/server/terminal/terminal-manager.d.ts +33 -2
  112. package/dist/server/terminal/terminal-manager.js +144 -18
  113. package/dist/server/terminal/terminal-output-coalescer.d.ts +4 -0
  114. package/dist/server/terminal/terminal-output-coalescer.js +18 -0
  115. package/dist/server/terminal/terminal-restore.d.ts +1 -0
  116. package/dist/server/terminal/terminal-restore.js +6 -0
  117. package/dist/server/terminal/terminal-session-controller.d.ts +4 -2
  118. package/dist/server/terminal/terminal-session-controller.js +65 -24
  119. package/dist/server/terminal/terminal-worker-process.js +146 -63
  120. package/dist/server/terminal/terminal-worker-protocol.d.ts +19 -14
  121. package/dist/server/terminal/terminal.d.ts +42 -0
  122. package/dist/server/terminal/terminal.js +235 -16
  123. package/dist/server/terminal/worker-terminal-manager.d.ts +1 -0
  124. package/dist/server/terminal/worker-terminal-manager.js +220 -36
  125. package/dist/server/utils/build-metadata-prompt.d.ts +8 -3
  126. package/dist/server/utils/build-metadata-prompt.js +10 -9
  127. package/dist/server/utils/github-remote.js +1 -1
  128. package/dist/server/utils/tree-kill.d.ts +2 -2
  129. package/dist/src/{utils/executable.js → executable-resolution/executable-resolution.js} +16 -14
  130. package/dist/src/executable-resolution/windows.js +62 -0
  131. package/dist/src/server/agent/provider-launch-config.js +1 -1
  132. package/dist/src/server/persisted-config.js +1 -0
  133. package/package.json +10 -5
  134. package/dist/server/server/agent/agent-metadata-generator.d.ts +0 -36
  135. package/dist/server/server/agent/agent-metadata-generator.js +0 -112
  136. package/dist/server/server/paseo-worktree-archive-service.d.ts +0 -41
  137. package/dist/server/server/paseo-worktree-archive-service.js +0 -144
  138. package/dist/server/utils/wrap-user-instructions.d.ts +0 -2
  139. package/dist/server/utils/wrap-user-instructions.js +0 -13
@@ -1,7 +1,8 @@
1
- import { killTerminalsUnderPath as killWorktreeTerminalsUnderPath } from "../server/paseo-worktree-archive-service.js";
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
- this.subscribedDirectories = new Set();
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 killTerminalsUnderPath(rootPath) {
124
- return killWorktreeTerminalsUnderPath({
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
- }, rootPath);
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 matchingRoots = Array.from(this.subscribedDirectories).filter((root) => this.isPathWithinRoot(root, event.cwd));
187
- for (const root of matchingRoots) {
188
- await this.emitTerminalsSnapshotForRoot(root);
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
- this.subscribedDirectories.add(msg.cwd);
193
- void this.emitTerminalsSnapshotForRoot(msg.cwd);
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 emitTerminalsSnapshotForRoot(cwd) {
199
- if (!this.terminalManager || !this.subscribedDirectories.has(cwd)) {
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(cwd)) {
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
- if (activeStream.outputBytesSinceSnapshot > MAX_TERMINAL_OUTPUT_FRAME_BYTES) {
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
- manager.subscribeTerminalsChanged((event) => {
82
- sendToParent({
83
- type: "terminalsChanged",
84
- cwd: event.cwd,
85
- terminals: event.terminals,
86
- });
87
- });
88
- async function handleRequest(message) {
89
- switch (message.type) {
90
- case "getTerminals": {
91
- const terminals = await manager.getTerminals(message.cwd);
92
- sendToParent({
93
- type: "response",
94
- requestId: message.requestId,
95
- ok: true,
96
- result: terminals.map(toTerminalInfo),
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
- case "createTerminal": {
101
- const session = await manager.createTerminal(message.options);
102
- watchTerminal(session);
103
- const initialSnapshot = session.getStateSnapshot();
104
- sendToParent({
105
- type: "terminalCreated",
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
- sendToParent({
110
- type: "response",
111
- requestId: message.requestId,
112
- ok: true,
113
- result: {
114
- terminal: toTerminalInfo(session),
115
- state: initialSnapshot.state,
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
- const session = manager.getTerminal(message.terminalId);
157
- sendToParent({
158
- type: "response",
159
- requestId: message.requestId,
160
- ok: true,
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.listDirectories(),
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 instanceof Error ? error.message : "Terminal worker request failed",
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: "terminalsChanged";
108
- cwd: string;
109
- terminals: WorkerTerminalInfo[];
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;