@vscode/component-explorer-cli 0.1.1-9 → 0.2.1-0

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 (134) hide show
  1. package/LICENSE +21 -0
  2. package/SECURITY.md +14 -0
  3. package/dist/WorktreePool.d.ts +22 -0
  4. package/dist/WorktreePool.d.ts.map +1 -0
  5. package/dist/WorktreePool.js +58 -0
  6. package/dist/WorktreePool.js.map +1 -0
  7. package/dist/WorktreePool.test.d.ts +2 -0
  8. package/dist/WorktreePool.test.d.ts.map +1 -0
  9. package/dist/_virtual/_build-info.js +4 -0
  10. package/dist/_virtual/_build-info.js.map +1 -0
  11. package/dist/browserPage.d.ts +15 -1
  12. package/dist/browserPage.d.ts.map +1 -1
  13. package/dist/browserPage.js +51 -7
  14. package/dist/browserPage.js.map +1 -1
  15. package/dist/commands/acceptCommand.d.ts.map +1 -1
  16. package/dist/commands/acceptCommand.js +3 -2
  17. package/dist/commands/acceptCommand.js.map +1 -1
  18. package/dist/commands/checkStabilityCommand.d.ts +12 -0
  19. package/dist/commands/checkStabilityCommand.d.ts.map +1 -0
  20. package/dist/commands/checkStabilityCommand.js +84 -0
  21. package/dist/commands/checkStabilityCommand.js.map +1 -0
  22. package/dist/commands/compareCommand.d.ts +1 -0
  23. package/dist/commands/compareCommand.d.ts.map +1 -1
  24. package/dist/commands/compareCommand.js +25 -4
  25. package/dist/commands/compareCommand.js.map +1 -1
  26. package/dist/commands/mcpCommand.d.ts +1 -0
  27. package/dist/commands/mcpCommand.d.ts.map +1 -1
  28. package/dist/commands/mcpCommand.js +23 -10
  29. package/dist/commands/mcpCommand.js.map +1 -1
  30. package/dist/commands/screenshotCommand.d.ts +2 -0
  31. package/dist/commands/screenshotCommand.d.ts.map +1 -1
  32. package/dist/commands/screenshotCommand.js +19 -4
  33. package/dist/commands/screenshotCommand.js.map +1 -1
  34. package/dist/commands/serveCommand.d.ts +4 -0
  35. package/dist/commands/serveCommand.d.ts.map +1 -1
  36. package/dist/commands/serveCommand.js +101 -26
  37. package/dist/commands/serveCommand.js.map +1 -1
  38. package/dist/commands/watchCommand.d.ts +2 -0
  39. package/dist/commands/watchCommand.d.ts.map +1 -1
  40. package/dist/commands/watchCommand.js +18 -66
  41. package/dist/commands/watchCommand.js.map +1 -1
  42. package/dist/comparison.d.ts +11 -1
  43. package/dist/comparison.d.ts.map +1 -1
  44. package/dist/comparison.js +25 -11
  45. package/dist/comparison.js.map +1 -1
  46. package/dist/component-explorer-config.schema.json +260 -55
  47. package/dist/componentExplorer.d.ts +21 -18
  48. package/dist/componentExplorer.d.ts.map +1 -1
  49. package/dist/componentExplorer.js +60 -19
  50. package/dist/componentExplorer.js.map +1 -1
  51. package/dist/daemon/DaemonService.d.ts +100 -11
  52. package/dist/daemon/DaemonService.d.ts.map +1 -1
  53. package/dist/daemon/DaemonService.js +512 -129
  54. package/dist/daemon/DaemonService.js.map +1 -1
  55. package/dist/daemon/dynamicSessions.test.d.ts +2 -0
  56. package/dist/daemon/dynamicSessions.test.d.ts.map +1 -0
  57. package/dist/daemon/lifecycle.d.ts +2 -1
  58. package/dist/daemon/lifecycle.d.ts.map +1 -1
  59. package/dist/daemon/lifecycle.js +52 -30
  60. package/dist/daemon/lifecycle.js.map +1 -1
  61. package/dist/daemon/pipeClient.d.ts.map +1 -1
  62. package/dist/daemon/pipeClient.js +81 -2
  63. package/dist/daemon/pipeClient.js.map +1 -1
  64. package/dist/daemon/pipeServer.d.ts +2 -1
  65. package/dist/daemon/pipeServer.d.ts.map +1 -1
  66. package/dist/daemon/pipeServer.js +59 -2
  67. package/dist/daemon/pipeServer.js.map +1 -1
  68. package/dist/daemon/version.d.ts +10 -0
  69. package/dist/daemon/version.d.ts.map +1 -0
  70. package/dist/daemon/version.js +17 -0
  71. package/dist/daemon/version.js.map +1 -0
  72. package/dist/dependencyInstaller.d.ts +2 -2
  73. package/dist/dependencyInstaller.d.ts.map +1 -1
  74. package/dist/dependencyInstaller.js.map +1 -1
  75. package/dist/explorerSession.d.ts +3 -3
  76. package/dist/explorerSession.d.ts.map +1 -1
  77. package/dist/explorerSession.js +26 -9
  78. package/dist/explorerSession.js.map +1 -1
  79. package/dist/git/gitIndexResolver.d.ts +25 -0
  80. package/dist/git/gitIndexResolver.d.ts.map +1 -0
  81. package/dist/git/gitIndexResolver.js +91 -0
  82. package/dist/git/gitIndexResolver.js.map +1 -0
  83. package/dist/git/gitIndexResolver.test.d.ts +2 -0
  84. package/dist/git/gitIndexResolver.test.d.ts.map +1 -0
  85. package/dist/git/gitService.d.ts +2 -0
  86. package/dist/git/gitService.d.ts.map +1 -1
  87. package/dist/git/gitService.js +6 -0
  88. package/dist/git/gitService.js.map +1 -1
  89. package/dist/git/gitWorktreeManager.d.ts +6 -0
  90. package/dist/git/gitWorktreeManager.d.ts.map +1 -1
  91. package/dist/git/gitWorktreeManager.js +42 -13
  92. package/dist/git/gitWorktreeManager.js.map +1 -1
  93. package/dist/git/gitWorktreeManager.test.d.ts +2 -0
  94. package/dist/git/gitWorktreeManager.test.d.ts.map +1 -0
  95. package/dist/git/testUtils.d.ts +13 -0
  96. package/dist/git/testUtils.d.ts.map +1 -0
  97. package/dist/httpServer.d.ts +18 -7
  98. package/dist/httpServer.d.ts.map +1 -1
  99. package/dist/httpServer.js +117 -18
  100. package/dist/httpServer.js.map +1 -1
  101. package/dist/httpServer.test.d.ts +2 -0
  102. package/dist/httpServer.test.d.ts.map +1 -0
  103. package/dist/index.js +11 -2
  104. package/dist/index.js.map +1 -1
  105. package/dist/logger.d.ts +1 -0
  106. package/dist/logger.d.ts.map +1 -1
  107. package/dist/logger.js +7 -1
  108. package/dist/logger.js.map +1 -1
  109. package/dist/mcp/McpServer.d.ts +18 -0
  110. package/dist/mcp/McpServer.d.ts.map +1 -1
  111. package/dist/mcp/McpServer.js +555 -13
  112. package/dist/mcp/McpServer.js.map +1 -1
  113. package/dist/mcp/TaskManager.d.ts +28 -0
  114. package/dist/mcp/TaskManager.d.ts.map +1 -0
  115. package/dist/mcp/TaskManager.js +54 -0
  116. package/dist/mcp/TaskManager.js.map +1 -0
  117. package/dist/packages/simple-api/dist/{chunk-Q24JOMNK.js → chunk-TAEFVNPN.js} +1 -1
  118. package/dist/packages/simple-api/dist/chunk-TAEFVNPN.js.map +1 -0
  119. package/dist/packages/simple-api/dist/express.js +11 -3
  120. package/dist/packages/simple-api/dist/express.js.map +1 -1
  121. package/dist/utils.d.ts +7 -0
  122. package/dist/utils.d.ts.map +1 -1
  123. package/dist/utils.js +6 -7
  124. package/dist/utils.js.map +1 -1
  125. package/dist/visualCache.d.ts +34 -0
  126. package/dist/visualCache.d.ts.map +1 -0
  127. package/dist/visualCache.js +90 -0
  128. package/dist/visualCache.js.map +1 -0
  129. package/dist/watchConfig.d.ts +68 -15
  130. package/dist/watchConfig.d.ts.map +1 -1
  131. package/dist/watchConfig.js +109 -65
  132. package/dist/watchConfig.js.map +1 -1
  133. package/package.json +21 -4
  134. package/dist/packages/simple-api/dist/chunk-Q24JOMNK.js.map +0 -1
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import * as path from 'node:path';
2
3
  import '../external/vscode-observables/observables/dist/observableInternal/index.js';
3
4
  import { observableValue } from '../external/vscode-observables/observables/dist/observableInternal/observables/observableValue.js';
4
5
  import '../external/vscode-observables/observables/dist/observableInternal/debugLocation.js';
@@ -10,11 +11,19 @@ import '../external/vscode-observables/observables/dist/observableInternal/obser
10
11
  import { createApiFactory } from '../packages/simple-api/dist/chunk-3R7GHWBM.js';
11
12
  import { AsyncStream } from '../packages/simple-api/dist/chunk-SGBCNXYH.js';
12
13
  import { ExplorerSession } from '../explorerSession.js';
14
+ import { GitIndexResolver } from '../git/gitIndexResolver.js';
15
+ import { DirtyWorktreeError } from '../git/gitWorktreeManager.js';
16
+ import { execGit } from '../git/gitUtils.js';
13
17
  import { PlaywrightBrowserPageFactory } from '../browserPage.js';
14
18
  import { DefaultComponentExplorerHttpServerFactory } from '../httpServer.js';
15
19
  import { installDependencies } from '../dependencyInstaller.js';
16
20
  import { FileApprovalStore } from './approvalStore.js';
21
+ import { VisualCache } from '../visualCache.js';
17
22
  import { contentHash } from '../screenshotCache.js';
23
+ import { WorktreePool } from '../WorktreePool.js';
24
+ import { ViteProjectRef } from '../viteProjectRef.js';
25
+ import { pluginProtocolVersionText, daemonApiVersionText } from './version.js';
26
+ export { isCompatibleVersion, parseVersion } from './version.js';
18
27
 
19
28
  // ---------------------------------------------------------------------------
20
29
  // ActivityTracker — monitors idle time and triggers shutdown
@@ -90,13 +99,16 @@ class DaemonService {
90
99
  _serverFactory;
91
100
  _sessions = new Map();
92
101
  _sessionConfigs = new Map();
102
+ _dynamicSessionMeta = new Map();
93
103
  _resolvers = new Map();
94
104
  _eventListenerCount = observableValue(this, 0);
95
105
  _eventListeners = new Set();
96
106
  _shutdownRequested = false;
97
107
  _shutdownResolvers = [];
98
108
  _activityTracker;
109
+ _worktreePool;
99
110
  approvals;
111
+ visualCache;
100
112
  api;
101
113
  constructor(_config, _pipeName, _logger, _browserFactory, _serverFactory, approvalStorePath, idleTimeoutMs) {
102
114
  this._config = _config;
@@ -105,23 +117,22 @@ class DaemonService {
105
117
  this._browserFactory = _browserFactory;
106
118
  this._serverFactory = _serverFactory;
107
119
  this.approvals = new FileApprovalStore(approvalStorePath);
120
+ this.visualCache = new VisualCache(`${_config.screenshotDir}/visual-cache.json`);
108
121
  this._activityTracker = new ActivityTracker(idleTimeoutMs, () => this.requestShutdown(), this._logger);
122
+ if (_config.worktreePool) {
123
+ this._worktreePool = new WorktreePool(_config.worktreePool.maxSlots, _config.repo.worktreeRootPath());
124
+ }
109
125
  this.api = this._buildApi();
110
126
  }
111
127
  static async create(config, logger, pipeName, options) {
112
- const browserFactory = new PlaywrightBrowserPageFactory();
128
+ const browserFactory = new PlaywrightBrowserPageFactory(options?.headed ?? false, 30_000, logger);
113
129
  const serverFactory = new DefaultComponentExplorerHttpServerFactory();
114
130
  const approvalStorePath = `${config.screenshotDir}/approvals.json`;
115
131
  const idleTimeoutMs = options?.idleTimeoutMs ?? 5 * 60 * 1000; // 5 minutes default
116
- // Wrap the logger so messages also appear on the event stream
117
- const eventStreamLogger = new EventStreamLogger(logger);
118
- const svc = new DaemonService(config, pipeName ?? '', eventStreamLogger, browserFactory, serverFactory, approvalStorePath, idleTimeoutMs);
119
- eventStreamLogger._emitter = (event) => svc._emit(event);
120
- const currentSession = config.sessions.find(s => s.source.kind === 'current');
121
- const resolveViteFrom = currentSession?.viteProject.cwd;
132
+ const svc = new DaemonService(config, pipeName ?? '', logger, browserFactory, serverFactory, approvalStorePath, idleTimeoutMs);
122
133
  for (const sessionConfig of config.sessions) {
123
134
  svc._sessionConfigs.set(sessionConfig.name, sessionConfig);
124
- await svc._setupSession(sessionConfig, config.repo, resolveViteFrom);
135
+ await svc._setupSession(sessionConfig);
125
136
  }
126
137
  return svc;
127
138
  }
@@ -175,7 +186,10 @@ class DaemonService {
175
186
  image: (args.includeImage ?? true)
176
187
  ? Buffer.from(result.screenshots[result.screenshots.length - 1].image).toString('base64')
177
188
  : undefined,
178
- errors: result.errors.length > 0 ? result.errors : undefined,
189
+ hasError: result.hasError,
190
+ error: result.error,
191
+ events: result.events.length > 0 ? result.events : undefined,
192
+ resultData: result.resultData,
179
193
  isStable,
180
194
  stabilityScreenshots,
181
195
  };
@@ -183,12 +197,15 @@ class DaemonService {
183
197
  const result = await session.explorer.screenshotFixture(args.fixtureId);
184
198
  this.assertSourceTreeId(args.sessionName, args.sourceTreeId);
185
199
  return {
186
- hash: contentHash(result.image),
200
+ hash: result.image ? contentHash(result.image) : undefined,
187
201
  sourceTreeId: args.sourceTreeId,
188
- image: (args.includeImage ?? true)
202
+ image: (args.includeImage ?? true) && result.image
189
203
  ? Buffer.from(result.image).toString('base64')
190
204
  : undefined,
191
- errors: result.errors.length > 0 ? result.errors : undefined,
205
+ hasError: result.hasError,
206
+ error: result.error,
207
+ events: result.events?.length > 0 ? result.events : undefined,
208
+ resultData: result.resultData,
192
209
  };
193
210
  }),
194
211
  takeBatch: createMethod({
@@ -211,11 +228,14 @@ class DaemonService {
211
228
  this.assertSourceTreeId(args.sessionName, args.sourceTreeId);
212
229
  screenshots.push({
213
230
  fixtureId,
214
- hash: contentHash(result.image),
215
- image: includeImages
231
+ hash: result.image ? contentHash(result.image) : undefined,
232
+ image: includeImages && result.image
216
233
  ? Buffer.from(result.image).toString('base64')
217
234
  : undefined,
218
- errors: result.errors.length > 0 ? result.errors : undefined,
235
+ hasError: result.hasError,
236
+ error: result.error,
237
+ events: result.events.length > 0 ? result.events : undefined,
238
+ resultData: result.resultData,
219
239
  });
220
240
  }
221
241
  this._logger.trace(`takeBatch: done in ${Date.now() - startTime}ms`);
@@ -236,24 +256,42 @@ class DaemonService {
236
256
  const currentSession = this.getSession(args.currentSessionName);
237
257
  this.assertSourceTreeId(args.baselineSessionName, args.baselineSourceTreeId);
238
258
  this.assertSourceTreeId(args.currentSessionName, args.currentSourceTreeId);
239
- const baselineResult = await baselineSession.explorer.screenshotFixture(args.fixtureId);
259
+ const [baselineResult, baselineFixtures] = await Promise.all([
260
+ baselineSession.explorer.screenshotFixture(args.fixtureId),
261
+ baselineSession.explorer.listFixtures(),
262
+ ]);
240
263
  this.assertSourceTreeId(args.baselineSessionName, args.baselineSourceTreeId);
241
- const currentResult = await currentSession.explorer.screenshotFixture(args.fixtureId);
264
+ const [currentResult, currentFixtures] = await Promise.all([
265
+ currentSession.explorer.screenshotFixture(args.fixtureId),
266
+ currentSession.explorer.listFixtures(),
267
+ ]);
242
268
  this.assertSourceTreeId(args.currentSessionName, args.currentSourceTreeId);
243
- const baselineHash = contentHash(baselineResult.image);
244
- const currentHash = contentHash(currentResult.image);
269
+ const baselineHash = baselineResult.image ? contentHash(baselineResult.image) : undefined;
270
+ const currentHash = currentResult.image ? contentHash(currentResult.image) : undefined;
245
271
  const includeImages = args.includeImages ?? false;
272
+ const currentFixture = currentFixtures.find(f => f.fixtureId === args.fixtureId);
273
+ const baselineFixture = baselineFixtures.find(f => f.fixtureId === args.fixtureId);
274
+ const labels = currentFixture?.labels;
275
+ const labelsChanged = baselineFixture && !arraysEqual(baselineFixture.labels, currentFixture?.labels ?? []);
246
276
  return {
247
277
  match: baselineHash === currentHash,
248
278
  baselineHash,
249
279
  currentHash,
250
- baselineImage: includeImages
280
+ baselineImage: includeImages && baselineResult.image
251
281
  ? Buffer.from(baselineResult.image).toString('base64') : undefined,
252
- currentImage: includeImages
282
+ currentImage: includeImages && currentResult.image
253
283
  ? Buffer.from(currentResult.image).toString('base64') : undefined,
254
- baselineErrors: baselineResult.errors.length > 0 ? baselineResult.errors : undefined,
255
- currentErrors: currentResult.errors.length > 0 ? currentResult.errors : undefined,
256
- approval: (baselineHash !== currentHash)
284
+ baselineHasError: baselineResult.hasError,
285
+ baselineError: baselineResult.error,
286
+ baselineEvents: baselineResult.events.length > 0 ? baselineResult.events : undefined,
287
+ baselineResultData: baselineResult.resultData,
288
+ currentHasError: currentResult.hasError,
289
+ currentError: currentResult.error,
290
+ currentEvents: currentResult.events.length > 0 ? currentResult.events : undefined,
291
+ currentResultData: currentResult.resultData,
292
+ labels,
293
+ labelsBefore: labelsChanged ? baselineFixture.labels : undefined,
294
+ approval: (baselineHash !== undefined && currentHash !== undefined && baselineHash !== currentHash)
257
295
  ? this.approvals.lookup({
258
296
  fixtureId: args.fixtureId,
259
297
  originalHash: baselineHash,
@@ -302,6 +340,29 @@ class DaemonService {
302
340
  return this.approvals.lookup(args) ?? null;
303
341
  }),
304
342
  }),
343
+ visualReview: createApi({
344
+ getStatus: createMethod({
345
+ args: {
346
+ fixtureId: z.string(),
347
+ expectedVisualDescriptions: z.array(z.string()),
348
+ screenshotHash: z.string(),
349
+ },
350
+ }, async (args) => {
351
+ this._activityTracker.reportActivity();
352
+ return this.visualCache.getReviewStatus(args.fixtureId, args.expectedVisualDescriptions, args.screenshotHash);
353
+ }),
354
+ approve: createMethod({
355
+ args: {
356
+ fixtureId: z.string(),
357
+ expectedVisualDescriptions: z.array(z.string()),
358
+ screenshotHash: z.string(),
359
+ comment: z.string(),
360
+ },
361
+ }, async (args) => {
362
+ this._activityTracker.reportActivity();
363
+ this.visualCache.approve(args.fixtureId, args.expectedVisualDescriptions, args.screenshotHash, args.comment);
364
+ }),
365
+ }),
305
366
  evaluate: createMethod({
306
367
  args: {
307
368
  sessionName: z.string(),
@@ -317,6 +378,17 @@ class DaemonService {
317
378
  this.assertSourceTreeId(args.sessionName, args.sourceTreeId);
318
379
  return { result };
319
380
  }),
381
+ setBrowserVisibility: createMethod({
382
+ args: { visible: z.boolean() },
383
+ }, async (args, ctx) => {
384
+ this._activityTracker.reportActivity();
385
+ this._logger.debug(`Set browser visibility: ${args.visible} (client=${ctx.clientName})`);
386
+ await this._browserFactory.setHeaded(args.visible);
387
+ for (const session of this._sessions.values()) {
388
+ session.explorer.closePage();
389
+ }
390
+ return { visible: args.visible };
391
+ }),
320
392
  events: createMethod({ args: {} }, async () => {
321
393
  return AsyncStream.fromIterable(this.eventStream());
322
394
  }),
@@ -325,6 +397,48 @@ class DaemonService {
325
397
  this._logger.trace(`API: sessions (client=${ctx.clientName}, eventListeners=${this._eventListeners.size})`);
326
398
  return this.getSessionInfos();
327
399
  }),
400
+ version: createMethod({ args: {} }, async () => {
401
+ this._activityTracker.reportActivity();
402
+ return {
403
+ daemonApiVersion: daemonApiVersionText,
404
+ pluginProtocolVersion: pluginProtocolVersionText,
405
+ };
406
+ }),
407
+ restartSession: createMethod({
408
+ args: { sessionName: z.string() },
409
+ }, async (args, ctx) => {
410
+ this._activityTracker.reportActivity();
411
+ this._logger.debug(`Restart session "${args.sessionName}" requested (client=${ctx.clientName})`);
412
+ await this._restartSession(args.sessionName);
413
+ return this.getSessionInfos();
414
+ }),
415
+ openSession: createMethod({
416
+ args: {
417
+ name: z.string(),
418
+ ref: z.string(),
419
+ },
420
+ }, async (args, ctx) => {
421
+ this._activityTracker.reportActivity();
422
+ this._logger.log(`Open session "${args.name}" @ ${args.ref} requested (client=${ctx.clientName})`);
423
+ return this._openDynamicSession(args.name, args.ref);
424
+ }),
425
+ closeSession: createMethod({
426
+ args: { name: z.string() },
427
+ }, async (args, ctx) => {
428
+ this._activityTracker.reportActivity();
429
+ this._logger.log(`Close session "${args.name}" requested (client=${ctx.clientName})`);
430
+ return this._closeDynamicSession(args.name);
431
+ }),
432
+ updateSessionRef: createMethod({
433
+ args: {
434
+ name: z.string(),
435
+ ref: z.string(),
436
+ },
437
+ }, async (args, ctx) => {
438
+ this._activityTracker.reportActivity();
439
+ this._logger.log(`Update session ref "${args.name}" → ${args.ref} requested (client=${ctx.clientName})`);
440
+ return this._updateDynamicSessionRef(args.name, args.ref);
441
+ }),
328
442
  shutdown: createMethod({ args: {} }, async (_args, ctx) => {
329
443
  this._logger.debug(`Shutdown requested via API (client=${ctx.clientName})`);
330
444
  this.requestShutdown();
@@ -340,18 +454,56 @@ class DaemonService {
340
454
  return session;
341
455
  }
342
456
  getSessionInfos(reader) {
343
- return this._config.sessions.map(sc => {
457
+ const infos = [];
458
+ const reportedNames = new Set();
459
+ // Static sessions (from config)
460
+ for (const sc of this._config.sessions) {
461
+ reportedNames.add(sc.name);
344
462
  const session = this._sessions.get(sc.name);
463
+ const meta = this._dynamicSessionMeta.get(sc.name);
345
464
  if (!session) {
346
- return { name: sc.name, sourceKind: sc.source.kind, isLoading: true };
465
+ infos.push({ name: sc.name, sourceKind: sc.source.kind, isLoading: true });
347
466
  }
348
- return {
349
- name: sc.name,
350
- sourceKind: sc.source.kind,
351
- serverUrl: session.serverUrl,
352
- sourceTreeId: reader ? session.sourceTreeId.read(reader).value : session.sourceTreeId.get().value,
353
- };
354
- });
467
+ else if (meta) {
468
+ infos.push({
469
+ name: sc.name,
470
+ sourceKind: 'worktree',
471
+ serverUrl: session.serverUrl,
472
+ sourceTreeId: reader ? session.sourceTreeId.read(reader).value : session.sourceTreeId.get().value,
473
+ worktreePath: meta.worktreePath,
474
+ ref: meta.ref,
475
+ });
476
+ }
477
+ else {
478
+ infos.push({
479
+ name: sc.name,
480
+ sourceKind: sc.source.kind,
481
+ serverUrl: session.serverUrl,
482
+ sourceTreeId: reader ? session.sourceTreeId.read(reader).value : session.sourceTreeId.get().value,
483
+ });
484
+ }
485
+ }
486
+ // Dynamic worktree sessions (not already reported as static)
487
+ for (const [name, meta] of this._dynamicSessionMeta) {
488
+ if (reportedNames.has(name)) {
489
+ continue;
490
+ }
491
+ const session = this._sessions.get(name);
492
+ if (!session) {
493
+ infos.push({ name, sourceKind: 'worktree', isLoading: true });
494
+ }
495
+ else {
496
+ infos.push({
497
+ name,
498
+ sourceKind: 'worktree',
499
+ serverUrl: session.serverUrl,
500
+ sourceTreeId: reader ? session.sourceTreeId.read(reader).value : session.sourceTreeId.get().value,
501
+ worktreePath: meta.worktreePath,
502
+ ref: meta.ref,
503
+ });
504
+ }
505
+ }
506
+ return infos;
355
507
  }
356
508
  waitForSession(sessionName) {
357
509
  const sessionObs = derived(this, reader => {
@@ -394,8 +546,10 @@ class DaemonService {
394
546
  self._eventListeners.add(listener);
395
547
  self._eventListenerCount.set(self._eventListeners.size, undefined);
396
548
  self._activityTracker.setActive(true);
549
+ self._logger.debug(`Event stream opened (listeners: ${self._eventListeners.size})`);
397
550
  const onShutdown = () => {
398
551
  done = true;
552
+ cleanup();
399
553
  if (resolve) {
400
554
  const r = resolve;
401
555
  resolve = undefined;
@@ -407,6 +561,7 @@ class DaemonService {
407
561
  self._eventListeners.delete(listener);
408
562
  self._eventListenerCount.set(self._eventListeners.size, undefined);
409
563
  self._activityTracker.setActive(false);
564
+ self._logger.debug(`Event stream closed (listeners: ${self._eventListeners.size})`);
410
565
  };
411
566
  return {
412
567
  next() {
@@ -421,6 +576,11 @@ class DaemonService {
421
576
  return() {
422
577
  done = true;
423
578
  cleanup();
579
+ if (resolve) {
580
+ const r = resolve;
581
+ resolve = undefined;
582
+ r({ value: undefined, done: true });
583
+ }
424
584
  return Promise.resolve({ value: undefined, done: true });
425
585
  },
426
586
  };
@@ -442,68 +602,57 @@ class DaemonService {
442
602
  _sourceChangeDisposables = [];
443
603
  _startSourceChangeWatchers() {
444
604
  for (const [name, session] of this._sessions) {
445
- let previousValue = session.sourceTreeId.get().value;
446
- const disposable = autorun(reader => {
447
- const current = session.sourceTreeId.read(reader);
448
- if (current.value !== previousValue) {
449
- this._logger.debug(`Source tree changed: ${name} ${previousValue} → ${current.value}`);
450
- previousValue = current.value;
451
- this._emit({ type: 'source-change', sessionName: name, sourceTreeId: current.value });
452
- }
453
- });
454
- this._sourceChangeDisposables.push(disposable);
455
- }
456
- // Watch for ref changes (worktree sessions)
457
- for (const [name, resolver] of this._resolvers) {
458
- let previousCommit = resolver.resolvedCommit.get();
459
- const disposable = autorun(reader => {
460
- const commit = resolver.resolvedCommit.read(reader);
461
- if (!previousCommit.equals(commit)) {
462
- previousCommit = commit;
463
- this._handleRefChange(name, resolver.ref, commit);
464
- }
465
- });
466
- this._sourceChangeDisposables.push(disposable);
605
+ this._addSourceChangeWatcher(name, session);
467
606
  }
468
607
  }
469
- async _handleRefChange(sessionName, ref, newCommit) {
470
- this._logger.log(`Ref ${ref} moved to ${newCommit.toShort()}`);
471
- const git = this._config.repo;
472
- const sessionConfig = this._sessionConfigs.get(sessionName);
473
- if (!sessionConfig || sessionConfig.source.kind !== 'worktree') {
608
+ _addSourceChangeWatcher(name, session) {
609
+ let previousValue = session.sourceTreeId.get().value;
610
+ const disposable = autorun(reader => {
611
+ const current = session.sourceTreeId.read(reader);
612
+ if (current.value !== previousValue) {
613
+ this._logger.debug(`Source tree changed: ${name} ${previousValue} → ${current.value}`);
614
+ previousValue = current.value;
615
+ this._emit({ type: 'source-change', sessionName: name, sourceTreeId: current.value });
616
+ }
617
+ });
618
+ this._sourceChangeDisposables.push(disposable);
619
+ }
620
+ async _handleRefChange(sessionName, ref, previousCommit, newCommit) {
621
+ const changedFiles = await this._getChangedFiles(previousCommit, newCommit);
622
+ this._logger.log(`Ref ${ref} moved to ${newCommit.toShort()} (${changedFiles.length} file(s) changed${changedFiles.length > 0 ? ': ' + changedFiles.join(', ') : ''})`);
623
+ const meta = this._dynamicSessionMeta.get(sessionName);
624
+ if (!meta) {
474
625
  return;
475
626
  }
476
- const wt = sessionConfig.source.worktree;
477
- const wtInfo = await git.worktrees.info(wt.worktreePath);
627
+ const git = this._config.repo;
628
+ const wtInfo = await git.worktrees.info(meta.worktreePath);
478
629
  if (wtInfo && wtInfo.isDirty) {
479
630
  this._logger.log(`Worktree is dirty, skipping update to ${newCommit.toShort()}`);
480
631
  return;
481
632
  }
482
- // Dispose old session
483
- const oldSession = this._sessions.get(sessionName);
484
- if (oldSession) {
485
- this._logger.debug(`Disposing session: ${sessionName}`);
486
- await oldSession.dispose();
487
- }
488
- // Checkout + reinstall
489
633
  if (wtInfo) {
490
- await git.worktrees.checkout(wt.worktreePath, newCommit);
634
+ await git.worktrees.checkout(meta.worktreePath, newCommit);
491
635
  }
492
636
  else {
493
- await git.worktrees.create(wt.worktreePath, newCommit);
637
+ await git.worktrees.create(meta.worktreePath, newCommit);
494
638
  }
495
- await installDependencies(wt.worktreePath, wt.install, this._logger);
496
- // Recreate session
497
- const currentSession = this._config.sessions.find(s => s.source.kind === 'current');
498
- const resolveViteFrom = currentSession?.viteProject.cwd;
499
- const newSession = await ExplorerSession.create(sessionConfig.name, sessionConfig.viteProject, this._serverFactory, this._browserFactory, {
500
- logger: this._logger,
501
- resolveViteFrom,
502
- hmrAllowedPaths: this._config.viteHmr?.allowedPaths,
503
- });
504
- this._sessions.set(sessionName, newSession);
639
+ const sessionConfig = this._sessionConfigs.get(sessionName);
640
+ const installSetup = (sessionConfig?.source.kind === 'worktree' ? sessionConfig.source.install : undefined)
641
+ ?? this._config.worktreePool?.setup
642
+ ?? { kind: 'auto' };
643
+ await installDependencies(meta.worktreePath, installSetup, this._logger);
505
644
  this._emit({ type: 'ref-change', sessionName, newCommit: newCommit.toShort() });
506
645
  }
646
+ async _getChangedFiles(oldCommit, newCommit) {
647
+ try {
648
+ const output = await execGit(this._config.repo.gitRoot, ['diff', '--name-only', oldCommit.hash, newCommit.hash]);
649
+ return output.trim().split('\n').filter(f => f.length > 0);
650
+ }
651
+ catch (e) {
652
+ this._logger.log(`Failed to get changed files (${oldCommit.toShort()}..${newCommit.toShort()}): ${e instanceof Error ? e.message : e}`);
653
+ return [];
654
+ }
655
+ }
507
656
  // -- Shutdown ------------------------------------------------------------
508
657
  requestShutdown() {
509
658
  this._shutdownRequested = true;
@@ -519,6 +668,10 @@ class DaemonService {
519
668
  d.dispose();
520
669
  }
521
670
  this._sourceChangeDisposables = [];
671
+ for (const d of this._dynamicRefWatchers.values()) {
672
+ d.dispose();
673
+ }
674
+ this._dynamicRefWatchers.clear();
522
675
  for (const session of this._sessions.values()) {
523
676
  await session.dispose();
524
677
  }
@@ -529,77 +682,307 @@ class DaemonService {
529
682
  await this._browserFactory.dispose();
530
683
  }
531
684
  // -- Private helpers -----------------------------------------------------
532
- /** @internal — also called by EventStreamLogger */
533
685
  _emit(event) {
534
686
  for (const listener of this._eventListeners) {
535
687
  listener(event);
536
688
  }
537
689
  }
538
- async _setupSession(sessionConfig, git, resolveViteFrom) {
690
+ async _restartSession(sessionName) {
691
+ const config = this._sessionConfigs.get(sessionName);
692
+ const meta = this._dynamicSessionMeta.get(sessionName);
693
+ if (!config && !meta) {
694
+ throw new Error(`Unknown session: "${sessionName}"`);
695
+ }
696
+ const existing = this._sessions.get(sessionName);
697
+ if (existing) {
698
+ this._logger.debug(`Disposing session: ${sessionName}`);
699
+ await existing.dispose();
700
+ this._sessions.delete(sessionName);
701
+ }
702
+ this._logger.log(`Restarting server: ${sessionName}`);
703
+ if (meta) {
704
+ await this._createWorktreeExplorerSession(sessionName, meta.worktreePath);
705
+ }
706
+ else if (config) {
707
+ await this._createExplorerSession(config);
708
+ }
709
+ }
710
+ async _createExplorerSession(sessionConfig) {
711
+ const session = await ExplorerSession.create(sessionConfig.name, sessionConfig.server, this._serverFactory, this._browserFactory, {
712
+ logger: this._logger,
713
+ daemonConfig: {
714
+ pipeName: this._pipeName,
715
+ sessionName: sessionConfig.name,
716
+ daemonApiVersion: daemonApiVersionText,
717
+ pluginProtocolVersion: pluginProtocolVersionText,
718
+ },
719
+ });
720
+ this._sessions.set(sessionConfig.name, session);
721
+ this._logger.debug(`Session ready: ${sessionConfig.name} (${session.serverUrl})`);
722
+ }
723
+ async _createWorktreeExplorerSession(sessionName, worktreePath) {
724
+ const defaultServer = this._config.defaultServerConfig;
725
+ if (defaultServer.kind !== 'vite') {
726
+ throw new Error(`Worktree sessions require a Vite server configuration`);
727
+ }
728
+ const configDirRelToGitRoot = path.relative(this._config.repo.gitRoot, this._config.configDir);
729
+ const worktreeConfigDir = path.resolve(worktreePath, configDirRelToGitRoot);
730
+ const defaultViteConfig = path.basename(defaultServer.viteProject.configFile ?? 'vite.config.ts');
731
+ const viteConfigPath = path.resolve(worktreeConfigDir, defaultViteConfig);
732
+ const viteProject = ViteProjectRef.fromViteConfigPath(viteConfigPath);
733
+ const currentSession = this._config.sessions.find(s => s.source.kind === 'current');
734
+ const resolveViteFrom = currentSession?.server.kind === 'vite' ? currentSession.server.viteProject.configFile : undefined;
735
+ this._logger.debug(`Worktree session "${sessionName}": resolveViteFrom=${resolveViteFrom}`);
736
+ const worktreeServerConfig = { kind: 'vite', viteProject, hmr: defaultServer.hmr };
737
+ const session = await ExplorerSession.create(sessionName, worktreeServerConfig, this._serverFactory, this._browserFactory, {
738
+ logger: this._logger,
739
+ resolveViteFrom,
740
+ daemonConfig: {
741
+ pipeName: this._pipeName,
742
+ sessionName,
743
+ daemonApiVersion: daemonApiVersionText,
744
+ pluginProtocolVersion: pluginProtocolVersionText,
745
+ },
746
+ });
747
+ this._sessions.set(sessionName, session);
748
+ this._logger.debug(`Session ready: ${sessionName} (${session.serverUrl})`);
749
+ }
750
+ async _setupSession(sessionConfig) {
539
751
  this._logger.debug(`Setting up session: ${sessionConfig.name} (${sessionConfig.source.kind})`);
752
+ this._logger.log(`Starting server: ${sessionConfig.name}`);
540
753
  if (sessionConfig.source.kind === 'worktree') {
541
- const wt = sessionConfig.source.worktree;
542
- const resolver = await git.createCommitResolver(wt.ref);
543
- this._resolvers.set(sessionConfig.name, resolver);
754
+ await this._setupWorktreeSession(sessionConfig, sessionConfig.source);
755
+ }
756
+ else {
757
+ await this._createExplorerSession(sessionConfig);
758
+ }
759
+ }
760
+ async _setupWorktreeSession(sessionConfig, source) {
761
+ if (!this._worktreePool) {
762
+ throw new Error(`Session "${sessionConfig.name}" requires a worktree but no worktree pool is available`);
763
+ }
764
+ const slot = this._worktreePool.allocate(sessionConfig.name);
765
+ const git = this._config.repo;
766
+ const ref = source.ref;
767
+ const resolver = ref === GitIndexResolver.INDEX_REF
768
+ ? await git.createIndexResolver()
769
+ : await git.createCommitResolver(ref);
770
+ this._resolvers.set(sessionConfig.name, resolver);
771
+ const resolvedCommit = resolver.resolvedCommit.get();
772
+ const wtInfo = await git.worktrees.info(slot.worktreePath);
773
+ if (!wtInfo) {
774
+ this._logger.log(`Creating worktree at ${slot.worktreePath} (${ref} @ ${resolvedCommit.toShort()})`);
775
+ await git.worktrees.create(slot.worktreePath, resolvedCommit);
776
+ }
777
+ else if (!wtInfo.checkedOutCommit.equals(resolvedCommit)) {
778
+ if (wtInfo.isDirty) {
779
+ throw new Error(`Worktree slot ${slot.index} is dirty. Dirty files:\n` +
780
+ wtInfo.dirtyFiles.map(f => ` ${f}`).join('\n'));
781
+ }
782
+ this._logger.log(`Updating worktree to ${resolvedCommit.toShort()}`);
783
+ await git.worktrees.checkout(slot.worktreePath, resolvedCommit);
784
+ }
785
+ else {
786
+ this._logger.log(`Worktree already at ${resolvedCommit.toShort()}`);
787
+ }
788
+ const installSetup = source.install ?? this._config.worktreePool?.setup ?? { kind: 'auto' };
789
+ await installDependencies(slot.worktreePath, installSetup, this._logger);
790
+ this._dynamicSessionMeta.set(sessionConfig.name, { ref, worktreePath: slot.worktreePath });
791
+ await this._createWorktreeExplorerSession(sessionConfig.name, slot.worktreePath);
792
+ this._addDynamicRefWatcher(sessionConfig.name, resolver);
793
+ }
794
+ // -- Dynamic session management ------------------------------------------
795
+ async _openDynamicSession(name, ref) {
796
+ if (this._sessions.has(name) || this._sessionConfigs.has(name) || this._dynamicSessionMeta.has(name)) {
797
+ return { error: `Session "${name}" already exists` };
798
+ }
799
+ if (!this._worktreePool) {
800
+ return { error: 'No worktree pool configured. Add a "worktree" section to your component-explorer.json config.' };
801
+ }
802
+ let slot;
803
+ try {
804
+ slot = this._worktreePool.allocate(name);
805
+ }
806
+ catch (e) {
807
+ return { error: e instanceof Error ? e.message : String(e) };
808
+ }
809
+ const git = this._config.repo;
810
+ const isIndex = ref === GitIndexResolver.INDEX_REF;
811
+ try {
812
+ let resolver;
813
+ if (isIndex) {
814
+ resolver = await git.createIndexResolver();
815
+ }
816
+ else {
817
+ resolver = await git.createCommitResolver(ref);
818
+ }
819
+ this._resolvers.set(name, resolver);
544
820
  const resolvedCommit = resolver.resolvedCommit.get();
545
- const wtInfo = await git.worktrees.info(wt.worktreePath);
821
+ const wtInfo = await git.worktrees.info(slot.worktreePath);
546
822
  if (!wtInfo) {
547
- this._logger.log(`Creating worktree at ${wt.worktreePath} (${wt.ref} @ ${resolvedCommit.toShort()})`);
548
- await git.worktrees.create(wt.worktreePath, resolvedCommit);
549
- await installDependencies(wt.worktreePath, wt.install, this._logger);
823
+ this._logger.log(`Creating worktree at ${slot.worktreePath} (${ref} @ ${resolvedCommit.toShort()})`);
824
+ await git.worktrees.create(slot.worktreePath, resolvedCommit);
550
825
  }
551
826
  else if (!wtInfo.checkedOutCommit.equals(resolvedCommit)) {
552
827
  if (wtInfo.isDirty) {
553
- this._logger.log(`Worktree is dirty, using existing checkout at ${wtInfo.checkedOutCommit.toShort()}`);
554
- }
555
- else {
556
- this._logger.log(`Updating worktree to ${resolvedCommit.toShort()}`);
557
- await git.worktrees.checkout(wt.worktreePath, resolvedCommit);
558
- await installDependencies(wt.worktreePath, wt.install, this._logger);
828
+ this._worktreePool.release(name);
829
+ this._resolvers.get(name)?.dispose();
830
+ this._resolvers.delete(name);
831
+ return {
832
+ error: `Worktree slot ${slot.index} is dirty. Dirty files:\n` +
833
+ wtInfo.dirtyFiles.map(f => ` ${f}`).join('\n'),
834
+ };
559
835
  }
836
+ this._logger.log(`Updating worktree to ${resolvedCommit.toShort()}`);
837
+ await git.worktrees.checkout(slot.worktreePath, resolvedCommit);
560
838
  }
561
839
  else {
562
840
  this._logger.log(`Worktree already at ${resolvedCommit.toShort()}`);
563
841
  }
842
+ const poolConfig = this._config.worktreePool;
843
+ await installDependencies(slot.worktreePath, poolConfig.setup, this._logger);
844
+ this._dynamicSessionMeta.set(name, { ref, worktreePath: slot.worktreePath });
845
+ await this._createWorktreeExplorerSession(name, slot.worktreePath);
846
+ this._addDynamicRefWatcher(name, resolver);
847
+ this._emit({ type: 'session-change' });
848
+ return { sessions: this.getSessionInfos() };
849
+ }
850
+ catch (e) {
851
+ this._worktreePool.release(name);
852
+ this._resolvers.get(name)?.dispose();
853
+ this._resolvers.delete(name);
854
+ this._dynamicSessionMeta.delete(name);
855
+ const msg = e instanceof Error ? e.message : String(e);
856
+ this._logger.log(`Failed to open session "${name}": ${msg}`);
857
+ return { error: `Failed to open session: ${msg}` };
564
858
  }
565
- this._logger.log(`Starting server: ${sessionConfig.name}`);
566
- // Set daemon config env var so the Vite plugin enables the daemon proxy
567
- process.env.COMPONENT_EXPLORER_DAEMON_CONFIG = JSON.stringify({
568
- pipeName: this._pipeName,
569
- sessionName: sessionConfig.name,
570
- });
571
- const session = await ExplorerSession.create(sessionConfig.name, sessionConfig.viteProject, this._serverFactory, this._browserFactory, {
572
- logger: this._logger,
573
- resolveViteFrom: sessionConfig.source.kind === 'worktree' ? resolveViteFrom : undefined,
574
- hmrAllowedPaths: this._config.viteHmr?.allowedPaths,
575
- });
576
- this._sessions.set(sessionConfig.name, session);
577
- this._logger.debug(`Session ready: ${sessionConfig.name} (${session.serverUrl})`);
578
859
  }
579
- }
580
- // ---------------------------------------------------------------------------
581
- // EventStreamLogger — wraps a base logger and also emits log events
582
- // ---------------------------------------------------------------------------
583
- class EventStreamLogger {
584
- _base;
585
- _emitter;
586
- constructor(_base) {
587
- this._base = _base;
860
+ async _closeDynamicSession(name) {
861
+ if (!this._dynamicSessionMeta.has(name)) {
862
+ if (this._sessionConfigs.has(name)) {
863
+ return { error: `Session "${name}" is a static session and cannot be closed` };
864
+ }
865
+ return { error: `Session "${name}" does not exist` };
866
+ }
867
+ const session = this._sessions.get(name);
868
+ if (session) {
869
+ this._logger.debug(`Disposing session: ${name}`);
870
+ await session.dispose();
871
+ this._sessions.delete(name);
872
+ }
873
+ const resolver = this._resolvers.get(name);
874
+ if (resolver) {
875
+ resolver.dispose();
876
+ this._resolvers.delete(name);
877
+ }
878
+ // Remove any ref watcher disposable
879
+ const watcherDisposable = this._dynamicRefWatchers.get(name);
880
+ if (watcherDisposable) {
881
+ watcherDisposable.dispose();
882
+ this._dynamicRefWatchers.delete(name);
883
+ }
884
+ if (this._worktreePool) {
885
+ this._worktreePool.release(name);
886
+ }
887
+ this._dynamicSessionMeta.delete(name);
888
+ this._logger.log(`Session "${name}" closed`);
889
+ this._emit({ type: 'session-change' });
890
+ return { sessions: this.getSessionInfos() };
588
891
  }
589
- log(message) { this.info(message); }
590
- info(message) {
591
- this._base.info(message);
592
- this._emitter?.({ type: 'log', level: 'info', message });
892
+ async _updateDynamicSessionRef(name, newRef) {
893
+ const meta = this._dynamicSessionMeta.get(name);
894
+ if (!meta) {
895
+ if (this._sessionConfigs.has(name)) {
896
+ return { error: `Session "${name}" is a static session — use restartSession instead` };
897
+ }
898
+ return { error: `Session "${name}" does not exist` };
899
+ }
900
+ const git = this._config.repo;
901
+ // Check dirty before doing anything
902
+ const wtInfo = await git.worktrees.info(meta.worktreePath);
903
+ if (wtInfo && wtInfo.isDirty) {
904
+ return {
905
+ error: `Worktree is dirty, cannot update ref. Dirty files:\n` +
906
+ wtInfo.dirtyFiles.map(f => ` ${f}`).join('\n'),
907
+ };
908
+ }
909
+ // Dispose old resolver
910
+ const oldResolver = this._resolvers.get(name);
911
+ if (oldResolver) {
912
+ oldResolver.dispose();
913
+ this._resolvers.delete(name);
914
+ }
915
+ const oldWatcher = this._dynamicRefWatchers.get(name);
916
+ if (oldWatcher) {
917
+ oldWatcher.dispose();
918
+ this._dynamicRefWatchers.delete(name);
919
+ }
920
+ try {
921
+ const isIndex = newRef === GitIndexResolver.INDEX_REF;
922
+ let resolver;
923
+ if (isIndex) {
924
+ resolver = await git.createIndexResolver();
925
+ }
926
+ else {
927
+ resolver = await git.createCommitResolver(newRef);
928
+ }
929
+ this._resolvers.set(name, resolver);
930
+ const resolvedCommit = resolver.resolvedCommit.get();
931
+ // Checkout in worktree — don't restart Vite, let HMR handle it
932
+ if (wtInfo) {
933
+ if (!wtInfo.checkedOutCommit.equals(resolvedCommit)) {
934
+ await git.worktrees.checkout(meta.worktreePath, resolvedCommit);
935
+ }
936
+ }
937
+ else {
938
+ await git.worktrees.create(meta.worktreePath, resolvedCommit);
939
+ }
940
+ const sessionConfig = this._sessionConfigs.get(name);
941
+ const installSetup = (sessionConfig?.source.kind === 'worktree' ? sessionConfig.source.install : undefined)
942
+ ?? this._config.worktreePool?.setup
943
+ ?? { kind: 'auto' };
944
+ await installDependencies(meta.worktreePath, installSetup, this._logger);
945
+ meta.ref = newRef;
946
+ this._addDynamicRefWatcher(name, resolver);
947
+ return { sessions: this.getSessionInfos() };
948
+ }
949
+ catch (e) {
950
+ if (e instanceof DirtyWorktreeError) {
951
+ return {
952
+ error: `Worktree is dirty, cannot update ref. Dirty files:\n` +
953
+ e.dirtyFiles.map(f => ` ${f}`).join('\n'),
954
+ };
955
+ }
956
+ const msg = e instanceof Error ? e.message : String(e);
957
+ this._logger.log(`Failed to update session ref "${name}": ${msg}`);
958
+ return { error: `Failed to update session ref: ${msg}` };
959
+ }
593
960
  }
594
- debug(message) {
595
- this._base.debug(message);
596
- this._emitter?.({ type: 'log', level: 'debug', message });
961
+ _dynamicRefWatchers = new Map();
962
+ _addDynamicRefWatcher(sessionName, resolver) {
963
+ let previousCommit = resolver.resolvedCommit.get();
964
+ const disposable = autorun(reader => {
965
+ const commit = resolver.resolvedCommit.read(reader);
966
+ if (!previousCommit.equals(commit)) {
967
+ const prev = previousCommit;
968
+ previousCommit = commit;
969
+ this._handleRefChange(sessionName, resolver.ref, prev, commit);
970
+ }
971
+ });
972
+ this._dynamicRefWatchers.set(sessionName, disposable);
973
+ }
974
+ }
975
+ function arraysEqual(a, b) {
976
+ if (a.length !== b.length) {
977
+ return false;
597
978
  }
598
- trace(message) {
599
- this._base.trace(message);
600
- this._emitter?.({ type: 'log', level: 'trace', message });
979
+ for (let i = 0; i < a.length; i++) {
980
+ if (a[i] !== b[i]) {
981
+ return false;
982
+ }
601
983
  }
984
+ return true;
602
985
  }
603
986
 
604
- export { ActivityTracker, DaemonService, SourceTreeChangedError };
987
+ export { ActivityTracker, DaemonService, SourceTreeChangedError, daemonApiVersionText, pluginProtocolVersionText };
605
988
  //# sourceMappingURL=DaemonService.js.map