@junctionpanel/server 0.1.54 → 0.1.56

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 (64) hide show
  1. package/dist/server/client/daemon-client.d.ts +13 -0
  2. package/dist/server/client/daemon-client.d.ts.map +1 -1
  3. package/dist/server/client/daemon-client.js +89 -18
  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/persistence-hooks.js +11 -1
  47. package/dist/server/server/persistence-hooks.js.map +1 -1
  48. package/dist/server/server/session.d.ts +13 -0
  49. package/dist/server/server/session.d.ts.map +1 -1
  50. package/dist/server/server/session.js +411 -170
  51. package/dist/server/server/session.js.map +1 -1
  52. package/dist/server/shared/messages.d.ts +16 -0
  53. package/dist/server/shared/messages.d.ts.map +1 -1
  54. package/dist/server/shared/messages.js +19 -0
  55. package/dist/server/shared/messages.js.map +1 -1
  56. package/dist/server/shared/permission-questions.d.ts +43 -0
  57. package/dist/server/shared/permission-questions.d.ts.map +1 -0
  58. package/dist/server/shared/permission-questions.js +234 -0
  59. package/dist/server/shared/permission-questions.js.map +1 -0
  60. package/dist/server/utils/checkout-git.d.ts +1 -0
  61. package/dist/server/utils/checkout-git.d.ts.map +1 -1
  62. package/dist/server/utils/checkout-git.js +19 -6
  63. package/dist/server/utils/checkout-git.js.map +1 -1
  64. 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,6 +48,7 @@ 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;
@@ -133,6 +134,7 @@ export class Session {
133
134
  this.nextTerminalStreamId = 1;
134
135
  this.checkoutDiffSubscriptions = new Map();
135
136
  this.checkoutDiffTargets = new Map();
137
+ this.workspaceGitOperationStates = sharedWorkspaceGitOperationStates;
136
138
  const { clientId, userId, onMessage, onBinaryMessage, onLifecycleIntent, logger, downloadTokenStore, junctionHome, agentManager, agentStorage, createAgentMcpTransport, terminalManager, agentProviderRuntimeSettings, } = options;
137
139
  this.clientId = clientId;
138
140
  this.userId = userId;
@@ -309,7 +311,10 @@ export class Session {
309
311
  this.sessionLogger.error({ err: error, agentId: input.agentId }, `Failed to record user message for agent ${input.agentId}`);
310
312
  }
311
313
  const prompt = this.buildAgentPrompt(input.text, input.images);
312
- 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
+ });
313
318
  if (!started.ok) {
314
319
  throw new Error(started.error);
315
320
  }
@@ -2351,8 +2356,53 @@ export class Session {
2351
2356
  }
2352
2357
  return resolvedCandidate.startsWith(resolvedRoot + sep);
2353
2358
  }
2354
- async generateCommitMessage(cwd) {
2355
- 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 }));
2356
2406
  const schema = z.object({
2357
2407
  message: z
2358
2408
  .string()
@@ -2383,6 +2433,7 @@ export class Session {
2383
2433
  patch.length > 0 ? patch : '(No diff available)',
2384
2434
  ].join('\n');
2385
2435
  try {
2436
+ const { preferredProvider, providers } = await this.resolveStructuredHelperProviders(cwd, 'commit');
2386
2437
  const result = await generateStructuredAgentResponseWithFallback({
2387
2438
  manager: this.agentManager,
2388
2439
  cwd,
@@ -2390,7 +2441,9 @@ export class Session {
2390
2441
  schema,
2391
2442
  schemaName: 'CommitMessage',
2392
2443
  maxRetries: 2,
2393
- providers: DEFAULT_STRUCTURED_GENERATION_PROVIDERS,
2444
+ providers,
2445
+ preferredProvider,
2446
+ reason: 'commit',
2394
2447
  agentConfigOverrides: {
2395
2448
  title: 'Commit generator',
2396
2449
  internal: true,
@@ -2406,12 +2459,13 @@ export class Session {
2406
2459
  throw error;
2407
2460
  }
2408
2461
  }
2409
- async generatePullRequestText(cwd, baseRef) {
2410
- const diff = await getCheckoutDiff(cwd, {
2411
- mode: 'base',
2412
- baseRef,
2413
- includeStructured: true,
2414
- }, { 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 }));
2415
2469
  const schema = z.object({
2416
2470
  title: z.string().min(1).max(72),
2417
2471
  body: z.string().min(1),
@@ -2439,6 +2493,7 @@ export class Session {
2439
2493
  patch.length > 0 ? patch : '(No diff available)',
2440
2494
  ].join('\n');
2441
2495
  try {
2496
+ const { preferredProvider, providers } = await this.resolveStructuredHelperProviders(cwd, 'pr');
2442
2497
  return await generateStructuredAgentResponseWithFallback({
2443
2498
  manager: this.agentManager,
2444
2499
  cwd,
@@ -2446,7 +2501,9 @@ export class Session {
2446
2501
  schema,
2447
2502
  schemaName: 'PullRequest',
2448
2503
  maxRetries: 2,
2449
- providers: DEFAULT_STRUCTURED_GENERATION_PROVIDERS,
2504
+ providers,
2505
+ preferredProvider,
2506
+ reason: 'pr',
2450
2507
  agentConfigOverrides: {
2451
2508
  title: 'PR generator',
2452
2509
  internal: true,
@@ -2884,7 +2941,8 @@ export class Session {
2884
2941
  async handleCheckoutStatusRequest(msg) {
2885
2942
  const { cwd, requestId } = msg;
2886
2943
  try {
2887
- const status = await getCheckoutStatus(expandTilde(cwd), { junctionHome: this.junctionHome });
2944
+ const resolvedCwd = expandTilde(cwd);
2945
+ const status = await this.runWorkspaceGitRead(resolvedCwd, () => getCheckoutStatus(resolvedCwd, { junctionHome: this.junctionHome }));
2888
2946
  this.emit({
2889
2947
  type: 'checkout_status_response',
2890
2948
  payload: {
@@ -3273,9 +3331,7 @@ export class Session {
3273
3331
  if (target.subscriptions.size === 0) {
3274
3332
  return;
3275
3333
  }
3276
- const intervalMs = this.workspaceStatusHasActiveSubscribers(target)
3277
- ? WORKSPACE_STATUS_PR_ACTIVE_REFRESH_MS
3278
- : WORKSPACE_STATUS_PR_PASSIVE_REFRESH_MS;
3334
+ const intervalMs = this.resolveWorkspaceStatusPrRefreshIntervalMs(target);
3279
3335
  target.prRefreshInterval = setInterval(() => {
3280
3336
  this.scheduleWorkspaceStatusTargetRefresh(target, {
3281
3337
  includePr: true,
@@ -3298,6 +3354,11 @@ export class Session {
3298
3354
  });
3299
3355
  }, WORKSPACE_STATUS_GIT_REFRESH_MS);
3300
3356
  }
3357
+ resolveWorkspaceStatusPrRefreshIntervalMs(target) {
3358
+ return this.workspaceStatusHasActiveSubscribers(target)
3359
+ ? WORKSPACE_STATUS_PR_ACTIVE_REFRESH_MS
3360
+ : WORKSPACE_STATUS_PR_PASSIVE_REFRESH_MS;
3361
+ }
3301
3362
  removeWorkspaceStatusSubscription(subscriptionId) {
3302
3363
  const subscription = this.workspaceStatusSubscriptions.get(subscriptionId);
3303
3364
  if (!subscription) {
@@ -3432,6 +3493,7 @@ export class Session {
3432
3493
  || includePr
3433
3494
  || (previousHasLoadedPullRequest && !gitIdentityChanged);
3434
3495
  target.latestPayloadHasFullGitData = !snapshot.git.isGit || fullGit;
3496
+ this.updateWorkspaceStatusPrPolling(target);
3435
3497
  const fingerprint = this.workspaceStatusSnapshotFingerprint(snapshot);
3436
3498
  if (fingerprint !== target.latestFingerprint) {
3437
3499
  target.latestFingerprint = fingerprint;
@@ -3477,51 +3539,23 @@ export class Session {
3477
3539
  if (gitDir) {
3478
3540
  watchPaths.add(gitDir);
3479
3541
  }
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 }, () => {
3542
+ const watchSetup = this.startCheckoutWatchers({
3543
+ repoWatchPath,
3544
+ watchPaths,
3545
+ onChange: () => {
3485
3546
  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({
3547
+ },
3548
+ recursiveUnavailableMessage: 'Workspace status recursive watch unavailable; using non-recursive fallback',
3549
+ startFailedMessage: 'Failed to start workspace status watcher',
3550
+ watcherErrorMessage: 'Workspace status watcher error',
3551
+ context: { cwd, remoteName },
3552
+ });
3553
+ target.watchers = watchSetup.watchers;
3554
+ if (target.watchers.length === 0 || !watchSetup.hasRecursiveRepoCoverage) {
3555
+ const log = watchSetup.repoWatchPathMissing && watchSetup.failedWatcherCount === 0
3556
+ ? this.sessionLogger.debug.bind(this.sessionLogger)
3557
+ : this.sessionLogger.warn.bind(this.sessionLogger);
3558
+ log({
3525
3559
  cwd,
3526
3560
  remoteName,
3527
3561
  reason: target.watchers.length === 0 ? 'no_watchers' : 'missing_recursive_repo_root_coverage',
@@ -3665,7 +3699,107 @@ export class Session {
3665
3699
  return null;
3666
3700
  }
3667
3701
  }
3702
+ isExpectedMissingWatchPathError(error) {
3703
+ if (!error || typeof error !== 'object') {
3704
+ return false;
3705
+ }
3706
+ const code = 'code' in error && typeof error.code === 'string'
3707
+ ? error.code
3708
+ : null;
3709
+ return code === 'ENOENT' || code === 'ENOTDIR' || code === 'EPERM';
3710
+ }
3711
+ isWatchPathMissing(watchPath) {
3712
+ try {
3713
+ statSync(watchPath);
3714
+ return false;
3715
+ }
3716
+ catch (error) {
3717
+ return this.isExpectedMissingWatchPathError(error);
3718
+ }
3719
+ }
3720
+ startCheckoutWatchers(options) {
3721
+ const watchers = [];
3722
+ let hasRecursiveRepoCoverage = false;
3723
+ let repoWatchPathMissing = false;
3724
+ let failedWatcherCount = 0;
3725
+ const allowRecursiveRepoWatch = process.platform !== 'linux';
3726
+ for (const watchPath of options.watchPaths) {
3727
+ const shouldTryRecursive = watchPath === options.repoWatchPath && allowRecursiveRepoWatch;
3728
+ if (this.isWatchPathMissing(watchPath)) {
3729
+ if (watchPath === options.repoWatchPath) {
3730
+ repoWatchPathMissing = true;
3731
+ }
3732
+ continue;
3733
+ }
3734
+ const createWatcher = (recursive) => watch(watchPath, { recursive }, () => {
3735
+ options.onChange();
3736
+ });
3737
+ let watcher = null;
3738
+ let watcherIsRecursive = false;
3739
+ try {
3740
+ if (shouldTryRecursive) {
3741
+ watcher = createWatcher(true);
3742
+ watcherIsRecursive = true;
3743
+ }
3744
+ else {
3745
+ watcher = createWatcher(false);
3746
+ }
3747
+ }
3748
+ catch (error) {
3749
+ if (shouldTryRecursive) {
3750
+ try {
3751
+ watcher = createWatcher(false);
3752
+ this.sessionLogger.warn({ err: error, watchPath, ...options.context }, options.recursiveUnavailableMessage);
3753
+ }
3754
+ catch (fallbackError) {
3755
+ if (this.isExpectedMissingWatchPathError(fallbackError)
3756
+ && this.isWatchPathMissing(watchPath)) {
3757
+ if (watchPath === options.repoWatchPath) {
3758
+ repoWatchPathMissing = true;
3759
+ }
3760
+ continue;
3761
+ }
3762
+ failedWatcherCount += 1;
3763
+ this.sessionLogger.warn({ err: fallbackError, watchPath, ...options.context }, options.startFailedMessage);
3764
+ }
3765
+ }
3766
+ else {
3767
+ if (this.isExpectedMissingWatchPathError(error) && this.isWatchPathMissing(watchPath)) {
3768
+ if (watchPath === options.repoWatchPath) {
3769
+ repoWatchPathMissing = true;
3770
+ }
3771
+ continue;
3772
+ }
3773
+ failedWatcherCount += 1;
3774
+ this.sessionLogger.warn({ err: error, watchPath, ...options.context }, options.startFailedMessage);
3775
+ }
3776
+ }
3777
+ if (!watcher) {
3778
+ continue;
3779
+ }
3780
+ watcher.on('error', (error) => {
3781
+ if (this.isExpectedMissingWatchPathError(error) && this.isWatchPathMissing(watchPath)) {
3782
+ return;
3783
+ }
3784
+ this.sessionLogger.warn({ err: error, watchPath, ...options.context }, options.watcherErrorMessage);
3785
+ });
3786
+ watchers.push(watcher);
3787
+ if (watchPath === options.repoWatchPath && watcherIsRecursive) {
3788
+ hasRecursiveRepoCoverage = true;
3789
+ }
3790
+ }
3791
+ return {
3792
+ watchers,
3793
+ hasRecursiveRepoCoverage,
3794
+ repoWatchPathMissing,
3795
+ failedWatcherCount,
3796
+ };
3797
+ }
3668
3798
  scheduleCheckoutDiffTargetRefresh(target) {
3799
+ if (this.isWorkspaceGitWritePending(target.workspaceGitKey)) {
3800
+ target.refreshQueuedAfterWrite = true;
3801
+ return;
3802
+ }
3669
3803
  if (target.debounceTimer) {
3670
3804
  clearTimeout(target.debounceTimer);
3671
3805
  }
@@ -3695,11 +3829,11 @@ export class Session {
3695
3829
  const diffCwd = options?.diffCwd ?? cwd;
3696
3830
  const detail = options?.detail ?? 'full';
3697
3831
  try {
3698
- const diffResult = await getCheckoutDiff(diffCwd, {
3832
+ const diffResult = await this.runWorkspaceGitRead(diffCwd, () => getCheckoutDiff(diffCwd, {
3699
3833
  mode: compare.mode,
3700
3834
  baseRef: compare.baseRef,
3701
3835
  includeStructured: true,
3702
- }, { junctionHome: this.junctionHome });
3836
+ }, { junctionHome: this.junctionHome }));
3703
3837
  const files = [...(diffResult.structured ?? [])].map((file) => detail === 'summary'
3704
3838
  ? {
3705
3839
  ...file,
@@ -3759,10 +3893,12 @@ export class Session {
3759
3893
  return existing;
3760
3894
  }
3761
3895
  const watchRoot = await this.resolveCheckoutWatchRoot(cwd);
3896
+ const workspaceGitKey = watchRoot ?? cwd;
3762
3897
  const target = {
3763
3898
  key: targetKey,
3764
3899
  cwd,
3765
3900
  diffCwd: watchRoot ?? cwd,
3901
+ workspaceGitKey,
3766
3902
  compare,
3767
3903
  detail,
3768
3904
  subscriptions: new Set(),
@@ -3771,6 +3907,7 @@ export class Session {
3771
3907
  debounceTimer: null,
3772
3908
  refreshPromise: null,
3773
3909
  refreshQueued: false,
3910
+ refreshQueuedAfterWrite: false,
3774
3911
  latestPayload: null,
3775
3912
  latestFingerprint: null,
3776
3913
  };
@@ -3780,55 +3917,27 @@ export class Session {
3780
3917
  if (gitDir) {
3781
3918
  watchPaths.add(gitDir);
3782
3919
  }
3783
- let hasRecursiveRepoCoverage = false;
3784
- const allowRecursiveRepoWatch = process.platform !== 'linux';
3785
- for (const watchPath of watchPaths) {
3786
- const shouldTryRecursive = watchPath === repoWatchPath && allowRecursiveRepoWatch;
3787
- const createWatcher = (recursive) => watch(watchPath, { recursive }, () => {
3920
+ const watchSetup = this.startCheckoutWatchers({
3921
+ repoWatchPath,
3922
+ watchPaths,
3923
+ onChange: () => {
3788
3924
  this.scheduleCheckoutDiffTargetRefresh(target);
3789
- });
3790
- let watcher = null;
3791
- let watcherIsRecursive = false;
3792
- try {
3793
- if (shouldTryRecursive) {
3794
- watcher = createWatcher(true);
3795
- watcherIsRecursive = true;
3796
- }
3797
- else {
3798
- watcher = createWatcher(false);
3799
- }
3800
- }
3801
- catch (error) {
3802
- if (shouldTryRecursive) {
3803
- try {
3804
- watcher = createWatcher(false);
3805
- this.sessionLogger.warn({ err: error, watchPath, cwd, compare }, 'Checkout diff recursive watch unavailable; using non-recursive fallback');
3806
- }
3807
- catch (fallbackError) {
3808
- this.sessionLogger.warn({ err: fallbackError, watchPath, cwd, compare }, 'Failed to start checkout diff watcher');
3809
- }
3810
- }
3811
- else {
3812
- this.sessionLogger.warn({ err: error, watchPath, cwd, compare }, 'Failed to start checkout diff watcher');
3813
- }
3814
- }
3815
- if (!watcher) {
3816
- continue;
3817
- }
3818
- watcher.on('error', (error) => {
3819
- this.sessionLogger.warn({ err: error, watchPath, cwd, compare }, 'Checkout diff watcher error');
3820
- });
3821
- target.watchers.push(watcher);
3822
- if (watchPath === repoWatchPath && watcherIsRecursive) {
3823
- hasRecursiveRepoCoverage = true;
3824
- }
3825
- }
3826
- const missingRepoCoverage = !hasRecursiveRepoCoverage;
3925
+ },
3926
+ recursiveUnavailableMessage: 'Checkout diff recursive watch unavailable; using non-recursive fallback',
3927
+ startFailedMessage: 'Failed to start checkout diff watcher',
3928
+ watcherErrorMessage: 'Checkout diff watcher error',
3929
+ context: { cwd, compare },
3930
+ });
3931
+ target.watchers = watchSetup.watchers;
3932
+ const missingRepoCoverage = !watchSetup.hasRecursiveRepoCoverage;
3827
3933
  if (target.watchers.length === 0 || missingRepoCoverage) {
3828
3934
  target.fallbackRefreshInterval = setInterval(() => {
3829
3935
  this.scheduleCheckoutDiffTargetRefresh(target);
3830
3936
  }, CHECKOUT_DIFF_FALLBACK_REFRESH_MS);
3831
- this.sessionLogger.warn({
3937
+ const log = watchSetup.repoWatchPathMissing && watchSetup.failedWatcherCount === 0
3938
+ ? this.sessionLogger.debug.bind(this.sessionLogger)
3939
+ : this.sessionLogger.warn.bind(this.sessionLogger);
3940
+ log({
3832
3941
  cwd,
3833
3942
  compare,
3834
3943
  intervalMs: CHECKOUT_DIFF_FALLBACK_REFRESH_MS,
@@ -3848,13 +3957,26 @@ export class Session {
3848
3957
  this.checkoutDiffSubscriptions.set(msg.subscriptionId, {
3849
3958
  targetKey: target.key,
3850
3959
  });
3851
- const snapshot = target.latestPayload ??
3852
- (await this.computeCheckoutDiffSnapshot(cwd, compare, {
3960
+ if (target.refreshPromise) {
3961
+ await target.refreshPromise;
3962
+ }
3963
+ const canReuseLatestPayload = target.latestPayload !== null &&
3964
+ !target.refreshQueuedAfterWrite &&
3965
+ !this.isWorkspaceGitWritePending(target.workspaceGitKey);
3966
+ const shouldPreserveQueuedFingerprint = !canReuseLatestPayload && target.refreshQueuedAfterWrite;
3967
+ const snapshot = canReuseLatestPayload
3968
+ ? target.latestPayload
3969
+ : await this.computeCheckoutDiffSnapshot(cwd, compare, {
3853
3970
  diffCwd: target.diffCwd,
3854
3971
  detail: target.detail,
3855
- }));
3972
+ });
3973
+ if (!snapshot) {
3974
+ throw new Error('Checkout diff snapshot was unavailable');
3975
+ }
3856
3976
  target.latestPayload = snapshot;
3857
- target.latestFingerprint = this.checkoutDiffSnapshotFingerprint(snapshot);
3977
+ if (!shouldPreserveQueuedFingerprint) {
3978
+ target.latestFingerprint = this.checkoutDiffSnapshotFingerprint(snapshot);
3979
+ }
3858
3980
  this.emit({
3859
3981
  type: 'subscribe_checkout_diff_response',
3860
3982
  payload: {
@@ -3876,21 +3998,105 @@ export class Session {
3876
3998
  this.scheduleCheckoutDiffTargetRefresh(target);
3877
3999
  }
3878
4000
  }
4001
+ getWorkspaceGitOperationState(workspaceGitKey) {
4002
+ const existing = this.workspaceGitOperationStates.get(workspaceGitKey);
4003
+ if (existing) {
4004
+ return existing;
4005
+ }
4006
+ const created = {
4007
+ writeBarrier: Promise.resolve(),
4008
+ pendingWrites: 0,
4009
+ activeReaders: 0,
4010
+ readerDrain: Promise.resolve(),
4011
+ resolveReaderDrain: null,
4012
+ };
4013
+ this.workspaceGitOperationStates.set(workspaceGitKey, created);
4014
+ return created;
4015
+ }
4016
+ isWorkspaceGitWritePending(workspaceGitKey) {
4017
+ return (this.workspaceGitOperationStates.get(workspaceGitKey)?.pendingWrites ?? 0) > 0;
4018
+ }
4019
+ async resolveWorkspaceGitOperationKey(cwd) {
4020
+ const resolvedCwd = expandTilde(cwd);
4021
+ return (await this.resolveCheckoutWatchRoot(resolvedCwd)) ?? resolvedCwd;
4022
+ }
4023
+ flushQueuedCheckoutDiffRefreshesForWorkspace(workspaceGitKey) {
4024
+ for (const target of this.checkoutDiffTargets.values()) {
4025
+ if (target.workspaceGitKey !== workspaceGitKey || !target.refreshQueuedAfterWrite) {
4026
+ continue;
4027
+ }
4028
+ target.refreshQueuedAfterWrite = false;
4029
+ this.scheduleCheckoutDiffTargetRefresh(target);
4030
+ }
4031
+ }
4032
+ async runWorkspaceGitRead(cwd, operation) {
4033
+ const workspaceGitKey = await this.resolveWorkspaceGitOperationKey(cwd);
4034
+ const state = this.getWorkspaceGitOperationState(workspaceGitKey);
4035
+ while (state.pendingWrites > 0) {
4036
+ await state.writeBarrier;
4037
+ }
4038
+ state.activeReaders += 1;
4039
+ if (state.activeReaders === 1) {
4040
+ state.readerDrain = new Promise((resolve) => {
4041
+ state.resolveReaderDrain = resolve;
4042
+ });
4043
+ }
4044
+ try {
4045
+ return await operation();
4046
+ }
4047
+ finally {
4048
+ state.activeReaders = Math.max(0, state.activeReaders - 1);
4049
+ if (state.activeReaders === 0) {
4050
+ state.resolveReaderDrain?.();
4051
+ state.resolveReaderDrain = null;
4052
+ state.readerDrain = Promise.resolve();
4053
+ }
4054
+ }
4055
+ }
4056
+ async runWorkspaceGitWrite(cwd, operation) {
4057
+ const workspaceGitKey = await this.resolveWorkspaceGitOperationKey(cwd);
4058
+ const state = this.getWorkspaceGitOperationState(workspaceGitKey);
4059
+ const previousBarrier = state.writeBarrier;
4060
+ let releaseCurrentBarrier = () => { };
4061
+ const currentBarrier = new Promise((resolve) => {
4062
+ releaseCurrentBarrier = resolve;
4063
+ });
4064
+ state.pendingWrites += 1;
4065
+ state.writeBarrier = previousBarrier.then(() => currentBarrier);
4066
+ try {
4067
+ await previousBarrier;
4068
+ while (state.activeReaders > 0) {
4069
+ await state.readerDrain;
4070
+ }
4071
+ return await operation();
4072
+ }
4073
+ finally {
4074
+ state.pendingWrites = Math.max(0, state.pendingWrites - 1);
4075
+ releaseCurrentBarrier();
4076
+ if (state.pendingWrites === 0) {
4077
+ state.writeBarrier = Promise.resolve();
4078
+ this.flushQueuedCheckoutDiffRefreshesForWorkspace(workspaceGitKey);
4079
+ }
4080
+ }
4081
+ }
3879
4082
  async handleCheckoutCommitRequest(msg) {
3880
4083
  const { cwd, requestId } = msg;
3881
4084
  try {
3882
4085
  let message = msg.message?.trim() ?? '';
3883
4086
  if (!message) {
3884
- message = await this.generateCommitMessage(cwd);
4087
+ const diff = await this.runWorkspaceGitRead(cwd, () => getCheckoutDiff(cwd, { mode: 'uncommitted', includeStructured: true }, { junctionHome: this.junctionHome }));
4088
+ message = await this.generateCommitMessage(cwd, diff);
3885
4089
  }
3886
4090
  if (!message) {
3887
4091
  throw new Error('Commit message is required');
3888
4092
  }
3889
- await commitChanges(cwd, {
3890
- message,
3891
- addAll: msg.addAll ?? true,
4093
+ await this.runWorkspaceGitWrite(cwd, async () => {
4094
+ await commitChanges(cwd, {
4095
+ message,
4096
+ addAll: msg.addAll ?? true,
4097
+ });
4098
+ this.scheduleCheckoutDiffRefreshForCwd(cwd);
3892
4099
  });
3893
- this.scheduleCheckoutDiffRefreshForCwd(cwd);
3894
4100
  this.scheduleWorkspaceStatusRefreshForCwd(cwd);
3895
4101
  this.emit({
3896
4102
  type: 'checkout_commit_response',
@@ -3917,45 +4123,54 @@ export class Session {
3917
4123
  async handleCheckoutMergeRequest(msg) {
3918
4124
  const { cwd, requestId } = msg;
3919
4125
  try {
3920
- const status = await getCheckoutStatus(cwd, { junctionHome: this.junctionHome });
3921
- if (!status.isGit) {
3922
- try {
3923
- await execAsync('git rev-parse --is-inside-work-tree', {
4126
+ const mergeOperationCwd = await resolveMergeToBaseOperationCwd(cwd, { baseRef: msg.baseRef }, { junctionHome: this.junctionHome });
4127
+ await this.runWorkspaceGitWrite(mergeOperationCwd, async () => {
4128
+ const status = await getCheckoutStatus(cwd, { junctionHome: this.junctionHome });
4129
+ if (!status.isGit) {
4130
+ try {
4131
+ await execAsync('git rev-parse --is-inside-work-tree', {
4132
+ cwd,
4133
+ env: READ_ONLY_GIT_ENV,
4134
+ });
4135
+ }
4136
+ catch (error) {
4137
+ const details = typeof error?.stderr === 'string'
4138
+ ? String(error.stderr).trim()
4139
+ : error instanceof Error
4140
+ ? error.message
4141
+ : String(error);
4142
+ throw new Error(`Not a git repository: ${cwd}\n${details}`.trim());
4143
+ }
4144
+ }
4145
+ if (msg.requireCleanTarget) {
4146
+ const { stdout } = await execAsync('git status --porcelain', {
3924
4147
  cwd,
3925
4148
  env: READ_ONLY_GIT_ENV,
3926
4149
  });
4150
+ if (stdout.trim().length > 0) {
4151
+ throw new Error('Working directory has uncommitted changes.');
4152
+ }
3927
4153
  }
3928
- catch (error) {
3929
- const details = typeof error?.stderr === 'string'
3930
- ? String(error.stderr).trim()
3931
- : error instanceof Error
3932
- ? error.message
3933
- : String(error);
3934
- throw new Error(`Not a git repository: ${cwd}\n${details}`.trim());
4154
+ let baseRef = msg.baseRef ?? (status.isGit ? status.baseRef : null);
4155
+ if (!baseRef) {
4156
+ throw new Error('Base branch is required for merge');
3935
4157
  }
3936
- }
3937
- if (msg.requireCleanTarget) {
3938
- const { stdout } = await execAsync('git status --porcelain', {
3939
- cwd,
3940
- env: READ_ONLY_GIT_ENV,
3941
- });
3942
- if (stdout.trim().length > 0) {
3943
- throw new Error('Working directory has uncommitted changes.');
4158
+ if (baseRef.startsWith('origin/')) {
4159
+ baseRef = baseRef.slice('origin/'.length);
3944
4160
  }
3945
- }
3946
- let baseRef = msg.baseRef ?? (status.isGit ? status.baseRef : null);
3947
- if (!baseRef) {
3948
- throw new Error('Base branch is required for merge');
3949
- }
3950
- if (baseRef.startsWith('origin/')) {
3951
- baseRef = baseRef.slice('origin/'.length);
3952
- }
3953
- await mergeToBase(cwd, {
3954
- baseRef,
3955
- mode: msg.strategy === 'squash' ? 'squash' : 'merge',
3956
- }, { junctionHome: this.junctionHome });
3957
- this.scheduleCheckoutDiffRefreshForCwd(cwd);
4161
+ await mergeToBase(cwd, {
4162
+ baseRef,
4163
+ mode: msg.strategy === 'squash' ? 'squash' : 'merge',
4164
+ }, { junctionHome: this.junctionHome });
4165
+ this.scheduleCheckoutDiffRefreshForCwd(cwd);
4166
+ if (resolve(mergeOperationCwd) !== resolve(cwd)) {
4167
+ this.scheduleCheckoutDiffRefreshForCwd(mergeOperationCwd);
4168
+ }
4169
+ });
3958
4170
  this.scheduleWorkspaceStatusRefreshForCwd(cwd, { includePr: true });
4171
+ if (resolve(mergeOperationCwd) !== resolve(cwd)) {
4172
+ this.scheduleWorkspaceStatusRefreshForCwd(mergeOperationCwd, { includePr: true });
4173
+ }
3959
4174
  this.emit({
3960
4175
  type: 'checkout_merge_response',
3961
4176
  payload: {
@@ -3981,21 +4196,23 @@ export class Session {
3981
4196
  async handleCheckoutMergeFromBaseRequest(msg) {
3982
4197
  const { cwd, requestId } = msg;
3983
4198
  try {
3984
- if (msg.requireCleanTarget ?? true) {
3985
- const { stdout } = await execAsync('git status --porcelain', {
3986
- cwd,
3987
- env: READ_ONLY_GIT_ENV,
3988
- });
3989
- if (stdout.trim().length > 0) {
3990
- throw new Error('Working directory has uncommitted changes.');
4199
+ await this.runWorkspaceGitWrite(cwd, async () => {
4200
+ if (msg.requireCleanTarget ?? true) {
4201
+ const { stdout } = await execAsync('git status --porcelain', {
4202
+ cwd,
4203
+ env: READ_ONLY_GIT_ENV,
4204
+ });
4205
+ if (stdout.trim().length > 0) {
4206
+ throw new Error('Working directory has uncommitted changes.');
4207
+ }
3991
4208
  }
3992
- }
3993
- await mergeFromBase(cwd, {
3994
- baseRef: msg.baseRef,
3995
- remoteName: msg.remoteName,
3996
- requireCleanTarget: msg.requireCleanTarget ?? true,
4209
+ await mergeFromBase(cwd, {
4210
+ baseRef: msg.baseRef,
4211
+ remoteName: msg.remoteName,
4212
+ requireCleanTarget: msg.requireCleanTarget ?? true,
4213
+ });
4214
+ this.scheduleCheckoutDiffRefreshForCwd(cwd);
3997
4215
  });
3998
- this.scheduleCheckoutDiffRefreshForCwd(cwd);
3999
4216
  this.scheduleWorkspaceStatusRefreshForCwd(cwd, { includePr: true });
4000
4217
  this.emit({
4001
4218
  type: 'checkout_merge_from_base_response',
@@ -4022,7 +4239,7 @@ export class Session {
4022
4239
  async handleCheckoutPushRequest(msg) {
4023
4240
  const { cwd, requestId } = msg;
4024
4241
  try {
4025
- await pushCurrentBranch(cwd, { remoteName: msg.remoteName });
4242
+ await this.runWorkspaceGitWrite(cwd, () => pushCurrentBranch(cwd, { remoteName: msg.remoteName }));
4026
4243
  this.scheduleWorkspaceStatusRefreshForCwd(cwd, { includePr: true });
4027
4244
  this.emit({
4028
4245
  type: 'checkout_push_response',
@@ -4052,18 +4269,23 @@ export class Session {
4052
4269
  let title = msg.title?.trim() ?? '';
4053
4270
  let body = msg.body?.trim() ?? '';
4054
4271
  if (!title || !body) {
4055
- const generated = await this.generatePullRequestText(cwd, msg.baseRef);
4272
+ const diff = await this.runWorkspaceGitRead(cwd, () => getCheckoutDiff(cwd, {
4273
+ mode: 'base',
4274
+ baseRef: msg.baseRef,
4275
+ includeStructured: true,
4276
+ }, { junctionHome: this.junctionHome }));
4277
+ const generated = await this.generatePullRequestText(cwd, msg.baseRef, diff);
4056
4278
  if (!title)
4057
4279
  title = generated.title;
4058
4280
  if (!body)
4059
4281
  body = generated.body;
4060
4282
  }
4061
- const result = await createPullRequest(cwd, {
4283
+ const result = await this.runWorkspaceGitWrite(cwd, () => createPullRequest(cwd, {
4062
4284
  title,
4063
4285
  body,
4064
4286
  base: msg.baseRef,
4065
4287
  remoteName: msg.remoteName,
4066
- });
4288
+ }));
4067
4289
  this.scheduleWorkspaceStatusRefreshForCwd(cwd, { includePr: true });
4068
4290
  this.emit({
4069
4291
  type: 'checkout_pr_create_response',
@@ -4121,11 +4343,11 @@ export class Session {
4121
4343
  const cwd = expandTilde(msg.cwd);
4122
4344
  const { requestId } = msg;
4123
4345
  try {
4124
- const result = await searchPullRequests(cwd, {
4346
+ const result = await this.runWorkspaceGitRead(cwd, () => searchPullRequests(cwd, {
4125
4347
  query: msg.query,
4126
4348
  limit: msg.limit,
4127
4349
  remoteName: msg.remoteName,
4128
- });
4350
+ }));
4129
4351
  this.emit({
4130
4352
  type: 'checkout_pr_search_response',
4131
4353
  payload: {
@@ -4153,7 +4375,7 @@ export class Session {
4153
4375
  async handleCheckoutPrFailureLogsRequest(msg) {
4154
4376
  const { cwd, requestId } = msg;
4155
4377
  try {
4156
- const result = await getPullRequestFailureLogs(cwd, { remoteName: msg.remoteName });
4378
+ const result = await this.runWorkspaceGitRead(cwd, () => getPullRequestFailureLogs(cwd, { remoteName: msg.remoteName }));
4157
4379
  this.emit({
4158
4380
  type: 'checkout_pr_failure_logs_response',
4159
4381
  payload: {
@@ -4536,8 +4758,8 @@ export class Session {
4536
4758
  */
4537
4759
  async handleWorkspaceFileExplorerRequest(request) {
4538
4760
  const { cwd, path: requestedPath = '.', mode, requestId, ref } = request;
4761
+ const root = expandTilde(cwd);
4539
4762
  try {
4540
- const root = expandTilde(cwd);
4541
4763
  if (ref) {
4542
4764
  this.assertSafeGitRef(ref, 'workspace file ref');
4543
4765
  }
@@ -4584,6 +4806,25 @@ export class Session {
4584
4806
  }
4585
4807
  }
4586
4808
  catch (error) {
4809
+ if (mode === 'file'
4810
+ && request.allowMissing
4811
+ && isWorkspaceExplorerMissingPathError(error)
4812
+ && (ref != null || existsSync(root))) {
4813
+ this.emit({
4814
+ type: 'workspace_file_explorer_response',
4815
+ payload: {
4816
+ cwd,
4817
+ path: requestedPath,
4818
+ ref: ref ?? null,
4819
+ mode,
4820
+ directory: null,
4821
+ file: null,
4822
+ error: null,
4823
+ requestId,
4824
+ },
4825
+ });
4826
+ return;
4827
+ }
4587
4828
  const log = isWorkspaceExplorerMissingPathError(error)
4588
4829
  ? this.sessionLogger.debug.bind(this.sessionLogger)
4589
4830
  : this.sessionLogger.error.bind(this.sessionLogger);