@junctionpanel/server 0.1.53 → 0.1.55

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 (71) hide show
  1. package/dist/server/client/daemon-client.d.ts +30 -1
  2. package/dist/server/client/daemon-client.d.ts.map +1 -1
  3. package/dist/server/client/daemon-client.js +236 -24
  4. package/dist/server/client/daemon-client.js.map +1 -1
  5. package/dist/server/server/agent/activity-curator.d.ts.map +1 -1
  6. package/dist/server/server/agent/activity-curator.js +12 -0
  7. package/dist/server/server/agent/activity-curator.js.map +1 -1
  8. package/dist/server/server/agent/agent-manager.d.ts +8 -0
  9. package/dist/server/server/agent/agent-manager.d.ts.map +1 -1
  10. package/dist/server/server/agent/agent-manager.js +175 -29
  11. package/dist/server/server/agent/agent-manager.js.map +1 -1
  12. package/dist/server/server/agent/agent-metadata-generator.d.ts.map +1 -1
  13. package/dist/server/server/agent/agent-metadata-generator.js +11 -2
  14. package/dist/server/server/agent/agent-metadata-generator.js.map +1 -1
  15. package/dist/server/server/agent/agent-projections.d.ts.map +1 -1
  16. package/dist/server/server/agent/agent-projections.js +0 -1
  17. package/dist/server/server/agent/agent-projections.js.map +1 -1
  18. package/dist/server/server/agent/agent-response-loop.d.ts +27 -0
  19. package/dist/server/server/agent/agent-response-loop.d.ts.map +1 -1
  20. package/dist/server/server/agent/agent-response-loop.js +78 -4
  21. package/dist/server/server/agent/agent-response-loop.js.map +1 -1
  22. package/dist/server/server/agent/agent-sdk-types.d.ts +17 -1
  23. package/dist/server/server/agent/agent-sdk-types.d.ts.map +1 -1
  24. package/dist/server/server/agent/agent-sdk-types.js.map +1 -1
  25. package/dist/server/server/agent/agent-storage.d.ts +0 -3
  26. package/dist/server/server/agent/agent-storage.d.ts.map +1 -1
  27. package/dist/server/server/agent/agent-storage.js +0 -1
  28. package/dist/server/server/agent/agent-storage.js.map +1 -1
  29. package/dist/server/server/agent/provider-model-cache.d.ts +14 -0
  30. package/dist/server/server/agent/provider-model-cache.d.ts.map +1 -0
  31. package/dist/server/server/agent/provider-model-cache.js +71 -0
  32. package/dist/server/server/agent/provider-model-cache.js.map +1 -0
  33. package/dist/server/server/agent/providers/claude/model-catalog.d.ts.map +1 -1
  34. package/dist/server/server/agent/providers/claude/model-catalog.js +15 -1
  35. package/dist/server/server/agent/providers/claude/model-catalog.js.map +1 -1
  36. package/dist/server/server/agent/providers/claude-agent.d.ts +1 -0
  37. package/dist/server/server/agent/providers/claude-agent.d.ts.map +1 -1
  38. package/dist/server/server/agent/providers/claude-agent.js +43 -12
  39. package/dist/server/server/agent/providers/claude-agent.js.map +1 -1
  40. package/dist/server/server/agent/providers/gemini-agent.d.ts.map +1 -1
  41. package/dist/server/server/agent/providers/gemini-agent.js +36 -1
  42. package/dist/server/server/agent/providers/gemini-agent.js.map +1 -1
  43. package/dist/server/server/agent/providers/opencode-agent.js +8 -2
  44. package/dist/server/server/agent/providers/opencode-agent.js.map +1 -1
  45. package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts.map +1 -1
  46. package/dist/server/server/file-explorer/service.d.ts +6 -1
  47. package/dist/server/server/file-explorer/service.d.ts.map +1 -1
  48. package/dist/server/server/file-explorer/service.js +72 -23
  49. package/dist/server/server/file-explorer/service.js.map +1 -1
  50. package/dist/server/server/persistence-hooks.js +11 -1
  51. package/dist/server/server/persistence-hooks.js.map +1 -1
  52. package/dist/server/server/session.d.ts +38 -0
  53. package/dist/server/server/session.d.ts.map +1 -1
  54. package/dist/server/server/session.js +956 -207
  55. package/dist/server/server/session.js.map +1 -1
  56. package/dist/server/server/tool-call-preview.d.ts.map +1 -1
  57. package/dist/server/server/tool-call-preview.js +5 -3
  58. package/dist/server/server/tool-call-preview.js.map +1 -1
  59. package/dist/server/shared/messages.d.ts +7630 -2138
  60. package/dist/server/shared/messages.d.ts.map +1 -1
  61. package/dist/server/shared/messages.js +64 -0
  62. package/dist/server/shared/messages.js.map +1 -1
  63. package/dist/server/shared/permission-questions.d.ts +43 -0
  64. package/dist/server/shared/permission-questions.d.ts.map +1 -0
  65. package/dist/server/shared/permission-questions.js +234 -0
  66. package/dist/server/shared/permission-questions.js.map +1 -0
  67. package/dist/server/utils/checkout-git.d.ts +1 -0
  68. package/dist/server/utils/checkout-git.d.ts.map +1 -1
  69. package/dist/server/utils/checkout-git.js +19 -6
  70. package/dist/server/utils/checkout-git.js.map +1 -1
  71. package/package.json +2 -2
@@ -1,5 +1,5 @@
1
1
  import { v4 as uuidv4 } from 'uuid';
2
- import { existsSync, watch } from 'node:fs';
2
+ import { existsSync, statSync, watch } from 'node:fs';
3
3
  import { exec } from 'child_process';
4
4
  import { promisify } from 'util';
5
5
  import { resolve, sep, basename, dirname, parse as parsePath } from 'path';
@@ -17,13 +17,13 @@ import { sanitizeTimelineItemForTransport, } from './tool-call-preview.js';
17
17
  import { toAgentPayload, resolveEffectiveThinkingOptionId, toStoredAgentPayload, } from './agent/agent-projections.js';
18
18
  import { appendTimelineItemIfAgentKnown, emitLiveTimelineItemIfAgentKnown, } from './agent/timeline-append.js';
19
19
  import { projectTimelineRows, selectTimelineWindowByProjectedLimit, } from './agent/timeline-projection.js';
20
- import { DEFAULT_STRUCTURED_GENERATION_PROVIDERS, StructuredAgentFallbackError, StructuredAgentResponseError, generateStructuredAgentResponseWithFallback, } from './agent/agent-response-loop.js';
20
+ import { DEFAULT_STRUCTURED_GENERATION_PROVIDERS, StructuredAgentFallbackError, StructuredAgentResponseError, generateStructuredAgentResponseWithFallback, resolveStructuredGenerationPreferredProviderForCwd, resolveStructuredGenerationProviders, } from './agent/agent-response-loop.js';
21
21
  import { isValidAgentProvider, AGENT_PROVIDER_IDS } from './agent/provider-manifest.js';
22
22
  import { listDirectoryEntries, readExplorerFile, getDownloadableFileInfo, isWorkspaceExplorerMissingPathError, } from './file-explorer/service.js';
23
23
  import { slugify, validateBranchSlug, listJunctionWorktrees, deleteJunctionWorktree, isJunctionOwnedWorktreeCwd, resolveJunctionWorktreeRootForCwd, createInRepoWorktree, attachInRepoWorktree, restoreInRepoWorktree, } from '../utils/worktree.js';
24
24
  import { readJunctionWorktreeMetadata } from '../utils/worktree-metadata.js';
25
25
  import { runAsyncWorktreeBootstrap } from './worktree-bootstrap.js';
26
- import { getCheckoutDiff, getCheckoutStatus, getCheckoutStatusLite, listBranchSuggestions, NotGitRepoError, MergeConflictError, MergeFromBaseConflictError, commitChanges, mergeToBase, mergeFromBase, pushCurrentBranch, createPullRequest, getPullRequestFailureLogs, getPullRequestStatus, searchPullRequests, listGitRemotes, mergePullRequest, resolveBaseRefWithSource, } from '../utils/checkout-git.js';
26
+ import { getCheckoutDiff, getCheckoutStatus, getCheckoutStatusLite, listBranchSuggestions, NotGitRepoError, MergeConflictError, MergeFromBaseConflictError, commitChanges, mergeToBase, mergeFromBase, pushCurrentBranch, createPullRequest, getPullRequestFailureLogs, getPullRequestStatus, searchPullRequests, listGitRemotes, mergePullRequest, resolveMergeToBaseOperationCwd, resolveBaseRefWithSource, } from '../utils/checkout-git.js';
27
27
  import { getProjectIcon } from '../utils/project-icon.js';
28
28
  import { expandTilde } from '../utils/path.js';
29
29
  import { searchHomeDirectories, searchWorkspaceEntries, searchWorkspaceEntriesAtGitRef, searchGitRepositories, checkIsGitRepo, } from '../utils/directory-suggestions.js';
@@ -48,9 +48,14 @@ const READ_ONLY_GIT_ENV = {
48
48
  const DEFAULT_STORED_TIMELINE_FETCH_LIMIT = 200;
49
49
  const pendingAgentInitializations = new Map();
50
50
  const pendingAgentMessageExecutions = new Map();
51
+ const sharedWorkspaceGitOperationStates = new Map();
51
52
  const DEFAULT_AGENT_PROVIDER = AGENT_PROVIDER_IDS[0];
52
53
  const CHECKOUT_DIFF_WATCH_DEBOUNCE_MS = 150;
53
54
  const CHECKOUT_DIFF_FALLBACK_REFRESH_MS = 5000;
55
+ const WORKSPACE_STATUS_WATCH_DEBOUNCE_MS = 250;
56
+ const WORKSPACE_STATUS_GIT_REFRESH_MS = 3000;
57
+ const WORKSPACE_STATUS_PR_ACTIVE_REFRESH_MS = 15000;
58
+ const WORKSPACE_STATUS_PR_PASSIVE_REFRESH_MS = 60000;
54
59
  const TERMINAL_STREAM_WINDOW_BYTES = 256 * 1024;
55
60
  const TERMINAL_STREAM_MAX_PENDING_BYTES = 2 * 1024 * 1024;
56
61
  const TERMINAL_STREAM_MAX_PENDING_CHUNKS = 2048;
@@ -119,6 +124,8 @@ export class Session {
119
124
  this.clientActivity = null;
120
125
  this.MOBILE_BACKGROUND_STREAM_GRACE_MS = 60000;
121
126
  this.subscribedTerminalDirectories = new Set();
127
+ this.workspaceStatusSubscriptions = new Map();
128
+ this.workspaceStatusTargets = new Map();
122
129
  this.unsubscribeTerminalsChanged = null;
123
130
  this.terminalSubscriptions = new Map();
124
131
  this.terminalExitSubscriptions = new Map();
@@ -127,6 +134,7 @@ export class Session {
127
134
  this.nextTerminalStreamId = 1;
128
135
  this.checkoutDiffSubscriptions = new Map();
129
136
  this.checkoutDiffTargets = new Map();
137
+ this.workspaceGitOperationStates = sharedWorkspaceGitOperationStates;
130
138
  const { clientId, userId, onMessage, onBinaryMessage, onLifecycleIntent, logger, downloadTokenStore, junctionHome, agentManager, agentStorage, createAgentMcpTransport, terminalManager, agentProviderRuntimeSettings, } = options;
131
139
  this.clientId = clientId;
132
140
  this.userId = userId;
@@ -303,7 +311,10 @@ export class Session {
303
311
  this.sessionLogger.error({ err: error, agentId: input.agentId }, `Failed to record user message for agent ${input.agentId}`);
304
312
  }
305
313
  const prompt = this.buildAgentPrompt(input.text, input.images);
306
- const started = this.startAgentStream(input.agentId, prompt, input.runOptions);
314
+ const started = this.startAgentStream(input.agentId, prompt, {
315
+ ...input.runOptions,
316
+ ...(input.messageId ? { messageId: input.messageId } : {}),
317
+ });
307
318
  if (!started.ok) {
308
319
  throw new Error(started.error);
309
320
  }
@@ -863,6 +874,12 @@ export class Session {
863
874
  case 'checkout_status_request':
864
875
  await this.handleCheckoutStatusRequest(msg);
865
876
  break;
877
+ case 'subscribe_workspace_status_request':
878
+ await this.handleSubscribeWorkspaceStatusRequest(msg);
879
+ break;
880
+ case 'unsubscribe_workspace_status_request':
881
+ this.handleUnsubscribeWorkspaceStatusRequest(msg);
882
+ break;
866
883
  case 'validate_branch_request':
867
884
  await this.handleValidateBranchRequest(msg);
868
885
  break;
@@ -2339,8 +2356,53 @@ export class Session {
2339
2356
  }
2340
2357
  return resolvedCandidate.startsWith(resolvedRoot + sep);
2341
2358
  }
2342
- async generateCommitMessage(cwd) {
2343
- const diff = await getCheckoutDiff(cwd, { mode: 'uncommitted', includeStructured: true }, { junctionHome: this.junctionHome });
2359
+ async resolvePreferredStructuredProviderForCwd(cwd) {
2360
+ const resolvedCwd = expandTilde(cwd);
2361
+ const storedRecords = new Map((await this.agentStorage.list()).map((record) => [record.id, record]));
2362
+ const liveCandidates = this.agentManager.listAgents().map((agent) => {
2363
+ const storedRecord = storedRecords.get(agent.id);
2364
+ return {
2365
+ provider: agent.provider,
2366
+ cwd: agent.cwd,
2367
+ lastActivityAt: agent.lastUserMessageAt ?? agent.updatedAt,
2368
+ updatedAt: agent.updatedAt,
2369
+ archivedAt: storedRecord?.archivedAt ?? null,
2370
+ internal: agent.internal ?? storedRecord?.internal ?? false,
2371
+ };
2372
+ });
2373
+ const storedCandidates = Array.from(storedRecords.values()).map((record) => ({
2374
+ provider: record.provider,
2375
+ cwd: record.cwd,
2376
+ lastActivityAt: record.lastActivityAt ?? record.lastUserMessageAt ?? record.updatedAt,
2377
+ updatedAt: record.updatedAt,
2378
+ archivedAt: record.archivedAt ?? null,
2379
+ internal: record.internal ?? false,
2380
+ }));
2381
+ return resolveStructuredGenerationPreferredProviderForCwd({
2382
+ cwd: resolvedCwd,
2383
+ candidates: [...liveCandidates, ...storedCandidates],
2384
+ });
2385
+ }
2386
+ async resolveStructuredHelperProviders(cwd, reason) {
2387
+ let preferredProvider = null;
2388
+ try {
2389
+ preferredProvider = await this.resolvePreferredStructuredProviderForCwd(cwd);
2390
+ }
2391
+ catch (error) {
2392
+ this.sessionLogger.warn({ err: error, cwd, reason }, 'Failed to resolve preferred provider for structured helper generation');
2393
+ }
2394
+ return {
2395
+ preferredProvider,
2396
+ providers: resolveStructuredGenerationProviders({
2397
+ preferredProvider,
2398
+ reason,
2399
+ providers: DEFAULT_STRUCTURED_GENERATION_PROVIDERS,
2400
+ }),
2401
+ };
2402
+ }
2403
+ async generateCommitMessage(cwd, diffOverride) {
2404
+ const diff = diffOverride ??
2405
+ (await getCheckoutDiff(cwd, { mode: 'uncommitted', includeStructured: true }, { junctionHome: this.junctionHome }));
2344
2406
  const schema = z.object({
2345
2407
  message: z
2346
2408
  .string()
@@ -2371,6 +2433,7 @@ export class Session {
2371
2433
  patch.length > 0 ? patch : '(No diff available)',
2372
2434
  ].join('\n');
2373
2435
  try {
2436
+ const { preferredProvider, providers } = await this.resolveStructuredHelperProviders(cwd, 'commit');
2374
2437
  const result = await generateStructuredAgentResponseWithFallback({
2375
2438
  manager: this.agentManager,
2376
2439
  cwd,
@@ -2378,7 +2441,9 @@ export class Session {
2378
2441
  schema,
2379
2442
  schemaName: 'CommitMessage',
2380
2443
  maxRetries: 2,
2381
- providers: DEFAULT_STRUCTURED_GENERATION_PROVIDERS,
2444
+ providers,
2445
+ preferredProvider,
2446
+ reason: 'commit',
2382
2447
  agentConfigOverrides: {
2383
2448
  title: 'Commit generator',
2384
2449
  internal: true,
@@ -2394,12 +2459,13 @@ export class Session {
2394
2459
  throw error;
2395
2460
  }
2396
2461
  }
2397
- async generatePullRequestText(cwd, baseRef) {
2398
- const diff = await getCheckoutDiff(cwd, {
2399
- mode: 'base',
2400
- baseRef,
2401
- includeStructured: true,
2402
- }, { junctionHome: this.junctionHome });
2462
+ async generatePullRequestText(cwd, baseRef, diffOverride) {
2463
+ const diff = diffOverride ??
2464
+ (await getCheckoutDiff(cwd, {
2465
+ mode: 'base',
2466
+ baseRef,
2467
+ includeStructured: true,
2468
+ }, { junctionHome: this.junctionHome }));
2403
2469
  const schema = z.object({
2404
2470
  title: z.string().min(1).max(72),
2405
2471
  body: z.string().min(1),
@@ -2427,6 +2493,7 @@ export class Session {
2427
2493
  patch.length > 0 ? patch : '(No diff available)',
2428
2494
  ].join('\n');
2429
2495
  try {
2496
+ const { preferredProvider, providers } = await this.resolveStructuredHelperProviders(cwd, 'pr');
2430
2497
  return await generateStructuredAgentResponseWithFallback({
2431
2498
  manager: this.agentManager,
2432
2499
  cwd,
@@ -2434,7 +2501,9 @@ export class Session {
2434
2501
  schema,
2435
2502
  schemaName: 'PullRequest',
2436
2503
  maxRetries: 2,
2437
- providers: DEFAULT_STRUCTURED_GENERATION_PROVIDERS,
2504
+ providers,
2505
+ preferredProvider,
2506
+ reason: 'pr',
2438
2507
  agentConfigOverrides: {
2439
2508
  title: 'PR generator',
2440
2509
  internal: true,
@@ -2702,101 +2771,192 @@ export class Session {
2702
2771
  throw error;
2703
2772
  }
2704
2773
  }
2774
+ buildCheckoutStatusPayload(cwd, status) {
2775
+ if (!status.isGit) {
2776
+ return {
2777
+ cwd,
2778
+ isGit: false,
2779
+ repoRoot: null,
2780
+ currentBranch: null,
2781
+ isDirty: null,
2782
+ baseRef: null,
2783
+ aheadBehind: null,
2784
+ aheadOfOrigin: null,
2785
+ behindOfOrigin: null,
2786
+ hasUpstream: false,
2787
+ upstreamBranch: null,
2788
+ hasRemote: false,
2789
+ remoteUrl: null,
2790
+ isJunctionOwnedWorktree: false,
2791
+ error: null,
2792
+ };
2793
+ }
2794
+ if (status.isJunctionOwnedWorktree) {
2795
+ return {
2796
+ cwd,
2797
+ isGit: true,
2798
+ repoRoot: status.repoRoot ?? null,
2799
+ mainRepoRoot: status.mainRepoRoot,
2800
+ currentBranch: status.currentBranch ?? null,
2801
+ isDirty: status.isDirty ?? null,
2802
+ baseRef: status.baseRef,
2803
+ aheadBehind: status.aheadBehind ?? null,
2804
+ aheadOfOrigin: status.aheadOfOrigin ?? null,
2805
+ behindOfOrigin: status.behindOfOrigin ?? null,
2806
+ hasUpstream: status.hasUpstream,
2807
+ upstreamBranch: status.upstreamBranch ?? null,
2808
+ hasRemote: status.hasRemote,
2809
+ remoteUrl: status.remoteUrl,
2810
+ isJunctionOwnedWorktree: true,
2811
+ error: null,
2812
+ };
2813
+ }
2814
+ return {
2815
+ cwd,
2816
+ isGit: true,
2817
+ repoRoot: status.repoRoot ?? null,
2818
+ currentBranch: status.currentBranch ?? null,
2819
+ isDirty: status.isDirty ?? null,
2820
+ baseRef: status.baseRef ?? null,
2821
+ aheadBehind: status.aheadBehind ?? null,
2822
+ aheadOfOrigin: status.aheadOfOrigin ?? null,
2823
+ behindOfOrigin: status.behindOfOrigin ?? null,
2824
+ hasUpstream: status.hasUpstream,
2825
+ upstreamBranch: status.upstreamBranch ?? null,
2826
+ hasRemote: status.hasRemote,
2827
+ remoteUrl: status.remoteUrl,
2828
+ isJunctionOwnedWorktree: false,
2829
+ error: null,
2830
+ };
2831
+ }
2832
+ buildCheckoutStatusErrorPayload(cwd, error) {
2833
+ return {
2834
+ cwd,
2835
+ isGit: false,
2836
+ repoRoot: null,
2837
+ currentBranch: null,
2838
+ isDirty: null,
2839
+ baseRef: null,
2840
+ aheadBehind: null,
2841
+ aheadOfOrigin: null,
2842
+ behindOfOrigin: null,
2843
+ hasUpstream: false,
2844
+ upstreamBranch: null,
2845
+ hasRemote: false,
2846
+ remoteUrl: null,
2847
+ isJunctionOwnedWorktree: false,
2848
+ error: this.toCheckoutError(error),
2849
+ requestId: '',
2850
+ };
2851
+ }
2852
+ buildCheckoutStatusLitePayload(cwd, repoRoot, status) {
2853
+ if (!status.isGit) {
2854
+ return {
2855
+ cwd,
2856
+ isGit: false,
2857
+ repoRoot: null,
2858
+ currentBranch: null,
2859
+ isDirty: null,
2860
+ baseRef: null,
2861
+ aheadBehind: null,
2862
+ aheadOfOrigin: null,
2863
+ behindOfOrigin: null,
2864
+ hasUpstream: false,
2865
+ upstreamBranch: null,
2866
+ hasRemote: Boolean(status.remoteUrl),
2867
+ remoteUrl: status.remoteUrl,
2868
+ isJunctionOwnedWorktree: false,
2869
+ error: null,
2870
+ };
2871
+ }
2872
+ return {
2873
+ cwd,
2874
+ isGit: true,
2875
+ repoRoot,
2876
+ currentBranch: status.currentBranch ?? null,
2877
+ isDirty: false,
2878
+ baseRef: null,
2879
+ aheadBehind: null,
2880
+ aheadOfOrigin: null,
2881
+ behindOfOrigin: null,
2882
+ hasUpstream: false,
2883
+ upstreamBranch: null,
2884
+ hasRemote: Boolean(status.remoteUrl),
2885
+ remoteUrl: status.remoteUrl,
2886
+ isJunctionOwnedWorktree: false,
2887
+ error: null,
2888
+ };
2889
+ }
2890
+ toWorkspaceGitStatus(payload) {
2891
+ const normalizedPayload = payload;
2892
+ const { cwd: _cwd, error: _error, requestId: _requestId, ...git } = normalizedPayload;
2893
+ return git;
2894
+ }
2895
+ async resolveWorkspaceGitSnapshot(cwd, options) {
2896
+ try {
2897
+ const payload = await (async () => {
2898
+ if (!options?.lite) {
2899
+ return this.buildCheckoutStatusPayload(cwd, await getCheckoutStatus(expandTilde(cwd), { junctionHome: this.junctionHome }));
2900
+ }
2901
+ const liteStatus = await getCheckoutStatusLite(expandTilde(cwd), {
2902
+ junctionHome: this.junctionHome,
2903
+ });
2904
+ if (liteStatus.isGit && liteStatus.isJunctionOwnedWorktree) {
2905
+ return this.buildCheckoutStatusPayload(cwd, await getCheckoutStatus(expandTilde(cwd), { junctionHome: this.junctionHome }));
2906
+ }
2907
+ return this.buildCheckoutStatusLitePayload(cwd, options.repoRoot ?? cwd, liteStatus);
2908
+ })();
2909
+ return {
2910
+ git: this.toWorkspaceGitStatus(payload),
2911
+ gitError: payload.error,
2912
+ };
2913
+ }
2914
+ catch (error) {
2915
+ const payload = this.buildCheckoutStatusErrorPayload(cwd, error);
2916
+ return {
2917
+ git: this.toWorkspaceGitStatus(payload),
2918
+ gitError: payload.error,
2919
+ };
2920
+ }
2921
+ }
2922
+ async resolveWorkspacePullRequestSnapshot(cwd, remoteName) {
2923
+ try {
2924
+ const prStatus = await getPullRequestStatus(expandTilde(cwd), {
2925
+ ...(remoteName ? { remoteName } : {}),
2926
+ });
2927
+ return {
2928
+ pullRequest: prStatus.status,
2929
+ pullRequestError: null,
2930
+ githubFeaturesEnabled: prStatus.githubFeaturesEnabled,
2931
+ };
2932
+ }
2933
+ catch (error) {
2934
+ return {
2935
+ pullRequest: null,
2936
+ pullRequestError: this.toCheckoutError(error),
2937
+ githubFeaturesEnabled: true,
2938
+ };
2939
+ }
2940
+ }
2705
2941
  async handleCheckoutStatusRequest(msg) {
2706
2942
  const { cwd, requestId } = msg;
2707
- const resolvedCwd = expandTilde(cwd);
2708
2943
  try {
2709
- const status = await getCheckoutStatus(resolvedCwd, { junctionHome: this.junctionHome });
2710
- if (!status.isGit) {
2711
- this.emit({
2712
- type: 'checkout_status_response',
2713
- payload: {
2714
- cwd,
2715
- isGit: false,
2716
- repoRoot: null,
2717
- currentBranch: null,
2718
- isDirty: null,
2719
- baseRef: null,
2720
- aheadBehind: null,
2721
- aheadOfOrigin: null,
2722
- behindOfOrigin: null,
2723
- hasUpstream: false,
2724
- upstreamBranch: null,
2725
- hasRemote: false,
2726
- remoteUrl: null,
2727
- isJunctionOwnedWorktree: false,
2728
- error: null,
2729
- requestId,
2730
- },
2731
- });
2732
- return;
2733
- }
2734
- if (status.isJunctionOwnedWorktree) {
2735
- this.emit({
2736
- type: 'checkout_status_response',
2737
- payload: {
2738
- cwd,
2739
- isGit: true,
2740
- repoRoot: status.repoRoot ?? null,
2741
- mainRepoRoot: status.mainRepoRoot,
2742
- currentBranch: status.currentBranch ?? null,
2743
- isDirty: status.isDirty ?? null,
2744
- baseRef: status.baseRef,
2745
- aheadBehind: status.aheadBehind ?? null,
2746
- aheadOfOrigin: status.aheadOfOrigin ?? null,
2747
- behindOfOrigin: status.behindOfOrigin ?? null,
2748
- hasUpstream: status.hasUpstream,
2749
- upstreamBranch: status.upstreamBranch ?? null,
2750
- hasRemote: status.hasRemote,
2751
- remoteUrl: status.remoteUrl,
2752
- isJunctionOwnedWorktree: true,
2753
- error: null,
2754
- requestId,
2755
- },
2756
- });
2757
- return;
2758
- }
2944
+ const resolvedCwd = expandTilde(cwd);
2945
+ const status = await this.runWorkspaceGitRead(resolvedCwd, () => getCheckoutStatus(resolvedCwd, { junctionHome: this.junctionHome }));
2759
2946
  this.emit({
2760
2947
  type: 'checkout_status_response',
2761
2948
  payload: {
2762
- cwd,
2763
- isGit: true,
2764
- repoRoot: status.repoRoot ?? null,
2765
- currentBranch: status.currentBranch ?? null,
2766
- isDirty: status.isDirty ?? null,
2767
- baseRef: status.baseRef ?? null,
2768
- aheadBehind: status.aheadBehind ?? null,
2769
- aheadOfOrigin: status.aheadOfOrigin ?? null,
2770
- behindOfOrigin: status.behindOfOrigin ?? null,
2771
- hasUpstream: status.hasUpstream,
2772
- upstreamBranch: status.upstreamBranch ?? null,
2773
- hasRemote: status.hasRemote,
2774
- remoteUrl: status.remoteUrl,
2775
- isJunctionOwnedWorktree: false,
2776
- error: null,
2949
+ ...this.buildCheckoutStatusPayload(cwd, status),
2777
2950
  requestId,
2778
2951
  },
2779
2952
  });
2780
2953
  }
2781
2954
  catch (error) {
2955
+ const payload = this.buildCheckoutStatusErrorPayload(cwd, error);
2782
2956
  this.emit({
2783
2957
  type: 'checkout_status_response',
2784
2958
  payload: {
2785
- cwd,
2786
- isGit: false,
2787
- repoRoot: null,
2788
- currentBranch: null,
2789
- isDirty: null,
2790
- baseRef: null,
2791
- aheadBehind: null,
2792
- aheadOfOrigin: null,
2793
- behindOfOrigin: null,
2794
- hasUpstream: false,
2795
- upstreamBranch: null,
2796
- hasRemote: false,
2797
- remoteUrl: null,
2798
- isJunctionOwnedWorktree: false,
2799
- error: this.toCheckoutError(error),
2959
+ ...payload,
2800
2960
  requestId,
2801
2961
  },
2802
2962
  });
@@ -3114,6 +3274,365 @@ export class Session {
3114
3274
  });
3115
3275
  }
3116
3276
  }
3277
+ buildWorkspaceStatusTargetKey(cwd, remoteName) {
3278
+ return JSON.stringify([cwd, remoteName ?? '']);
3279
+ }
3280
+ closeWorkspaceStatusWatchTarget(target) {
3281
+ if (target.debounceTimer) {
3282
+ clearTimeout(target.debounceTimer);
3283
+ target.debounceTimer = null;
3284
+ }
3285
+ if (target.gitRefreshInterval) {
3286
+ clearInterval(target.gitRefreshInterval);
3287
+ target.gitRefreshInterval = null;
3288
+ }
3289
+ if (target.prRefreshInterval) {
3290
+ clearInterval(target.prRefreshInterval);
3291
+ target.prRefreshInterval = null;
3292
+ }
3293
+ for (const watcher of target.watchers) {
3294
+ watcher.close();
3295
+ }
3296
+ target.watchers = [];
3297
+ }
3298
+ workspaceStatusHasActiveSubscribers(target) {
3299
+ for (const mode of target.subscriptions.values()) {
3300
+ if (mode === 'active') {
3301
+ return true;
3302
+ }
3303
+ }
3304
+ return false;
3305
+ }
3306
+ hasWorkspaceStatusTarget(target) {
3307
+ return this.workspaceStatusTargets.get(target.key) === target && target.subscriptions.size > 0;
3308
+ }
3309
+ workspaceStatusGitIdentityChanged(previousSnapshot, nextGit) {
3310
+ if (!previousSnapshot) {
3311
+ return false;
3312
+ }
3313
+ const previousGit = previousSnapshot.git;
3314
+ if (previousGit.isGit !== nextGit.isGit) {
3315
+ return true;
3316
+ }
3317
+ if (!previousGit.isGit || !nextGit.isGit) {
3318
+ return false;
3319
+ }
3320
+ return (previousGit.currentBranch !== nextGit.currentBranch
3321
+ || previousGit.repoRoot !== nextGit.repoRoot
3322
+ || previousGit.isJunctionOwnedWorktree !== nextGit.isJunctionOwnedWorktree
3323
+ || ((previousGit.isJunctionOwnedWorktree ? previousGit.mainRepoRoot : null)
3324
+ !== (nextGit.isJunctionOwnedWorktree ? nextGit.mainRepoRoot : null)));
3325
+ }
3326
+ updateWorkspaceStatusPrPolling(target) {
3327
+ if (target.prRefreshInterval) {
3328
+ clearInterval(target.prRefreshInterval);
3329
+ target.prRefreshInterval = null;
3330
+ }
3331
+ if (target.subscriptions.size === 0) {
3332
+ return;
3333
+ }
3334
+ const intervalMs = this.resolveWorkspaceStatusPrRefreshIntervalMs(target);
3335
+ target.prRefreshInterval = setInterval(() => {
3336
+ this.scheduleWorkspaceStatusTargetRefresh(target, {
3337
+ includePr: true,
3338
+ debounce: false,
3339
+ });
3340
+ }, intervalMs);
3341
+ }
3342
+ updateWorkspaceStatusGitPolling(target) {
3343
+ if (target.gitRefreshInterval) {
3344
+ clearInterval(target.gitRefreshInterval);
3345
+ target.gitRefreshInterval = null;
3346
+ }
3347
+ if (!this.workspaceStatusHasActiveSubscribers(target)) {
3348
+ return;
3349
+ }
3350
+ target.gitRefreshInterval = setInterval(() => {
3351
+ this.scheduleWorkspaceStatusTargetRefresh(target, {
3352
+ includePr: false,
3353
+ debounce: false,
3354
+ });
3355
+ }, WORKSPACE_STATUS_GIT_REFRESH_MS);
3356
+ }
3357
+ resolveWorkspaceStatusPrRefreshIntervalMs(target) {
3358
+ if (this.workspaceStatusHasActiveSubscribers(target)) {
3359
+ return WORKSPACE_STATUS_PR_ACTIVE_REFRESH_MS;
3360
+ }
3361
+ const pullRequest = target.latestPayload?.pullRequest;
3362
+ const hasOpenPullRequest = pullRequest != null && !pullRequest.isMerged && pullRequest.state.toLowerCase() === 'open';
3363
+ return hasOpenPullRequest
3364
+ ? WORKSPACE_STATUS_PR_ACTIVE_REFRESH_MS
3365
+ : WORKSPACE_STATUS_PR_PASSIVE_REFRESH_MS;
3366
+ }
3367
+ removeWorkspaceStatusSubscription(subscriptionId) {
3368
+ const subscription = this.workspaceStatusSubscriptions.get(subscriptionId);
3369
+ if (!subscription) {
3370
+ return;
3371
+ }
3372
+ this.workspaceStatusSubscriptions.delete(subscriptionId);
3373
+ const target = this.workspaceStatusTargets.get(subscription.targetKey);
3374
+ if (!target) {
3375
+ return;
3376
+ }
3377
+ target.subscriptions.delete(subscriptionId);
3378
+ if (target.subscriptions.size === 0) {
3379
+ this.closeWorkspaceStatusWatchTarget(target);
3380
+ this.workspaceStatusTargets.delete(subscription.targetKey);
3381
+ return;
3382
+ }
3383
+ this.updateWorkspaceStatusGitPolling(target);
3384
+ this.updateWorkspaceStatusPrPolling(target);
3385
+ }
3386
+ scheduleWorkspaceStatusTargetRefresh(target, options) {
3387
+ const includePr = options?.includePr ?? false;
3388
+ const shouldDebounce = options?.debounce ?? true;
3389
+ if (!shouldDebounce) {
3390
+ void this.refreshWorkspaceStatusTarget(target, { includePr });
3391
+ return;
3392
+ }
3393
+ if (target.debounceTimer) {
3394
+ clearTimeout(target.debounceTimer);
3395
+ }
3396
+ if (includePr) {
3397
+ target.refreshIncludesPr = true;
3398
+ }
3399
+ target.debounceTimer = setTimeout(() => {
3400
+ target.debounceTimer = null;
3401
+ void this.refreshWorkspaceStatusTarget(target, {
3402
+ includePr: includePr || target.refreshIncludesPr,
3403
+ });
3404
+ }, WORKSPACE_STATUS_WATCH_DEBOUNCE_MS);
3405
+ }
3406
+ workspaceStatusSnapshotFingerprint(snapshot) {
3407
+ return JSON.stringify({
3408
+ cwd: snapshot.cwd,
3409
+ remoteName: snapshot.remoteName,
3410
+ git: snapshot.git,
3411
+ gitError: snapshot.gitError,
3412
+ pullRequest: snapshot.pullRequest,
3413
+ pullRequestError: snapshot.pullRequestError,
3414
+ githubFeaturesEnabled: snapshot.githubFeaturesEnabled,
3415
+ });
3416
+ }
3417
+ emitWorkspaceStatusChanged(_target, snapshot) {
3418
+ this.emit({
3419
+ type: 'workspace_status_changed',
3420
+ payload: snapshot,
3421
+ });
3422
+ }
3423
+ async computeWorkspaceStatusSnapshot(target, options) {
3424
+ const hasExplicitIncludePr = options != null && 'includePr' in options;
3425
+ const includePr = hasExplicitIncludePr ? Boolean(options?.includePr) : !target.latestPayload;
3426
+ const fullGit = options?.fullGit ?? this.workspaceStatusHasActiveSubscribers(target);
3427
+ const gitSnapshot = await this.resolveWorkspaceGitSnapshot(target.cwd, {
3428
+ lite: !fullGit,
3429
+ repoRoot: target.diffCwd || target.cwd,
3430
+ });
3431
+ const gitIdentityChanged = this.workspaceStatusGitIdentityChanged(target.latestPayload, gitSnapshot.git);
3432
+ let pullRequest = target.latestPayload?.pullRequest ?? null;
3433
+ let pullRequestError = target.latestPayload?.pullRequestError ?? null;
3434
+ let githubFeaturesEnabled = target.latestPayload?.githubFeaturesEnabled ?? false;
3435
+ if (!gitSnapshot.git.isGit) {
3436
+ pullRequest = null;
3437
+ pullRequestError = null;
3438
+ githubFeaturesEnabled = false;
3439
+ }
3440
+ else if (includePr) {
3441
+ const prSnapshot = await this.resolveWorkspacePullRequestSnapshot(target.cwd, target.remoteName);
3442
+ pullRequest = prSnapshot.pullRequest;
3443
+ pullRequestError = prSnapshot.pullRequestError;
3444
+ githubFeaturesEnabled = prSnapshot.githubFeaturesEnabled;
3445
+ }
3446
+ else if (gitIdentityChanged) {
3447
+ pullRequest = null;
3448
+ pullRequestError = null;
3449
+ githubFeaturesEnabled = false;
3450
+ }
3451
+ return {
3452
+ cwd: target.cwd,
3453
+ remoteName: target.remoteName,
3454
+ git: gitSnapshot.git,
3455
+ gitError: gitSnapshot.gitError,
3456
+ pullRequest,
3457
+ pullRequestError,
3458
+ githubFeaturesEnabled,
3459
+ refreshedAt: new Date().toISOString(),
3460
+ };
3461
+ }
3462
+ async refreshWorkspaceStatusTarget(target, options) {
3463
+ if (target.refreshPromise) {
3464
+ target.refreshQueued = true;
3465
+ target.refreshIncludesPr = target.refreshIncludesPr || Boolean(options?.includePr);
3466
+ return;
3467
+ }
3468
+ target.refreshPromise = (async () => {
3469
+ do {
3470
+ const hasExplicitIncludePr = options != null && 'includePr' in options;
3471
+ const fullGit = this.workspaceStatusHasActiveSubscribers(target);
3472
+ const previousHasLoadedPullRequest = target.hasLoadedPullRequest;
3473
+ const includePr = target.refreshIncludesPr
3474
+ || (hasExplicitIncludePr ? Boolean(options?.includePr) : !target.latestPayload);
3475
+ target.refreshQueued = false;
3476
+ target.refreshIncludesPr = false;
3477
+ const snapshot = await this.computeWorkspaceStatusSnapshot(target, {
3478
+ includePr,
3479
+ fullGit,
3480
+ });
3481
+ if (!this.hasWorkspaceStatusTarget(target)) {
3482
+ return;
3483
+ }
3484
+ const activeSubscribersPresent = this.workspaceStatusHasActiveSubscribers(target);
3485
+ const gitIdentityChanged = this.workspaceStatusGitIdentityChanged(target.latestPayload, snapshot.git);
3486
+ const needsPrReplay = (!includePr && target.refreshIncludesPr)
3487
+ || (!includePr && fullGit && activeSubscribersPresent && gitIdentityChanged);
3488
+ const needsFullGitReplay = !fullGit && activeSubscribersPresent;
3489
+ if (needsPrReplay
3490
+ || needsFullGitReplay) {
3491
+ target.refreshQueued = true;
3492
+ target.refreshIncludesPr = target.refreshIncludesPr || needsPrReplay || needsFullGitReplay;
3493
+ continue;
3494
+ }
3495
+ target.latestPayload = snapshot;
3496
+ target.hasLoadedPullRequest =
3497
+ !snapshot.git.isGit
3498
+ || includePr
3499
+ || (previousHasLoadedPullRequest && !gitIdentityChanged);
3500
+ target.latestPayloadHasFullGitData = !snapshot.git.isGit || fullGit;
3501
+ this.updateWorkspaceStatusPrPolling(target);
3502
+ const fingerprint = this.workspaceStatusSnapshotFingerprint(snapshot);
3503
+ if (fingerprint !== target.latestFingerprint) {
3504
+ target.latestFingerprint = fingerprint;
3505
+ this.emitWorkspaceStatusChanged(target, snapshot);
3506
+ }
3507
+ } while (target.refreshQueued);
3508
+ })();
3509
+ try {
3510
+ await target.refreshPromise;
3511
+ }
3512
+ finally {
3513
+ target.refreshPromise = null;
3514
+ }
3515
+ }
3516
+ async ensureWorkspaceStatusWatchTarget(cwd, remoteName) {
3517
+ const targetKey = this.buildWorkspaceStatusTargetKey(cwd, remoteName);
3518
+ const existing = this.workspaceStatusTargets.get(targetKey);
3519
+ if (existing) {
3520
+ return existing;
3521
+ }
3522
+ const watchRoot = await this.resolveCheckoutWatchRoot(cwd);
3523
+ const target = {
3524
+ key: targetKey,
3525
+ cwd,
3526
+ diffCwd: watchRoot ?? cwd,
3527
+ remoteName,
3528
+ subscriptions: new Map(),
3529
+ watchers: [],
3530
+ gitRefreshInterval: null,
3531
+ prRefreshInterval: null,
3532
+ debounceTimer: null,
3533
+ refreshPromise: null,
3534
+ refreshQueued: false,
3535
+ refreshIncludesPr: false,
3536
+ latestPayload: null,
3537
+ latestFingerprint: null,
3538
+ hasLoadedPullRequest: false,
3539
+ latestPayloadHasFullGitData: false,
3540
+ };
3541
+ const repoWatchPath = watchRoot ?? cwd;
3542
+ const watchPaths = new Set([repoWatchPath]);
3543
+ const gitDir = await this.resolveCheckoutGitDir(cwd);
3544
+ if (gitDir) {
3545
+ watchPaths.add(gitDir);
3546
+ }
3547
+ const watchSetup = this.startCheckoutWatchers({
3548
+ repoWatchPath,
3549
+ watchPaths,
3550
+ onChange: () => {
3551
+ this.scheduleWorkspaceStatusTargetRefresh(target);
3552
+ },
3553
+ recursiveUnavailableMessage: 'Workspace status recursive watch unavailable; using non-recursive fallback',
3554
+ startFailedMessage: 'Failed to start workspace status watcher',
3555
+ watcherErrorMessage: 'Workspace status watcher error',
3556
+ context: { cwd, remoteName },
3557
+ });
3558
+ target.watchers = watchSetup.watchers;
3559
+ if (target.watchers.length === 0 || !watchSetup.hasRecursiveRepoCoverage) {
3560
+ const log = watchSetup.repoWatchPathMissing && watchSetup.failedWatcherCount === 0
3561
+ ? this.sessionLogger.debug.bind(this.sessionLogger)
3562
+ : this.sessionLogger.warn.bind(this.sessionLogger);
3563
+ log({
3564
+ cwd,
3565
+ remoteName,
3566
+ reason: target.watchers.length === 0 ? 'no_watchers' : 'missing_recursive_repo_root_coverage',
3567
+ }, 'Workspace status watchers running with timed refresh support only');
3568
+ }
3569
+ this.workspaceStatusTargets.set(targetKey, target);
3570
+ return target;
3571
+ }
3572
+ async handleSubscribeWorkspaceStatusRequest(msg) {
3573
+ const cwd = expandTilde(msg.cwd);
3574
+ const remoteName = msg.remoteName?.trim() ? msg.remoteName.trim() : null;
3575
+ const mode = msg.mode ?? 'passive';
3576
+ if (remoteName) {
3577
+ this.assertSafeRemoteName(remoteName);
3578
+ }
3579
+ this.removeWorkspaceStatusSubscription(msg.subscriptionId);
3580
+ const target = await this.ensureWorkspaceStatusWatchTarget(cwd, remoteName);
3581
+ target.subscriptions.set(msg.subscriptionId, mode);
3582
+ this.workspaceStatusSubscriptions.set(msg.subscriptionId, {
3583
+ targetKey: target.key,
3584
+ });
3585
+ this.updateWorkspaceStatusGitPolling(target);
3586
+ this.updateWorkspaceStatusPrPolling(target);
3587
+ const fullGit = this.workspaceStatusHasActiveSubscribers(target);
3588
+ const includePr = fullGit;
3589
+ const previousFingerprint = target.latestFingerprint;
3590
+ const hadExistingSubscribers = target.subscriptions.size > 1;
3591
+ const snapshot = target.latestPayload
3592
+ && (!includePr || target.hasLoadedPullRequest)
3593
+ && (!fullGit || target.latestPayloadHasFullGitData)
3594
+ && !(fullGit && target.latestPayload.gitError)
3595
+ ? target.latestPayload
3596
+ : await this.computeWorkspaceStatusSnapshot(target, { includePr, fullGit });
3597
+ const hasLoadedPullRequest = target.hasLoadedPullRequest || includePr || !snapshot.git.isGit;
3598
+ target.latestPayload = snapshot;
3599
+ target.hasLoadedPullRequest = hasLoadedPullRequest;
3600
+ target.latestPayloadHasFullGitData = !snapshot.git.isGit || fullGit;
3601
+ const nextFingerprint = this.workspaceStatusSnapshotFingerprint(snapshot);
3602
+ target.latestFingerprint = nextFingerprint;
3603
+ this.emit({
3604
+ type: 'subscribe_workspace_status_response',
3605
+ payload: {
3606
+ subscriptionId: msg.subscriptionId,
3607
+ ...snapshot,
3608
+ requestId: msg.requestId,
3609
+ },
3610
+ });
3611
+ if (hadExistingSubscribers && nextFingerprint !== previousFingerprint) {
3612
+ this.emitWorkspaceStatusChanged(target, snapshot);
3613
+ }
3614
+ if (!fullGit && snapshot.git.isGit && !hasLoadedPullRequest) {
3615
+ this.scheduleWorkspaceStatusTargetRefresh(target, {
3616
+ includePr: true,
3617
+ debounce: false,
3618
+ });
3619
+ }
3620
+ }
3621
+ handleUnsubscribeWorkspaceStatusRequest(msg) {
3622
+ this.removeWorkspaceStatusSubscription(msg.subscriptionId);
3623
+ }
3624
+ scheduleWorkspaceStatusRefreshForCwd(cwd, options) {
3625
+ const resolvedCwd = expandTilde(cwd);
3626
+ for (const target of this.workspaceStatusTargets.values()) {
3627
+ if (target.cwd !== resolvedCwd && target.diffCwd !== resolvedCwd) {
3628
+ continue;
3629
+ }
3630
+ this.scheduleWorkspaceStatusTargetRefresh(target, {
3631
+ includePr: options?.includePr,
3632
+ debounce: false,
3633
+ });
3634
+ }
3635
+ }
3117
3636
  normalizeCheckoutDiffCompare(compare) {
3118
3637
  if (compare.mode === 'uncommitted' || compare.mode === 'staged' || compare.mode === 'unstaged') {
3119
3638
  return { mode: compare.mode };
@@ -3121,11 +3640,12 @@ export class Session {
3121
3640
  const trimmedBaseRef = compare.baseRef?.trim();
3122
3641
  return trimmedBaseRef ? { mode: 'base', baseRef: trimmedBaseRef } : { mode: 'base' };
3123
3642
  }
3124
- buildCheckoutDiffTargetKey(cwd, compare) {
3643
+ buildCheckoutDiffTargetKey(cwd, compare, detail) {
3125
3644
  return JSON.stringify([
3126
3645
  cwd,
3127
3646
  compare.mode,
3128
3647
  compare.mode === 'base' ? (compare.baseRef ?? '') : '',
3648
+ detail,
3129
3649
  ]);
3130
3650
  }
3131
3651
  closeCheckoutDiffWatchTarget(target) {
@@ -3184,7 +3704,107 @@ export class Session {
3184
3704
  return null;
3185
3705
  }
3186
3706
  }
3707
+ isExpectedMissingWatchPathError(error) {
3708
+ if (!error || typeof error !== 'object') {
3709
+ return false;
3710
+ }
3711
+ const code = 'code' in error && typeof error.code === 'string'
3712
+ ? error.code
3713
+ : null;
3714
+ return code === 'ENOENT' || code === 'ENOTDIR' || code === 'EPERM';
3715
+ }
3716
+ isWatchPathMissing(watchPath) {
3717
+ try {
3718
+ statSync(watchPath);
3719
+ return false;
3720
+ }
3721
+ catch (error) {
3722
+ return this.isExpectedMissingWatchPathError(error);
3723
+ }
3724
+ }
3725
+ startCheckoutWatchers(options) {
3726
+ const watchers = [];
3727
+ let hasRecursiveRepoCoverage = false;
3728
+ let repoWatchPathMissing = false;
3729
+ let failedWatcherCount = 0;
3730
+ const allowRecursiveRepoWatch = process.platform !== 'linux';
3731
+ for (const watchPath of options.watchPaths) {
3732
+ const shouldTryRecursive = watchPath === options.repoWatchPath && allowRecursiveRepoWatch;
3733
+ if (this.isWatchPathMissing(watchPath)) {
3734
+ if (watchPath === options.repoWatchPath) {
3735
+ repoWatchPathMissing = true;
3736
+ }
3737
+ continue;
3738
+ }
3739
+ const createWatcher = (recursive) => watch(watchPath, { recursive }, () => {
3740
+ options.onChange();
3741
+ });
3742
+ let watcher = null;
3743
+ let watcherIsRecursive = false;
3744
+ try {
3745
+ if (shouldTryRecursive) {
3746
+ watcher = createWatcher(true);
3747
+ watcherIsRecursive = true;
3748
+ }
3749
+ else {
3750
+ watcher = createWatcher(false);
3751
+ }
3752
+ }
3753
+ catch (error) {
3754
+ if (shouldTryRecursive) {
3755
+ try {
3756
+ watcher = createWatcher(false);
3757
+ this.sessionLogger.warn({ err: error, watchPath, ...options.context }, options.recursiveUnavailableMessage);
3758
+ }
3759
+ catch (fallbackError) {
3760
+ if (this.isExpectedMissingWatchPathError(fallbackError)
3761
+ && this.isWatchPathMissing(watchPath)) {
3762
+ if (watchPath === options.repoWatchPath) {
3763
+ repoWatchPathMissing = true;
3764
+ }
3765
+ continue;
3766
+ }
3767
+ failedWatcherCount += 1;
3768
+ this.sessionLogger.warn({ err: fallbackError, watchPath, ...options.context }, options.startFailedMessage);
3769
+ }
3770
+ }
3771
+ else {
3772
+ if (this.isExpectedMissingWatchPathError(error) && this.isWatchPathMissing(watchPath)) {
3773
+ if (watchPath === options.repoWatchPath) {
3774
+ repoWatchPathMissing = true;
3775
+ }
3776
+ continue;
3777
+ }
3778
+ failedWatcherCount += 1;
3779
+ this.sessionLogger.warn({ err: error, watchPath, ...options.context }, options.startFailedMessage);
3780
+ }
3781
+ }
3782
+ if (!watcher) {
3783
+ continue;
3784
+ }
3785
+ watcher.on('error', (error) => {
3786
+ if (this.isExpectedMissingWatchPathError(error) && this.isWatchPathMissing(watchPath)) {
3787
+ return;
3788
+ }
3789
+ this.sessionLogger.warn({ err: error, watchPath, ...options.context }, options.watcherErrorMessage);
3790
+ });
3791
+ watchers.push(watcher);
3792
+ if (watchPath === options.repoWatchPath && watcherIsRecursive) {
3793
+ hasRecursiveRepoCoverage = true;
3794
+ }
3795
+ }
3796
+ return {
3797
+ watchers,
3798
+ hasRecursiveRepoCoverage,
3799
+ repoWatchPathMissing,
3800
+ failedWatcherCount,
3801
+ };
3802
+ }
3187
3803
  scheduleCheckoutDiffTargetRefresh(target) {
3804
+ if (this.isWorkspaceGitWritePending(target.workspaceGitKey)) {
3805
+ target.refreshQueuedAfterWrite = true;
3806
+ return;
3807
+ }
3188
3808
  if (target.debounceTimer) {
3189
3809
  clearTimeout(target.debounceTimer);
3190
3810
  }
@@ -3212,13 +3832,19 @@ export class Session {
3212
3832
  }
3213
3833
  async computeCheckoutDiffSnapshot(cwd, compare, options) {
3214
3834
  const diffCwd = options?.diffCwd ?? cwd;
3835
+ const detail = options?.detail ?? 'full';
3215
3836
  try {
3216
- const diffResult = await getCheckoutDiff(diffCwd, {
3837
+ const diffResult = await this.runWorkspaceGitRead(diffCwd, () => getCheckoutDiff(diffCwd, {
3217
3838
  mode: compare.mode,
3218
3839
  baseRef: compare.baseRef,
3219
3840
  includeStructured: true,
3220
- }, { junctionHome: this.junctionHome });
3221
- const files = [...(diffResult.structured ?? [])];
3841
+ }, { junctionHome: this.junctionHome }));
3842
+ const files = [...(diffResult.structured ?? [])].map((file) => detail === 'summary'
3843
+ ? {
3844
+ ...file,
3845
+ hunks: [],
3846
+ }
3847
+ : file);
3222
3848
  files.sort((a, b) => {
3223
3849
  if (a.path === b.path)
3224
3850
  return 0;
@@ -3248,6 +3874,7 @@ export class Session {
3248
3874
  target.refreshQueued = false;
3249
3875
  const snapshot = await this.computeCheckoutDiffSnapshot(target.cwd, target.compare, {
3250
3876
  diffCwd: target.diffCwd,
3877
+ detail: target.detail,
3251
3878
  });
3252
3879
  target.latestPayload = snapshot;
3253
3880
  const fingerprint = this.checkoutDiffSnapshotFingerprint(snapshot);
@@ -3264,24 +3891,28 @@ export class Session {
3264
3891
  target.refreshPromise = null;
3265
3892
  }
3266
3893
  }
3267
- async ensureCheckoutDiffWatchTarget(cwd, compare) {
3268
- const targetKey = this.buildCheckoutDiffTargetKey(cwd, compare);
3894
+ async ensureCheckoutDiffWatchTarget(cwd, compare, detail) {
3895
+ const targetKey = this.buildCheckoutDiffTargetKey(cwd, compare, detail);
3269
3896
  const existing = this.checkoutDiffTargets.get(targetKey);
3270
3897
  if (existing) {
3271
3898
  return existing;
3272
3899
  }
3273
3900
  const watchRoot = await this.resolveCheckoutWatchRoot(cwd);
3901
+ const workspaceGitKey = watchRoot ?? cwd;
3274
3902
  const target = {
3275
3903
  key: targetKey,
3276
3904
  cwd,
3277
3905
  diffCwd: watchRoot ?? cwd,
3906
+ workspaceGitKey,
3278
3907
  compare,
3908
+ detail,
3279
3909
  subscriptions: new Set(),
3280
3910
  watchers: [],
3281
3911
  fallbackRefreshInterval: null,
3282
3912
  debounceTimer: null,
3283
3913
  refreshPromise: null,
3284
3914
  refreshQueued: false,
3915
+ refreshQueuedAfterWrite: false,
3285
3916
  latestPayload: null,
3286
3917
  latestFingerprint: null,
3287
3918
  };
@@ -3291,55 +3922,27 @@ export class Session {
3291
3922
  if (gitDir) {
3292
3923
  watchPaths.add(gitDir);
3293
3924
  }
3294
- let hasRecursiveRepoCoverage = false;
3295
- const allowRecursiveRepoWatch = process.platform !== 'linux';
3296
- for (const watchPath of watchPaths) {
3297
- const shouldTryRecursive = watchPath === repoWatchPath && allowRecursiveRepoWatch;
3298
- const createWatcher = (recursive) => watch(watchPath, { recursive }, () => {
3925
+ const watchSetup = this.startCheckoutWatchers({
3926
+ repoWatchPath,
3927
+ watchPaths,
3928
+ onChange: () => {
3299
3929
  this.scheduleCheckoutDiffTargetRefresh(target);
3300
- });
3301
- let watcher = null;
3302
- let watcherIsRecursive = false;
3303
- try {
3304
- if (shouldTryRecursive) {
3305
- watcher = createWatcher(true);
3306
- watcherIsRecursive = true;
3307
- }
3308
- else {
3309
- watcher = createWatcher(false);
3310
- }
3311
- }
3312
- catch (error) {
3313
- if (shouldTryRecursive) {
3314
- try {
3315
- watcher = createWatcher(false);
3316
- this.sessionLogger.warn({ err: error, watchPath, cwd, compare }, 'Checkout diff recursive watch unavailable; using non-recursive fallback');
3317
- }
3318
- catch (fallbackError) {
3319
- this.sessionLogger.warn({ err: fallbackError, watchPath, cwd, compare }, 'Failed to start checkout diff watcher');
3320
- }
3321
- }
3322
- else {
3323
- this.sessionLogger.warn({ err: error, watchPath, cwd, compare }, 'Failed to start checkout diff watcher');
3324
- }
3325
- }
3326
- if (!watcher) {
3327
- continue;
3328
- }
3329
- watcher.on('error', (error) => {
3330
- this.sessionLogger.warn({ err: error, watchPath, cwd, compare }, 'Checkout diff watcher error');
3331
- });
3332
- target.watchers.push(watcher);
3333
- if (watchPath === repoWatchPath && watcherIsRecursive) {
3334
- hasRecursiveRepoCoverage = true;
3335
- }
3336
- }
3337
- const missingRepoCoverage = !hasRecursiveRepoCoverage;
3930
+ },
3931
+ recursiveUnavailableMessage: 'Checkout diff recursive watch unavailable; using non-recursive fallback',
3932
+ startFailedMessage: 'Failed to start checkout diff watcher',
3933
+ watcherErrorMessage: 'Checkout diff watcher error',
3934
+ context: { cwd, compare },
3935
+ });
3936
+ target.watchers = watchSetup.watchers;
3937
+ const missingRepoCoverage = !watchSetup.hasRecursiveRepoCoverage;
3338
3938
  if (target.watchers.length === 0 || missingRepoCoverage) {
3339
3939
  target.fallbackRefreshInterval = setInterval(() => {
3340
3940
  this.scheduleCheckoutDiffTargetRefresh(target);
3341
3941
  }, CHECKOUT_DIFF_FALLBACK_REFRESH_MS);
3342
- this.sessionLogger.warn({
3942
+ const log = watchSetup.repoWatchPathMissing && watchSetup.failedWatcherCount === 0
3943
+ ? this.sessionLogger.debug.bind(this.sessionLogger)
3944
+ : this.sessionLogger.warn.bind(this.sessionLogger);
3945
+ log({
3343
3946
  cwd,
3344
3947
  compare,
3345
3948
  intervalMs: CHECKOUT_DIFF_FALLBACK_REFRESH_MS,
@@ -3352,18 +3955,33 @@ export class Session {
3352
3955
  async handleSubscribeCheckoutDiffRequest(msg) {
3353
3956
  const cwd = expandTilde(msg.cwd);
3354
3957
  const compare = this.normalizeCheckoutDiffCompare(msg.compare);
3958
+ const detail = msg.detail ?? 'full';
3355
3959
  this.removeCheckoutDiffSubscription(msg.subscriptionId);
3356
- const target = await this.ensureCheckoutDiffWatchTarget(cwd, compare);
3960
+ const target = await this.ensureCheckoutDiffWatchTarget(cwd, compare, detail);
3357
3961
  target.subscriptions.add(msg.subscriptionId);
3358
3962
  this.checkoutDiffSubscriptions.set(msg.subscriptionId, {
3359
3963
  targetKey: target.key,
3360
3964
  });
3361
- const snapshot = target.latestPayload ??
3362
- (await this.computeCheckoutDiffSnapshot(cwd, compare, {
3965
+ if (target.refreshPromise) {
3966
+ await target.refreshPromise;
3967
+ }
3968
+ const canReuseLatestPayload = target.latestPayload !== null &&
3969
+ !target.refreshQueuedAfterWrite &&
3970
+ !this.isWorkspaceGitWritePending(target.workspaceGitKey);
3971
+ const shouldPreserveQueuedFingerprint = !canReuseLatestPayload && target.refreshQueuedAfterWrite;
3972
+ const snapshot = canReuseLatestPayload
3973
+ ? target.latestPayload
3974
+ : await this.computeCheckoutDiffSnapshot(cwd, compare, {
3363
3975
  diffCwd: target.diffCwd,
3364
- }));
3976
+ detail: target.detail,
3977
+ });
3978
+ if (!snapshot) {
3979
+ throw new Error('Checkout diff snapshot was unavailable');
3980
+ }
3365
3981
  target.latestPayload = snapshot;
3366
- target.latestFingerprint = this.checkoutDiffSnapshotFingerprint(snapshot);
3982
+ if (!shouldPreserveQueuedFingerprint) {
3983
+ target.latestFingerprint = this.checkoutDiffSnapshotFingerprint(snapshot);
3984
+ }
3367
3985
  this.emit({
3368
3986
  type: 'subscribe_checkout_diff_response',
3369
3987
  payload: {
@@ -3385,21 +4003,106 @@ export class Session {
3385
4003
  this.scheduleCheckoutDiffTargetRefresh(target);
3386
4004
  }
3387
4005
  }
4006
+ getWorkspaceGitOperationState(workspaceGitKey) {
4007
+ const existing = this.workspaceGitOperationStates.get(workspaceGitKey);
4008
+ if (existing) {
4009
+ return existing;
4010
+ }
4011
+ const created = {
4012
+ writeBarrier: Promise.resolve(),
4013
+ pendingWrites: 0,
4014
+ activeReaders: 0,
4015
+ readerDrain: Promise.resolve(),
4016
+ resolveReaderDrain: null,
4017
+ };
4018
+ this.workspaceGitOperationStates.set(workspaceGitKey, created);
4019
+ return created;
4020
+ }
4021
+ isWorkspaceGitWritePending(workspaceGitKey) {
4022
+ return (this.workspaceGitOperationStates.get(workspaceGitKey)?.pendingWrites ?? 0) > 0;
4023
+ }
4024
+ async resolveWorkspaceGitOperationKey(cwd) {
4025
+ const resolvedCwd = expandTilde(cwd);
4026
+ return (await this.resolveCheckoutWatchRoot(resolvedCwd)) ?? resolvedCwd;
4027
+ }
4028
+ flushQueuedCheckoutDiffRefreshesForWorkspace(workspaceGitKey) {
4029
+ for (const target of this.checkoutDiffTargets.values()) {
4030
+ if (target.workspaceGitKey !== workspaceGitKey || !target.refreshQueuedAfterWrite) {
4031
+ continue;
4032
+ }
4033
+ target.refreshQueuedAfterWrite = false;
4034
+ this.scheduleCheckoutDiffTargetRefresh(target);
4035
+ }
4036
+ }
4037
+ async runWorkspaceGitRead(cwd, operation) {
4038
+ const workspaceGitKey = await this.resolveWorkspaceGitOperationKey(cwd);
4039
+ const state = this.getWorkspaceGitOperationState(workspaceGitKey);
4040
+ while (state.pendingWrites > 0) {
4041
+ await state.writeBarrier;
4042
+ }
4043
+ state.activeReaders += 1;
4044
+ if (state.activeReaders === 1) {
4045
+ state.readerDrain = new Promise((resolve) => {
4046
+ state.resolveReaderDrain = resolve;
4047
+ });
4048
+ }
4049
+ try {
4050
+ return await operation();
4051
+ }
4052
+ finally {
4053
+ state.activeReaders = Math.max(0, state.activeReaders - 1);
4054
+ if (state.activeReaders === 0) {
4055
+ state.resolveReaderDrain?.();
4056
+ state.resolveReaderDrain = null;
4057
+ state.readerDrain = Promise.resolve();
4058
+ }
4059
+ }
4060
+ }
4061
+ async runWorkspaceGitWrite(cwd, operation) {
4062
+ const workspaceGitKey = await this.resolveWorkspaceGitOperationKey(cwd);
4063
+ const state = this.getWorkspaceGitOperationState(workspaceGitKey);
4064
+ const previousBarrier = state.writeBarrier;
4065
+ let releaseCurrentBarrier = () => { };
4066
+ const currentBarrier = new Promise((resolve) => {
4067
+ releaseCurrentBarrier = resolve;
4068
+ });
4069
+ state.pendingWrites += 1;
4070
+ state.writeBarrier = previousBarrier.then(() => currentBarrier);
4071
+ try {
4072
+ await previousBarrier;
4073
+ while (state.activeReaders > 0) {
4074
+ await state.readerDrain;
4075
+ }
4076
+ return await operation();
4077
+ }
4078
+ finally {
4079
+ state.pendingWrites = Math.max(0, state.pendingWrites - 1);
4080
+ releaseCurrentBarrier();
4081
+ if (state.pendingWrites === 0) {
4082
+ state.writeBarrier = Promise.resolve();
4083
+ this.flushQueuedCheckoutDiffRefreshesForWorkspace(workspaceGitKey);
4084
+ }
4085
+ }
4086
+ }
3388
4087
  async handleCheckoutCommitRequest(msg) {
3389
4088
  const { cwd, requestId } = msg;
3390
4089
  try {
3391
4090
  let message = msg.message?.trim() ?? '';
3392
4091
  if (!message) {
3393
- message = await this.generateCommitMessage(cwd);
4092
+ const diff = await this.runWorkspaceGitRead(cwd, () => getCheckoutDiff(cwd, { mode: 'uncommitted', includeStructured: true }, { junctionHome: this.junctionHome }));
4093
+ message = await this.generateCommitMessage(cwd, diff);
3394
4094
  }
3395
4095
  if (!message) {
3396
4096
  throw new Error('Commit message is required');
3397
4097
  }
3398
- await commitChanges(cwd, {
3399
- message,
3400
- addAll: msg.addAll ?? true,
4098
+ await this.runWorkspaceGitWrite(cwd, async () => {
4099
+ await commitChanges(cwd, {
4100
+ message,
4101
+ addAll: msg.addAll ?? true,
4102
+ });
4103
+ this.scheduleCheckoutDiffRefreshForCwd(cwd);
3401
4104
  });
3402
- this.scheduleCheckoutDiffRefreshForCwd(cwd);
4105
+ this.scheduleWorkspaceStatusRefreshForCwd(cwd);
3403
4106
  this.emit({
3404
4107
  type: 'checkout_commit_response',
3405
4108
  payload: {
@@ -3425,44 +4128,54 @@ export class Session {
3425
4128
  async handleCheckoutMergeRequest(msg) {
3426
4129
  const { cwd, requestId } = msg;
3427
4130
  try {
3428
- const status = await getCheckoutStatus(cwd, { junctionHome: this.junctionHome });
3429
- if (!status.isGit) {
3430
- try {
3431
- await execAsync('git rev-parse --is-inside-work-tree', {
4131
+ const mergeOperationCwd = await resolveMergeToBaseOperationCwd(cwd, { baseRef: msg.baseRef }, { junctionHome: this.junctionHome });
4132
+ await this.runWorkspaceGitWrite(mergeOperationCwd, async () => {
4133
+ const status = await getCheckoutStatus(cwd, { junctionHome: this.junctionHome });
4134
+ if (!status.isGit) {
4135
+ try {
4136
+ await execAsync('git rev-parse --is-inside-work-tree', {
4137
+ cwd,
4138
+ env: READ_ONLY_GIT_ENV,
4139
+ });
4140
+ }
4141
+ catch (error) {
4142
+ const details = typeof error?.stderr === 'string'
4143
+ ? String(error.stderr).trim()
4144
+ : error instanceof Error
4145
+ ? error.message
4146
+ : String(error);
4147
+ throw new Error(`Not a git repository: ${cwd}\n${details}`.trim());
4148
+ }
4149
+ }
4150
+ if (msg.requireCleanTarget) {
4151
+ const { stdout } = await execAsync('git status --porcelain', {
3432
4152
  cwd,
3433
4153
  env: READ_ONLY_GIT_ENV,
3434
4154
  });
4155
+ if (stdout.trim().length > 0) {
4156
+ throw new Error('Working directory has uncommitted changes.');
4157
+ }
3435
4158
  }
3436
- catch (error) {
3437
- const details = typeof error?.stderr === 'string'
3438
- ? String(error.stderr).trim()
3439
- : error instanceof Error
3440
- ? error.message
3441
- : String(error);
3442
- throw new Error(`Not a git repository: ${cwd}\n${details}`.trim());
4159
+ let baseRef = msg.baseRef ?? (status.isGit ? status.baseRef : null);
4160
+ if (!baseRef) {
4161
+ throw new Error('Base branch is required for merge');
3443
4162
  }
3444
- }
3445
- if (msg.requireCleanTarget) {
3446
- const { stdout } = await execAsync('git status --porcelain', {
3447
- cwd,
3448
- env: READ_ONLY_GIT_ENV,
3449
- });
3450
- if (stdout.trim().length > 0) {
3451
- throw new Error('Working directory has uncommitted changes.');
4163
+ if (baseRef.startsWith('origin/')) {
4164
+ baseRef = baseRef.slice('origin/'.length);
3452
4165
  }
4166
+ await mergeToBase(cwd, {
4167
+ baseRef,
4168
+ mode: msg.strategy === 'squash' ? 'squash' : 'merge',
4169
+ }, { junctionHome: this.junctionHome });
4170
+ this.scheduleCheckoutDiffRefreshForCwd(cwd);
4171
+ if (resolve(mergeOperationCwd) !== resolve(cwd)) {
4172
+ this.scheduleCheckoutDiffRefreshForCwd(mergeOperationCwd);
4173
+ }
4174
+ });
4175
+ this.scheduleWorkspaceStatusRefreshForCwd(cwd, { includePr: true });
4176
+ if (resolve(mergeOperationCwd) !== resolve(cwd)) {
4177
+ this.scheduleWorkspaceStatusRefreshForCwd(mergeOperationCwd, { includePr: true });
3453
4178
  }
3454
- let baseRef = msg.baseRef ?? (status.isGit ? status.baseRef : null);
3455
- if (!baseRef) {
3456
- throw new Error('Base branch is required for merge');
3457
- }
3458
- if (baseRef.startsWith('origin/')) {
3459
- baseRef = baseRef.slice('origin/'.length);
3460
- }
3461
- await mergeToBase(cwd, {
3462
- baseRef,
3463
- mode: msg.strategy === 'squash' ? 'squash' : 'merge',
3464
- }, { junctionHome: this.junctionHome });
3465
- this.scheduleCheckoutDiffRefreshForCwd(cwd);
3466
4179
  this.emit({
3467
4180
  type: 'checkout_merge_response',
3468
4181
  payload: {
@@ -3488,21 +4201,24 @@ export class Session {
3488
4201
  async handleCheckoutMergeFromBaseRequest(msg) {
3489
4202
  const { cwd, requestId } = msg;
3490
4203
  try {
3491
- if (msg.requireCleanTarget ?? true) {
3492
- const { stdout } = await execAsync('git status --porcelain', {
3493
- cwd,
3494
- env: READ_ONLY_GIT_ENV,
3495
- });
3496
- if (stdout.trim().length > 0) {
3497
- throw new Error('Working directory has uncommitted changes.');
4204
+ await this.runWorkspaceGitWrite(cwd, async () => {
4205
+ if (msg.requireCleanTarget ?? true) {
4206
+ const { stdout } = await execAsync('git status --porcelain', {
4207
+ cwd,
4208
+ env: READ_ONLY_GIT_ENV,
4209
+ });
4210
+ if (stdout.trim().length > 0) {
4211
+ throw new Error('Working directory has uncommitted changes.');
4212
+ }
3498
4213
  }
3499
- }
3500
- await mergeFromBase(cwd, {
3501
- baseRef: msg.baseRef,
3502
- remoteName: msg.remoteName,
3503
- requireCleanTarget: msg.requireCleanTarget ?? true,
4214
+ await mergeFromBase(cwd, {
4215
+ baseRef: msg.baseRef,
4216
+ remoteName: msg.remoteName,
4217
+ requireCleanTarget: msg.requireCleanTarget ?? true,
4218
+ });
4219
+ this.scheduleCheckoutDiffRefreshForCwd(cwd);
3504
4220
  });
3505
- this.scheduleCheckoutDiffRefreshForCwd(cwd);
4221
+ this.scheduleWorkspaceStatusRefreshForCwd(cwd, { includePr: true });
3506
4222
  this.emit({
3507
4223
  type: 'checkout_merge_from_base_response',
3508
4224
  payload: {
@@ -3528,7 +4244,8 @@ export class Session {
3528
4244
  async handleCheckoutPushRequest(msg) {
3529
4245
  const { cwd, requestId } = msg;
3530
4246
  try {
3531
- await pushCurrentBranch(cwd, { remoteName: msg.remoteName });
4247
+ await this.runWorkspaceGitWrite(cwd, () => pushCurrentBranch(cwd, { remoteName: msg.remoteName }));
4248
+ this.scheduleWorkspaceStatusRefreshForCwd(cwd, { includePr: true });
3532
4249
  this.emit({
3533
4250
  type: 'checkout_push_response',
3534
4251
  payload: {
@@ -3557,18 +4274,24 @@ export class Session {
3557
4274
  let title = msg.title?.trim() ?? '';
3558
4275
  let body = msg.body?.trim() ?? '';
3559
4276
  if (!title || !body) {
3560
- const generated = await this.generatePullRequestText(cwd, msg.baseRef);
4277
+ const diff = await this.runWorkspaceGitRead(cwd, () => getCheckoutDiff(cwd, {
4278
+ mode: 'base',
4279
+ baseRef: msg.baseRef,
4280
+ includeStructured: true,
4281
+ }, { junctionHome: this.junctionHome }));
4282
+ const generated = await this.generatePullRequestText(cwd, msg.baseRef, diff);
3561
4283
  if (!title)
3562
4284
  title = generated.title;
3563
4285
  if (!body)
3564
4286
  body = generated.body;
3565
4287
  }
3566
- const result = await createPullRequest(cwd, {
4288
+ const result = await this.runWorkspaceGitWrite(cwd, () => createPullRequest(cwd, {
3567
4289
  title,
3568
4290
  body,
3569
4291
  base: msg.baseRef,
3570
4292
  remoteName: msg.remoteName,
3571
- });
4293
+ }));
4294
+ this.scheduleWorkspaceStatusRefreshForCwd(cwd, { includePr: true });
3572
4295
  this.emit({
3573
4296
  type: 'checkout_pr_create_response',
3574
4297
  payload: {
@@ -3625,11 +4348,11 @@ export class Session {
3625
4348
  const cwd = expandTilde(msg.cwd);
3626
4349
  const { requestId } = msg;
3627
4350
  try {
3628
- const result = await searchPullRequests(cwd, {
4351
+ const result = await this.runWorkspaceGitRead(cwd, () => searchPullRequests(cwd, {
3629
4352
  query: msg.query,
3630
4353
  limit: msg.limit,
3631
4354
  remoteName: msg.remoteName,
3632
- });
4355
+ }));
3633
4356
  this.emit({
3634
4357
  type: 'checkout_pr_search_response',
3635
4358
  payload: {
@@ -3657,7 +4380,7 @@ export class Session {
3657
4380
  async handleCheckoutPrFailureLogsRequest(msg) {
3658
4381
  const { cwd, requestId } = msg;
3659
4382
  try {
3660
- const result = await getPullRequestFailureLogs(cwd, { remoteName: msg.remoteName });
4383
+ const result = await this.runWorkspaceGitRead(cwd, () => getPullRequestFailureLogs(cwd, { remoteName: msg.remoteName }));
3661
4384
  this.emit({
3662
4385
  type: 'checkout_pr_failure_logs_response',
3663
4386
  payload: {
@@ -3689,6 +4412,7 @@ export class Session {
3689
4412
  method: msg.method ?? 'squash',
3690
4413
  remoteName: msg.remoteName,
3691
4414
  });
4415
+ this.scheduleWorkspaceStatusRefreshForCwd(cwd, { includePr: true });
3692
4416
  this.emit({
3693
4417
  type: 'checkout_pr_merge_response',
3694
4418
  payload: {
@@ -4039,8 +4763,8 @@ export class Session {
4039
4763
  */
4040
4764
  async handleWorkspaceFileExplorerRequest(request) {
4041
4765
  const { cwd, path: requestedPath = '.', mode, requestId, ref } = request;
4766
+ const root = expandTilde(cwd);
4042
4767
  try {
4043
- const root = expandTilde(cwd);
4044
4768
  if (ref) {
4045
4769
  this.assertSafeGitRef(ref, 'workspace file ref');
4046
4770
  }
@@ -4049,6 +4773,7 @@ export class Session {
4049
4773
  root,
4050
4774
  relativePath: requestedPath,
4051
4775
  ref,
4776
+ detail: request.detail ?? 'full',
4052
4777
  });
4053
4778
  this.emit({
4054
4779
  type: 'workspace_file_explorer_response',
@@ -4086,6 +4811,25 @@ export class Session {
4086
4811
  }
4087
4812
  }
4088
4813
  catch (error) {
4814
+ if (mode === 'file'
4815
+ && request.allowMissing
4816
+ && isWorkspaceExplorerMissingPathError(error)
4817
+ && (ref != null || existsSync(root))) {
4818
+ this.emit({
4819
+ type: 'workspace_file_explorer_response',
4820
+ payload: {
4821
+ cwd,
4822
+ path: requestedPath,
4823
+ ref: ref ?? null,
4824
+ mode,
4825
+ directory: null,
4826
+ file: null,
4827
+ error: null,
4828
+ requestId,
4829
+ },
4830
+ });
4831
+ return;
4832
+ }
4089
4833
  const log = isWorkspaceExplorerMissingPathError(error)
4090
4834
  ? this.sessionLogger.debug.bind(this.sessionLogger)
4091
4835
  : this.sessionLogger.error.bind(this.sessionLogger);
@@ -4978,6 +5722,11 @@ export class Session {
4978
5722
  }
4979
5723
  this.checkoutDiffTargets.clear();
4980
5724
  this.checkoutDiffSubscriptions.clear();
5725
+ for (const target of this.workspaceStatusTargets.values()) {
5726
+ this.closeWorkspaceStatusWatchTarget(target);
5727
+ }
5728
+ this.workspaceStatusTargets.clear();
5729
+ this.workspaceStatusSubscriptions.clear();
4981
5730
  }
4982
5731
  // ============================================================================
4983
5732
  // Terminal Handlers