@getpaseo/server 0.1.4 → 0.1.7

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 (86) hide show
  1. package/dist/scripts/daemon-runner.js +31 -7
  2. package/dist/scripts/daemon-runner.js.map +1 -1
  3. package/dist/server/client/daemon-client-terminal-stream-manager.d.ts.map +1 -1
  4. package/dist/server/client/daemon-client-terminal-stream-manager.js +4 -0
  5. package/dist/server/client/daemon-client-terminal-stream-manager.js.map +1 -1
  6. package/dist/server/client/daemon-client.d.ts +25 -15
  7. package/dist/server/client/daemon-client.d.ts.map +1 -1
  8. package/dist/server/client/daemon-client.js +47 -23
  9. package/dist/server/client/daemon-client.js.map +1 -1
  10. package/dist/server/server/agent/agent-management-mcp.d.ts +2 -0
  11. package/dist/server/server/agent/agent-management-mcp.d.ts.map +1 -1
  12. package/dist/server/server/agent/agent-management-mcp.js +29 -4
  13. package/dist/server/server/agent/agent-management-mcp.js.map +1 -1
  14. package/dist/server/server/agent/agent-manager.d.ts +3 -0
  15. package/dist/server/server/agent/agent-manager.d.ts.map +1 -1
  16. package/dist/server/server/agent/agent-manager.js +27 -5
  17. package/dist/server/server/agent/agent-manager.js.map +1 -1
  18. package/dist/server/server/agent/agent-sdk-types.d.ts +14 -0
  19. package/dist/server/server/agent/agent-sdk-types.d.ts.map +1 -1
  20. package/dist/server/server/agent/mcp-server.d.ts +2 -0
  21. package/dist/server/server/agent/mcp-server.d.ts.map +1 -1
  22. package/dist/server/server/agent/mcp-server.js +30 -5
  23. package/dist/server/server/agent/mcp-server.js.map +1 -1
  24. package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts +42 -0
  25. package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts.map +1 -1
  26. package/dist/server/server/agent/timeline-append.d.ts +10 -0
  27. package/dist/server/server/agent/timeline-append.d.ts.map +1 -0
  28. package/dist/server/server/agent/timeline-append.js +27 -0
  29. package/dist/server/server/agent/timeline-append.js.map +1 -0
  30. package/dist/server/server/bootstrap.d.ts.map +1 -1
  31. package/dist/server/server/bootstrap.js +3 -0
  32. package/dist/server/server/bootstrap.js.map +1 -1
  33. package/dist/server/server/exports.d.ts +1 -0
  34. package/dist/server/server/exports.d.ts.map +1 -1
  35. package/dist/server/server/exports.js +1 -0
  36. package/dist/server/server/exports.js.map +1 -1
  37. package/dist/server/server/persisted-config.d.ts +8 -8
  38. package/dist/server/server/session.d.ts +20 -6
  39. package/dist/server/server/session.d.ts.map +1 -1
  40. package/dist/server/server/session.js +409 -173
  41. package/dist/server/server/session.js.map +1 -1
  42. package/dist/server/server/speech/providers/local/sherpa/sherpa-onnx-node-loader.d.ts.map +1 -1
  43. package/dist/server/server/speech/providers/local/sherpa/sherpa-onnx-node-loader.js +85 -27
  44. package/dist/server/server/speech/providers/local/sherpa/sherpa-onnx-node-loader.js.map +1 -1
  45. package/dist/server/server/speech/providers/local/sherpa/sherpa-runtime-env.d.ts +18 -0
  46. package/dist/server/server/speech/providers/local/sherpa/sherpa-runtime-env.d.ts.map +1 -0
  47. package/dist/server/server/speech/providers/local/sherpa/sherpa-runtime-env.js +68 -0
  48. package/dist/server/server/speech/providers/local/sherpa/sherpa-runtime-env.js.map +1 -0
  49. package/dist/server/server/utils/syntax-highlighter.d.ts.map +1 -1
  50. package/dist/server/server/utils/syntax-highlighter.js +4 -0
  51. package/dist/server/server/utils/syntax-highlighter.js.map +1 -1
  52. package/dist/server/server/worktree-bootstrap.d.ts +29 -0
  53. package/dist/server/server/worktree-bootstrap.d.ts.map +1 -0
  54. package/dist/server/server/worktree-bootstrap.js +454 -0
  55. package/dist/server/server/worktree-bootstrap.js.map +1 -0
  56. package/dist/server/shared/binary-mux.d.ts.map +1 -1
  57. package/dist/server/shared/binary-mux.js +13 -0
  58. package/dist/server/shared/binary-mux.js.map +1 -1
  59. package/dist/server/shared/messages.d.ts +2279 -2493
  60. package/dist/server/shared/messages.d.ts.map +1 -1
  61. package/dist/server/shared/messages.js +71 -25
  62. package/dist/server/shared/messages.js.map +1 -1
  63. package/dist/server/shared/tool-call-display.d.ts.map +1 -1
  64. package/dist/server/shared/tool-call-display.js +4 -0
  65. package/dist/server/shared/tool-call-display.js.map +1 -1
  66. package/dist/server/terminal/terminal-manager.d.ts +16 -0
  67. package/dist/server/terminal/terminal-manager.d.ts.map +1 -1
  68. package/dist/server/terminal/terminal-manager.js +105 -24
  69. package/dist/server/terminal/terminal-manager.js.map +1 -1
  70. package/dist/server/terminal/terminal.d.ts +1 -0
  71. package/dist/server/terminal/terminal.d.ts.map +1 -1
  72. package/dist/server/terminal/terminal.js +54 -6
  73. package/dist/server/terminal/terminal.js.map +1 -1
  74. package/dist/server/utils/directory-suggestions.d.ts +9 -0
  75. package/dist/server/utils/directory-suggestions.d.ts.map +1 -0
  76. package/dist/server/utils/directory-suggestions.js +328 -0
  77. package/dist/server/utils/directory-suggestions.js.map +1 -0
  78. package/dist/server/utils/worktree-metadata.d.ts +28 -2
  79. package/dist/server/utils/worktree-metadata.d.ts.map +1 -1
  80. package/dist/server/utils/worktree-metadata.js +43 -1
  81. package/dist/server/utils/worktree-metadata.js.map +1 -1
  82. package/dist/server/utils/worktree.d.ts +45 -0
  83. package/dist/server/utils/worktree.d.ts.map +1 -1
  84. package/dist/server/utils/worktree.js +183 -18
  85. package/dist/server/utils/worktree.js.map +1 -1
  86. package/package.json +3 -2
@@ -4,6 +4,7 @@ import { stat } from "fs/promises";
4
4
  import { exec } from "child_process";
5
5
  import { promisify } from "util";
6
6
  import { join, resolve, sep } from "path";
7
+ import { homedir } from "node:os";
7
8
  import { z } from "zod";
8
9
  import { serializeAgentStreamEvent, } from "./messages.js";
9
10
  import { BinaryMuxChannel, TerminalBinaryFlags, TerminalBinaryMessageType, } from "../shared/binary-mux.js";
@@ -17,16 +18,19 @@ import { experimental_createMCPClient } from "ai";
17
18
  import { buildProviderRegistry } from "./agent/provider-registry.js";
18
19
  import { scheduleAgentMetadataGeneration } from "./agent/agent-metadata-generator.js";
19
20
  import { toAgentPayload } from "./agent/agent-projections.js";
21
+ import { appendTimelineItemIfAgentKnown, emitLiveTimelineItemIfAgentKnown, } from "./agent/timeline-append.js";
20
22
  import { projectTimelineRows } from "./agent/timeline-projection.js";
21
23
  import { StructuredAgentResponseError, generateStructuredAgentResponse, } from "./agent/agent-response-loop.js";
22
24
  import { isValidAgentProvider, AGENT_PROVIDER_IDS } from "./agent/provider-manifest.js";
23
25
  import { buildVoiceAgentMcpServerConfig, buildVoiceModeSystemPrompt, stripVoiceModeSystemPrompt, } from "./voice-config.js";
24
26
  import { isVoicePermissionAllowed } from "./voice-permission-policy.js";
25
27
  import { listDirectoryEntries, readExplorerFile, getDownloadableFileInfo, } from "./file-explorer/service.js";
26
- import { createWorktree, runWorktreeSetupCommands, WorktreeSetupError, slugify, validateBranchSlug, listPaseoWorktrees, deletePaseoWorktree, isPaseoOwnedWorktreeCwd, resolvePaseoWorktreeRootForCwd, } from "../utils/worktree.js";
28
+ import { slugify, validateBranchSlug, listPaseoWorktrees, deletePaseoWorktree, isPaseoOwnedWorktreeCwd, resolvePaseoWorktreeRootForCwd, } from "../utils/worktree.js";
29
+ import { createAgentWorktree, runAsyncWorktreeBootstrap, } from "./worktree-bootstrap.js";
27
30
  import { getCheckoutDiff, getCheckoutStatus, getCheckoutStatusLite, listBranchSuggestions, NotGitRepoError, MergeConflictError, MergeFromBaseConflictError, commitChanges, mergeToBase, mergeFromBase, pushCurrentBranch, createPullRequest, getPullRequestStatus, } from "../utils/checkout-git.js";
28
31
  import { getProjectIcon } from "../utils/project-icon.js";
29
32
  import { expandTilde } from "../utils/path.js";
33
+ import { searchHomeDirectories } from "../utils/directory-suggestions.js";
30
34
  import { ensureLocalSpeechModels, getLocalSpeechModelDir, listLocalSpeechModels, } from "./speech/providers/local/models.js";
31
35
  const execAsync = promisify(exec);
32
36
  const READ_ONLY_GIT_ENV = {
@@ -37,8 +41,6 @@ const pendingAgentInitializations = new Map();
37
41
  let restartRequested = false;
38
42
  const DEFAULT_AGENT_PROVIDER = AGENT_PROVIDER_IDS[0];
39
43
  const RESTART_EXIT_DELAY_MS = 250;
40
- const PROJECT_PLACEMENT_CACHE_TTL_MS = 10000;
41
- const MAX_AGENTS_PER_PROJECT = 5;
42
44
  const CHECKOUT_DIFF_WATCH_DEBOUNCE_MS = 150;
43
45
  const CHECKOUT_DIFF_FALLBACK_REFRESH_MS = 5000;
44
46
  const TERMINAL_STREAM_WINDOW_BYTES = 256 * 1024;
@@ -90,17 +92,17 @@ function deriveRemoteProjectKey(remoteUrl) {
90
92
  }
91
93
  return `remote:${cleanedHost}/${cleanedPath}`;
92
94
  }
93
- function deriveProjectGroupingKey(cwd, remoteUrl) {
94
- const remoteKey = deriveRemoteProjectKey(remoteUrl);
95
+ function deriveProjectGroupingKey(options) {
96
+ const remoteKey = deriveRemoteProjectKey(options.remoteUrl);
95
97
  if (remoteKey) {
96
98
  return remoteKey;
97
99
  }
98
100
  const worktreeMarker = ".paseo/worktrees/";
99
- const idx = cwd.indexOf(worktreeMarker);
101
+ const idx = options.cwd.indexOf(worktreeMarker);
100
102
  if (idx !== -1) {
101
- return cwd.slice(0, idx).replace(/\/$/, "");
103
+ return options.cwd.slice(0, idx).replace(/\/$/, "");
102
104
  }
103
- return cwd;
105
+ return options.cwd;
104
106
  }
105
107
  function deriveProjectGroupingName(projectKey) {
106
108
  const githubRemotePrefix = "remote:github.com/";
@@ -110,6 +112,13 @@ function deriveProjectGroupingName(projectKey) {
110
112
  const segments = projectKey.split(/[\\/]/).filter(Boolean);
111
113
  return segments[segments.length - 1] || projectKey;
112
114
  }
115
+ class SessionRequestError extends Error {
116
+ constructor(code, message) {
117
+ super(message);
118
+ this.code = code;
119
+ this.name = "SessionRequestError";
120
+ }
121
+ }
113
122
  const PCM_SAMPLE_RATE = 16000;
114
123
  const PCM_CHANNELS = 1;
115
124
  const PCM_BITS_PER_SAMPLE = 16;
@@ -209,10 +218,12 @@ export class Session {
209
218
  this.agentTools = null;
210
219
  this.unsubscribeAgentEvents = null;
211
220
  this.agentUpdatesSubscription = null;
212
- this.projectPlacementCache = new Map();
213
221
  this.clientActivity = null;
214
222
  this.MOBILE_BACKGROUND_STREAM_GRACE_MS = 60000;
223
+ this.subscribedTerminalDirectories = new Set();
224
+ this.unsubscribeTerminalsChanged = null;
215
225
  this.terminalSubscriptions = new Map();
226
+ this.terminalExitSubscriptions = new Map();
216
227
  this.terminalStreams = new Map();
217
228
  this.terminalStreamByTerminalId = new Map();
218
229
  this.nextTerminalStreamId = 1;
@@ -232,6 +243,9 @@ export class Session {
232
243
  this.agentStorage = agentStorage;
233
244
  this.createAgentMcpTransport = createAgentMcpTransport;
234
245
  this.terminalManager = terminalManager;
246
+ if (this.terminalManager) {
247
+ this.unsubscribeTerminalsChanged = this.terminalManager.subscribeTerminalsChanged((event) => this.handleTerminalsChanged(event));
248
+ }
235
249
  this.voiceAgentMcpStdio = voice?.voiceAgentMcpStdio ?? null;
236
250
  const configuredModelsDir = dictation?.localModels?.modelsDir?.trim();
237
251
  this.localSpeechModelsDir =
@@ -619,26 +633,16 @@ export class Session {
619
633
  const checkout = await getCheckoutStatusLite(cwd, { paseoHome: this.paseoHome })
620
634
  .then((status) => this.toProjectCheckoutLite(cwd, status))
621
635
  .catch(() => this.buildFallbackProjectCheckout(cwd));
622
- const projectKey = deriveProjectGroupingKey(cwd, checkout.remoteUrl);
636
+ const projectKey = deriveProjectGroupingKey({
637
+ cwd,
638
+ remoteUrl: checkout.remoteUrl,
639
+ });
623
640
  return {
624
641
  projectKey,
625
642
  projectName: deriveProjectGroupingName(projectKey),
626
643
  checkout,
627
644
  };
628
645
  }
629
- getProjectPlacement(cwd) {
630
- const now = Date.now();
631
- const cached = this.projectPlacementCache.get(cwd);
632
- if (cached && cached.expiresAt > now) {
633
- return cached.promise;
634
- }
635
- const promise = this.buildProjectPlacement(cwd);
636
- this.projectPlacementCache.set(cwd, {
637
- expiresAt: now + PROJECT_PLACEMENT_CACHE_TTL_MS,
638
- promise,
639
- });
640
- return promise;
641
- }
642
646
  async forwardAgentUpdate(agent) {
643
647
  try {
644
648
  const subscription = this.agentUpdatesSubscription;
@@ -648,7 +652,7 @@ export class Session {
648
652
  const payload = await this.buildAgentPayload(agent);
649
653
  const matches = this.matchesAgentFilter(payload, subscription.filter);
650
654
  if (matches) {
651
- const project = await this.getProjectPlacement(payload.cwd);
655
+ const project = await this.buildProjectPlacement(payload.cwd);
652
656
  this.emit({
653
657
  type: "agent_update",
654
658
  payload: { kind: "upsert", agent: payload, project },
@@ -680,10 +684,7 @@ export class Session {
680
684
  this.handleAudioPlayed(msg.id);
681
685
  break;
682
686
  case "fetch_agents_request":
683
- await this.handleFetchAgents(msg.requestId, msg.filter);
684
- break;
685
- case "fetch_agents_grouped_by_project_request":
686
- await this.handleFetchAgentsGroupedByProject(msg.requestId, msg.filter);
687
+ await this.handleFetchAgents(msg);
687
688
  break;
688
689
  case "fetch_agent_request":
689
690
  await this.handleFetchAgent(msg.agentId, msg.requestId);
@@ -789,6 +790,9 @@ export class Session {
789
790
  case "branch_suggestions_request":
790
791
  await this.handleBranchSuggestionsRequest(msg);
791
792
  break;
793
+ case "directory_suggestions_request":
794
+ await this.handleDirectorySuggestionsRequest(msg);
795
+ break;
792
796
  case "subscribe_checkout_diff_request":
793
797
  await this.handleSubscribeCheckoutDiffRequest(msg);
794
798
  break;
@@ -868,6 +872,12 @@ export class Session {
868
872
  case "register_push_token":
869
873
  this.handleRegisterPushToken(msg.token);
870
874
  break;
875
+ case "subscribe_terminals_request":
876
+ this.handleSubscribeTerminalsRequest(msg);
877
+ break;
878
+ case "unsubscribe_terminals_request":
879
+ this.handleUnsubscribeTerminalsRequest(msg);
880
+ break;
871
881
  case "list_terminals_request":
872
882
  await this.handleListTerminalsRequest(msg);
873
883
  break;
@@ -1546,7 +1556,10 @@ export class Session {
1546
1556
  }
1547
1557
  const prompt = this.buildAgentPrompt(text, images);
1548
1558
  try {
1549
- this.agentManager.recordUserMessage(agentId, text, { messageId });
1559
+ this.agentManager.recordUserMessage(agentId, text, {
1560
+ messageId,
1561
+ emitState: false,
1562
+ });
1550
1563
  }
1551
1564
  catch (error) {
1552
1565
  this.sessionLogger.error({ err: error, agentId }, `Failed to record user message for agent ${agentId}`);
@@ -1606,7 +1619,22 @@ export class Session {
1606
1619
  });
1607
1620
  }
1608
1621
  if (worktreeConfig) {
1609
- void this.runAsyncWorktreeSetup(snapshot.id, worktreeConfig);
1622
+ void runAsyncWorktreeBootstrap({
1623
+ agentId: snapshot.id,
1624
+ worktree: worktreeConfig,
1625
+ terminalManager: this.terminalManager,
1626
+ appendTimelineItem: (item) => appendTimelineItemIfAgentKnown({
1627
+ agentManager: this.agentManager,
1628
+ agentId: snapshot.id,
1629
+ item,
1630
+ }),
1631
+ emitLiveTimelineItem: (item) => emitLiveTimelineItemIfAgentKnown({
1632
+ agentManager: this.agentManager,
1633
+ agentId: snapshot.id,
1634
+ item,
1635
+ }),
1636
+ logger: this.sessionLogger,
1637
+ });
1610
1638
  }
1611
1639
  this.sessionLogger.info({ agentId: snapshot.id, provider: snapshot.provider }, `Created agent ${snapshot.id} (${snapshot.provider})`);
1612
1640
  }
@@ -1776,12 +1804,11 @@ export class Session {
1776
1804
  throw new Error("A branch name is required when creating a worktree.");
1777
1805
  }
1778
1806
  this.sessionLogger.info({ worktreeSlug: normalized.worktreeSlug ?? targetBranch, branch: targetBranch }, `Creating worktree '${normalized.worktreeSlug ?? targetBranch}' for branch ${targetBranch}`);
1779
- const createdWorktree = await createWorktree({
1807
+ const createdWorktree = await createAgentWorktree({
1780
1808
  branchName: targetBranch,
1781
1809
  cwd,
1782
1810
  baseBranch: normalized.baseBranch,
1783
1811
  worktreeSlug: normalized.worktreeSlug ?? targetBranch,
1784
- runSetup: false,
1785
1812
  paseoHome: this.paseoHome,
1786
1813
  });
1787
1814
  cwd = createdWorktree.worktreePath;
@@ -1805,100 +1832,6 @@ export class Session {
1805
1832
  worktreeConfig,
1806
1833
  };
1807
1834
  }
1808
- async runAsyncWorktreeSetup(agentId, worktree) {
1809
- const callId = uuidv4();
1810
- let results = [];
1811
- try {
1812
- const started = await this.safeAppendTimelineItem(agentId, {
1813
- type: "tool_call",
1814
- name: "paseo_worktree_setup",
1815
- callId,
1816
- status: "running",
1817
- detail: {
1818
- type: "unknown",
1819
- input: {
1820
- worktreePath: worktree.worktreePath,
1821
- branchName: worktree.branchName,
1822
- },
1823
- output: null,
1824
- },
1825
- error: null,
1826
- });
1827
- if (!started) {
1828
- return;
1829
- }
1830
- results = await runWorktreeSetupCommands({
1831
- worktreePath: worktree.worktreePath,
1832
- branchName: worktree.branchName,
1833
- cleanupOnFailure: false,
1834
- });
1835
- await this.safeAppendTimelineItem(agentId, {
1836
- type: "tool_call",
1837
- name: "paseo_worktree_setup",
1838
- callId,
1839
- status: "completed",
1840
- detail: {
1841
- type: "unknown",
1842
- input: {
1843
- worktreePath: worktree.worktreePath,
1844
- branchName: worktree.branchName,
1845
- },
1846
- output: {
1847
- worktreePath: worktree.worktreePath,
1848
- commands: results.map((result) => ({
1849
- command: result.command,
1850
- cwd: result.cwd,
1851
- exitCode: result.exitCode,
1852
- output: `${result.stdout ?? ""}${result.stderr ? `\n${result.stderr}` : ""}`.trim(),
1853
- })),
1854
- },
1855
- },
1856
- error: null,
1857
- });
1858
- }
1859
- catch (error) {
1860
- if (error instanceof WorktreeSetupError) {
1861
- results = error.results;
1862
- }
1863
- const message = error instanceof Error ? error.message : String(error);
1864
- await this.safeAppendTimelineItem(agentId, {
1865
- type: "tool_call",
1866
- name: "paseo_worktree_setup",
1867
- callId,
1868
- status: "failed",
1869
- detail: {
1870
- type: "unknown",
1871
- input: {
1872
- worktreePath: worktree.worktreePath,
1873
- branchName: worktree.branchName,
1874
- },
1875
- output: {
1876
- worktreePath: worktree.worktreePath,
1877
- commands: results.map((result) => ({
1878
- command: result.command,
1879
- cwd: result.cwd,
1880
- exitCode: result.exitCode,
1881
- output: `${result.stdout ?? ""}${result.stderr ? `\n${result.stderr}` : ""}`.trim(),
1882
- })),
1883
- },
1884
- },
1885
- error: { message },
1886
- });
1887
- }
1888
- }
1889
- async safeAppendTimelineItem(agentId, item) {
1890
- try {
1891
- await this.agentManager.appendTimelineItem(agentId, item);
1892
- return true;
1893
- }
1894
- catch (error) {
1895
- const message = error instanceof Error ? error.message : String(error);
1896
- if (message.includes("Unknown agent")) {
1897
- return false;
1898
- }
1899
- throw error;
1900
- }
1901
- }
1902
1835
  async handleListProviderModelsRequest(msg) {
1903
1836
  const fetchedAt = new Date().toISOString();
1904
1837
  try {
@@ -2583,8 +2516,9 @@ export class Session {
2583
2516
  }
2584
2517
  async handleCheckoutStatusRequest(msg) {
2585
2518
  const { cwd, requestId } = msg;
2519
+ const resolvedCwd = expandTilde(cwd);
2586
2520
  try {
2587
- const status = await getCheckoutStatus(cwd, { paseoHome: this.paseoHome });
2521
+ const status = await getCheckoutStatus(resolvedCwd, { paseoHome: this.paseoHome });
2588
2522
  if (!status.isGit) {
2589
2523
  this.emit({
2590
2524
  type: "checkout_status_response",
@@ -2764,6 +2698,34 @@ export class Session {
2764
2698
  });
2765
2699
  }
2766
2700
  }
2701
+ async handleDirectorySuggestionsRequest(msg) {
2702
+ const { query, limit, requestId } = msg;
2703
+ try {
2704
+ const directories = await searchHomeDirectories({
2705
+ homeDir: process.env.HOME ?? homedir(),
2706
+ query,
2707
+ limit,
2708
+ });
2709
+ this.emit({
2710
+ type: "directory_suggestions_response",
2711
+ payload: {
2712
+ directories,
2713
+ error: null,
2714
+ requestId,
2715
+ },
2716
+ });
2717
+ }
2718
+ catch (error) {
2719
+ this.emit({
2720
+ type: "directory_suggestions_response",
2721
+ payload: {
2722
+ directories: [],
2723
+ error: error instanceof Error ? error.message : String(error),
2724
+ requestId,
2725
+ },
2726
+ });
2727
+ }
2728
+ }
2767
2729
  normalizeCheckoutDiffCompare(compare) {
2768
2730
  if (compare.mode === "uncommitted") {
2769
2731
  return { mode: "uncommitted" };
@@ -3672,64 +3634,230 @@ export class Session {
3672
3634
  }
3673
3635
  return this.buildStoredAgentPayload(record);
3674
3636
  }
3675
- async handleFetchAgents(requestId, filter) {
3637
+ normalizeFetchAgentsSort(sort) {
3638
+ const fallback = [
3639
+ { key: "updated_at", direction: "desc" },
3640
+ ];
3641
+ if (!sort || sort.length === 0) {
3642
+ return fallback;
3643
+ }
3644
+ const deduped = [];
3645
+ const seen = new Set();
3646
+ for (const entry of sort) {
3647
+ if (seen.has(entry.key)) {
3648
+ continue;
3649
+ }
3650
+ seen.add(entry.key);
3651
+ deduped.push(entry);
3652
+ }
3653
+ return deduped.length > 0 ? deduped : fallback;
3654
+ }
3655
+ getStatusPriority(agent) {
3656
+ const requiresAttention = agent.requiresAttention ?? false;
3657
+ const attentionReason = agent.attentionReason ?? null;
3658
+ if (requiresAttention && attentionReason === "permission") {
3659
+ return 0;
3660
+ }
3661
+ if (agent.status === "error" || attentionReason === "error") {
3662
+ return 1;
3663
+ }
3664
+ if (agent.status === "running") {
3665
+ return 2;
3666
+ }
3667
+ if (agent.status === "initializing") {
3668
+ return 3;
3669
+ }
3670
+ return 4;
3671
+ }
3672
+ getFetchAgentsSortValue(entry, key) {
3673
+ switch (key) {
3674
+ case "status_priority":
3675
+ return this.getStatusPriority(entry.agent);
3676
+ case "created_at":
3677
+ return Date.parse(entry.agent.createdAt);
3678
+ case "updated_at":
3679
+ return Date.parse(entry.agent.updatedAt);
3680
+ case "title":
3681
+ return entry.agent.title?.toLocaleLowerCase() ?? "";
3682
+ }
3683
+ }
3684
+ compareSortValues(left, right) {
3685
+ if (left === right) {
3686
+ return 0;
3687
+ }
3688
+ if (left === null) {
3689
+ return -1;
3690
+ }
3691
+ if (right === null) {
3692
+ return 1;
3693
+ }
3694
+ if (typeof left === "number" && typeof right === "number") {
3695
+ return left < right ? -1 : 1;
3696
+ }
3697
+ return String(left).localeCompare(String(right));
3698
+ }
3699
+ compareFetchAgentsEntries(left, right, sort) {
3700
+ for (const spec of sort) {
3701
+ const leftValue = this.getFetchAgentsSortValue(left, spec.key);
3702
+ const rightValue = this.getFetchAgentsSortValue(right, spec.key);
3703
+ const base = this.compareSortValues(leftValue, rightValue);
3704
+ if (base === 0) {
3705
+ continue;
3706
+ }
3707
+ return spec.direction === "asc" ? base : -base;
3708
+ }
3709
+ return left.agent.id.localeCompare(right.agent.id);
3710
+ }
3711
+ encodeFetchAgentsCursor(entry, sort) {
3712
+ const values = {};
3713
+ for (const spec of sort) {
3714
+ values[spec.key] = this.getFetchAgentsSortValue(entry, spec.key);
3715
+ }
3716
+ return Buffer.from(JSON.stringify({
3717
+ sort,
3718
+ values,
3719
+ id: entry.agent.id,
3720
+ }), "utf8").toString("base64url");
3721
+ }
3722
+ decodeFetchAgentsCursor(cursor, sort) {
3723
+ let parsed;
3676
3724
  try {
3677
- const agents = await this.listAgentPayloads(filter);
3678
- this.emit({
3679
- type: "fetch_agents_response",
3680
- payload: { requestId, agents },
3681
- });
3725
+ parsed = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
3682
3726
  }
3683
- catch (error) {
3684
- this.sessionLogger.error({ err: error }, "Failed to handle fetch_agents_request");
3685
- this.emit({
3686
- type: "fetch_agents_response",
3687
- payload: { requestId, agents: [] },
3688
- });
3689
- }
3690
- }
3691
- async listAgentsGroupedByProjectPayload(filter) {
3692
- const agents = await this.listAgentPayloads(filter);
3693
- const visibleAgents = agents
3694
- .filter((agent) => !agent.archivedAt)
3695
- .sort((left, right) => Date.parse(right.updatedAt || "") - Date.parse(left.updatedAt || ""));
3696
- const grouped = new Map();
3697
- // Warm project placement status for all visible roots up front to avoid serial N+1 latency.
3698
- for (const agent of visibleAgents) {
3699
- void this.getProjectPlacement(agent.cwd);
3700
- }
3701
- for (const agent of visibleAgents) {
3702
- const project = await this.getProjectPlacement(agent.cwd);
3703
- const projectKey = project.projectKey;
3704
- let group = grouped.get(projectKey);
3705
- if (!group) {
3706
- group = {
3707
- projectKey,
3708
- projectName: project.projectName,
3709
- agents: [],
3710
- };
3711
- grouped.set(projectKey, group);
3727
+ catch {
3728
+ throw new SessionRequestError("invalid_cursor", "Invalid fetch_agents cursor");
3729
+ }
3730
+ if (!parsed || typeof parsed !== "object") {
3731
+ throw new SessionRequestError("invalid_cursor", "Invalid fetch_agents cursor");
3732
+ }
3733
+ const payload = parsed;
3734
+ if (!Array.isArray(payload.sort) || typeof payload.id !== "string") {
3735
+ throw new SessionRequestError("invalid_cursor", "Invalid fetch_agents cursor");
3736
+ }
3737
+ if (!payload.values || typeof payload.values !== "object") {
3738
+ throw new SessionRequestError("invalid_cursor", "Invalid fetch_agents cursor");
3739
+ }
3740
+ const cursorSort = [];
3741
+ for (const item of payload.sort) {
3742
+ if (!item ||
3743
+ typeof item !== "object" ||
3744
+ typeof item.key !== "string" ||
3745
+ typeof item.direction !== "string") {
3746
+ throw new SessionRequestError("invalid_cursor", "Invalid fetch_agents cursor");
3747
+ }
3748
+ const key = item.key;
3749
+ const direction = item.direction;
3750
+ if ((key !== "status_priority" &&
3751
+ key !== "created_at" &&
3752
+ key !== "updated_at" &&
3753
+ key !== "title") ||
3754
+ (direction !== "asc" && direction !== "desc")) {
3755
+ throw new SessionRequestError("invalid_cursor", "Invalid fetch_agents cursor");
3712
3756
  }
3713
- if (group.agents.length >= MAX_AGENTS_PER_PROJECT) {
3757
+ cursorSort.push({ key, direction });
3758
+ }
3759
+ if (cursorSort.length !== sort.length ||
3760
+ cursorSort.some((entry, index) => entry.key !== sort[index]?.key ||
3761
+ entry.direction !== sort[index]?.direction)) {
3762
+ throw new SessionRequestError("invalid_cursor", "fetch_agents cursor does not match current sort");
3763
+ }
3764
+ return {
3765
+ sort: cursorSort,
3766
+ values: payload.values,
3767
+ id: payload.id,
3768
+ };
3769
+ }
3770
+ compareEntryWithCursor(entry, cursor, sort) {
3771
+ for (const spec of sort) {
3772
+ const leftValue = this.getFetchAgentsSortValue(entry, spec.key);
3773
+ const rightValue = cursor.values[spec.key] !== undefined ? cursor.values[spec.key] ?? null : null;
3774
+ const base = this.compareSortValues(leftValue, rightValue);
3775
+ if (base === 0) {
3714
3776
  continue;
3715
3777
  }
3716
- group.agents.push({ agent, checkout: project.checkout });
3778
+ return spec.direction === "asc" ? base : -base;
3717
3779
  }
3718
- return Array.from(grouped.values());
3780
+ return entry.agent.id.localeCompare(cursor.id);
3719
3781
  }
3720
- async handleFetchAgentsGroupedByProject(requestId, filter) {
3782
+ async listFetchAgentsEntries(request) {
3783
+ const filter = request.filter;
3784
+ const sort = this.normalizeFetchAgentsSort(request.sort);
3785
+ const includeArchived = filter?.includeArchived ?? false;
3786
+ let agents = await this.listAgentPayloads({
3787
+ labels: filter?.labels,
3788
+ });
3789
+ if (!includeArchived) {
3790
+ agents = agents.filter((agent) => !agent.archivedAt);
3791
+ }
3792
+ if (filter?.statuses && filter.statuses.length > 0) {
3793
+ const statuses = new Set(filter.statuses);
3794
+ agents = agents.filter((agent) => statuses.has(agent.status));
3795
+ }
3796
+ if (typeof filter?.requiresAttention === "boolean") {
3797
+ agents = agents.filter((agent) => (agent.requiresAttention ?? false) === filter.requiresAttention);
3798
+ }
3799
+ const placementByCwd = new Map();
3800
+ const getPlacement = (cwd) => {
3801
+ const existing = placementByCwd.get(cwd);
3802
+ if (existing) {
3803
+ return existing;
3804
+ }
3805
+ const placementPromise = this.buildProjectPlacement(cwd);
3806
+ placementByCwd.set(cwd, placementPromise);
3807
+ return placementPromise;
3808
+ };
3809
+ let entries = await Promise.all(agents.map(async (agent) => ({
3810
+ agent,
3811
+ project: await getPlacement(agent.cwd),
3812
+ })));
3813
+ if (filter?.projectKeys && filter.projectKeys.length > 0) {
3814
+ const projectKeys = new Set(filter.projectKeys.filter((item) => item.trim().length > 0));
3815
+ entries = entries.filter((entry) => projectKeys.has(entry.project.projectKey));
3816
+ }
3817
+ entries.sort((left, right) => this.compareFetchAgentsEntries(left, right, sort));
3818
+ const cursorToken = request.page?.cursor;
3819
+ if (cursorToken) {
3820
+ const cursor = this.decodeFetchAgentsCursor(cursorToken, sort);
3821
+ entries = entries.filter((entry) => this.compareEntryWithCursor(entry, cursor, sort) > 0);
3822
+ }
3823
+ const limit = request.page?.limit ?? entries.length;
3824
+ const pagedEntries = entries.slice(0, limit);
3825
+ const hasMore = entries.length > limit;
3826
+ const nextCursor = hasMore && pagedEntries.length > 0
3827
+ ? this.encodeFetchAgentsCursor(pagedEntries[pagedEntries.length - 1], sort)
3828
+ : null;
3829
+ return {
3830
+ entries: pagedEntries,
3831
+ pageInfo: {
3832
+ nextCursor,
3833
+ prevCursor: request.page?.cursor ?? null,
3834
+ hasMore,
3835
+ },
3836
+ };
3837
+ }
3838
+ async handleFetchAgents(request) {
3721
3839
  try {
3722
- const groups = await this.listAgentsGroupedByProjectPayload(filter);
3840
+ const payload = await this.listFetchAgentsEntries(request);
3723
3841
  this.emit({
3724
- type: "fetch_agents_grouped_by_project_response",
3725
- payload: { requestId, groups },
3842
+ type: "fetch_agents_response",
3843
+ payload: {
3844
+ requestId: request.requestId,
3845
+ ...payload,
3846
+ },
3726
3847
  });
3727
3848
  }
3728
3849
  catch (error) {
3729
- this.sessionLogger.error({ err: error }, "Failed to handle fetch_agents_grouped_by_project_request");
3850
+ const code = error instanceof SessionRequestError ? error.code : "fetch_agents_failed";
3851
+ const message = error instanceof Error ? error.message : "Failed to fetch agents";
3852
+ this.sessionLogger.error({ err: error }, "Failed to handle fetch_agents_request");
3730
3853
  this.emit({
3731
- type: "fetch_agents_grouped_by_project_response",
3732
- payload: { requestId, groups: [] },
3854
+ type: "rpc_error",
3855
+ payload: {
3856
+ requestId: request.requestId,
3857
+ requestType: request.type,
3858
+ error: message,
3859
+ code,
3860
+ },
3733
3861
  });
3734
3862
  }
3735
3863
  }
@@ -3845,7 +3973,10 @@ export class Session {
3845
3973
  await this.ensureAgentLoaded(agentId);
3846
3974
  await this.interruptAgentIfRunning(agentId);
3847
3975
  try {
3848
- this.agentManager.recordUserMessage(agentId, msg.text, { messageId: msg.messageId });
3976
+ this.agentManager.recordUserMessage(agentId, msg.text, {
3977
+ messageId: msg.messageId,
3978
+ emitState: false,
3979
+ });
3849
3980
  }
3850
3981
  catch (error) {
3851
3982
  this.sessionLogger.error({ err: error, agentId }, "Failed to record user message for send_agent_message_request");
@@ -4502,10 +4633,19 @@ export class Session {
4502
4633
  await this.disableVoiceModeForActiveAgent(true);
4503
4634
  this.isVoiceMode = false;
4504
4635
  // Unsubscribe from all terminals
4636
+ if (this.unsubscribeTerminalsChanged) {
4637
+ this.unsubscribeTerminalsChanged();
4638
+ this.unsubscribeTerminalsChanged = null;
4639
+ }
4640
+ this.subscribedTerminalDirectories.clear();
4505
4641
  for (const unsubscribe of this.terminalSubscriptions.values()) {
4506
4642
  unsubscribe();
4507
4643
  }
4508
4644
  this.terminalSubscriptions.clear();
4645
+ for (const unsubscribeExit of this.terminalExitSubscriptions.values()) {
4646
+ unsubscribeExit();
4647
+ }
4648
+ this.terminalExitSubscriptions.clear();
4509
4649
  this.detachAllTerminalStreams({ emitExit: false });
4510
4650
  for (const target of this.checkoutDiffTargets.values()) {
4511
4651
  this.closeCheckoutDiffWatchTarget(target);
@@ -4516,6 +4656,96 @@ export class Session {
4516
4656
  // ============================================================================
4517
4657
  // Terminal Handlers
4518
4658
  // ============================================================================
4659
+ ensureTerminalExitSubscription(terminal) {
4660
+ if (this.terminalExitSubscriptions.has(terminal.id)) {
4661
+ return;
4662
+ }
4663
+ const unsubscribeExit = terminal.onExit(() => {
4664
+ this.handleTerminalExited(terminal.id);
4665
+ });
4666
+ this.terminalExitSubscriptions.set(terminal.id, unsubscribeExit);
4667
+ }
4668
+ handleTerminalExited(terminalId) {
4669
+ const unsubscribeExit = this.terminalExitSubscriptions.get(terminalId);
4670
+ if (unsubscribeExit) {
4671
+ unsubscribeExit();
4672
+ this.terminalExitSubscriptions.delete(terminalId);
4673
+ }
4674
+ const unsubscribe = this.terminalSubscriptions.get(terminalId);
4675
+ if (unsubscribe) {
4676
+ try {
4677
+ unsubscribe();
4678
+ }
4679
+ catch (error) {
4680
+ this.sessionLogger.warn({ err: error, terminalId }, "Failed to unsubscribe terminal after process exit");
4681
+ }
4682
+ this.terminalSubscriptions.delete(terminalId);
4683
+ }
4684
+ const streamId = this.terminalStreamByTerminalId.get(terminalId);
4685
+ if (typeof streamId === "number") {
4686
+ this.detachTerminalStream(streamId, { emitExit: true });
4687
+ }
4688
+ }
4689
+ emitTerminalsChangedSnapshot(input) {
4690
+ this.emit({
4691
+ type: "terminals_changed",
4692
+ payload: {
4693
+ cwd: input.cwd,
4694
+ terminals: input.terminals,
4695
+ },
4696
+ });
4697
+ }
4698
+ handleTerminalsChanged(event) {
4699
+ if (!this.subscribedTerminalDirectories.has(event.cwd)) {
4700
+ return;
4701
+ }
4702
+ this.emitTerminalsChangedSnapshot({
4703
+ cwd: event.cwd,
4704
+ terminals: event.terminals.map((terminal) => ({
4705
+ id: terminal.id,
4706
+ name: terminal.name,
4707
+ })),
4708
+ });
4709
+ }
4710
+ handleSubscribeTerminalsRequest(msg) {
4711
+ this.subscribedTerminalDirectories.add(msg.cwd);
4712
+ void this.emitInitialTerminalsChangedSnapshot(msg.cwd);
4713
+ }
4714
+ handleUnsubscribeTerminalsRequest(msg) {
4715
+ this.subscribedTerminalDirectories.delete(msg.cwd);
4716
+ }
4717
+ async emitInitialTerminalsChangedSnapshot(cwd) {
4718
+ if (!this.terminalManager || !this.subscribedTerminalDirectories.has(cwd)) {
4719
+ return;
4720
+ }
4721
+ const hadDirectoryBeforeSubscribe = this.terminalManager
4722
+ .listDirectories()
4723
+ .includes(cwd);
4724
+ try {
4725
+ const terminals = await this.terminalManager.getTerminals(cwd);
4726
+ for (const terminal of terminals) {
4727
+ this.ensureTerminalExitSubscription(terminal);
4728
+ }
4729
+ // New directories auto-create Terminal 1, which already emits through
4730
+ // terminal-manager change listeners.
4731
+ if (!hadDirectoryBeforeSubscribe) {
4732
+ return;
4733
+ }
4734
+ if (!this.subscribedTerminalDirectories.has(cwd)) {
4735
+ return;
4736
+ }
4737
+ this.emitTerminalsChangedSnapshot({
4738
+ cwd,
4739
+ terminals: terminals.map((terminal) => ({
4740
+ id: terminal.id,
4741
+ name: terminal.name,
4742
+ })),
4743
+ });
4744
+ }
4745
+ catch (error) {
4746
+ this.sessionLogger.warn({ err: error, cwd }, "Failed to emit initial terminal snapshot");
4747
+ }
4748
+ }
4519
4749
  async handleListTerminalsRequest(msg) {
4520
4750
  if (!this.terminalManager) {
4521
4751
  this.emit({
@@ -4530,6 +4760,9 @@ export class Session {
4530
4760
  }
4531
4761
  try {
4532
4762
  const terminals = await this.terminalManager.getTerminals(msg.cwd);
4763
+ for (const terminal of terminals) {
4764
+ this.ensureTerminalExitSubscription(terminal);
4765
+ }
4533
4766
  this.emit({
4534
4767
  type: "list_terminals_response",
4535
4768
  payload: {
@@ -4568,6 +4801,7 @@ export class Session {
4568
4801
  cwd: msg.cwd,
4569
4802
  name: msg.name,
4570
4803
  });
4804
+ this.ensureTerminalExitSubscription(session);
4571
4805
  this.emit({
4572
4806
  type: "create_terminal_response",
4573
4807
  payload: {
@@ -4615,6 +4849,7 @@ export class Session {
4615
4849
  });
4616
4850
  return;
4617
4851
  }
4852
+ this.ensureTerminalExitSubscription(session);
4618
4853
  // Unsubscribe from previous subscription if any
4619
4854
  const existing = this.terminalSubscriptions.get(msg.terminalId);
4620
4855
  if (existing) {
@@ -4660,6 +4895,7 @@ export class Session {
4660
4895
  this.sessionLogger.warn({ terminalId: msg.terminalId }, "Terminal not found for input");
4661
4896
  return;
4662
4897
  }
4898
+ this.ensureTerminalExitSubscription(session);
4663
4899
  session.send(msg.message);
4664
4900
  }
4665
4901
  async handleKillTerminalRequest(msg) {