@getpaseo/server 0.1.92 → 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 (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/models.js +15 -0
  7. package/dist/server/server/agent/providers/claude/task-notification-tool-call.d.ts +2 -2
  8. package/dist/server/server/agent/providers/mock-load-test-agent.js +55 -28
  9. package/dist/server/server/agent/providers/pi/agent.js +3 -1
  10. package/dist/server/server/agent/providers/pi/session-descriptor.d.ts +5 -0
  11. package/dist/server/server/agent/providers/pi/session-descriptor.js +74 -12
  12. package/dist/server/server/agent/runtime-mcp-config.d.ts +6 -0
  13. package/dist/server/server/agent/runtime-mcp-config.js +3 -0
  14. package/dist/server/server/auth.d.ts +13 -0
  15. package/dist/server/server/auth.js +35 -0
  16. package/dist/server/server/bootstrap.d.ts +2 -0
  17. package/dist/server/server/bootstrap.js +28 -1
  18. package/dist/server/server/config.js +3 -1
  19. package/dist/server/server/daemon-config-store.js +3 -0
  20. package/dist/server/server/loop-service.d.ts +6 -6
  21. package/dist/server/server/persisted-config.d.ts +61 -0
  22. package/dist/server/server/persisted-config.js +2 -0
  23. package/dist/server/server/session.d.ts +1 -0
  24. package/dist/server/server/session.js +50 -3
  25. package/dist/server/server/websocket-server.js +2 -0
  26. package/dist/server/server/workspace-git-service.d.ts +4 -1
  27. package/dist/server/server/workspace-git-service.js +40 -22
  28. package/dist/server/services/github-service.d.ts +57 -0
  29. package/dist/server/services/github-service.js +327 -3
  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
@@ -1,4 +1,5 @@
1
1
  import { compare, compareSync, hashSync } from "bcryptjs";
2
+ import { timingSafeEqual } from "node:crypto";
2
3
  export const DAEMON_PASSWORD_BCRYPT_COST = 12;
3
4
  export function isBearerTokenValid(input) {
4
5
  return isBearerTokenValidSync(input);
@@ -98,6 +99,16 @@ const BEARER_AUTH_BYPASS_PATHS = new Set([
98
99
  // rejects requests without a valid token (400/403), so dropping the bearer
99
100
  // here does not make the route unauthenticated.
100
101
  "/api/files/download",
102
+ // The daemon injects its own agents' Paseo MCP connections at this endpoint
103
+ // (and connects its own per-client MCP client here). Those connections cannot
104
+ // carry the daemon password — it is only known in plaintext when set via env,
105
+ // never when set via the app — so the route authenticates them with a
106
+ // per-daemon-run capability token instead (see isAgentMcpRequestAuthorized).
107
+ // The token is injected only into local agent configs/sessions and never sent
108
+ // to remote clients, and the route still rejects callers presenting neither
109
+ // the token nor a valid daemon password, so dropping the global bearer here
110
+ // does not make the endpoint unauthenticated.
111
+ "/mcp/agents",
101
112
  ]);
102
113
  export function shouldBypassBearerAuth(method, path) {
103
114
  if (method === "OPTIONS") {
@@ -105,4 +116,28 @@ export function shouldBypassBearerAuth(method, path) {
105
116
  }
106
117
  return BEARER_AUTH_BYPASS_PATHS.has(path);
107
118
  }
119
+ /**
120
+ * Authorizes a request to the Agent MCP endpoint (/mcp/agents), which is exempt
121
+ * from the global daemon-password middleware. Accepts either the per-daemon-run
122
+ * capability token the daemon injects into its own agents' configs and MCP
123
+ * client, or a valid daemon-password bearer (so existing password-authenticated
124
+ * callers keep working). When no daemon password is configured the endpoint is
125
+ * open, matching the global middleware's behavior.
126
+ */
127
+ export async function isAgentMcpRequestAuthorized(input) {
128
+ if (!input.password) {
129
+ return true;
130
+ }
131
+ const token = extractHttpBearerToken(input.authorizationHeader);
132
+ if (input.capabilityToken !== null && token !== null) {
133
+ // Constant-time compare; length-guard first because timingSafeEqual throws
134
+ // on differing buffer lengths.
135
+ const provided = Buffer.from(token);
136
+ const expected = Buffer.from(input.capabilityToken);
137
+ if (provided.length === expected.length && timingSafeEqual(provided, expected)) {
138
+ return true;
139
+ }
140
+ }
141
+ return isBearerTokenValidAsync({ password: input.password, token });
142
+ }
108
143
  //# sourceMappingURL=auth.js.map
@@ -19,6 +19,7 @@ import { AgentStorage } from "./agent/agent-storage.js";
19
19
  import type { TerminalManager } from "../terminal/terminal-manager.js";
20
20
  import type { PushNotificationSender } from "./push/notifications.js";
21
21
  import type { AgentClient, AgentProvider } from "./agent/agent-sdk-types.js";
22
+ import type { TerminalProfile } from "@getpaseo/protocol/messages";
22
23
  import type { AgentProviderRuntimeSettingsMap, ProviderOverride } from "./agent/provider-launch-config.js";
23
24
  import type { PersistedConfig } from "./persisted-config.js";
24
25
  import { type ServiceProxySubsystem } from "./service-proxy.js";
@@ -57,6 +58,7 @@ export interface PaseoDaemonConfig {
57
58
  mcpInjectIntoAgents?: boolean;
58
59
  autoArchiveAfterMerge?: boolean;
59
60
  appendSystemPrompt?: string;
61
+ terminalProfiles?: TerminalProfile[];
60
62
  staticDir: string;
61
63
  mcpDebug: boolean;
62
64
  isDev?: boolean;
@@ -104,7 +104,7 @@ import { ScriptHealthMonitor } from "./script-health-monitor.js";
104
104
  import { createScriptStatusEmitter } from "./script-status-projection.js";
105
105
  import { WorkspaceScriptRuntimeStore } from "./workspace-script-runtime-store.js";
106
106
  import { isHostnameAllowed } from "./hostnames.js";
107
- import { createRequireBearerMiddleware } from "./auth.js";
107
+ import { createRequireBearerMiddleware, isAgentMcpRequestAuthorized, } from "./auth.js";
108
108
  const MAX_MCP_DEBUG_BATCH_ITEMS = 10;
109
109
  const REDACTED_LOG_VALUE = "[redacted]";
110
110
  const DOWNLOAD_OPEN_FLAGS = process.platform === "win32" ? constants.O_RDONLY : constants.O_RDONLY | constants.O_NOFOLLOW;
@@ -174,6 +174,9 @@ export async function createPaseoDaemon(config, rootLogger) {
174
174
  },
175
175
  autoArchiveAfterMerge: config.autoArchiveAfterMerge ?? false,
176
176
  appendSystemPrompt: config.appendSystemPrompt ?? "",
177
+ ...(config.terminalProfiles !== undefined
178
+ ? { terminalProfiles: config.terminalProfiles }
179
+ : {}),
177
180
  }, logger);
178
181
  const serverId = getOrCreateServerId(config.paseoHome, { logger });
179
182
  const daemonKeyPair = await loadOrCreateDaemonKeyPair(config.paseoHome, logger);
@@ -183,6 +186,14 @@ export async function createPaseoDaemon(config, rootLogger) {
183
186
  const downloadTokenStore = new DownloadTokenStore({
184
187
  ttlMs: downloadTokenTtlMs,
185
188
  });
189
+ // Capability token authenticating the daemon's own agents to the loopback
190
+ // Agent MCP endpoint (/mcp/agents). Random per daemon run, injected only into
191
+ // local agent configs and the daemon's own MCP client — never sent to remote
192
+ // clients — so it cannot be replayed off-box. This lets the injected MCP
193
+ // authenticate even when the daemon password is set via the app (hash only,
194
+ // no plaintext available). Mirrors the /api/files/download capability-token
195
+ // pattern.
196
+ const agentMcpAuthToken = randomUUID();
186
197
  const listenTarget = parseListenString(config.listen);
187
198
  const app = express();
188
199
  let boundListenTarget = null;
@@ -375,6 +386,10 @@ export async function createPaseoDaemon(config, rootLogger) {
375
386
  providerDefinitions: initialAgentManagerState.providerDefinitions,
376
387
  registry: agentStorage,
377
388
  appendSystemPrompt: config.appendSystemPrompt,
389
+ onWorkspaceStateMayHaveChanged: ({ cwd }) => {
390
+ workspaceGitService.onWorkspaceStateMayHaveChanged(cwd);
391
+ },
392
+ mcpAuthToken: agentMcpAuthToken,
378
393
  logger,
379
394
  });
380
395
  const detachAgentStoragePersistence = attachAgentStoragePersistence(logger, agentManager, agentStorage);
@@ -586,6 +601,18 @@ export async function createPaseoDaemon(config, rootLogger) {
586
601
  return transport;
587
602
  };
588
603
  const runAgentMcpRequest = async (req, res) => {
604
+ // This route is exempt from the global daemon-password middleware, so it
605
+ // authenticates here using the injected capability token (or a valid
606
+ // daemon password). Without this, a password-protected daemon would be
607
+ // wide open on its agent control plane.
608
+ if (!(await isAgentMcpRequestAuthorized({
609
+ password: config.auth?.password,
610
+ capabilityToken: agentMcpAuthToken,
611
+ authorizationHeader: req.header("authorization"),
612
+ }))) {
613
+ res.status(401).json({ error: "Unauthorized" });
614
+ return;
615
+ }
589
616
  if (config.mcpDebug) {
590
617
  logger.debug({
591
618
  method: req.method,
@@ -192,6 +192,7 @@ function resolveStaticLoadConfigSettings(env, cli, persisted) {
192
192
  mcpInjectIntoAgents: cli?.mcpInjectIntoAgents ?? persisted.daemon?.mcp?.injectIntoAgents ?? false,
193
193
  autoArchiveAfterMerge: persisted.daemon?.autoArchiveAfterMerge ?? false,
194
194
  appendSystemPrompt: resolveAppendSystemPrompt(persisted),
195
+ terminalProfiles: persisted.daemon?.terminalProfiles,
195
196
  hostnames: mergeHostnames([
196
197
  persisted.daemon?.hostnames,
197
198
  parseHostnamesEnv(env.PASEO_HOSTNAMES ?? env.PASEO_ALLOWED_HOSTS),
@@ -204,7 +205,7 @@ export function loadConfig(paseoHome, options) {
204
205
  const env = options?.env ?? process.env;
205
206
  const persisted = loadPersistedConfig(paseoHome);
206
207
  const listen = resolveListenAddress(env, options?.cli, persisted);
207
- const { mcpEnabled, mcpInjectIntoAgents, autoArchiveAfterMerge, appendSystemPrompt, hostnames, appBaseUrl, } = resolveStaticLoadConfigSettings(env, options?.cli, persisted);
208
+ const { mcpEnabled, mcpInjectIntoAgents, autoArchiveAfterMerge, appendSystemPrompt, terminalProfiles, hostnames, appBaseUrl, } = resolveStaticLoadConfigSettings(env, options?.cli, persisted);
208
209
  const relay = resolveRelayConfig({
209
210
  env,
210
211
  persisted,
@@ -229,6 +230,7 @@ export function loadConfig(paseoHome, options) {
229
230
  mcpInjectIntoAgents,
230
231
  autoArchiveAfterMerge,
231
232
  appendSystemPrompt,
233
+ terminalProfiles,
232
234
  mcpDebug: env.MCP_DEBUG === "1",
233
235
  isDev: resolvePaseoNodeEnv(env) === "development",
234
236
  agentStoragePath: path.join(paseoHome, "agents"),
@@ -147,6 +147,9 @@ function mergeMutableConfigIntoPersistedConfig(params) {
147
147
  },
148
148
  autoArchiveAfterMerge: mutable.autoArchiveAfterMerge,
149
149
  appendSystemPrompt: mutable.appendSystemPrompt,
150
+ ...(mutable.terminalProfiles !== undefined
151
+ ? { terminalProfiles: mutable.terminalProfiles }
152
+ : {}),
150
153
  },
151
154
  agents: nextAgents,
152
155
  };
@@ -15,14 +15,14 @@ declare const LoopLogEntrySchema: z.ZodObject<{
15
15
  level: "error" | "info";
16
16
  seq: number;
17
17
  timestamp: string;
18
- source: "worker" | "loop" | "verifier" | "verify-check";
18
+ source: "loop" | "worker" | "verifier" | "verify-check";
19
19
  iteration: number | null;
20
20
  }, {
21
21
  text: string;
22
22
  level: "error" | "info";
23
23
  seq: number;
24
24
  timestamp: string;
25
- source: "worker" | "loop" | "verifier" | "verify-check";
25
+ source: "loop" | "worker" | "verifier" | "verify-check";
26
26
  iteration: number | null;
27
27
  }>;
28
28
  declare const LoopVerifyCheckResultSchema: z.ZodObject<{
@@ -314,14 +314,14 @@ declare const LoopRecordSchema: z.ZodObject<{
314
314
  level: "error" | "info";
315
315
  seq: number;
316
316
  timestamp: string;
317
- source: "worker" | "loop" | "verifier" | "verify-check";
317
+ source: "loop" | "worker" | "verifier" | "verify-check";
318
318
  iteration: number | null;
319
319
  }, {
320
320
  text: string;
321
321
  level: "error" | "info";
322
322
  seq: number;
323
323
  timestamp: string;
324
- source: "worker" | "loop" | "verifier" | "verify-check";
324
+ source: "loop" | "worker" | "verifier" | "verify-check";
325
325
  iteration: number | null;
326
326
  }>, "many">;
327
327
  nextLogSeq: z.ZodNumber;
@@ -384,7 +384,7 @@ declare const LoopRecordSchema: z.ZodObject<{
384
384
  level: "error" | "info";
385
385
  seq: number;
386
386
  timestamp: string;
387
- source: "worker" | "loop" | "verifier" | "verify-check";
387
+ source: "loop" | "worker" | "verifier" | "verify-check";
388
388
  iteration: number | null;
389
389
  }[];
390
390
  nextLogSeq: number;
@@ -445,7 +445,7 @@ declare const LoopRecordSchema: z.ZodObject<{
445
445
  level: "error" | "info";
446
446
  seq: number;
447
447
  timestamp: string;
448
- source: "worker" | "loop" | "verifier" | "verify-check";
448
+ source: "loop" | "worker" | "verifier" | "verify-check";
449
449
  iteration: number | null;
450
450
  }[];
451
451
  nextLogSeq: number;
@@ -21,6 +21,25 @@ export declare const PersistedConfigSchema: z.ZodObject<{
21
21
  }, z.ZodTypeAny, "passthrough">>>;
22
22
  autoArchiveAfterMerge: z.ZodOptional<z.ZodBoolean>;
23
23
  appendSystemPrompt: z.ZodOptional<z.ZodString>;
24
+ terminalProfiles: z.ZodOptional<z.ZodArray<z.ZodObject<{
25
+ id: z.ZodString;
26
+ name: z.ZodString;
27
+ command: z.ZodString;
28
+ args: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
29
+ icon: z.ZodOptional<z.ZodString>;
30
+ }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
31
+ id: z.ZodString;
32
+ name: z.ZodString;
33
+ command: z.ZodString;
34
+ args: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
35
+ icon: z.ZodOptional<z.ZodString>;
36
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
37
+ id: z.ZodString;
38
+ name: z.ZodString;
39
+ command: z.ZodString;
40
+ args: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
41
+ icon: z.ZodOptional<z.ZodString>;
42
+ }, z.ZodTypeAny, "passthrough">>, "many">>;
24
43
  cors: z.ZodOptional<z.ZodObject<{
25
44
  allowedOrigins: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
26
45
  }, "strict", z.ZodTypeAny, {
@@ -80,6 +99,13 @@ export declare const PersistedConfigSchema: z.ZodObject<{
80
99
  allowedHosts?: true | string[] | undefined;
81
100
  autoArchiveAfterMerge?: boolean | undefined;
82
101
  appendSystemPrompt?: string | undefined;
102
+ terminalProfiles?: z.objectOutputType<{
103
+ id: z.ZodString;
104
+ name: z.ZodString;
105
+ command: z.ZodString;
106
+ args: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
107
+ icon: z.ZodOptional<z.ZodString>;
108
+ }, z.ZodTypeAny, "passthrough">[] | undefined;
83
109
  cors?: {
84
110
  allowedOrigins?: string[] | undefined;
85
111
  } | undefined;
@@ -108,6 +134,13 @@ export declare const PersistedConfigSchema: z.ZodObject<{
108
134
  allowedHosts?: true | string[] | undefined;
109
135
  autoArchiveAfterMerge?: boolean | undefined;
110
136
  appendSystemPrompt?: string | undefined;
137
+ terminalProfiles?: z.objectInputType<{
138
+ id: z.ZodString;
139
+ name: z.ZodString;
140
+ command: z.ZodString;
141
+ args: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
142
+ icon: z.ZodOptional<z.ZodString>;
143
+ }, z.ZodTypeAny, "passthrough">[] | undefined;
111
144
  cors?: {
112
145
  allowedOrigins?: string[] | undefined;
113
146
  } | undefined;
@@ -135,6 +168,13 @@ export declare const PersistedConfigSchema: z.ZodObject<{
135
168
  hostnames?: true | string[] | undefined;
136
169
  autoArchiveAfterMerge?: boolean | undefined;
137
170
  appendSystemPrompt?: string | undefined;
171
+ terminalProfiles?: z.objectOutputType<{
172
+ id: z.ZodString;
173
+ name: z.ZodString;
174
+ command: z.ZodString;
175
+ args: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
176
+ icon: z.ZodOptional<z.ZodString>;
177
+ }, z.ZodTypeAny, "passthrough">[] | undefined;
138
178
  cors?: {
139
179
  allowedOrigins?: string[] | undefined;
140
180
  } | undefined;
@@ -163,6 +203,13 @@ export declare const PersistedConfigSchema: z.ZodObject<{
163
203
  allowedHosts?: true | string[] | undefined;
164
204
  autoArchiveAfterMerge?: boolean | undefined;
165
205
  appendSystemPrompt?: string | undefined;
206
+ terminalProfiles?: z.objectInputType<{
207
+ id: z.ZodString;
208
+ name: z.ZodString;
209
+ command: z.ZodString;
210
+ args: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
211
+ icon: z.ZodOptional<z.ZodString>;
212
+ }, z.ZodTypeAny, "passthrough">[] | undefined;
166
213
  cors?: {
167
214
  allowedOrigins?: string[] | undefined;
168
215
  } | undefined;
@@ -853,6 +900,13 @@ export declare const PersistedConfigSchema: z.ZodObject<{
853
900
  hostnames?: true | string[] | undefined;
854
901
  autoArchiveAfterMerge?: boolean | undefined;
855
902
  appendSystemPrompt?: string | undefined;
903
+ terminalProfiles?: z.objectOutputType<{
904
+ id: z.ZodString;
905
+ name: z.ZodString;
906
+ command: z.ZodString;
907
+ args: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
908
+ icon: z.ZodOptional<z.ZodString>;
909
+ }, z.ZodTypeAny, "passthrough">[] | undefined;
856
910
  cors?: {
857
911
  allowedOrigins?: string[] | undefined;
858
912
  } | undefined;
@@ -992,6 +1046,13 @@ export declare const PersistedConfigSchema: z.ZodObject<{
992
1046
  allowedHosts?: true | string[] | undefined;
993
1047
  autoArchiveAfterMerge?: boolean | undefined;
994
1048
  appendSystemPrompt?: string | undefined;
1049
+ terminalProfiles?: z.objectInputType<{
1050
+ id: z.ZodString;
1051
+ name: z.ZodString;
1052
+ command: z.ZodString;
1053
+ args: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
1054
+ icon: z.ZodOptional<z.ZodString>;
1055
+ }, z.ZodTypeAny, "passthrough">[] | undefined;
995
1056
  cors?: {
996
1057
  allowedOrigins?: string[] | undefined;
997
1058
  } | undefined;
@@ -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(),
@@ -415,6 +415,7 @@ export declare class Session {
415
415
  private resolveCurrentPullRequest;
416
416
  private handleCheckoutPrStatusRequest;
417
417
  private handlePullRequestTimelineRequest;
418
+ private handleCheckoutGithubGetCheckDetailsRequest;
418
419
  private handlePaseoWorktreeListRequest;
419
420
  private handlePaseoWorktreeArchiveRequest;
420
421
  /**
@@ -356,6 +356,12 @@ export class Session {
356
356
  hasBinaryChannel: () => this.onBinaryMessage !== null,
357
357
  isPathWithinRoot: (rootPath, candidatePath) => this.isPathWithinRoot(rootPath, candidatePath),
358
358
  sessionLogger: this.sessionLogger,
359
+ listTerminalWorkspaceRoots: async () => {
360
+ const workspaces = await this.workspaceRegistry.list();
361
+ return workspaces
362
+ .filter((workspace) => !workspace.archivedAt)
363
+ .map((workspace) => workspace.cwd);
364
+ },
359
365
  clientSupportsWrapReflow: () => this.clientCapabilities.has(CLIENT_CAPS.terminalReflowableSnapshot),
360
366
  });
361
367
  this.createAgentLifecycleDispatch = new CreateAgentLifecycleDispatch({
@@ -586,7 +592,10 @@ export class Session {
586
592
  this.sessionLogger.info("Skipping Agent MCP initialization because no MCP base URL is configured");
587
593
  return;
588
594
  }
589
- const transport = new StreamableHTTPClientTransport(new URL(this.mcpBaseUrl));
595
+ const authToken = this.agentManager.getMcpAuthToken();
596
+ const transport = new StreamableHTTPClientTransport(new URL(this.mcpBaseUrl), authToken
597
+ ? { requestInit: { headers: { Authorization: `Bearer ${authToken}` } } }
598
+ : undefined);
590
599
  this.agentMcpClient = await experimental_createMCPClient({
591
600
  transport,
592
601
  });
@@ -1279,6 +1288,8 @@ export class Session {
1279
1288
  return this.handleCheckoutPrMergeRequest(msg);
1280
1289
  case "checkout.github.set_auto_merge.request":
1281
1290
  return this.handleCheckoutGithubSetAutoMergeRequest(msg);
1291
+ case "checkout.github.get_check_details.request":
1292
+ return this.handleCheckoutGithubGetCheckDetailsRequest(msg);
1282
1293
  case "checkout_pr_status_request":
1283
1294
  return this.handleCheckoutPrStatusRequest(msg);
1284
1295
  case "pull_request_timeline_request":
@@ -4276,6 +4287,43 @@ export class Session {
4276
4287
  });
4277
4288
  }
4278
4289
  }
4290
+ async handleCheckoutGithubGetCheckDetailsRequest(msg) {
4291
+ const { cwd, repoOwner, repoName, checkRunId, workflowRunId, requestId } = msg;
4292
+ try {
4293
+ const details = await this.github.getGitHubCheckDetails({
4294
+ cwd,
4295
+ repoOwner,
4296
+ repoName,
4297
+ checkRunId,
4298
+ workflowRunId,
4299
+ });
4300
+ this.emit({
4301
+ type: "checkout.github.get_check_details.response",
4302
+ payload: {
4303
+ cwd,
4304
+ success: true,
4305
+ details,
4306
+ error: null,
4307
+ requestId,
4308
+ },
4309
+ });
4310
+ }
4311
+ catch (error) {
4312
+ this.emit({
4313
+ type: "checkout.github.get_check_details.response",
4314
+ payload: {
4315
+ cwd,
4316
+ success: false,
4317
+ details: null,
4318
+ error: {
4319
+ code: "UNKNOWN",
4320
+ message: error instanceof Error ? error.message : String(error),
4321
+ },
4322
+ requestId,
4323
+ },
4324
+ });
4325
+ }
4326
+ }
4279
4327
  async handlePaseoWorktreeListRequest(msg) {
4280
4328
  return handleWorktreeListRequest({
4281
4329
  emit: (message) => this.emit(message),
@@ -7135,7 +7183,6 @@ function isValidGitHubRepoSegment(value) {
7135
7183
  return /^[A-Za-z0-9._-]+$/.test(value);
7136
7184
  }
7137
7185
  function toPullRequestTimelinePayloadItem(item) {
7138
- const { authorUrl: _authorUrl, ...payload } = item;
7139
- return payload;
7186
+ return item;
7140
7187
  }
7141
7188
  //# sourceMappingURL=session.js.map
@@ -85,6 +85,7 @@ function createFallbackWorkspaceGitService() {
85
85
  unsubscribe: () => { },
86
86
  }),
87
87
  scheduleRefreshForCwd: () => { },
88
+ onWorkspaceStateMayHaveChanged: () => { },
88
89
  dispose: () => { },
89
90
  };
90
91
  }
@@ -719,6 +720,7 @@ export class VoiceAssistantWebSocketServer {
719
720
  providersSnapshot: true,
720
721
  // COMPAT(checkoutGithubSetAutoMerge): added in v0.1.75, remove gate after 2026-11-13.
721
722
  checkoutGithubSetAutoMerge: true,
723
+ githubCheckDetails: true,
722
724
  // COMPAT(daemonStatusRpc): added in v0.1.76, remove gate after 2026-11-18.
723
725
  daemonStatusRpc: true,
724
726
  // COMPAT(terminalRestoreModes): added in v0.1.81, remove gate after 2026-11-23.
@@ -89,6 +89,7 @@ export interface WorkspaceGitService {
89
89
  unsubscribe: () => void;
90
90
  }>;
91
91
  scheduleRefreshForCwd(cwd: string): void;
92
+ onWorkspaceStateMayHaveChanged(cwd: string): void;
92
93
  dispose(): void;
93
94
  }
94
95
  export type WorkspaceGitListener = (snapshot: WorkspaceGitRuntimeSnapshot) => void;
@@ -201,6 +202,7 @@ export declare class WorkspaceGitServiceImpl implements WorkspaceGitService {
201
202
  unsubscribe: () => void;
202
203
  }>;
203
204
  scheduleRefreshForCwd(cwd: string): void;
205
+ onWorkspaceStateMayHaveChanged(cwd: string): void;
204
206
  dispose(): void;
205
207
  private ensureWorkspaceTarget;
206
208
  private readAuxiliaryCache;
@@ -233,7 +235,8 @@ export declare class WorkspaceGitServiceImpl implements WorkspaceGitService {
233
235
  private normalizeRefreshRequest;
234
236
  private resolveGitHubRemoteForTarget;
235
237
  private shouldThrottleNonForcedRefresh;
236
- private mergeQueuedRefresh;
238
+ private buildScheduledRefreshRequest;
239
+ private mergeRefreshRequests;
237
240
  private runWorkspaceRefreshLoop;
238
241
  private refreshSnapshot;
239
242
  private refreshGitSnapshot;
@@ -12,7 +12,7 @@ import { listPaseoWorktrees } from "../utils/worktree.js";
12
12
  import { READ_ONLY_GIT_ENV } from "./checkout-git-utils.js";
13
13
  import { buildWorkspaceGitMetadataFromSnapshot, } from "./workspace-git-metadata.js";
14
14
  import { checkoutLiteFromGitSnapshot, normalizeWorkspaceId } from "./workspace-registry-model.js";
15
- const WORKSPACE_GIT_WATCH_DEBOUNCE_MS = 500;
15
+ const WORKSPACE_GIT_WATCH_DEBOUNCE_MS = 1000;
16
16
  const BACKGROUND_GIT_FETCH_INTERVAL_MS = 180000;
17
17
  export const WORKSPACE_GIT_SELF_HEAL_INTERVAL_MS = 60000;
18
18
  const WORKING_TREE_WATCH_FALLBACK_REFRESH_MS = 5000;
@@ -295,6 +295,19 @@ export class WorkspaceGitServiceImpl {
295
295
  this.scheduleWorkspaceRefresh(target);
296
296
  }
297
297
  }
298
+ onWorkspaceStateMayHaveChanged(cwd) {
299
+ const normalizedCwd = normalizeWorkspaceId(cwd);
300
+ const target = this.workspaceTargets.get(normalizedCwd);
301
+ if (!target || target.closed) {
302
+ return;
303
+ }
304
+ this.deps.github.invalidate({ cwd: normalizedCwd });
305
+ this.scheduleWorkspaceRefresh(target, {
306
+ force: true,
307
+ includeGitHub: true,
308
+ reason: "external-state-change",
309
+ });
310
+ }
298
311
  dispose() {
299
312
  for (const target of this.workspaceTargets.values()) {
300
313
  this.closeWorkspaceTarget(target);
@@ -384,6 +397,7 @@ export class WorkspaceGitServiceImpl {
384
397
  listeners: new Set(),
385
398
  watchers: [],
386
399
  debounceTimer: null,
400
+ pendingDebounceRequest: null,
387
401
  selfHealTimer: null,
388
402
  githubPollSubscription: null,
389
403
  githubPollKey: null,
@@ -640,6 +654,8 @@ export class WorkspaceGitServiceImpl {
640
654
  if (!target || target.closed || this.workspaceTargets.get(target.cwd) !== target) {
641
655
  return;
642
656
  }
657
+ const request = this.buildScheduledRefreshRequest(options);
658
+ target.pendingDebounceRequest = this.mergeRefreshRequests(target.pendingDebounceRequest, request);
643
659
  if (target.debounceTimer) {
644
660
  clearTimeout(target.debounceTimer);
645
661
  }
@@ -648,12 +664,11 @@ export class WorkspaceGitServiceImpl {
648
664
  return;
649
665
  }
650
666
  target.debounceTimer = null;
651
- void this.refreshWorkspaceTarget(target, {
652
- force: options?.force === true,
653
- includeGitHub: false,
654
- reason: options?.reason ?? "watch",
655
- notify: true,
656
- });
667
+ const merged = target.pendingDebounceRequest;
668
+ target.pendingDebounceRequest = null;
669
+ if (merged) {
670
+ void this.refreshWorkspaceTarget(target, merged);
671
+ }
657
672
  }, WORKSPACE_GIT_WATCH_DEBOUNCE_MS);
658
673
  }
659
674
  startWorkspaceSubscriptionTimers(target) {
@@ -923,7 +938,7 @@ export class WorkspaceGitServiceImpl {
923
938
  const needsForcedRefresh = request.force && !target.refreshState.force;
924
939
  const needsGitHubRefresh = request.force && request.includeGitHub && !target.refreshState.includeGitHub;
925
940
  if (needsForcedRefresh || needsGitHubRefresh) {
926
- target.refreshState.queued = this.mergeQueuedRefresh(target.refreshState.queued, request);
941
+ target.refreshState.queued = this.mergeRefreshRequests(target.refreshState.queued, request);
927
942
  }
928
943
  return target.refreshState.promise;
929
944
  }
@@ -975,23 +990,26 @@ export class WorkspaceGitServiceImpl {
975
990
  }
976
991
  return this.deps.now().getTime() - target.lastShellOutAtMs < WORKSPACE_GIT_INTERNAL_MIN_GAP_MS;
977
992
  }
978
- mergeQueuedRefresh(queued, request) {
979
- if (!queued) {
980
- return {
981
- force: request.force,
982
- includeGitHub: request.includeGitHub,
983
- reason: request.reason,
984
- notify: request.notify,
985
- };
993
+ buildScheduledRefreshRequest(options) {
994
+ return {
995
+ force: options?.force === true,
996
+ includeGitHub: options?.includeGitHub ?? false,
997
+ reason: options?.reason ?? "watch",
998
+ notify: true,
999
+ };
1000
+ }
1001
+ mergeRefreshRequests(pending, request) {
1002
+ if (!pending) {
1003
+ return request;
986
1004
  }
987
- const force = queued.force || request.force;
988
- const upgradesForce = request.force && !queued.force;
989
- const upgradesGitHub = request.includeGitHub && !queued.includeGitHub;
1005
+ const force = pending.force || request.force;
1006
+ const upgradesForce = request.force && !pending.force;
1007
+ const upgradesGitHub = request.includeGitHub && !pending.includeGitHub;
990
1008
  return {
991
1009
  force,
992
- includeGitHub: queued.includeGitHub || request.includeGitHub,
993
- reason: upgradesForce || upgradesGitHub ? request.reason : queued.reason,
994
- notify: queued.notify || request.notify,
1010
+ includeGitHub: pending.includeGitHub || request.includeGitHub,
1011
+ reason: upgradesForce || upgradesGitHub ? request.reason : pending.reason,
1012
+ notify: pending.notify || request.notify,
995
1013
  };
996
1014
  }
997
1015
  async runWorkspaceRefreshLoop(target, initialRequest) {