@getpaseo/server 0.1.93 → 0.1.94

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 (32) hide show
  1. package/dist/server/server/agent/agent-manager.d.ts +14 -0
  2. package/dist/server/server/agent/agent-manager.js +36 -0
  3. package/dist/server/server/agent/agent-sdk-types.d.ts +1 -0
  4. package/dist/server/server/agent/providers/claude/agent.d.ts +1 -0
  5. package/dist/server/server/agent/providers/claude/agent.js +58 -3
  6. package/dist/server/server/agent/providers/claude/task-notification-tool-call.d.ts +2 -2
  7. package/dist/server/server/agent/providers/mock-load-test-agent.js +55 -28
  8. package/dist/server/server/agent/providers/pi/agent.js +3 -1
  9. package/dist/server/server/agent/providers/pi/session-descriptor.d.ts +5 -0
  10. package/dist/server/server/agent/providers/pi/session-descriptor.js +74 -12
  11. package/dist/server/server/agent/runtime-mcp-config.d.ts +6 -0
  12. package/dist/server/server/agent/runtime-mcp-config.js +3 -0
  13. package/dist/server/server/auth.d.ts +13 -0
  14. package/dist/server/server/auth.js +35 -0
  15. package/dist/server/server/bootstrap.d.ts +2 -0
  16. package/dist/server/server/bootstrap.js +28 -1
  17. package/dist/server/server/config.js +3 -1
  18. package/dist/server/server/daemon-config-store.js +3 -0
  19. package/dist/server/server/loop-service.d.ts +6 -6
  20. package/dist/server/server/persisted-config.d.ts +61 -0
  21. package/dist/server/server/persisted-config.js +2 -0
  22. package/dist/server/server/session.d.ts +1 -0
  23. package/dist/server/server/session.js +50 -3
  24. package/dist/server/server/websocket-server.js +2 -0
  25. package/dist/server/server/workspace-git-service.d.ts +4 -1
  26. package/dist/server/server/workspace-git-service.js +40 -22
  27. package/dist/server/services/github-service.d.ts +57 -0
  28. package/dist/server/services/github-service.js +327 -3
  29. package/dist/server/terminal/terminal-session-controller.d.ts +6 -0
  30. package/dist/server/terminal/terminal-session-controller.js +36 -2
  31. package/dist/src/server/persisted-config.js +2 -0
  32. package/package.json +5 -5
@@ -4,6 +4,12 @@ import { resolveGitHubRemote } from "../utils/github-remote.js";
4
4
  import { runGitCommand } from "../utils/run-git-command.js";
5
5
  import { execCommand } from "../utils/spawn.js";
6
6
  const DEFAULT_GITHUB_CACHE_TTL_MS = 30000;
7
+ const CHECK_ANNOTATION_PAGE_MAX = 20;
8
+ const CHECK_LOG_TAIL_MAX_LINES = 200;
9
+ const CHECK_LOG_TAIL_MAX_BYTES = 16 * 1024;
10
+ const CHECK_LOG_TAIL_CACHE_MAX_ENTRIES = 128;
11
+ const ACTIONS_JOB_PAGE_MAX = 100;
12
+ const FAILED_CHECK_JOB_LIMIT = 5;
7
13
  export const GITHUB_POLL_FAST_INTERVAL_MS = 20000;
8
14
  export const GITHUB_POLL_SLOW_INTERVAL_MS = 120000;
9
15
  export const GITHUB_POLL_ERROR_BACKOFF_CAP_MS = 300000;
@@ -35,6 +41,7 @@ const GitHubPullRequestSummarySchema = z.object({
35
41
  });
36
42
  const PullRequestCheckRunNodeSchema = z.object({
37
43
  __typename: z.literal("CheckRun"),
44
+ databaseId: z.number().nullable().optional(),
38
45
  name: z.string(),
39
46
  workflowName: z.string().nullable().optional(),
40
47
  conclusion: z.string().nullable().optional(),
@@ -69,6 +76,54 @@ const PullRequestStatusCheckRollupArraySchema = z.array(z.unknown());
69
76
  const LegacyPullRequestStatusCheckRollupSchema = z.object({
70
77
  contexts: z.array(z.unknown()),
71
78
  });
79
+ const GitHubCheckRunDetailsSchema = z.object({
80
+ id: z.number(),
81
+ name: z.string().catch(""),
82
+ status: z.string().nullable().optional(),
83
+ conclusion: z.string().nullable().optional(),
84
+ html_url: z.string().nullable().optional(),
85
+ details_url: z.string().nullable().optional(),
86
+ output: z
87
+ .object({
88
+ title: z.string().nullable().optional(),
89
+ summary: z.string().nullable().optional(),
90
+ text: z.string().nullable().optional(),
91
+ })
92
+ .nullable()
93
+ .optional(),
94
+ check_suite: z
95
+ .object({
96
+ workflow_run: z
97
+ .object({
98
+ id: z.number().nullable().optional(),
99
+ })
100
+ .nullable()
101
+ .optional(),
102
+ })
103
+ .nullable()
104
+ .optional(),
105
+ });
106
+ const GitHubCheckAnnotationSchema = z.object({
107
+ path: z.string().optional(),
108
+ start_line: z.number().optional(),
109
+ end_line: z.number().optional(),
110
+ annotation_level: z.string().optional(),
111
+ message: z.string().optional(),
112
+ title: z.string().optional(),
113
+ raw_details: z.string().optional(),
114
+ });
115
+ const GitHubCheckAnnotationsSchema = z.array(GitHubCheckAnnotationSchema).catch([]);
116
+ const GitHubActionsJobSchema = z.object({
117
+ id: z.number(),
118
+ name: z.string().catch(""),
119
+ status: z.string().nullable().optional(),
120
+ conclusion: z.string().nullable().optional(),
121
+ html_url: z.string().nullable().optional(),
122
+ completed_at: z.string().nullable().optional(),
123
+ });
124
+ const GitHubActionsJobsSchema = z.object({
125
+ jobs: z.array(GitHubActionsJobSchema).catch([]),
126
+ });
72
127
  const PullRequestReviewDecisionSchema = z
73
128
  .enum(["APPROVED", "CHANGES_REQUESTED", "REVIEW_REQUIRED"])
74
129
  .nullable()
@@ -142,6 +197,7 @@ const TimelineAuthorSchema = z
142
197
  .object({
143
198
  login: z.string().optional(),
144
199
  url: z.string().nullable().optional(),
200
+ avatarUrl: z.string().nullable().optional(),
145
201
  })
146
202
  .nullable()
147
203
  .optional();
@@ -160,6 +216,27 @@ const PullRequestTimelineCommentNodeSchema = z.object({
160
216
  createdAt: z.string().nullable().catch(null),
161
217
  author: TimelineAuthorSchema,
162
218
  });
219
+ const PullRequestReviewThreadCommentNodeSchema = PullRequestTimelineCommentNodeSchema.extend({
220
+ pullRequestReview: z
221
+ .object({ id: z.string().catch("") })
222
+ .nullable()
223
+ .optional()
224
+ .catch(null),
225
+ });
226
+ const PullRequestReviewThreadNodeSchema = z.object({
227
+ id: z.string().catch(""),
228
+ path: z.string().catch(""),
229
+ line: z.number().nullable().optional().catch(null),
230
+ startLine: z.number().nullable().optional().catch(null),
231
+ isResolved: z.boolean().catch(false),
232
+ isOutdated: z.boolean().catch(false),
233
+ comments: z
234
+ .object({
235
+ nodes: z.array(PullRequestReviewThreadCommentNodeSchema).catch([]),
236
+ pageInfo: z.object({ hasNextPage: z.boolean().catch(false) }).catch({ hasNextPage: false }),
237
+ })
238
+ .catch({ nodes: [], pageInfo: { hasNextPage: false } }),
239
+ });
163
240
  const PullRequestTimelinePageInfoSchema = z.object({
164
241
  hasNextPage: z.boolean().catch(false),
165
242
  });
@@ -183,6 +260,12 @@ const PullRequestTimelineGraphqlSchema = z.object({
183
260
  pageInfo: PullRequestTimelinePageInfoSchema.catch({ hasNextPage: false }),
184
261
  })
185
262
  .catch({ nodes: [], pageInfo: { hasNextPage: false } }),
263
+ reviewThreads: z
264
+ .object({
265
+ nodes: z.array(PullRequestReviewThreadNodeSchema).catch([]),
266
+ pageInfo: PullRequestTimelinePageInfoSchema.catch({ hasNextPage: false }),
267
+ })
268
+ .catch({ nodes: [], pageInfo: { hasNextPage: false } }),
186
269
  })
187
270
  .nullable()
188
271
  .optional(),
@@ -301,6 +384,7 @@ query PullRequestTimeline($owner: String!, $name: String!, $number: Int!) {
301
384
  author {
302
385
  login
303
386
  url
387
+ avatarUrl
304
388
  }
305
389
  }
306
390
  pageInfo {
@@ -316,6 +400,39 @@ query PullRequestTimeline($owner: String!, $name: String!, $number: Int!) {
316
400
  author {
317
401
  login
318
402
  url
403
+ avatarUrl
404
+ }
405
+ }
406
+ pageInfo {
407
+ hasNextPage
408
+ }
409
+ }
410
+ reviewThreads(first: 100) {
411
+ nodes {
412
+ id
413
+ path
414
+ line
415
+ startLine
416
+ isResolved
417
+ isOutdated
418
+ comments(first: 100) {
419
+ nodes {
420
+ id
421
+ body
422
+ url
423
+ createdAt
424
+ author {
425
+ login
426
+ url
427
+ avatarUrl
428
+ }
429
+ pullRequestReview {
430
+ id
431
+ }
432
+ }
433
+ pageInfo {
434
+ hasNextPage
435
+ }
319
436
  }
320
437
  }
321
438
  pageInfo {
@@ -362,6 +479,7 @@ export function createGitHubService(options = {}) {
362
479
  const cache = new Map();
363
480
  const inFlight = new Map();
364
481
  const pollTargets = new Map();
482
+ const checkLogTailCache = new Map();
365
483
  let api;
366
484
  async function cached(params) {
367
485
  if (params.readOptions?.force && !params.readOptions.reason) {
@@ -651,6 +769,79 @@ export function createGitHubService(options = {}) {
651
769
  },
652
770
  });
653
771
  },
772
+ getGitHubCheckDetails(input) {
773
+ return cached({
774
+ cwd: input.cwd,
775
+ method: "getGitHubCheckDetails",
776
+ args: {
777
+ repoOwner: input.repoOwner,
778
+ repoName: input.repoName,
779
+ checkRunId: input.checkRunId,
780
+ workflowRunId: input.workflowRunId,
781
+ },
782
+ readOptions: input,
783
+ load: async () => {
784
+ const repoPath = `repos/${input.repoOwner}/${input.repoName}`;
785
+ const checkRun = parseGitHubCheckRunDetails(await run(["api", `${repoPath}/check-runs/${input.checkRunId}`], { cwd: input.cwd }));
786
+ const annotations = parseGitHubCheckAnnotations(await run([
787
+ "api",
788
+ `${repoPath}/check-runs/${input.checkRunId}/annotations`,
789
+ "-f",
790
+ `per_page=${CHECK_ANNOTATION_PAGE_MAX}`,
791
+ ], {
792
+ cwd: input.cwd,
793
+ }));
794
+ const workflowRunId = input.workflowRunId ?? checkRun.workflowRunId ?? null;
795
+ const failedJobs = [];
796
+ let truncated = annotations.length >= CHECK_ANNOTATION_PAGE_MAX;
797
+ if (typeof workflowRunId === "number") {
798
+ const jobs = parseGitHubActionsJobs(await run([
799
+ "api",
800
+ `${repoPath}/actions/runs/${workflowRunId}/jobs`,
801
+ "-f",
802
+ `per_page=${ACTIONS_JOB_PAGE_MAX}`,
803
+ ], {
804
+ cwd: input.cwd,
805
+ }));
806
+ const failed = jobs.filter(isFailedActionsJob);
807
+ truncated || (truncated = jobs.length >= ACTIONS_JOB_PAGE_MAX);
808
+ truncated || (truncated = failed.length > FAILED_CHECK_JOB_LIMIT);
809
+ for (const job of failed.slice(0, FAILED_CHECK_JOB_LIMIT)) {
810
+ const log = await getCachedCheckLogTail({
811
+ cwd: input.cwd,
812
+ repoPath,
813
+ job,
814
+ run,
815
+ cache: checkLogTailCache,
816
+ });
817
+ truncated || (truncated = log.logTruncated);
818
+ failedJobs.push({
819
+ jobId: job.jobId,
820
+ name: job.name,
821
+ status: job.status,
822
+ conclusion: job.conclusion,
823
+ url: job.url,
824
+ logTail: log.logTail,
825
+ logTruncated: log.logTruncated,
826
+ });
827
+ }
828
+ }
829
+ return {
830
+ checkRunId: checkRun.checkRunId,
831
+ workflowRunId,
832
+ name: checkRun.name,
833
+ status: checkRun.status,
834
+ conclusion: checkRun.conclusion,
835
+ url: checkRun.url,
836
+ detailsUrl: checkRun.detailsUrl,
837
+ output: checkRun.output,
838
+ annotations,
839
+ failedJobs,
840
+ truncated,
841
+ };
842
+ },
843
+ });
844
+ },
654
845
  async searchIssuesAndPrs(input) {
655
846
  if (input.force && !input.reason) {
656
847
  throw new Error("GitHubService forced read requires a reason");
@@ -1356,10 +1547,17 @@ function parseIssueSummaries(stdout) {
1356
1547
  function parsePullRequestTimeline(stdout, identity) {
1357
1548
  const parsed = PullRequestTimelineGraphqlSchema.parse(JSON.parse(stdout || "{}"));
1358
1549
  const pullRequest = parsed.data?.repository?.pullRequest;
1550
+ const reviewThreadItems = pullRequest
1551
+ ? pullRequest.reviewThreads.nodes.flatMap(toPullRequestTimelineReviewThreadItems)
1552
+ : [];
1553
+ const reviewThreadItemIds = new Set(reviewThreadItems.map((item) => item.id).filter((id) => id.length > 0));
1359
1554
  const items = pullRequest
1360
1555
  ? [
1361
1556
  ...pullRequest.reviews.nodes.flatMap(toPullRequestTimelineReviewItem),
1362
- ...pullRequest.comments.nodes.map(toPullRequestTimelineCommentItem),
1557
+ ...pullRequest.comments.nodes
1558
+ .filter((comment) => !reviewThreadItemIds.has(comment.id))
1559
+ .map(toPullRequestTimelineCommentItem),
1560
+ ...reviewThreadItems,
1363
1561
  ].sort(compareTimelineItems)
1364
1562
  : [];
1365
1563
  return {
@@ -1367,8 +1565,11 @@ function parsePullRequestTimeline(stdout, identity) {
1367
1565
  repoOwner: identity.repoOwner,
1368
1566
  repoName: identity.repoName,
1369
1567
  items,
1370
- // S3 deliberately caps timeline fetches at the first 100 reviews and first 100 comments.
1371
- truncated: Boolean(pullRequest?.reviews.pageInfo.hasNextPage || pullRequest?.comments.pageInfo.hasNextPage),
1568
+ // S3 deliberately caps timeline fetches at the first 100 reviews, comments, and review threads.
1569
+ truncated: Boolean(pullRequest?.reviews.pageInfo.hasNextPage ||
1570
+ pullRequest?.comments.pageInfo.hasNextPage ||
1571
+ pullRequest?.reviewThreads.pageInfo.hasNextPage ||
1572
+ pullRequest?.reviewThreads.nodes.some((thread) => thread.comments.pageInfo.hasNextPage)),
1372
1573
  error: pullRequest ? null : { kind: "not_found", message: "Pull request not found" },
1373
1574
  };
1374
1575
  }
@@ -1383,6 +1584,7 @@ function toPullRequestTimelineReviewItem(review) {
1383
1584
  id: review.id,
1384
1585
  author: review.author?.login ?? "unknown",
1385
1586
  authorUrl: review.author?.url ?? null,
1587
+ avatarUrl: review.author?.avatarUrl ?? null,
1386
1588
  body: review.body ?? "",
1387
1589
  createdAt: parseOptionalTime(review.submittedAt ?? null),
1388
1590
  url: review.url,
@@ -1396,11 +1598,129 @@ function toPullRequestTimelineCommentItem(comment) {
1396
1598
  id: comment.id,
1397
1599
  author: comment.author?.login ?? "unknown",
1398
1600
  authorUrl: comment.author?.url ?? null,
1601
+ avatarUrl: comment.author?.avatarUrl ?? null,
1399
1602
  body: comment.body ?? "",
1400
1603
  createdAt: parseOptionalTime(comment.createdAt ?? null),
1401
1604
  url: comment.url,
1402
1605
  };
1403
1606
  }
1607
+ function toPullRequestTimelineReviewThreadItems(thread) {
1608
+ return thread.comments.nodes.map((comment) => ({
1609
+ ...toPullRequestTimelineCommentItem(comment),
1610
+ ...(comment.pullRequestReview?.id ? { reviewId: comment.pullRequestReview.id } : {}),
1611
+ location: {
1612
+ path: thread.path,
1613
+ ...(thread.line !== null && thread.line !== undefined ? { line: thread.line } : {}),
1614
+ ...(thread.startLine !== null && thread.startLine !== undefined
1615
+ ? { startLine: thread.startLine }
1616
+ : {}),
1617
+ ...(thread.id ? { threadId: thread.id } : {}),
1618
+ isResolved: thread.isResolved,
1619
+ isOutdated: thread.isOutdated,
1620
+ },
1621
+ }));
1622
+ }
1623
+ function parseGitHubCheckRunDetails(stdout) {
1624
+ const parsed = GitHubCheckRunDetailsSchema.parse(JSON.parse(stdout || "{}"));
1625
+ return {
1626
+ checkRunId: parsed.id,
1627
+ workflowRunId: parsed.check_suite?.workflow_run?.id ?? null,
1628
+ name: parsed.name,
1629
+ status: parsed.status,
1630
+ conclusion: parsed.conclusion,
1631
+ url: parsed.html_url,
1632
+ detailsUrl: parsed.details_url,
1633
+ output: parsed.output,
1634
+ annotations: [],
1635
+ failedJobs: [],
1636
+ truncated: false,
1637
+ };
1638
+ }
1639
+ function parseGitHubCheckAnnotations(stdout) {
1640
+ return GitHubCheckAnnotationsSchema.parse(JSON.parse(stdout || "[]")).map((annotation) => {
1641
+ const result = {};
1642
+ if (annotation.path)
1643
+ result.path = annotation.path;
1644
+ if (annotation.start_line !== undefined)
1645
+ result.startLine = annotation.start_line;
1646
+ if (annotation.end_line !== undefined)
1647
+ result.endLine = annotation.end_line;
1648
+ if (annotation.annotation_level)
1649
+ result.annotationLevel = annotation.annotation_level;
1650
+ if (annotation.message)
1651
+ result.message = annotation.message;
1652
+ if (annotation.title)
1653
+ result.title = annotation.title;
1654
+ if (annotation.raw_details)
1655
+ result.rawDetails = annotation.raw_details;
1656
+ return result;
1657
+ });
1658
+ }
1659
+ function parseGitHubActionsJobs(stdout) {
1660
+ return GitHubActionsJobsSchema.parse(JSON.parse(stdout || "{}")).jobs.map((job) => {
1661
+ const result = {
1662
+ jobId: job.id,
1663
+ name: job.name,
1664
+ status: job.status,
1665
+ conclusion: job.conclusion,
1666
+ url: job.html_url,
1667
+ };
1668
+ if (job.completed_at)
1669
+ result.completedAt = job.completed_at;
1670
+ return result;
1671
+ });
1672
+ }
1673
+ function isFailedActionsJob(job) {
1674
+ return (job.conclusion === "failure" ||
1675
+ job.conclusion === "cancelled" ||
1676
+ job.conclusion === "timed_out" ||
1677
+ job.conclusion === "action_required");
1678
+ }
1679
+ async function getCachedCheckLogTail(input) {
1680
+ const key = `${input.job.jobId}:${input.job.completedAt ?? ""}`;
1681
+ const cached = input.cache.get(key);
1682
+ if (cached) {
1683
+ input.cache.delete(key);
1684
+ input.cache.set(key, cached);
1685
+ return cached;
1686
+ }
1687
+ const capped = capCheckLogTail(await input.run(["api", `${input.repoPath}/actions/jobs/${input.job.jobId}/logs`], {
1688
+ cwd: input.cwd,
1689
+ }));
1690
+ input.cache.set(key, capped);
1691
+ while (input.cache.size > CHECK_LOG_TAIL_CACHE_MAX_ENTRIES) {
1692
+ const oldestKey = input.cache.keys().next().value;
1693
+ if (!oldestKey) {
1694
+ break;
1695
+ }
1696
+ input.cache.delete(oldestKey);
1697
+ }
1698
+ return capped;
1699
+ }
1700
+ function capCheckLogTail(log) {
1701
+ const lines = log.split("\n");
1702
+ let truncated = lines.length > CHECK_LOG_TAIL_MAX_LINES;
1703
+ let tail = lines.slice(-CHECK_LOG_TAIL_MAX_LINES).join("\n");
1704
+ if (Buffer.byteLength(tail, "utf8") > CHECK_LOG_TAIL_MAX_BYTES) {
1705
+ truncated = true;
1706
+ tail = utf8SuffixWithinBytes(tail, CHECK_LOG_TAIL_MAX_BYTES);
1707
+ }
1708
+ return { logTail: tail, logTruncated: truncated };
1709
+ }
1710
+ function utf8SuffixWithinBytes(value, maxBytes) {
1711
+ let lowerBound = 0;
1712
+ let upperBound = value.length;
1713
+ while (lowerBound < upperBound) {
1714
+ const midpoint = Math.floor((lowerBound + upperBound) / 2);
1715
+ if (Buffer.byteLength(value.slice(midpoint), "utf8") > maxBytes) {
1716
+ lowerBound = midpoint + 1;
1717
+ }
1718
+ else {
1719
+ upperBound = midpoint;
1720
+ }
1721
+ }
1722
+ return value.slice(lowerBound);
1723
+ }
1404
1724
  function mapTimelineReviewState(state, body) {
1405
1725
  switch (state) {
1406
1726
  case "APPROVED":
@@ -1541,6 +1861,10 @@ function buildPullRequestCheck(context) {
1541
1861
  ...(typeof context.workflowName === "string" && context.workflowName.trim().length > 0
1542
1862
  ? { workflow: context.workflowName }
1543
1863
  : {}),
1864
+ ...(typeof context.databaseId === "number" ? { checkRunId: context.databaseId } : {}),
1865
+ ...(typeof context.checkSuite?.workflowRun?.databaseId === "number"
1866
+ ? { workflowRunId: context.checkSuite.workflowRun.databaseId }
1867
+ : {}),
1544
1868
  ...formatCheckRunDuration(context),
1545
1869
  recency: getCheckRunRecency(context),
1546
1870
  };
@@ -9,6 +9,7 @@ export interface TerminalSessionControllerOptions {
9
9
  hasBinaryChannel: () => boolean;
10
10
  isPathWithinRoot: (rootPath: string, candidatePath: string) => boolean;
11
11
  sessionLogger: pino.Logger;
12
+ listTerminalWorkspaceRoots?: () => Promise<readonly string[]>;
12
13
  clientSupportsWrapReflow?: () => boolean;
13
14
  }
14
15
  export interface TerminalSessionControllerMetrics {
@@ -22,6 +23,7 @@ export declare class TerminalSessionController {
22
23
  private readonly hasBinaryChannel;
23
24
  private readonly isPathWithinRoot;
24
25
  private readonly sessionLogger;
26
+ private readonly listTerminalWorkspaceRoots;
25
27
  private readonly clientSupportsWrapReflow;
26
28
  private readonly subscribedDirectories;
27
29
  private unsubscribeTerminalsChanged;
@@ -50,6 +52,10 @@ export declare class TerminalSessionController {
50
52
  private emitTerminalsSnapshotForRoot;
51
53
  private handleListTerminalsRequest;
52
54
  private getAllTerminalSessions;
55
+ private getTerminalsForWorkspaceRoot;
56
+ private terminalBelongsToRoot;
57
+ private resolveTerminalOwnerRoot;
58
+ private isSamePath;
53
59
  private handleCreateTerminalRequest;
54
60
  private handleRenameTerminalRequest;
55
61
  private handleSubscribeTerminalRequest;
@@ -29,6 +29,7 @@ export class TerminalSessionController {
29
29
  this.hasBinaryChannel = options.hasBinaryChannel;
30
30
  this.isPathWithinRoot = options.isPathWithinRoot;
31
31
  this.sessionLogger = options.sessionLogger;
32
+ this.listTerminalWorkspaceRoots = options.listTerminalWorkspaceRoots ?? (async () => []);
32
33
  this.clientSupportsWrapReflow = options.clientSupportsWrapReflow ?? (() => false);
33
34
  }
34
35
  start() {
@@ -199,7 +200,7 @@ export class TerminalSessionController {
199
200
  return;
200
201
  }
201
202
  try {
202
- const terminals = await this.terminalManager.getTerminals(cwd);
203
+ const terminals = await this.getTerminalsForWorkspaceRoot(cwd);
203
204
  for (const terminal of terminals) {
204
205
  this.ensureExitSubscription(terminal);
205
206
  }
@@ -229,7 +230,7 @@ export class TerminalSessionController {
229
230
  }
230
231
  try {
231
232
  const terminals = typeof msg.cwd === "string"
232
- ? await this.terminalManager.getTerminals(msg.cwd)
233
+ ? await this.getTerminalsForWorkspaceRoot(msg.cwd)
233
234
  : await this.getAllTerminalSessions();
234
235
  for (const terminal of terminals) {
235
236
  this.ensureExitSubscription(terminal);
@@ -264,6 +265,39 @@ export class TerminalSessionController {
264
265
  const terminalsByDirectory = await Promise.all(directories.map((cwd) => manager.getTerminals(cwd)));
265
266
  return terminalsByDirectory.flat();
266
267
  }
268
+ async getTerminalsForWorkspaceRoot(cwd) {
269
+ if (!this.terminalManager) {
270
+ return [];
271
+ }
272
+ const terminals = await this.terminalManager.getTerminals(cwd);
273
+ const workspaceRoots = await this.listTerminalWorkspaceRoots();
274
+ if (workspaceRoots.length === 0) {
275
+ return terminals;
276
+ }
277
+ return terminals.filter((terminal) => this.terminalBelongsToRoot(cwd, terminal.cwd, workspaceRoots));
278
+ }
279
+ terminalBelongsToRoot(rootCwd, terminalCwd, workspaceRoots) {
280
+ const ownerRoot = this.resolveTerminalOwnerRoot(terminalCwd, workspaceRoots);
281
+ if (!ownerRoot) {
282
+ return this.isPathWithinRoot(rootCwd, terminalCwd);
283
+ }
284
+ return this.isSamePath(rootCwd, ownerRoot);
285
+ }
286
+ resolveTerminalOwnerRoot(terminalCwd, workspaceRoots) {
287
+ let ownerRoot = null;
288
+ for (const workspaceRoot of workspaceRoots) {
289
+ if (!this.isPathWithinRoot(workspaceRoot, terminalCwd)) {
290
+ continue;
291
+ }
292
+ if (!ownerRoot || workspaceRoot.length > ownerRoot.length) {
293
+ ownerRoot = workspaceRoot;
294
+ }
295
+ }
296
+ return ownerRoot;
297
+ }
298
+ isSamePath(firstPath, secondPath) {
299
+ return (this.isPathWithinRoot(firstPath, secondPath) && this.isPathWithinRoot(secondPath, firstPath));
300
+ }
267
301
  async handleCreateTerminalRequest(msg) {
268
302
  if (!this.terminalManager) {
269
303
  this.emit({
@@ -3,6 +3,7 @@ import path from "node:path";
3
3
  import { z } from "zod";
4
4
  import { AgentProviderRuntimeSettingsMapSchema, migrateProviderSettings, ProviderOverridesSchema, } from "./agent/provider-launch-config.js";
5
5
  import { ensurePrivateFile, writePrivateFileAtomicSync } from "./private-files.js";
6
+ import { TerminalProfileSchema } from "@getpaseo/protocol/messages";
6
7
  export const LogLevelSchema = z.enum(["trace", "debug", "info", "warn", "error", "fatal"]);
7
8
  export const LogFormatSchema = z.enum(["pretty", "json"]);
8
9
  const LogConfigSchema = z
@@ -187,6 +188,7 @@ export const PersistedConfigSchema = z
187
188
  .optional(),
188
189
  autoArchiveAfterMerge: z.boolean().optional(),
189
190
  appendSystemPrompt: z.string().optional(),
191
+ terminalProfiles: z.array(TerminalProfileSchema).optional(),
190
192
  cors: z
191
193
  .object({
192
194
  allowedOrigins: z.array(z.string()).optional(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getpaseo/server",
3
- "version": "0.1.93",
3
+ "version": "0.1.94",
4
4
  "description": "Paseo backend server",
5
5
  "files": [
6
6
  "dist/server",
@@ -59,10 +59,10 @@
59
59
  "dependencies": {
60
60
  "@agentclientprotocol/sdk": "^0.17.1",
61
61
  "@anthropic-ai/claude-agent-sdk": "^0.2.133",
62
- "@getpaseo/client": "0.1.93",
63
- "@getpaseo/highlight": "0.1.93",
64
- "@getpaseo/protocol": "0.1.93",
65
- "@getpaseo/relay": "0.1.93",
62
+ "@getpaseo/client": "0.1.94",
63
+ "@getpaseo/highlight": "0.1.94",
64
+ "@getpaseo/protocol": "0.1.94",
65
+ "@getpaseo/relay": "0.1.94",
66
66
  "@isaacs/ttlcache": "^2.1.4",
67
67
  "@modelcontextprotocol/sdk": "^1.20.1",
68
68
  "@opencode-ai/sdk": "1.14.46",