@junctionpanel/server 0.1.52 → 0.1.54

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 (30) hide show
  1. package/dist/server/client/daemon-client.d.ts +17 -1
  2. package/dist/server/client/daemon-client.d.ts.map +1 -1
  3. package/dist/server/client/daemon-client.js +147 -6
  4. package/dist/server/client/daemon-client.js.map +1 -1
  5. package/dist/server/server/agent/agent-metadata-generator.d.ts.map +1 -1
  6. package/dist/server/server/agent/agent-metadata-generator.js +54 -17
  7. package/dist/server/server/agent/agent-metadata-generator.js.map +1 -1
  8. package/dist/server/server/file-explorer/service.d.ts +6 -1
  9. package/dist/server/server/file-explorer/service.d.ts.map +1 -1
  10. package/dist/server/server/file-explorer/service.js +72 -23
  11. package/dist/server/server/file-explorer/service.js.map +1 -1
  12. package/dist/server/server/session.d.ts +25 -0
  13. package/dist/server/server/session.d.ts.map +1 -1
  14. package/dist/server/server/session.js +589 -86
  15. package/dist/server/server/session.js.map +1 -1
  16. package/dist/server/server/tool-call-preview.d.ts.map +1 -1
  17. package/dist/server/server/tool-call-preview.js +5 -3
  18. package/dist/server/server/tool-call-preview.js.map +1 -1
  19. package/dist/server/shared/messages.d.ts +7614 -2138
  20. package/dist/server/shared/messages.d.ts.map +1 -1
  21. package/dist/server/shared/messages.js +45 -0
  22. package/dist/server/shared/messages.js.map +1 -1
  23. package/dist/server/utils/checkout-git.d.ts.map +1 -1
  24. package/dist/server/utils/checkout-git.js +28 -3
  25. package/dist/server/utils/checkout-git.js.map +1 -1
  26. package/dist/server/utils/worktree.d.ts +1 -1
  27. package/dist/server/utils/worktree.d.ts.map +1 -1
  28. package/dist/server/utils/worktree.js +7 -1
  29. package/dist/server/utils/worktree.js.map +1 -1
  30. package/package.json +2 -2
@@ -51,6 +51,10 @@ const pendingAgentMessageExecutions = new Map();
51
51
  const DEFAULT_AGENT_PROVIDER = AGENT_PROVIDER_IDS[0];
52
52
  const CHECKOUT_DIFF_WATCH_DEBOUNCE_MS = 150;
53
53
  const CHECKOUT_DIFF_FALLBACK_REFRESH_MS = 5000;
54
+ const WORKSPACE_STATUS_WATCH_DEBOUNCE_MS = 250;
55
+ const WORKSPACE_STATUS_GIT_REFRESH_MS = 3000;
56
+ const WORKSPACE_STATUS_PR_ACTIVE_REFRESH_MS = 15000;
57
+ const WORKSPACE_STATUS_PR_PASSIVE_REFRESH_MS = 60000;
54
58
  const TERMINAL_STREAM_WINDOW_BYTES = 256 * 1024;
55
59
  const TERMINAL_STREAM_MAX_PENDING_BYTES = 2 * 1024 * 1024;
56
60
  const TERMINAL_STREAM_MAX_PENDING_CHUNKS = 2048;
@@ -119,6 +123,8 @@ export class Session {
119
123
  this.clientActivity = null;
120
124
  this.MOBILE_BACKGROUND_STREAM_GRACE_MS = 60000;
121
125
  this.subscribedTerminalDirectories = new Set();
126
+ this.workspaceStatusSubscriptions = new Map();
127
+ this.workspaceStatusTargets = new Map();
122
128
  this.unsubscribeTerminalsChanged = null;
123
129
  this.terminalSubscriptions = new Map();
124
130
  this.terminalExitSubscriptions = new Map();
@@ -863,6 +869,12 @@ export class Session {
863
869
  case 'checkout_status_request':
864
870
  await this.handleCheckoutStatusRequest(msg);
865
871
  break;
872
+ case 'subscribe_workspace_status_request':
873
+ await this.handleSubscribeWorkspaceStatusRequest(msg);
874
+ break;
875
+ case 'unsubscribe_workspace_status_request':
876
+ this.handleUnsubscribeWorkspaceStatusRequest(msg);
877
+ break;
866
878
  case 'validate_branch_request':
867
879
  await this.handleValidateBranchRequest(msg);
868
880
  break;
@@ -2702,101 +2714,191 @@ export class Session {
2702
2714
  throw error;
2703
2715
  }
2704
2716
  }
2717
+ buildCheckoutStatusPayload(cwd, status) {
2718
+ if (!status.isGit) {
2719
+ return {
2720
+ cwd,
2721
+ isGit: false,
2722
+ repoRoot: null,
2723
+ currentBranch: null,
2724
+ isDirty: null,
2725
+ baseRef: null,
2726
+ aheadBehind: null,
2727
+ aheadOfOrigin: null,
2728
+ behindOfOrigin: null,
2729
+ hasUpstream: false,
2730
+ upstreamBranch: null,
2731
+ hasRemote: false,
2732
+ remoteUrl: null,
2733
+ isJunctionOwnedWorktree: false,
2734
+ error: null,
2735
+ };
2736
+ }
2737
+ if (status.isJunctionOwnedWorktree) {
2738
+ return {
2739
+ cwd,
2740
+ isGit: true,
2741
+ repoRoot: status.repoRoot ?? null,
2742
+ mainRepoRoot: status.mainRepoRoot,
2743
+ currentBranch: status.currentBranch ?? null,
2744
+ isDirty: status.isDirty ?? null,
2745
+ baseRef: status.baseRef,
2746
+ aheadBehind: status.aheadBehind ?? null,
2747
+ aheadOfOrigin: status.aheadOfOrigin ?? null,
2748
+ behindOfOrigin: status.behindOfOrigin ?? null,
2749
+ hasUpstream: status.hasUpstream,
2750
+ upstreamBranch: status.upstreamBranch ?? null,
2751
+ hasRemote: status.hasRemote,
2752
+ remoteUrl: status.remoteUrl,
2753
+ isJunctionOwnedWorktree: true,
2754
+ error: null,
2755
+ };
2756
+ }
2757
+ return {
2758
+ cwd,
2759
+ isGit: true,
2760
+ repoRoot: status.repoRoot ?? null,
2761
+ currentBranch: status.currentBranch ?? null,
2762
+ isDirty: status.isDirty ?? null,
2763
+ baseRef: status.baseRef ?? null,
2764
+ aheadBehind: status.aheadBehind ?? null,
2765
+ aheadOfOrigin: status.aheadOfOrigin ?? null,
2766
+ behindOfOrigin: status.behindOfOrigin ?? null,
2767
+ hasUpstream: status.hasUpstream,
2768
+ upstreamBranch: status.upstreamBranch ?? null,
2769
+ hasRemote: status.hasRemote,
2770
+ remoteUrl: status.remoteUrl,
2771
+ isJunctionOwnedWorktree: false,
2772
+ error: null,
2773
+ };
2774
+ }
2775
+ buildCheckoutStatusErrorPayload(cwd, error) {
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: this.toCheckoutError(error),
2792
+ requestId: '',
2793
+ };
2794
+ }
2795
+ buildCheckoutStatusLitePayload(cwd, repoRoot, status) {
2796
+ if (!status.isGit) {
2797
+ return {
2798
+ cwd,
2799
+ isGit: false,
2800
+ repoRoot: null,
2801
+ currentBranch: null,
2802
+ isDirty: null,
2803
+ baseRef: null,
2804
+ aheadBehind: null,
2805
+ aheadOfOrigin: null,
2806
+ behindOfOrigin: null,
2807
+ hasUpstream: false,
2808
+ upstreamBranch: null,
2809
+ hasRemote: Boolean(status.remoteUrl),
2810
+ remoteUrl: status.remoteUrl,
2811
+ isJunctionOwnedWorktree: false,
2812
+ error: null,
2813
+ };
2814
+ }
2815
+ return {
2816
+ cwd,
2817
+ isGit: true,
2818
+ repoRoot,
2819
+ currentBranch: status.currentBranch ?? null,
2820
+ isDirty: false,
2821
+ baseRef: null,
2822
+ aheadBehind: null,
2823
+ aheadOfOrigin: null,
2824
+ behindOfOrigin: null,
2825
+ hasUpstream: false,
2826
+ upstreamBranch: null,
2827
+ hasRemote: Boolean(status.remoteUrl),
2828
+ remoteUrl: status.remoteUrl,
2829
+ isJunctionOwnedWorktree: false,
2830
+ error: null,
2831
+ };
2832
+ }
2833
+ toWorkspaceGitStatus(payload) {
2834
+ const normalizedPayload = payload;
2835
+ const { cwd: _cwd, error: _error, requestId: _requestId, ...git } = normalizedPayload;
2836
+ return git;
2837
+ }
2838
+ async resolveWorkspaceGitSnapshot(cwd, options) {
2839
+ try {
2840
+ const payload = await (async () => {
2841
+ if (!options?.lite) {
2842
+ return this.buildCheckoutStatusPayload(cwd, await getCheckoutStatus(expandTilde(cwd), { junctionHome: this.junctionHome }));
2843
+ }
2844
+ const liteStatus = await getCheckoutStatusLite(expandTilde(cwd), {
2845
+ junctionHome: this.junctionHome,
2846
+ });
2847
+ if (liteStatus.isGit && liteStatus.isJunctionOwnedWorktree) {
2848
+ return this.buildCheckoutStatusPayload(cwd, await getCheckoutStatus(expandTilde(cwd), { junctionHome: this.junctionHome }));
2849
+ }
2850
+ return this.buildCheckoutStatusLitePayload(cwd, options.repoRoot ?? cwd, liteStatus);
2851
+ })();
2852
+ return {
2853
+ git: this.toWorkspaceGitStatus(payload),
2854
+ gitError: payload.error,
2855
+ };
2856
+ }
2857
+ catch (error) {
2858
+ const payload = this.buildCheckoutStatusErrorPayload(cwd, error);
2859
+ return {
2860
+ git: this.toWorkspaceGitStatus(payload),
2861
+ gitError: payload.error,
2862
+ };
2863
+ }
2864
+ }
2865
+ async resolveWorkspacePullRequestSnapshot(cwd, remoteName) {
2866
+ try {
2867
+ const prStatus = await getPullRequestStatus(expandTilde(cwd), {
2868
+ ...(remoteName ? { remoteName } : {}),
2869
+ });
2870
+ return {
2871
+ pullRequest: prStatus.status,
2872
+ pullRequestError: null,
2873
+ githubFeaturesEnabled: prStatus.githubFeaturesEnabled,
2874
+ };
2875
+ }
2876
+ catch (error) {
2877
+ return {
2878
+ pullRequest: null,
2879
+ pullRequestError: this.toCheckoutError(error),
2880
+ githubFeaturesEnabled: true,
2881
+ };
2882
+ }
2883
+ }
2705
2884
  async handleCheckoutStatusRequest(msg) {
2706
2885
  const { cwd, requestId } = msg;
2707
- const resolvedCwd = expandTilde(cwd);
2708
2886
  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
- }
2887
+ const status = await getCheckoutStatus(expandTilde(cwd), { junctionHome: this.junctionHome });
2759
2888
  this.emit({
2760
2889
  type: 'checkout_status_response',
2761
2890
  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,
2891
+ ...this.buildCheckoutStatusPayload(cwd, status),
2777
2892
  requestId,
2778
2893
  },
2779
2894
  });
2780
2895
  }
2781
2896
  catch (error) {
2897
+ const payload = this.buildCheckoutStatusErrorPayload(cwd, error);
2782
2898
  this.emit({
2783
2899
  type: 'checkout_status_response',
2784
2900
  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),
2901
+ ...payload,
2800
2902
  requestId,
2801
2903
  },
2802
2904
  });
@@ -3114,6 +3216,384 @@ export class Session {
3114
3216
  });
3115
3217
  }
3116
3218
  }
3219
+ buildWorkspaceStatusTargetKey(cwd, remoteName) {
3220
+ return JSON.stringify([cwd, remoteName ?? '']);
3221
+ }
3222
+ closeWorkspaceStatusWatchTarget(target) {
3223
+ if (target.debounceTimer) {
3224
+ clearTimeout(target.debounceTimer);
3225
+ target.debounceTimer = null;
3226
+ }
3227
+ if (target.gitRefreshInterval) {
3228
+ clearInterval(target.gitRefreshInterval);
3229
+ target.gitRefreshInterval = null;
3230
+ }
3231
+ if (target.prRefreshInterval) {
3232
+ clearInterval(target.prRefreshInterval);
3233
+ target.prRefreshInterval = null;
3234
+ }
3235
+ for (const watcher of target.watchers) {
3236
+ watcher.close();
3237
+ }
3238
+ target.watchers = [];
3239
+ }
3240
+ workspaceStatusHasActiveSubscribers(target) {
3241
+ for (const mode of target.subscriptions.values()) {
3242
+ if (mode === 'active') {
3243
+ return true;
3244
+ }
3245
+ }
3246
+ return false;
3247
+ }
3248
+ hasWorkspaceStatusTarget(target) {
3249
+ return this.workspaceStatusTargets.get(target.key) === target && target.subscriptions.size > 0;
3250
+ }
3251
+ workspaceStatusGitIdentityChanged(previousSnapshot, nextGit) {
3252
+ if (!previousSnapshot) {
3253
+ return false;
3254
+ }
3255
+ const previousGit = previousSnapshot.git;
3256
+ if (previousGit.isGit !== nextGit.isGit) {
3257
+ return true;
3258
+ }
3259
+ if (!previousGit.isGit || !nextGit.isGit) {
3260
+ return false;
3261
+ }
3262
+ return (previousGit.currentBranch !== nextGit.currentBranch
3263
+ || previousGit.repoRoot !== nextGit.repoRoot
3264
+ || previousGit.isJunctionOwnedWorktree !== nextGit.isJunctionOwnedWorktree
3265
+ || ((previousGit.isJunctionOwnedWorktree ? previousGit.mainRepoRoot : null)
3266
+ !== (nextGit.isJunctionOwnedWorktree ? nextGit.mainRepoRoot : null)));
3267
+ }
3268
+ updateWorkspaceStatusPrPolling(target) {
3269
+ if (target.prRefreshInterval) {
3270
+ clearInterval(target.prRefreshInterval);
3271
+ target.prRefreshInterval = null;
3272
+ }
3273
+ if (target.subscriptions.size === 0) {
3274
+ return;
3275
+ }
3276
+ const intervalMs = this.workspaceStatusHasActiveSubscribers(target)
3277
+ ? WORKSPACE_STATUS_PR_ACTIVE_REFRESH_MS
3278
+ : WORKSPACE_STATUS_PR_PASSIVE_REFRESH_MS;
3279
+ target.prRefreshInterval = setInterval(() => {
3280
+ this.scheduleWorkspaceStatusTargetRefresh(target, {
3281
+ includePr: true,
3282
+ debounce: false,
3283
+ });
3284
+ }, intervalMs);
3285
+ }
3286
+ updateWorkspaceStatusGitPolling(target) {
3287
+ if (target.gitRefreshInterval) {
3288
+ clearInterval(target.gitRefreshInterval);
3289
+ target.gitRefreshInterval = null;
3290
+ }
3291
+ if (!this.workspaceStatusHasActiveSubscribers(target)) {
3292
+ return;
3293
+ }
3294
+ target.gitRefreshInterval = setInterval(() => {
3295
+ this.scheduleWorkspaceStatusTargetRefresh(target, {
3296
+ includePr: false,
3297
+ debounce: false,
3298
+ });
3299
+ }, WORKSPACE_STATUS_GIT_REFRESH_MS);
3300
+ }
3301
+ removeWorkspaceStatusSubscription(subscriptionId) {
3302
+ const subscription = this.workspaceStatusSubscriptions.get(subscriptionId);
3303
+ if (!subscription) {
3304
+ return;
3305
+ }
3306
+ this.workspaceStatusSubscriptions.delete(subscriptionId);
3307
+ const target = this.workspaceStatusTargets.get(subscription.targetKey);
3308
+ if (!target) {
3309
+ return;
3310
+ }
3311
+ target.subscriptions.delete(subscriptionId);
3312
+ if (target.subscriptions.size === 0) {
3313
+ this.closeWorkspaceStatusWatchTarget(target);
3314
+ this.workspaceStatusTargets.delete(subscription.targetKey);
3315
+ return;
3316
+ }
3317
+ this.updateWorkspaceStatusGitPolling(target);
3318
+ this.updateWorkspaceStatusPrPolling(target);
3319
+ }
3320
+ scheduleWorkspaceStatusTargetRefresh(target, options) {
3321
+ const includePr = options?.includePr ?? false;
3322
+ const shouldDebounce = options?.debounce ?? true;
3323
+ if (!shouldDebounce) {
3324
+ void this.refreshWorkspaceStatusTarget(target, { includePr });
3325
+ return;
3326
+ }
3327
+ if (target.debounceTimer) {
3328
+ clearTimeout(target.debounceTimer);
3329
+ }
3330
+ if (includePr) {
3331
+ target.refreshIncludesPr = true;
3332
+ }
3333
+ target.debounceTimer = setTimeout(() => {
3334
+ target.debounceTimer = null;
3335
+ void this.refreshWorkspaceStatusTarget(target, {
3336
+ includePr: includePr || target.refreshIncludesPr,
3337
+ });
3338
+ }, WORKSPACE_STATUS_WATCH_DEBOUNCE_MS);
3339
+ }
3340
+ workspaceStatusSnapshotFingerprint(snapshot) {
3341
+ return JSON.stringify({
3342
+ cwd: snapshot.cwd,
3343
+ remoteName: snapshot.remoteName,
3344
+ git: snapshot.git,
3345
+ gitError: snapshot.gitError,
3346
+ pullRequest: snapshot.pullRequest,
3347
+ pullRequestError: snapshot.pullRequestError,
3348
+ githubFeaturesEnabled: snapshot.githubFeaturesEnabled,
3349
+ });
3350
+ }
3351
+ emitWorkspaceStatusChanged(_target, snapshot) {
3352
+ this.emit({
3353
+ type: 'workspace_status_changed',
3354
+ payload: snapshot,
3355
+ });
3356
+ }
3357
+ async computeWorkspaceStatusSnapshot(target, options) {
3358
+ const hasExplicitIncludePr = options != null && 'includePr' in options;
3359
+ const includePr = hasExplicitIncludePr ? Boolean(options?.includePr) : !target.latestPayload;
3360
+ const fullGit = options?.fullGit ?? this.workspaceStatusHasActiveSubscribers(target);
3361
+ const gitSnapshot = await this.resolveWorkspaceGitSnapshot(target.cwd, {
3362
+ lite: !fullGit,
3363
+ repoRoot: target.diffCwd || target.cwd,
3364
+ });
3365
+ const gitIdentityChanged = this.workspaceStatusGitIdentityChanged(target.latestPayload, gitSnapshot.git);
3366
+ let pullRequest = target.latestPayload?.pullRequest ?? null;
3367
+ let pullRequestError = target.latestPayload?.pullRequestError ?? null;
3368
+ let githubFeaturesEnabled = target.latestPayload?.githubFeaturesEnabled ?? false;
3369
+ if (!gitSnapshot.git.isGit) {
3370
+ pullRequest = null;
3371
+ pullRequestError = null;
3372
+ githubFeaturesEnabled = false;
3373
+ }
3374
+ else if (includePr) {
3375
+ const prSnapshot = await this.resolveWorkspacePullRequestSnapshot(target.cwd, target.remoteName);
3376
+ pullRequest = prSnapshot.pullRequest;
3377
+ pullRequestError = prSnapshot.pullRequestError;
3378
+ githubFeaturesEnabled = prSnapshot.githubFeaturesEnabled;
3379
+ }
3380
+ else if (gitIdentityChanged) {
3381
+ pullRequest = null;
3382
+ pullRequestError = null;
3383
+ githubFeaturesEnabled = false;
3384
+ }
3385
+ return {
3386
+ cwd: target.cwd,
3387
+ remoteName: target.remoteName,
3388
+ git: gitSnapshot.git,
3389
+ gitError: gitSnapshot.gitError,
3390
+ pullRequest,
3391
+ pullRequestError,
3392
+ githubFeaturesEnabled,
3393
+ refreshedAt: new Date().toISOString(),
3394
+ };
3395
+ }
3396
+ async refreshWorkspaceStatusTarget(target, options) {
3397
+ if (target.refreshPromise) {
3398
+ target.refreshQueued = true;
3399
+ target.refreshIncludesPr = target.refreshIncludesPr || Boolean(options?.includePr);
3400
+ return;
3401
+ }
3402
+ target.refreshPromise = (async () => {
3403
+ do {
3404
+ const hasExplicitIncludePr = options != null && 'includePr' in options;
3405
+ const fullGit = this.workspaceStatusHasActiveSubscribers(target);
3406
+ const previousHasLoadedPullRequest = target.hasLoadedPullRequest;
3407
+ const includePr = target.refreshIncludesPr
3408
+ || (hasExplicitIncludePr ? Boolean(options?.includePr) : !target.latestPayload);
3409
+ target.refreshQueued = false;
3410
+ target.refreshIncludesPr = false;
3411
+ const snapshot = await this.computeWorkspaceStatusSnapshot(target, {
3412
+ includePr,
3413
+ fullGit,
3414
+ });
3415
+ if (!this.hasWorkspaceStatusTarget(target)) {
3416
+ return;
3417
+ }
3418
+ const activeSubscribersPresent = this.workspaceStatusHasActiveSubscribers(target);
3419
+ const gitIdentityChanged = this.workspaceStatusGitIdentityChanged(target.latestPayload, snapshot.git);
3420
+ const needsPrReplay = (!includePr && target.refreshIncludesPr)
3421
+ || (!includePr && fullGit && activeSubscribersPresent && gitIdentityChanged);
3422
+ const needsFullGitReplay = !fullGit && activeSubscribersPresent;
3423
+ if (needsPrReplay
3424
+ || needsFullGitReplay) {
3425
+ target.refreshQueued = true;
3426
+ target.refreshIncludesPr = target.refreshIncludesPr || needsPrReplay || needsFullGitReplay;
3427
+ continue;
3428
+ }
3429
+ target.latestPayload = snapshot;
3430
+ target.hasLoadedPullRequest =
3431
+ !snapshot.git.isGit
3432
+ || includePr
3433
+ || (previousHasLoadedPullRequest && !gitIdentityChanged);
3434
+ target.latestPayloadHasFullGitData = !snapshot.git.isGit || fullGit;
3435
+ const fingerprint = this.workspaceStatusSnapshotFingerprint(snapshot);
3436
+ if (fingerprint !== target.latestFingerprint) {
3437
+ target.latestFingerprint = fingerprint;
3438
+ this.emitWorkspaceStatusChanged(target, snapshot);
3439
+ }
3440
+ } while (target.refreshQueued);
3441
+ })();
3442
+ try {
3443
+ await target.refreshPromise;
3444
+ }
3445
+ finally {
3446
+ target.refreshPromise = null;
3447
+ }
3448
+ }
3449
+ async ensureWorkspaceStatusWatchTarget(cwd, remoteName) {
3450
+ const targetKey = this.buildWorkspaceStatusTargetKey(cwd, remoteName);
3451
+ const existing = this.workspaceStatusTargets.get(targetKey);
3452
+ if (existing) {
3453
+ return existing;
3454
+ }
3455
+ const watchRoot = await this.resolveCheckoutWatchRoot(cwd);
3456
+ const target = {
3457
+ key: targetKey,
3458
+ cwd,
3459
+ diffCwd: watchRoot ?? cwd,
3460
+ remoteName,
3461
+ subscriptions: new Map(),
3462
+ watchers: [],
3463
+ gitRefreshInterval: null,
3464
+ prRefreshInterval: null,
3465
+ debounceTimer: null,
3466
+ refreshPromise: null,
3467
+ refreshQueued: false,
3468
+ refreshIncludesPr: false,
3469
+ latestPayload: null,
3470
+ latestFingerprint: null,
3471
+ hasLoadedPullRequest: false,
3472
+ latestPayloadHasFullGitData: false,
3473
+ };
3474
+ const repoWatchPath = watchRoot ?? cwd;
3475
+ const watchPaths = new Set([repoWatchPath]);
3476
+ const gitDir = await this.resolveCheckoutGitDir(cwd);
3477
+ if (gitDir) {
3478
+ watchPaths.add(gitDir);
3479
+ }
3480
+ let hasRecursiveRepoCoverage = false;
3481
+ const allowRecursiveRepoWatch = process.platform !== 'linux';
3482
+ for (const watchPath of watchPaths) {
3483
+ const shouldTryRecursive = watchPath === repoWatchPath && allowRecursiveRepoWatch;
3484
+ const createWatcher = (recursive) => watch(watchPath, { recursive }, () => {
3485
+ this.scheduleWorkspaceStatusTargetRefresh(target);
3486
+ });
3487
+ let watcher = null;
3488
+ let watcherIsRecursive = false;
3489
+ try {
3490
+ if (shouldTryRecursive) {
3491
+ watcher = createWatcher(true);
3492
+ watcherIsRecursive = true;
3493
+ }
3494
+ else {
3495
+ watcher = createWatcher(false);
3496
+ }
3497
+ }
3498
+ catch (error) {
3499
+ if (shouldTryRecursive) {
3500
+ try {
3501
+ watcher = createWatcher(false);
3502
+ this.sessionLogger.warn({ err: error, watchPath, cwd, remoteName }, 'Workspace status recursive watch unavailable; using non-recursive fallback');
3503
+ }
3504
+ catch (fallbackError) {
3505
+ this.sessionLogger.warn({ err: fallbackError, watchPath, cwd, remoteName }, 'Failed to start workspace status watcher');
3506
+ }
3507
+ }
3508
+ else {
3509
+ this.sessionLogger.warn({ err: error, watchPath, cwd, remoteName }, 'Failed to start workspace status watcher');
3510
+ }
3511
+ }
3512
+ if (!watcher) {
3513
+ continue;
3514
+ }
3515
+ watcher.on('error', (error) => {
3516
+ this.sessionLogger.warn({ err: error, watchPath, cwd, remoteName }, 'Workspace status watcher error');
3517
+ });
3518
+ target.watchers.push(watcher);
3519
+ if (watchPath === repoWatchPath && watcherIsRecursive) {
3520
+ hasRecursiveRepoCoverage = true;
3521
+ }
3522
+ }
3523
+ if (target.watchers.length === 0 || !hasRecursiveRepoCoverage) {
3524
+ this.sessionLogger.warn({
3525
+ cwd,
3526
+ remoteName,
3527
+ reason: target.watchers.length === 0 ? 'no_watchers' : 'missing_recursive_repo_root_coverage',
3528
+ }, 'Workspace status watchers running with timed refresh support only');
3529
+ }
3530
+ this.workspaceStatusTargets.set(targetKey, target);
3531
+ return target;
3532
+ }
3533
+ async handleSubscribeWorkspaceStatusRequest(msg) {
3534
+ const cwd = expandTilde(msg.cwd);
3535
+ const remoteName = msg.remoteName?.trim() ? msg.remoteName.trim() : null;
3536
+ const mode = msg.mode ?? 'passive';
3537
+ if (remoteName) {
3538
+ this.assertSafeRemoteName(remoteName);
3539
+ }
3540
+ this.removeWorkspaceStatusSubscription(msg.subscriptionId);
3541
+ const target = await this.ensureWorkspaceStatusWatchTarget(cwd, remoteName);
3542
+ target.subscriptions.set(msg.subscriptionId, mode);
3543
+ this.workspaceStatusSubscriptions.set(msg.subscriptionId, {
3544
+ targetKey: target.key,
3545
+ });
3546
+ this.updateWorkspaceStatusGitPolling(target);
3547
+ this.updateWorkspaceStatusPrPolling(target);
3548
+ const fullGit = this.workspaceStatusHasActiveSubscribers(target);
3549
+ const includePr = fullGit;
3550
+ const previousFingerprint = target.latestFingerprint;
3551
+ const hadExistingSubscribers = target.subscriptions.size > 1;
3552
+ const snapshot = target.latestPayload
3553
+ && (!includePr || target.hasLoadedPullRequest)
3554
+ && (!fullGit || target.latestPayloadHasFullGitData)
3555
+ && !(fullGit && target.latestPayload.gitError)
3556
+ ? target.latestPayload
3557
+ : await this.computeWorkspaceStatusSnapshot(target, { includePr, fullGit });
3558
+ const hasLoadedPullRequest = target.hasLoadedPullRequest || includePr || !snapshot.git.isGit;
3559
+ target.latestPayload = snapshot;
3560
+ target.hasLoadedPullRequest = hasLoadedPullRequest;
3561
+ target.latestPayloadHasFullGitData = !snapshot.git.isGit || fullGit;
3562
+ const nextFingerprint = this.workspaceStatusSnapshotFingerprint(snapshot);
3563
+ target.latestFingerprint = nextFingerprint;
3564
+ this.emit({
3565
+ type: 'subscribe_workspace_status_response',
3566
+ payload: {
3567
+ subscriptionId: msg.subscriptionId,
3568
+ ...snapshot,
3569
+ requestId: msg.requestId,
3570
+ },
3571
+ });
3572
+ if (hadExistingSubscribers && nextFingerprint !== previousFingerprint) {
3573
+ this.emitWorkspaceStatusChanged(target, snapshot);
3574
+ }
3575
+ if (!fullGit && snapshot.git.isGit && !hasLoadedPullRequest) {
3576
+ this.scheduleWorkspaceStatusTargetRefresh(target, {
3577
+ includePr: true,
3578
+ debounce: false,
3579
+ });
3580
+ }
3581
+ }
3582
+ handleUnsubscribeWorkspaceStatusRequest(msg) {
3583
+ this.removeWorkspaceStatusSubscription(msg.subscriptionId);
3584
+ }
3585
+ scheduleWorkspaceStatusRefreshForCwd(cwd, options) {
3586
+ const resolvedCwd = expandTilde(cwd);
3587
+ for (const target of this.workspaceStatusTargets.values()) {
3588
+ if (target.cwd !== resolvedCwd && target.diffCwd !== resolvedCwd) {
3589
+ continue;
3590
+ }
3591
+ this.scheduleWorkspaceStatusTargetRefresh(target, {
3592
+ includePr: options?.includePr,
3593
+ debounce: false,
3594
+ });
3595
+ }
3596
+ }
3117
3597
  normalizeCheckoutDiffCompare(compare) {
3118
3598
  if (compare.mode === 'uncommitted' || compare.mode === 'staged' || compare.mode === 'unstaged') {
3119
3599
  return { mode: compare.mode };
@@ -3121,11 +3601,12 @@ export class Session {
3121
3601
  const trimmedBaseRef = compare.baseRef?.trim();
3122
3602
  return trimmedBaseRef ? { mode: 'base', baseRef: trimmedBaseRef } : { mode: 'base' };
3123
3603
  }
3124
- buildCheckoutDiffTargetKey(cwd, compare) {
3604
+ buildCheckoutDiffTargetKey(cwd, compare, detail) {
3125
3605
  return JSON.stringify([
3126
3606
  cwd,
3127
3607
  compare.mode,
3128
3608
  compare.mode === 'base' ? (compare.baseRef ?? '') : '',
3609
+ detail,
3129
3610
  ]);
3130
3611
  }
3131
3612
  closeCheckoutDiffWatchTarget(target) {
@@ -3212,13 +3693,19 @@ export class Session {
3212
3693
  }
3213
3694
  async computeCheckoutDiffSnapshot(cwd, compare, options) {
3214
3695
  const diffCwd = options?.diffCwd ?? cwd;
3696
+ const detail = options?.detail ?? 'full';
3215
3697
  try {
3216
3698
  const diffResult = await getCheckoutDiff(diffCwd, {
3217
3699
  mode: compare.mode,
3218
3700
  baseRef: compare.baseRef,
3219
3701
  includeStructured: true,
3220
3702
  }, { junctionHome: this.junctionHome });
3221
- const files = [...(diffResult.structured ?? [])];
3703
+ const files = [...(diffResult.structured ?? [])].map((file) => detail === 'summary'
3704
+ ? {
3705
+ ...file,
3706
+ hunks: [],
3707
+ }
3708
+ : file);
3222
3709
  files.sort((a, b) => {
3223
3710
  if (a.path === b.path)
3224
3711
  return 0;
@@ -3248,6 +3735,7 @@ export class Session {
3248
3735
  target.refreshQueued = false;
3249
3736
  const snapshot = await this.computeCheckoutDiffSnapshot(target.cwd, target.compare, {
3250
3737
  diffCwd: target.diffCwd,
3738
+ detail: target.detail,
3251
3739
  });
3252
3740
  target.latestPayload = snapshot;
3253
3741
  const fingerprint = this.checkoutDiffSnapshotFingerprint(snapshot);
@@ -3264,8 +3752,8 @@ export class Session {
3264
3752
  target.refreshPromise = null;
3265
3753
  }
3266
3754
  }
3267
- async ensureCheckoutDiffWatchTarget(cwd, compare) {
3268
- const targetKey = this.buildCheckoutDiffTargetKey(cwd, compare);
3755
+ async ensureCheckoutDiffWatchTarget(cwd, compare, detail) {
3756
+ const targetKey = this.buildCheckoutDiffTargetKey(cwd, compare, detail);
3269
3757
  const existing = this.checkoutDiffTargets.get(targetKey);
3270
3758
  if (existing) {
3271
3759
  return existing;
@@ -3276,6 +3764,7 @@ export class Session {
3276
3764
  cwd,
3277
3765
  diffCwd: watchRoot ?? cwd,
3278
3766
  compare,
3767
+ detail,
3279
3768
  subscriptions: new Set(),
3280
3769
  watchers: [],
3281
3770
  fallbackRefreshInterval: null,
@@ -3352,8 +3841,9 @@ export class Session {
3352
3841
  async handleSubscribeCheckoutDiffRequest(msg) {
3353
3842
  const cwd = expandTilde(msg.cwd);
3354
3843
  const compare = this.normalizeCheckoutDiffCompare(msg.compare);
3844
+ const detail = msg.detail ?? 'full';
3355
3845
  this.removeCheckoutDiffSubscription(msg.subscriptionId);
3356
- const target = await this.ensureCheckoutDiffWatchTarget(cwd, compare);
3846
+ const target = await this.ensureCheckoutDiffWatchTarget(cwd, compare, detail);
3357
3847
  target.subscriptions.add(msg.subscriptionId);
3358
3848
  this.checkoutDiffSubscriptions.set(msg.subscriptionId, {
3359
3849
  targetKey: target.key,
@@ -3361,6 +3851,7 @@ export class Session {
3361
3851
  const snapshot = target.latestPayload ??
3362
3852
  (await this.computeCheckoutDiffSnapshot(cwd, compare, {
3363
3853
  diffCwd: target.diffCwd,
3854
+ detail: target.detail,
3364
3855
  }));
3365
3856
  target.latestPayload = snapshot;
3366
3857
  target.latestFingerprint = this.checkoutDiffSnapshotFingerprint(snapshot);
@@ -3400,6 +3891,7 @@ export class Session {
3400
3891
  addAll: msg.addAll ?? true,
3401
3892
  });
3402
3893
  this.scheduleCheckoutDiffRefreshForCwd(cwd);
3894
+ this.scheduleWorkspaceStatusRefreshForCwd(cwd);
3403
3895
  this.emit({
3404
3896
  type: 'checkout_commit_response',
3405
3897
  payload: {
@@ -3463,6 +3955,7 @@ export class Session {
3463
3955
  mode: msg.strategy === 'squash' ? 'squash' : 'merge',
3464
3956
  }, { junctionHome: this.junctionHome });
3465
3957
  this.scheduleCheckoutDiffRefreshForCwd(cwd);
3958
+ this.scheduleWorkspaceStatusRefreshForCwd(cwd, { includePr: true });
3466
3959
  this.emit({
3467
3960
  type: 'checkout_merge_response',
3468
3961
  payload: {
@@ -3503,6 +3996,7 @@ export class Session {
3503
3996
  requireCleanTarget: msg.requireCleanTarget ?? true,
3504
3997
  });
3505
3998
  this.scheduleCheckoutDiffRefreshForCwd(cwd);
3999
+ this.scheduleWorkspaceStatusRefreshForCwd(cwd, { includePr: true });
3506
4000
  this.emit({
3507
4001
  type: 'checkout_merge_from_base_response',
3508
4002
  payload: {
@@ -3529,6 +4023,7 @@ export class Session {
3529
4023
  const { cwd, requestId } = msg;
3530
4024
  try {
3531
4025
  await pushCurrentBranch(cwd, { remoteName: msg.remoteName });
4026
+ this.scheduleWorkspaceStatusRefreshForCwd(cwd, { includePr: true });
3532
4027
  this.emit({
3533
4028
  type: 'checkout_push_response',
3534
4029
  payload: {
@@ -3569,6 +4064,7 @@ export class Session {
3569
4064
  base: msg.baseRef,
3570
4065
  remoteName: msg.remoteName,
3571
4066
  });
4067
+ this.scheduleWorkspaceStatusRefreshForCwd(cwd, { includePr: true });
3572
4068
  this.emit({
3573
4069
  type: 'checkout_pr_create_response',
3574
4070
  payload: {
@@ -3689,6 +4185,7 @@ export class Session {
3689
4185
  method: msg.method ?? 'squash',
3690
4186
  remoteName: msg.remoteName,
3691
4187
  });
4188
+ this.scheduleWorkspaceStatusRefreshForCwd(cwd, { includePr: true });
3692
4189
  this.emit({
3693
4190
  type: 'checkout_pr_merge_response',
3694
4191
  payload: {
@@ -4049,6 +4546,7 @@ export class Session {
4049
4546
  root,
4050
4547
  relativePath: requestedPath,
4051
4548
  ref,
4549
+ detail: request.detail ?? 'full',
4052
4550
  });
4053
4551
  this.emit({
4054
4552
  type: 'workspace_file_explorer_response',
@@ -4978,6 +5476,11 @@ export class Session {
4978
5476
  }
4979
5477
  this.checkoutDiffTargets.clear();
4980
5478
  this.checkoutDiffSubscriptions.clear();
5479
+ for (const target of this.workspaceStatusTargets.values()) {
5480
+ this.closeWorkspaceStatusWatchTarget(target);
5481
+ }
5482
+ this.workspaceStatusTargets.clear();
5483
+ this.workspaceStatusSubscriptions.clear();
4981
5484
  }
4982
5485
  // ============================================================================
4983
5486
  // Terminal Handlers