@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.
- package/dist/server/client/daemon-client.d.ts +30 -1
- package/dist/server/client/daemon-client.d.ts.map +1 -1
- package/dist/server/client/daemon-client.js +236 -24
- package/dist/server/client/daemon-client.js.map +1 -1
- package/dist/server/server/agent/activity-curator.d.ts.map +1 -1
- package/dist/server/server/agent/activity-curator.js +12 -0
- package/dist/server/server/agent/activity-curator.js.map +1 -1
- package/dist/server/server/agent/agent-manager.d.ts +8 -0
- package/dist/server/server/agent/agent-manager.d.ts.map +1 -1
- package/dist/server/server/agent/agent-manager.js +175 -29
- package/dist/server/server/agent/agent-manager.js.map +1 -1
- package/dist/server/server/agent/agent-metadata-generator.d.ts.map +1 -1
- package/dist/server/server/agent/agent-metadata-generator.js +11 -2
- package/dist/server/server/agent/agent-metadata-generator.js.map +1 -1
- package/dist/server/server/agent/agent-projections.d.ts.map +1 -1
- package/dist/server/server/agent/agent-projections.js +0 -1
- package/dist/server/server/agent/agent-projections.js.map +1 -1
- package/dist/server/server/agent/agent-response-loop.d.ts +27 -0
- package/dist/server/server/agent/agent-response-loop.d.ts.map +1 -1
- package/dist/server/server/agent/agent-response-loop.js +78 -4
- package/dist/server/server/agent/agent-response-loop.js.map +1 -1
- package/dist/server/server/agent/agent-sdk-types.d.ts +17 -1
- package/dist/server/server/agent/agent-sdk-types.d.ts.map +1 -1
- package/dist/server/server/agent/agent-sdk-types.js.map +1 -1
- package/dist/server/server/agent/agent-storage.d.ts +0 -3
- package/dist/server/server/agent/agent-storage.d.ts.map +1 -1
- package/dist/server/server/agent/agent-storage.js +0 -1
- package/dist/server/server/agent/agent-storage.js.map +1 -1
- package/dist/server/server/agent/provider-model-cache.d.ts +14 -0
- package/dist/server/server/agent/provider-model-cache.d.ts.map +1 -0
- package/dist/server/server/agent/provider-model-cache.js +71 -0
- package/dist/server/server/agent/provider-model-cache.js.map +1 -0
- package/dist/server/server/agent/providers/claude/model-catalog.d.ts.map +1 -1
- package/dist/server/server/agent/providers/claude/model-catalog.js +15 -1
- package/dist/server/server/agent/providers/claude/model-catalog.js.map +1 -1
- package/dist/server/server/agent/providers/claude-agent.d.ts +1 -0
- package/dist/server/server/agent/providers/claude-agent.d.ts.map +1 -1
- package/dist/server/server/agent/providers/claude-agent.js +43 -12
- package/dist/server/server/agent/providers/claude-agent.js.map +1 -1
- package/dist/server/server/agent/providers/gemini-agent.d.ts.map +1 -1
- package/dist/server/server/agent/providers/gemini-agent.js +36 -1
- package/dist/server/server/agent/providers/gemini-agent.js.map +1 -1
- package/dist/server/server/agent/providers/opencode-agent.js +8 -2
- package/dist/server/server/agent/providers/opencode-agent.js.map +1 -1
- package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts.map +1 -1
- package/dist/server/server/file-explorer/service.d.ts +6 -1
- package/dist/server/server/file-explorer/service.d.ts.map +1 -1
- package/dist/server/server/file-explorer/service.js +72 -23
- package/dist/server/server/file-explorer/service.js.map +1 -1
- package/dist/server/server/persistence-hooks.js +11 -1
- package/dist/server/server/persistence-hooks.js.map +1 -1
- package/dist/server/server/session.d.ts +38 -0
- package/dist/server/server/session.d.ts.map +1 -1
- package/dist/server/server/session.js +956 -207
- package/dist/server/server/session.js.map +1 -1
- package/dist/server/server/tool-call-preview.d.ts.map +1 -1
- package/dist/server/server/tool-call-preview.js +5 -3
- package/dist/server/server/tool-call-preview.js.map +1 -1
- package/dist/server/shared/messages.d.ts +7630 -2138
- package/dist/server/shared/messages.d.ts.map +1 -1
- package/dist/server/shared/messages.js +64 -0
- package/dist/server/shared/messages.js.map +1 -1
- package/dist/server/shared/permission-questions.d.ts +43 -0
- package/dist/server/shared/permission-questions.d.ts.map +1 -0
- package/dist/server/shared/permission-questions.js +234 -0
- package/dist/server/shared/permission-questions.js.map +1 -0
- package/dist/server/utils/checkout-git.d.ts +1 -0
- package/dist/server/utils/checkout-git.d.ts.map +1 -1
- package/dist/server/utils/checkout-git.js +19 -6
- package/dist/server/utils/checkout-git.js.map +1 -1
- 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,
|
|
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
|
|
2343
|
-
const
|
|
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
|
|
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 =
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
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
|
|
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
|
|
2710
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
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
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3362
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
3399
|
-
|
|
3400
|
-
|
|
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.
|
|
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
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
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
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
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
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
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.
|
|
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
|
|
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
|