@getpaseo/server 0.1.93 → 0.1.95

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 (33) 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 +6 -0
  23. package/dist/server/server/session.js +115 -7
  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/server/workspace-reconciliation-service.js +10 -5
  28. package/dist/server/services/github-service.d.ts +57 -0
  29. package/dist/server/services/github-service.js +434 -5
  30. package/dist/server/terminal/terminal-session-controller.d.ts +6 -0
  31. package/dist/server/terminal/terminal-session-controller.js +36 -2
  32. package/dist/src/server/persisted-config.js +2 -0
  33. 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();
@@ -149,6 +205,7 @@ const PullRequestTimelineReviewNodeSchema = z.object({
149
205
  id: z.string().catch(""),
150
206
  state: z.string().catch(""),
151
207
  body: z.string().nullable().catch(null),
208
+ bodyHTML: z.string().nullable().catch(null),
152
209
  url: z.string().catch(""),
153
210
  submittedAt: z.string().nullable().catch(null),
154
211
  author: TimelineAuthorSchema,
@@ -156,10 +213,32 @@ const PullRequestTimelineReviewNodeSchema = z.object({
156
213
  const PullRequestTimelineCommentNodeSchema = z.object({
157
214
  id: z.string().catch(""),
158
215
  body: z.string().nullable().catch(null),
216
+ bodyHTML: z.string().nullable().catch(null),
159
217
  url: z.string().catch(""),
160
218
  createdAt: z.string().nullable().catch(null),
161
219
  author: TimelineAuthorSchema,
162
220
  });
221
+ const PullRequestReviewThreadCommentNodeSchema = PullRequestTimelineCommentNodeSchema.extend({
222
+ pullRequestReview: z
223
+ .object({ id: z.string().catch("") })
224
+ .nullable()
225
+ .optional()
226
+ .catch(null),
227
+ });
228
+ const PullRequestReviewThreadNodeSchema = z.object({
229
+ id: z.string().catch(""),
230
+ path: z.string().catch(""),
231
+ line: z.number().nullable().optional().catch(null),
232
+ startLine: z.number().nullable().optional().catch(null),
233
+ isResolved: z.boolean().catch(false),
234
+ isOutdated: z.boolean().catch(false),
235
+ comments: z
236
+ .object({
237
+ nodes: z.array(PullRequestReviewThreadCommentNodeSchema).catch([]),
238
+ pageInfo: z.object({ hasNextPage: z.boolean().catch(false) }).catch({ hasNextPage: false }),
239
+ })
240
+ .catch({ nodes: [], pageInfo: { hasNextPage: false } }),
241
+ });
163
242
  const PullRequestTimelinePageInfoSchema = z.object({
164
243
  hasNextPage: z.boolean().catch(false),
165
244
  });
@@ -183,6 +262,12 @@ const PullRequestTimelineGraphqlSchema = z.object({
183
262
  pageInfo: PullRequestTimelinePageInfoSchema.catch({ hasNextPage: false }),
184
263
  })
185
264
  .catch({ nodes: [], pageInfo: { hasNextPage: false } }),
265
+ reviewThreads: z
266
+ .object({
267
+ nodes: z.array(PullRequestReviewThreadNodeSchema).catch([]),
268
+ pageInfo: PullRequestTimelinePageInfoSchema.catch({ hasNextPage: false }),
269
+ })
270
+ .catch({ nodes: [], pageInfo: { hasNextPage: false } }),
186
271
  })
187
272
  .nullable()
188
273
  .optional(),
@@ -296,11 +381,13 @@ query PullRequestTimeline($owner: String!, $name: String!, $number: Int!) {
296
381
  id
297
382
  state
298
383
  body
384
+ bodyHTML
299
385
  url
300
386
  submittedAt
301
387
  author {
302
388
  login
303
389
  url
390
+ avatarUrl
304
391
  }
305
392
  }
306
393
  pageInfo {
@@ -311,11 +398,46 @@ query PullRequestTimeline($owner: String!, $name: String!, $number: Int!) {
311
398
  nodes {
312
399
  id
313
400
  body
401
+ bodyHTML
314
402
  url
315
403
  createdAt
316
404
  author {
317
405
  login
318
406
  url
407
+ avatarUrl
408
+ }
409
+ }
410
+ pageInfo {
411
+ hasNextPage
412
+ }
413
+ }
414
+ reviewThreads(first: 100) {
415
+ nodes {
416
+ id
417
+ path
418
+ line
419
+ startLine
420
+ isResolved
421
+ isOutdated
422
+ comments(first: 100) {
423
+ nodes {
424
+ id
425
+ body
426
+ bodyHTML
427
+ url
428
+ createdAt
429
+ author {
430
+ login
431
+ url
432
+ avatarUrl
433
+ }
434
+ pullRequestReview {
435
+ id
436
+ }
437
+ }
438
+ pageInfo {
439
+ hasNextPage
440
+ }
319
441
  }
320
442
  }
321
443
  pageInfo {
@@ -362,6 +484,7 @@ export function createGitHubService(options = {}) {
362
484
  const cache = new Map();
363
485
  const inFlight = new Map();
364
486
  const pollTargets = new Map();
487
+ const checkLogTailCache = new Map();
365
488
  let api;
366
489
  async function cached(params) {
367
490
  if (params.readOptions?.force && !params.readOptions.reason) {
@@ -651,6 +774,79 @@ export function createGitHubService(options = {}) {
651
774
  },
652
775
  });
653
776
  },
777
+ getGitHubCheckDetails(input) {
778
+ return cached({
779
+ cwd: input.cwd,
780
+ method: "getGitHubCheckDetails",
781
+ args: {
782
+ repoOwner: input.repoOwner,
783
+ repoName: input.repoName,
784
+ checkRunId: input.checkRunId,
785
+ workflowRunId: input.workflowRunId,
786
+ },
787
+ readOptions: input,
788
+ load: async () => {
789
+ const repoPath = `repos/${input.repoOwner}/${input.repoName}`;
790
+ const checkRun = parseGitHubCheckRunDetails(await run(["api", `${repoPath}/check-runs/${input.checkRunId}`], { cwd: input.cwd }));
791
+ const annotations = parseGitHubCheckAnnotations(await run([
792
+ "api",
793
+ `${repoPath}/check-runs/${input.checkRunId}/annotations`,
794
+ "-f",
795
+ `per_page=${CHECK_ANNOTATION_PAGE_MAX}`,
796
+ ], {
797
+ cwd: input.cwd,
798
+ }));
799
+ const workflowRunId = input.workflowRunId ?? checkRun.workflowRunId ?? null;
800
+ const failedJobs = [];
801
+ let truncated = annotations.length >= CHECK_ANNOTATION_PAGE_MAX;
802
+ if (typeof workflowRunId === "number") {
803
+ const jobs = parseGitHubActionsJobs(await run([
804
+ "api",
805
+ `${repoPath}/actions/runs/${workflowRunId}/jobs`,
806
+ "-f",
807
+ `per_page=${ACTIONS_JOB_PAGE_MAX}`,
808
+ ], {
809
+ cwd: input.cwd,
810
+ }));
811
+ const failed = jobs.filter(isFailedActionsJob);
812
+ truncated || (truncated = jobs.length >= ACTIONS_JOB_PAGE_MAX);
813
+ truncated || (truncated = failed.length > FAILED_CHECK_JOB_LIMIT);
814
+ for (const job of failed.slice(0, FAILED_CHECK_JOB_LIMIT)) {
815
+ const log = await getCachedCheckLogTail({
816
+ cwd: input.cwd,
817
+ repoPath,
818
+ job,
819
+ run,
820
+ cache: checkLogTailCache,
821
+ });
822
+ truncated || (truncated = log.logTruncated);
823
+ failedJobs.push({
824
+ jobId: job.jobId,
825
+ name: job.name,
826
+ status: job.status,
827
+ conclusion: job.conclusion,
828
+ url: job.url,
829
+ logTail: log.logTail,
830
+ logTruncated: log.logTruncated,
831
+ });
832
+ }
833
+ }
834
+ return {
835
+ checkRunId: checkRun.checkRunId,
836
+ workflowRunId,
837
+ name: checkRun.name,
838
+ status: checkRun.status,
839
+ conclusion: checkRun.conclusion,
840
+ url: checkRun.url,
841
+ detailsUrl: checkRun.detailsUrl,
842
+ output: checkRun.output,
843
+ annotations,
844
+ failedJobs,
845
+ truncated,
846
+ };
847
+ },
848
+ });
849
+ },
654
850
  async searchIssuesAndPrs(input) {
655
851
  if (input.force && !input.reason) {
656
852
  throw new Error("GitHubService forced read requires a reason");
@@ -1356,10 +1552,17 @@ function parseIssueSummaries(stdout) {
1356
1552
  function parsePullRequestTimeline(stdout, identity) {
1357
1553
  const parsed = PullRequestTimelineGraphqlSchema.parse(JSON.parse(stdout || "{}"));
1358
1554
  const pullRequest = parsed.data?.repository?.pullRequest;
1555
+ const reviewThreadItems = pullRequest
1556
+ ? pullRequest.reviewThreads.nodes.flatMap(toPullRequestTimelineReviewThreadItems)
1557
+ : [];
1558
+ const reviewThreadItemIds = new Set(reviewThreadItems.map((item) => item.id).filter((id) => id.length > 0));
1359
1559
  const items = pullRequest
1360
1560
  ? [
1361
1561
  ...pullRequest.reviews.nodes.flatMap(toPullRequestTimelineReviewItem),
1362
- ...pullRequest.comments.nodes.map(toPullRequestTimelineCommentItem),
1562
+ ...pullRequest.comments.nodes
1563
+ .filter((comment) => !reviewThreadItemIds.has(comment.id))
1564
+ .map(toPullRequestTimelineCommentItem),
1565
+ ...reviewThreadItems,
1363
1566
  ].sort(compareTimelineItems)
1364
1567
  : [];
1365
1568
  return {
@@ -1367,8 +1570,11 @@ function parsePullRequestTimeline(stdout, identity) {
1367
1570
  repoOwner: identity.repoOwner,
1368
1571
  repoName: identity.repoName,
1369
1572
  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),
1573
+ // S3 deliberately caps timeline fetches at the first 100 reviews, comments, and review threads.
1574
+ truncated: Boolean(pullRequest?.reviews.pageInfo.hasNextPage ||
1575
+ pullRequest?.comments.pageInfo.hasNextPage ||
1576
+ pullRequest?.reviewThreads.pageInfo.hasNextPage ||
1577
+ pullRequest?.reviewThreads.nodes.some((thread) => thread.comments.pageInfo.hasNextPage)),
1372
1578
  error: pullRequest ? null : { kind: "not_found", message: "Pull request not found" },
1373
1579
  };
1374
1580
  }
@@ -1383,7 +1589,8 @@ function toPullRequestTimelineReviewItem(review) {
1383
1589
  id: review.id,
1384
1590
  author: review.author?.login ?? "unknown",
1385
1591
  authorUrl: review.author?.url ?? null,
1386
- body: review.body ?? "",
1592
+ avatarUrl: review.author?.avatarUrl ?? null,
1593
+ body: normalizeGitHubTimelineBody(review.body ?? "", review.bodyHTML ?? ""),
1387
1594
  createdAt: parseOptionalTime(review.submittedAt ?? null),
1388
1595
  url: review.url,
1389
1596
  reviewState,
@@ -1396,11 +1603,229 @@ function toPullRequestTimelineCommentItem(comment) {
1396
1603
  id: comment.id,
1397
1604
  author: comment.author?.login ?? "unknown",
1398
1605
  authorUrl: comment.author?.url ?? null,
1399
- body: comment.body ?? "",
1606
+ avatarUrl: comment.author?.avatarUrl ?? null,
1607
+ body: normalizeGitHubTimelineBody(comment.body ?? "", comment.bodyHTML ?? ""),
1400
1608
  createdAt: parseOptionalTime(comment.createdAt ?? null),
1401
1609
  url: comment.url,
1402
1610
  };
1403
1611
  }
1612
+ const RAW_MARKDOWN_IMAGE_RE = /!\[[^\]]*\]\(\s*([^\s)]+)(?:\s+["'][^)]*["'])?\s*\)/g;
1613
+ const HTML_IMAGE_RE = /<img\b[^>]*\bsrc\s*=\s*(["'])(.*?)\1[^>]*>/gi;
1614
+ const GITHUB_RENDERED_IMAGE_HOSTS = new Set([
1615
+ "camo.githubusercontent.com",
1616
+ "private-user-images.githubusercontent.com",
1617
+ ]);
1618
+ function normalizeGitHubTimelineBody(body, bodyHTML) {
1619
+ const rawImages = extractRawImageSourceReferences(body);
1620
+ if (rawImages.length === 0) {
1621
+ return body;
1622
+ }
1623
+ const renderedSources = extractRenderedImageSources(bodyHTML);
1624
+ if (renderedSources.length !== rawImages.length) {
1625
+ return body;
1626
+ }
1627
+ let cursor = 0;
1628
+ let normalized = "";
1629
+ for (let index = 0; index < rawImages.length; index += 1) {
1630
+ const rawImage = rawImages[index];
1631
+ const renderedSrc = renderedSources[index];
1632
+ if (!rawImage ||
1633
+ !renderedSrc ||
1634
+ !isRawGitHubAttachmentSource(rawImage.src) ||
1635
+ !isGitHubRenderedImageSource(renderedSrc)) {
1636
+ return body;
1637
+ }
1638
+ normalized += body.slice(cursor, rawImage.start);
1639
+ normalized += renderedSrc;
1640
+ cursor = rawImage.end;
1641
+ }
1642
+ normalized += body.slice(cursor);
1643
+ return normalized;
1644
+ }
1645
+ function extractRawImageSourceReferences(source) {
1646
+ const references = [
1647
+ ...extractHtmlImageSourceReferences(source),
1648
+ ...extractMarkdownImageSourceReferences(source),
1649
+ ];
1650
+ return references.sort((left, right) => left.start - right.start);
1651
+ }
1652
+ function extractRenderedImageSources(source) {
1653
+ return extractHtmlImageSourceReferences(source).map((reference) => reference.src);
1654
+ }
1655
+ function extractHtmlImageSourceReferences(source) {
1656
+ const references = [];
1657
+ HTML_IMAGE_RE.lastIndex = 0;
1658
+ let match;
1659
+ while ((match = HTML_IMAGE_RE.exec(source)) !== null) {
1660
+ const src = decodeHtmlAttribute(match[2] ?? "");
1661
+ if (!src) {
1662
+ continue;
1663
+ }
1664
+ const rawAttributeSrc = match[2] ?? "";
1665
+ const start = match.index + match[0].indexOf(rawAttributeSrc);
1666
+ references.push({ src, start, end: start + rawAttributeSrc.length });
1667
+ }
1668
+ return references;
1669
+ }
1670
+ function extractMarkdownImageSourceReferences(source) {
1671
+ const references = [];
1672
+ RAW_MARKDOWN_IMAGE_RE.lastIndex = 0;
1673
+ let match;
1674
+ while ((match = RAW_MARKDOWN_IMAGE_RE.exec(source)) !== null) {
1675
+ const src = match[1] ?? "";
1676
+ if (!src) {
1677
+ continue;
1678
+ }
1679
+ const start = match.index + match[0].indexOf(src);
1680
+ references.push({ src, start, end: start + src.length });
1681
+ }
1682
+ return references;
1683
+ }
1684
+ function isRawGitHubAttachmentSource(src) {
1685
+ try {
1686
+ const url = new URL(src);
1687
+ return (url.protocol === "https:" &&
1688
+ url.hostname === "github.com" &&
1689
+ url.pathname.startsWith("/user-attachments/assets/"));
1690
+ }
1691
+ catch {
1692
+ return false;
1693
+ }
1694
+ }
1695
+ function isGitHubRenderedImageSource(src) {
1696
+ try {
1697
+ const url = new URL(src);
1698
+ return url.protocol === "https:" && GITHUB_RENDERED_IMAGE_HOSTS.has(url.hostname);
1699
+ }
1700
+ catch {
1701
+ return false;
1702
+ }
1703
+ }
1704
+ function decodeHtmlAttribute(value) {
1705
+ return value
1706
+ .replaceAll("&amp;", "&")
1707
+ .replaceAll("&quot;", '"')
1708
+ .replaceAll("&#39;", "'")
1709
+ .replaceAll("&lt;", "<")
1710
+ .replaceAll("&gt;", ">");
1711
+ }
1712
+ function toPullRequestTimelineReviewThreadItems(thread) {
1713
+ return thread.comments.nodes.map((comment) => ({
1714
+ ...toPullRequestTimelineCommentItem(comment),
1715
+ ...(comment.pullRequestReview?.id ? { reviewId: comment.pullRequestReview.id } : {}),
1716
+ location: {
1717
+ path: thread.path,
1718
+ ...(thread.line !== null && thread.line !== undefined ? { line: thread.line } : {}),
1719
+ ...(thread.startLine !== null && thread.startLine !== undefined
1720
+ ? { startLine: thread.startLine }
1721
+ : {}),
1722
+ ...(thread.id ? { threadId: thread.id } : {}),
1723
+ isResolved: thread.isResolved,
1724
+ isOutdated: thread.isOutdated,
1725
+ },
1726
+ }));
1727
+ }
1728
+ function parseGitHubCheckRunDetails(stdout) {
1729
+ const parsed = GitHubCheckRunDetailsSchema.parse(JSON.parse(stdout || "{}"));
1730
+ return {
1731
+ checkRunId: parsed.id,
1732
+ workflowRunId: parsed.check_suite?.workflow_run?.id ?? null,
1733
+ name: parsed.name,
1734
+ status: parsed.status,
1735
+ conclusion: parsed.conclusion,
1736
+ url: parsed.html_url,
1737
+ detailsUrl: parsed.details_url,
1738
+ output: parsed.output,
1739
+ annotations: [],
1740
+ failedJobs: [],
1741
+ truncated: false,
1742
+ };
1743
+ }
1744
+ function parseGitHubCheckAnnotations(stdout) {
1745
+ return GitHubCheckAnnotationsSchema.parse(JSON.parse(stdout || "[]")).map((annotation) => {
1746
+ const result = {};
1747
+ if (annotation.path)
1748
+ result.path = annotation.path;
1749
+ if (annotation.start_line !== undefined)
1750
+ result.startLine = annotation.start_line;
1751
+ if (annotation.end_line !== undefined)
1752
+ result.endLine = annotation.end_line;
1753
+ if (annotation.annotation_level)
1754
+ result.annotationLevel = annotation.annotation_level;
1755
+ if (annotation.message)
1756
+ result.message = annotation.message;
1757
+ if (annotation.title)
1758
+ result.title = annotation.title;
1759
+ if (annotation.raw_details)
1760
+ result.rawDetails = annotation.raw_details;
1761
+ return result;
1762
+ });
1763
+ }
1764
+ function parseGitHubActionsJobs(stdout) {
1765
+ return GitHubActionsJobsSchema.parse(JSON.parse(stdout || "{}")).jobs.map((job) => {
1766
+ const result = {
1767
+ jobId: job.id,
1768
+ name: job.name,
1769
+ status: job.status,
1770
+ conclusion: job.conclusion,
1771
+ url: job.html_url,
1772
+ };
1773
+ if (job.completed_at)
1774
+ result.completedAt = job.completed_at;
1775
+ return result;
1776
+ });
1777
+ }
1778
+ function isFailedActionsJob(job) {
1779
+ return (job.conclusion === "failure" ||
1780
+ job.conclusion === "cancelled" ||
1781
+ job.conclusion === "timed_out" ||
1782
+ job.conclusion === "action_required");
1783
+ }
1784
+ async function getCachedCheckLogTail(input) {
1785
+ const key = `${input.job.jobId}:${input.job.completedAt ?? ""}`;
1786
+ const cached = input.cache.get(key);
1787
+ if (cached) {
1788
+ input.cache.delete(key);
1789
+ input.cache.set(key, cached);
1790
+ return cached;
1791
+ }
1792
+ const capped = capCheckLogTail(await input.run(["api", `${input.repoPath}/actions/jobs/${input.job.jobId}/logs`], {
1793
+ cwd: input.cwd,
1794
+ }));
1795
+ input.cache.set(key, capped);
1796
+ while (input.cache.size > CHECK_LOG_TAIL_CACHE_MAX_ENTRIES) {
1797
+ const oldestKey = input.cache.keys().next().value;
1798
+ if (!oldestKey) {
1799
+ break;
1800
+ }
1801
+ input.cache.delete(oldestKey);
1802
+ }
1803
+ return capped;
1804
+ }
1805
+ function capCheckLogTail(log) {
1806
+ const lines = log.split("\n");
1807
+ let truncated = lines.length > CHECK_LOG_TAIL_MAX_LINES;
1808
+ let tail = lines.slice(-CHECK_LOG_TAIL_MAX_LINES).join("\n");
1809
+ if (Buffer.byteLength(tail, "utf8") > CHECK_LOG_TAIL_MAX_BYTES) {
1810
+ truncated = true;
1811
+ tail = utf8SuffixWithinBytes(tail, CHECK_LOG_TAIL_MAX_BYTES);
1812
+ }
1813
+ return { logTail: tail, logTruncated: truncated };
1814
+ }
1815
+ function utf8SuffixWithinBytes(value, maxBytes) {
1816
+ let lowerBound = 0;
1817
+ let upperBound = value.length;
1818
+ while (lowerBound < upperBound) {
1819
+ const midpoint = Math.floor((lowerBound + upperBound) / 2);
1820
+ if (Buffer.byteLength(value.slice(midpoint), "utf8") > maxBytes) {
1821
+ lowerBound = midpoint + 1;
1822
+ }
1823
+ else {
1824
+ upperBound = midpoint;
1825
+ }
1826
+ }
1827
+ return value.slice(lowerBound);
1828
+ }
1404
1829
  function mapTimelineReviewState(state, body) {
1405
1830
  switch (state) {
1406
1831
  case "APPROVED":
@@ -1541,6 +1966,10 @@ function buildPullRequestCheck(context) {
1541
1966
  ...(typeof context.workflowName === "string" && context.workflowName.trim().length > 0
1542
1967
  ? { workflow: context.workflowName }
1543
1968
  : {}),
1969
+ ...(typeof context.databaseId === "number" ? { checkRunId: context.databaseId } : {}),
1970
+ ...(typeof context.checkSuite?.workflowRun?.databaseId === "number"
1971
+ ? { workflowRunId: context.checkSuite.workflowRun.databaseId }
1972
+ : {}),
1544
1973
  ...formatCheckRunDuration(context),
1545
1974
  recency: getCheckRunRecency(context),
1546
1975
  };
@@ -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(),