@getpaseo/server 0.1.4 → 0.1.6

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 (62) hide show
  1. package/dist/server/client/daemon-client-terminal-stream-manager.d.ts.map +1 -1
  2. package/dist/server/client/daemon-client-terminal-stream-manager.js +4 -0
  3. package/dist/server/client/daemon-client-terminal-stream-manager.js.map +1 -1
  4. package/dist/server/client/daemon-client.d.ts +20 -15
  5. package/dist/server/client/daemon-client.d.ts.map +1 -1
  6. package/dist/server/client/daemon-client.js +35 -23
  7. package/dist/server/client/daemon-client.js.map +1 -1
  8. package/dist/server/server/agent/agent-management-mcp.d.ts +2 -0
  9. package/dist/server/server/agent/agent-management-mcp.d.ts.map +1 -1
  10. package/dist/server/server/agent/agent-management-mcp.js +29 -4
  11. package/dist/server/server/agent/agent-management-mcp.js.map +1 -1
  12. package/dist/server/server/agent/agent-manager.d.ts +3 -0
  13. package/dist/server/server/agent/agent-manager.d.ts.map +1 -1
  14. package/dist/server/server/agent/agent-manager.js +27 -5
  15. package/dist/server/server/agent/agent-manager.js.map +1 -1
  16. package/dist/server/server/agent/agent-sdk-types.d.ts +14 -0
  17. package/dist/server/server/agent/agent-sdk-types.d.ts.map +1 -1
  18. package/dist/server/server/agent/mcp-server.d.ts +2 -0
  19. package/dist/server/server/agent/mcp-server.d.ts.map +1 -1
  20. package/dist/server/server/agent/mcp-server.js +30 -5
  21. package/dist/server/server/agent/mcp-server.js.map +1 -1
  22. package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts +42 -0
  23. package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts.map +1 -1
  24. package/dist/server/server/agent/timeline-append.d.ts +10 -0
  25. package/dist/server/server/agent/timeline-append.d.ts.map +1 -0
  26. package/dist/server/server/agent/timeline-append.js +27 -0
  27. package/dist/server/server/agent/timeline-append.js.map +1 -0
  28. package/dist/server/server/bootstrap.d.ts.map +1 -1
  29. package/dist/server/server/bootstrap.js +3 -0
  30. package/dist/server/server/bootstrap.js.map +1 -1
  31. package/dist/server/server/persisted-config.d.ts +8 -8
  32. package/dist/server/server/session.d.ts +19 -6
  33. package/dist/server/server/session.d.ts.map +1 -1
  34. package/dist/server/server/session.js +373 -171
  35. package/dist/server/server/session.js.map +1 -1
  36. package/dist/server/server/worktree-bootstrap.d.ts +29 -0
  37. package/dist/server/server/worktree-bootstrap.d.ts.map +1 -0
  38. package/dist/server/server/worktree-bootstrap.js +407 -0
  39. package/dist/server/server/worktree-bootstrap.js.map +1 -0
  40. package/dist/server/shared/binary-mux.d.ts.map +1 -1
  41. package/dist/server/shared/binary-mux.js +13 -0
  42. package/dist/server/shared/binary-mux.js.map +1 -1
  43. package/dist/server/shared/messages.d.ts +2051 -2493
  44. package/dist/server/shared/messages.d.ts.map +1 -1
  45. package/dist/server/shared/messages.js +55 -25
  46. package/dist/server/shared/messages.js.map +1 -1
  47. package/dist/server/shared/tool-call-display.d.ts.map +1 -1
  48. package/dist/server/shared/tool-call-display.js +4 -0
  49. package/dist/server/shared/tool-call-display.js.map +1 -1
  50. package/dist/server/terminal/terminal-manager.d.ts +11 -0
  51. package/dist/server/terminal/terminal-manager.d.ts.map +1 -1
  52. package/dist/server/terminal/terminal-manager.js +75 -24
  53. package/dist/server/terminal/terminal-manager.js.map +1 -1
  54. package/dist/server/terminal/terminal.d.ts +1 -0
  55. package/dist/server/terminal/terminal.d.ts.map +1 -1
  56. package/dist/server/terminal/terminal.js +54 -6
  57. package/dist/server/terminal/terminal.js.map +1 -1
  58. package/dist/server/utils/worktree.d.ts +32 -0
  59. package/dist/server/utils/worktree.d.ts.map +1 -1
  60. package/dist/server/utils/worktree.js +127 -6
  61. package/dist/server/utils/worktree.js.map +1 -1
  62. package/package.json +2 -2
@@ -17,13 +17,15 @@ import { experimental_createMCPClient } from "ai";
17
17
  import { buildProviderRegistry } from "./agent/provider-registry.js";
18
18
  import { scheduleAgentMetadataGeneration } from "./agent/agent-metadata-generator.js";
19
19
  import { toAgentPayload } from "./agent/agent-projections.js";
20
+ import { appendTimelineItemIfAgentKnown, emitLiveTimelineItemIfAgentKnown, } from "./agent/timeline-append.js";
20
21
  import { projectTimelineRows } from "./agent/timeline-projection.js";
21
22
  import { StructuredAgentResponseError, generateStructuredAgentResponse, } from "./agent/agent-response-loop.js";
22
23
  import { isValidAgentProvider, AGENT_PROVIDER_IDS } from "./agent/provider-manifest.js";
23
24
  import { buildVoiceAgentMcpServerConfig, buildVoiceModeSystemPrompt, stripVoiceModeSystemPrompt, } from "./voice-config.js";
24
25
  import { isVoicePermissionAllowed } from "./voice-permission-policy.js";
25
26
  import { listDirectoryEntries, readExplorerFile, getDownloadableFileInfo, } from "./file-explorer/service.js";
26
- import { createWorktree, runWorktreeSetupCommands, WorktreeSetupError, slugify, validateBranchSlug, listPaseoWorktrees, deletePaseoWorktree, isPaseoOwnedWorktreeCwd, resolvePaseoWorktreeRootForCwd, } from "../utils/worktree.js";
27
+ import { slugify, validateBranchSlug, listPaseoWorktrees, deletePaseoWorktree, isPaseoOwnedWorktreeCwd, resolvePaseoWorktreeRootForCwd, } from "../utils/worktree.js";
28
+ import { createAgentWorktree, runAsyncWorktreeBootstrap, } from "./worktree-bootstrap.js";
27
29
  import { getCheckoutDiff, getCheckoutStatus, getCheckoutStatusLite, listBranchSuggestions, NotGitRepoError, MergeConflictError, MergeFromBaseConflictError, commitChanges, mergeToBase, mergeFromBase, pushCurrentBranch, createPullRequest, getPullRequestStatus, } from "../utils/checkout-git.js";
28
30
  import { getProjectIcon } from "../utils/project-icon.js";
29
31
  import { expandTilde } from "../utils/path.js";
@@ -37,8 +39,6 @@ const pendingAgentInitializations = new Map();
37
39
  let restartRequested = false;
38
40
  const DEFAULT_AGENT_PROVIDER = AGENT_PROVIDER_IDS[0];
39
41
  const RESTART_EXIT_DELAY_MS = 250;
40
- const PROJECT_PLACEMENT_CACHE_TTL_MS = 10000;
41
- const MAX_AGENTS_PER_PROJECT = 5;
42
42
  const CHECKOUT_DIFF_WATCH_DEBOUNCE_MS = 150;
43
43
  const CHECKOUT_DIFF_FALLBACK_REFRESH_MS = 5000;
44
44
  const TERMINAL_STREAM_WINDOW_BYTES = 256 * 1024;
@@ -90,17 +90,17 @@ function deriveRemoteProjectKey(remoteUrl) {
90
90
  }
91
91
  return `remote:${cleanedHost}/${cleanedPath}`;
92
92
  }
93
- function deriveProjectGroupingKey(cwd, remoteUrl) {
94
- const remoteKey = deriveRemoteProjectKey(remoteUrl);
93
+ function deriveProjectGroupingKey(options) {
94
+ const remoteKey = deriveRemoteProjectKey(options.remoteUrl);
95
95
  if (remoteKey) {
96
96
  return remoteKey;
97
97
  }
98
98
  const worktreeMarker = ".paseo/worktrees/";
99
- const idx = cwd.indexOf(worktreeMarker);
99
+ const idx = options.cwd.indexOf(worktreeMarker);
100
100
  if (idx !== -1) {
101
- return cwd.slice(0, idx).replace(/\/$/, "");
101
+ return options.cwd.slice(0, idx).replace(/\/$/, "");
102
102
  }
103
- return cwd;
103
+ return options.cwd;
104
104
  }
105
105
  function deriveProjectGroupingName(projectKey) {
106
106
  const githubRemotePrefix = "remote:github.com/";
@@ -110,6 +110,13 @@ function deriveProjectGroupingName(projectKey) {
110
110
  const segments = projectKey.split(/[\\/]/).filter(Boolean);
111
111
  return segments[segments.length - 1] || projectKey;
112
112
  }
113
+ class SessionRequestError extends Error {
114
+ constructor(code, message) {
115
+ super(message);
116
+ this.code = code;
117
+ this.name = "SessionRequestError";
118
+ }
119
+ }
113
120
  const PCM_SAMPLE_RATE = 16000;
114
121
  const PCM_CHANNELS = 1;
115
122
  const PCM_BITS_PER_SAMPLE = 16;
@@ -209,10 +216,12 @@ export class Session {
209
216
  this.agentTools = null;
210
217
  this.unsubscribeAgentEvents = null;
211
218
  this.agentUpdatesSubscription = null;
212
- this.projectPlacementCache = new Map();
213
219
  this.clientActivity = null;
214
220
  this.MOBILE_BACKGROUND_STREAM_GRACE_MS = 60000;
221
+ this.subscribedTerminalDirectories = new Set();
222
+ this.unsubscribeTerminalsChanged = null;
215
223
  this.terminalSubscriptions = new Map();
224
+ this.terminalExitSubscriptions = new Map();
216
225
  this.terminalStreams = new Map();
217
226
  this.terminalStreamByTerminalId = new Map();
218
227
  this.nextTerminalStreamId = 1;
@@ -232,6 +241,9 @@ export class Session {
232
241
  this.agentStorage = agentStorage;
233
242
  this.createAgentMcpTransport = createAgentMcpTransport;
234
243
  this.terminalManager = terminalManager;
244
+ if (this.terminalManager) {
245
+ this.unsubscribeTerminalsChanged = this.terminalManager.subscribeTerminalsChanged((event) => this.handleTerminalsChanged(event));
246
+ }
235
247
  this.voiceAgentMcpStdio = voice?.voiceAgentMcpStdio ?? null;
236
248
  const configuredModelsDir = dictation?.localModels?.modelsDir?.trim();
237
249
  this.localSpeechModelsDir =
@@ -619,26 +631,16 @@ export class Session {
619
631
  const checkout = await getCheckoutStatusLite(cwd, { paseoHome: this.paseoHome })
620
632
  .then((status) => this.toProjectCheckoutLite(cwd, status))
621
633
  .catch(() => this.buildFallbackProjectCheckout(cwd));
622
- const projectKey = deriveProjectGroupingKey(cwd, checkout.remoteUrl);
634
+ const projectKey = deriveProjectGroupingKey({
635
+ cwd,
636
+ remoteUrl: checkout.remoteUrl,
637
+ });
623
638
  return {
624
639
  projectKey,
625
640
  projectName: deriveProjectGroupingName(projectKey),
626
641
  checkout,
627
642
  };
628
643
  }
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
644
  async forwardAgentUpdate(agent) {
643
645
  try {
644
646
  const subscription = this.agentUpdatesSubscription;
@@ -648,7 +650,7 @@ export class Session {
648
650
  const payload = await this.buildAgentPayload(agent);
649
651
  const matches = this.matchesAgentFilter(payload, subscription.filter);
650
652
  if (matches) {
651
- const project = await this.getProjectPlacement(payload.cwd);
653
+ const project = await this.buildProjectPlacement(payload.cwd);
652
654
  this.emit({
653
655
  type: "agent_update",
654
656
  payload: { kind: "upsert", agent: payload, project },
@@ -680,10 +682,7 @@ export class Session {
680
682
  this.handleAudioPlayed(msg.id);
681
683
  break;
682
684
  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);
685
+ await this.handleFetchAgents(msg);
687
686
  break;
688
687
  case "fetch_agent_request":
689
688
  await this.handleFetchAgent(msg.agentId, msg.requestId);
@@ -868,6 +867,12 @@ export class Session {
868
867
  case "register_push_token":
869
868
  this.handleRegisterPushToken(msg.token);
870
869
  break;
870
+ case "subscribe_terminals_request":
871
+ this.handleSubscribeTerminalsRequest(msg);
872
+ break;
873
+ case "unsubscribe_terminals_request":
874
+ this.handleUnsubscribeTerminalsRequest(msg);
875
+ break;
871
876
  case "list_terminals_request":
872
877
  await this.handleListTerminalsRequest(msg);
873
878
  break;
@@ -1546,7 +1551,10 @@ export class Session {
1546
1551
  }
1547
1552
  const prompt = this.buildAgentPrompt(text, images);
1548
1553
  try {
1549
- this.agentManager.recordUserMessage(agentId, text, { messageId });
1554
+ this.agentManager.recordUserMessage(agentId, text, {
1555
+ messageId,
1556
+ emitState: false,
1557
+ });
1550
1558
  }
1551
1559
  catch (error) {
1552
1560
  this.sessionLogger.error({ err: error, agentId }, `Failed to record user message for agent ${agentId}`);
@@ -1606,7 +1614,22 @@ export class Session {
1606
1614
  });
1607
1615
  }
1608
1616
  if (worktreeConfig) {
1609
- void this.runAsyncWorktreeSetup(snapshot.id, worktreeConfig);
1617
+ void runAsyncWorktreeBootstrap({
1618
+ agentId: snapshot.id,
1619
+ worktree: worktreeConfig,
1620
+ terminalManager: this.terminalManager,
1621
+ appendTimelineItem: (item) => appendTimelineItemIfAgentKnown({
1622
+ agentManager: this.agentManager,
1623
+ agentId: snapshot.id,
1624
+ item,
1625
+ }),
1626
+ emitLiveTimelineItem: (item) => emitLiveTimelineItemIfAgentKnown({
1627
+ agentManager: this.agentManager,
1628
+ agentId: snapshot.id,
1629
+ item,
1630
+ }),
1631
+ logger: this.sessionLogger,
1632
+ });
1610
1633
  }
1611
1634
  this.sessionLogger.info({ agentId: snapshot.id, provider: snapshot.provider }, `Created agent ${snapshot.id} (${snapshot.provider})`);
1612
1635
  }
@@ -1776,12 +1799,11 @@ export class Session {
1776
1799
  throw new Error("A branch name is required when creating a worktree.");
1777
1800
  }
1778
1801
  this.sessionLogger.info({ worktreeSlug: normalized.worktreeSlug ?? targetBranch, branch: targetBranch }, `Creating worktree '${normalized.worktreeSlug ?? targetBranch}' for branch ${targetBranch}`);
1779
- const createdWorktree = await createWorktree({
1802
+ const createdWorktree = await createAgentWorktree({
1780
1803
  branchName: targetBranch,
1781
1804
  cwd,
1782
1805
  baseBranch: normalized.baseBranch,
1783
1806
  worktreeSlug: normalized.worktreeSlug ?? targetBranch,
1784
- runSetup: false,
1785
1807
  paseoHome: this.paseoHome,
1786
1808
  });
1787
1809
  cwd = createdWorktree.worktreePath;
@@ -1805,100 +1827,6 @@ export class Session {
1805
1827
  worktreeConfig,
1806
1828
  };
1807
1829
  }
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
1830
  async handleListProviderModelsRequest(msg) {
1903
1831
  const fetchedAt = new Date().toISOString();
1904
1832
  try {
@@ -3672,64 +3600,230 @@ export class Session {
3672
3600
  }
3673
3601
  return this.buildStoredAgentPayload(record);
3674
3602
  }
3675
- async handleFetchAgents(requestId, filter) {
3676
- try {
3677
- const agents = await this.listAgentPayloads(filter);
3678
- this.emit({
3679
- type: "fetch_agents_response",
3680
- payload: { requestId, agents },
3681
- });
3603
+ normalizeFetchAgentsSort(sort) {
3604
+ const fallback = [
3605
+ { key: "updated_at", direction: "desc" },
3606
+ ];
3607
+ if (!sort || sort.length === 0) {
3608
+ return fallback;
3609
+ }
3610
+ const deduped = [];
3611
+ const seen = new Set();
3612
+ for (const entry of sort) {
3613
+ if (seen.has(entry.key)) {
3614
+ continue;
3615
+ }
3616
+ seen.add(entry.key);
3617
+ deduped.push(entry);
3682
3618
  }
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
- });
3619
+ return deduped.length > 0 ? deduped : fallback;
3620
+ }
3621
+ getStatusPriority(agent) {
3622
+ const requiresAttention = agent.requiresAttention ?? false;
3623
+ const attentionReason = agent.attentionReason ?? null;
3624
+ if (requiresAttention && attentionReason === "permission") {
3625
+ return 0;
3626
+ }
3627
+ if (agent.status === "error" || attentionReason === "error") {
3628
+ return 1;
3629
+ }
3630
+ if (agent.status === "running") {
3631
+ return 2;
3689
3632
  }
3633
+ if (agent.status === "initializing") {
3634
+ return 3;
3635
+ }
3636
+ return 4;
3690
3637
  }
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);
3638
+ getFetchAgentsSortValue(entry, key) {
3639
+ switch (key) {
3640
+ case "status_priority":
3641
+ return this.getStatusPriority(entry.agent);
3642
+ case "created_at":
3643
+ return Date.parse(entry.agent.createdAt);
3644
+ case "updated_at":
3645
+ return Date.parse(entry.agent.updatedAt);
3646
+ case "title":
3647
+ return entry.agent.title?.toLocaleLowerCase() ?? "";
3648
+ }
3649
+ }
3650
+ compareSortValues(left, right) {
3651
+ if (left === right) {
3652
+ return 0;
3653
+ }
3654
+ if (left === null) {
3655
+ return -1;
3656
+ }
3657
+ if (right === null) {
3658
+ return 1;
3659
+ }
3660
+ if (typeof left === "number" && typeof right === "number") {
3661
+ return left < right ? -1 : 1;
3662
+ }
3663
+ return String(left).localeCompare(String(right));
3664
+ }
3665
+ compareFetchAgentsEntries(left, right, sort) {
3666
+ for (const spec of sort) {
3667
+ const leftValue = this.getFetchAgentsSortValue(left, spec.key);
3668
+ const rightValue = this.getFetchAgentsSortValue(right, spec.key);
3669
+ const base = this.compareSortValues(leftValue, rightValue);
3670
+ if (base === 0) {
3671
+ continue;
3712
3672
  }
3713
- if (group.agents.length >= MAX_AGENTS_PER_PROJECT) {
3673
+ return spec.direction === "asc" ? base : -base;
3674
+ }
3675
+ return left.agent.id.localeCompare(right.agent.id);
3676
+ }
3677
+ encodeFetchAgentsCursor(entry, sort) {
3678
+ const values = {};
3679
+ for (const spec of sort) {
3680
+ values[spec.key] = this.getFetchAgentsSortValue(entry, spec.key);
3681
+ }
3682
+ return Buffer.from(JSON.stringify({
3683
+ sort,
3684
+ values,
3685
+ id: entry.agent.id,
3686
+ }), "utf8").toString("base64url");
3687
+ }
3688
+ decodeFetchAgentsCursor(cursor, sort) {
3689
+ let parsed;
3690
+ try {
3691
+ parsed = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
3692
+ }
3693
+ catch {
3694
+ throw new SessionRequestError("invalid_cursor", "Invalid fetch_agents cursor");
3695
+ }
3696
+ if (!parsed || typeof parsed !== "object") {
3697
+ throw new SessionRequestError("invalid_cursor", "Invalid fetch_agents cursor");
3698
+ }
3699
+ const payload = parsed;
3700
+ if (!Array.isArray(payload.sort) || typeof payload.id !== "string") {
3701
+ throw new SessionRequestError("invalid_cursor", "Invalid fetch_agents cursor");
3702
+ }
3703
+ if (!payload.values || typeof payload.values !== "object") {
3704
+ throw new SessionRequestError("invalid_cursor", "Invalid fetch_agents cursor");
3705
+ }
3706
+ const cursorSort = [];
3707
+ for (const item of payload.sort) {
3708
+ if (!item ||
3709
+ typeof item !== "object" ||
3710
+ typeof item.key !== "string" ||
3711
+ typeof item.direction !== "string") {
3712
+ throw new SessionRequestError("invalid_cursor", "Invalid fetch_agents cursor");
3713
+ }
3714
+ const key = item.key;
3715
+ const direction = item.direction;
3716
+ if ((key !== "status_priority" &&
3717
+ key !== "created_at" &&
3718
+ key !== "updated_at" &&
3719
+ key !== "title") ||
3720
+ (direction !== "asc" && direction !== "desc")) {
3721
+ throw new SessionRequestError("invalid_cursor", "Invalid fetch_agents cursor");
3722
+ }
3723
+ cursorSort.push({ key, direction });
3724
+ }
3725
+ if (cursorSort.length !== sort.length ||
3726
+ cursorSort.some((entry, index) => entry.key !== sort[index]?.key ||
3727
+ entry.direction !== sort[index]?.direction)) {
3728
+ throw new SessionRequestError("invalid_cursor", "fetch_agents cursor does not match current sort");
3729
+ }
3730
+ return {
3731
+ sort: cursorSort,
3732
+ values: payload.values,
3733
+ id: payload.id,
3734
+ };
3735
+ }
3736
+ compareEntryWithCursor(entry, cursor, sort) {
3737
+ for (const spec of sort) {
3738
+ const leftValue = this.getFetchAgentsSortValue(entry, spec.key);
3739
+ const rightValue = cursor.values[spec.key] !== undefined ? cursor.values[spec.key] ?? null : null;
3740
+ const base = this.compareSortValues(leftValue, rightValue);
3741
+ if (base === 0) {
3714
3742
  continue;
3715
3743
  }
3716
- group.agents.push({ agent, checkout: project.checkout });
3744
+ return spec.direction === "asc" ? base : -base;
3745
+ }
3746
+ return entry.agent.id.localeCompare(cursor.id);
3747
+ }
3748
+ async listFetchAgentsEntries(request) {
3749
+ const filter = request.filter;
3750
+ const sort = this.normalizeFetchAgentsSort(request.sort);
3751
+ const includeArchived = filter?.includeArchived ?? false;
3752
+ let agents = await this.listAgentPayloads({
3753
+ labels: filter?.labels,
3754
+ });
3755
+ if (!includeArchived) {
3756
+ agents = agents.filter((agent) => !agent.archivedAt);
3717
3757
  }
3718
- return Array.from(grouped.values());
3758
+ if (filter?.statuses && filter.statuses.length > 0) {
3759
+ const statuses = new Set(filter.statuses);
3760
+ agents = agents.filter((agent) => statuses.has(agent.status));
3761
+ }
3762
+ if (typeof filter?.requiresAttention === "boolean") {
3763
+ agents = agents.filter((agent) => (agent.requiresAttention ?? false) === filter.requiresAttention);
3764
+ }
3765
+ const placementByCwd = new Map();
3766
+ const getPlacement = (cwd) => {
3767
+ const existing = placementByCwd.get(cwd);
3768
+ if (existing) {
3769
+ return existing;
3770
+ }
3771
+ const placementPromise = this.buildProjectPlacement(cwd);
3772
+ placementByCwd.set(cwd, placementPromise);
3773
+ return placementPromise;
3774
+ };
3775
+ let entries = await Promise.all(agents.map(async (agent) => ({
3776
+ agent,
3777
+ project: await getPlacement(agent.cwd),
3778
+ })));
3779
+ if (filter?.projectKeys && filter.projectKeys.length > 0) {
3780
+ const projectKeys = new Set(filter.projectKeys.filter((item) => item.trim().length > 0));
3781
+ entries = entries.filter((entry) => projectKeys.has(entry.project.projectKey));
3782
+ }
3783
+ entries.sort((left, right) => this.compareFetchAgentsEntries(left, right, sort));
3784
+ const cursorToken = request.page?.cursor;
3785
+ if (cursorToken) {
3786
+ const cursor = this.decodeFetchAgentsCursor(cursorToken, sort);
3787
+ entries = entries.filter((entry) => this.compareEntryWithCursor(entry, cursor, sort) > 0);
3788
+ }
3789
+ const limit = request.page?.limit ?? entries.length;
3790
+ const pagedEntries = entries.slice(0, limit);
3791
+ const hasMore = entries.length > limit;
3792
+ const nextCursor = hasMore && pagedEntries.length > 0
3793
+ ? this.encodeFetchAgentsCursor(pagedEntries[pagedEntries.length - 1], sort)
3794
+ : null;
3795
+ return {
3796
+ entries: pagedEntries,
3797
+ pageInfo: {
3798
+ nextCursor,
3799
+ prevCursor: request.page?.cursor ?? null,
3800
+ hasMore,
3801
+ },
3802
+ };
3719
3803
  }
3720
- async handleFetchAgentsGroupedByProject(requestId, filter) {
3804
+ async handleFetchAgents(request) {
3721
3805
  try {
3722
- const groups = await this.listAgentsGroupedByProjectPayload(filter);
3806
+ const payload = await this.listFetchAgentsEntries(request);
3723
3807
  this.emit({
3724
- type: "fetch_agents_grouped_by_project_response",
3725
- payload: { requestId, groups },
3808
+ type: "fetch_agents_response",
3809
+ payload: {
3810
+ requestId: request.requestId,
3811
+ ...payload,
3812
+ },
3726
3813
  });
3727
3814
  }
3728
3815
  catch (error) {
3729
- this.sessionLogger.error({ err: error }, "Failed to handle fetch_agents_grouped_by_project_request");
3816
+ const code = error instanceof SessionRequestError ? error.code : "fetch_agents_failed";
3817
+ const message = error instanceof Error ? error.message : "Failed to fetch agents";
3818
+ this.sessionLogger.error({ err: error }, "Failed to handle fetch_agents_request");
3730
3819
  this.emit({
3731
- type: "fetch_agents_grouped_by_project_response",
3732
- payload: { requestId, groups: [] },
3820
+ type: "rpc_error",
3821
+ payload: {
3822
+ requestId: request.requestId,
3823
+ requestType: request.type,
3824
+ error: message,
3825
+ code,
3826
+ },
3733
3827
  });
3734
3828
  }
3735
3829
  }
@@ -3845,7 +3939,10 @@ export class Session {
3845
3939
  await this.ensureAgentLoaded(agentId);
3846
3940
  await this.interruptAgentIfRunning(agentId);
3847
3941
  try {
3848
- this.agentManager.recordUserMessage(agentId, msg.text, { messageId: msg.messageId });
3942
+ this.agentManager.recordUserMessage(agentId, msg.text, {
3943
+ messageId: msg.messageId,
3944
+ emitState: false,
3945
+ });
3849
3946
  }
3850
3947
  catch (error) {
3851
3948
  this.sessionLogger.error({ err: error, agentId }, "Failed to record user message for send_agent_message_request");
@@ -4502,10 +4599,19 @@ export class Session {
4502
4599
  await this.disableVoiceModeForActiveAgent(true);
4503
4600
  this.isVoiceMode = false;
4504
4601
  // Unsubscribe from all terminals
4602
+ if (this.unsubscribeTerminalsChanged) {
4603
+ this.unsubscribeTerminalsChanged();
4604
+ this.unsubscribeTerminalsChanged = null;
4605
+ }
4606
+ this.subscribedTerminalDirectories.clear();
4505
4607
  for (const unsubscribe of this.terminalSubscriptions.values()) {
4506
4608
  unsubscribe();
4507
4609
  }
4508
4610
  this.terminalSubscriptions.clear();
4611
+ for (const unsubscribeExit of this.terminalExitSubscriptions.values()) {
4612
+ unsubscribeExit();
4613
+ }
4614
+ this.terminalExitSubscriptions.clear();
4509
4615
  this.detachAllTerminalStreams({ emitExit: false });
4510
4616
  for (const target of this.checkoutDiffTargets.values()) {
4511
4617
  this.closeCheckoutDiffWatchTarget(target);
@@ -4516,6 +4622,96 @@ export class Session {
4516
4622
  // ============================================================================
4517
4623
  // Terminal Handlers
4518
4624
  // ============================================================================
4625
+ ensureTerminalExitSubscription(terminal) {
4626
+ if (this.terminalExitSubscriptions.has(terminal.id)) {
4627
+ return;
4628
+ }
4629
+ const unsubscribeExit = terminal.onExit(() => {
4630
+ this.handleTerminalExited(terminal.id);
4631
+ });
4632
+ this.terminalExitSubscriptions.set(terminal.id, unsubscribeExit);
4633
+ }
4634
+ handleTerminalExited(terminalId) {
4635
+ const unsubscribeExit = this.terminalExitSubscriptions.get(terminalId);
4636
+ if (unsubscribeExit) {
4637
+ unsubscribeExit();
4638
+ this.terminalExitSubscriptions.delete(terminalId);
4639
+ }
4640
+ const unsubscribe = this.terminalSubscriptions.get(terminalId);
4641
+ if (unsubscribe) {
4642
+ try {
4643
+ unsubscribe();
4644
+ }
4645
+ catch (error) {
4646
+ this.sessionLogger.warn({ err: error, terminalId }, "Failed to unsubscribe terminal after process exit");
4647
+ }
4648
+ this.terminalSubscriptions.delete(terminalId);
4649
+ }
4650
+ const streamId = this.terminalStreamByTerminalId.get(terminalId);
4651
+ if (typeof streamId === "number") {
4652
+ this.detachTerminalStream(streamId, { emitExit: true });
4653
+ }
4654
+ }
4655
+ emitTerminalsChangedSnapshot(input) {
4656
+ this.emit({
4657
+ type: "terminals_changed",
4658
+ payload: {
4659
+ cwd: input.cwd,
4660
+ terminals: input.terminals,
4661
+ },
4662
+ });
4663
+ }
4664
+ handleTerminalsChanged(event) {
4665
+ if (!this.subscribedTerminalDirectories.has(event.cwd)) {
4666
+ return;
4667
+ }
4668
+ this.emitTerminalsChangedSnapshot({
4669
+ cwd: event.cwd,
4670
+ terminals: event.terminals.map((terminal) => ({
4671
+ id: terminal.id,
4672
+ name: terminal.name,
4673
+ })),
4674
+ });
4675
+ }
4676
+ handleSubscribeTerminalsRequest(msg) {
4677
+ this.subscribedTerminalDirectories.add(msg.cwd);
4678
+ void this.emitInitialTerminalsChangedSnapshot(msg.cwd);
4679
+ }
4680
+ handleUnsubscribeTerminalsRequest(msg) {
4681
+ this.subscribedTerminalDirectories.delete(msg.cwd);
4682
+ }
4683
+ async emitInitialTerminalsChangedSnapshot(cwd) {
4684
+ if (!this.terminalManager || !this.subscribedTerminalDirectories.has(cwd)) {
4685
+ return;
4686
+ }
4687
+ const hadDirectoryBeforeSubscribe = this.terminalManager
4688
+ .listDirectories()
4689
+ .includes(cwd);
4690
+ try {
4691
+ const terminals = await this.terminalManager.getTerminals(cwd);
4692
+ for (const terminal of terminals) {
4693
+ this.ensureTerminalExitSubscription(terminal);
4694
+ }
4695
+ // New directories auto-create Terminal 1, which already emits through
4696
+ // terminal-manager change listeners.
4697
+ if (!hadDirectoryBeforeSubscribe) {
4698
+ return;
4699
+ }
4700
+ if (!this.subscribedTerminalDirectories.has(cwd)) {
4701
+ return;
4702
+ }
4703
+ this.emitTerminalsChangedSnapshot({
4704
+ cwd,
4705
+ terminals: terminals.map((terminal) => ({
4706
+ id: terminal.id,
4707
+ name: terminal.name,
4708
+ })),
4709
+ });
4710
+ }
4711
+ catch (error) {
4712
+ this.sessionLogger.warn({ err: error, cwd }, "Failed to emit initial terminal snapshot");
4713
+ }
4714
+ }
4519
4715
  async handleListTerminalsRequest(msg) {
4520
4716
  if (!this.terminalManager) {
4521
4717
  this.emit({
@@ -4530,6 +4726,9 @@ export class Session {
4530
4726
  }
4531
4727
  try {
4532
4728
  const terminals = await this.terminalManager.getTerminals(msg.cwd);
4729
+ for (const terminal of terminals) {
4730
+ this.ensureTerminalExitSubscription(terminal);
4731
+ }
4533
4732
  this.emit({
4534
4733
  type: "list_terminals_response",
4535
4734
  payload: {
@@ -4568,6 +4767,7 @@ export class Session {
4568
4767
  cwd: msg.cwd,
4569
4768
  name: msg.name,
4570
4769
  });
4770
+ this.ensureTerminalExitSubscription(session);
4571
4771
  this.emit({
4572
4772
  type: "create_terminal_response",
4573
4773
  payload: {
@@ -4615,6 +4815,7 @@ export class Session {
4615
4815
  });
4616
4816
  return;
4617
4817
  }
4818
+ this.ensureTerminalExitSubscription(session);
4618
4819
  // Unsubscribe from previous subscription if any
4619
4820
  const existing = this.terminalSubscriptions.get(msg.terminalId);
4620
4821
  if (existing) {
@@ -4660,6 +4861,7 @@ export class Session {
4660
4861
  this.sessionLogger.warn({ terminalId: msg.terminalId }, "Terminal not found for input");
4661
4862
  return;
4662
4863
  }
4864
+ this.ensureTerminalExitSubscription(session);
4663
4865
  session.send(msg.message);
4664
4866
  }
4665
4867
  async handleKillTerminalRequest(msg) {