summaryrefslogtreecommitdiff
path: root/packages/core/src/utils
diff options
context:
space:
mode:
authorJacob MacDonald <[email protected]>2025-08-18 14:09:02 -0700
committerGitHub <[email protected]>2025-08-18 21:09:02 +0000
commit3960ccf78197c140ee483fccd654680894cdea47 (patch)
treec6533f9afb4384ed8d7c0411adcaa1c98d3d4b20 /packages/core/src/utils
parent465ac9f547d0d684439886d1466c1a1133da611d (diff)
Add MCP Root change notifications (#6502)
Diffstat (limited to 'packages/core/src/utils')
-rw-r--r--packages/core/src/utils/workspaceContext.test.ts84
-rw-r--r--packages/core/src/utils/workspaceContext.ts46
2 files changed, 126 insertions, 4 deletions
diff --git a/packages/core/src/utils/workspaceContext.test.ts b/packages/core/src/utils/workspaceContext.test.ts
index 5446c287..30c9ae16 100644
--- a/packages/core/src/utils/workspaceContext.test.ts
+++ b/packages/core/src/utils/workspaceContext.test.ts
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import { describe, it, expect, beforeEach, afterEach } from 'vitest';
+import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
@@ -294,6 +294,88 @@ describe('WorkspaceContext with real filesystem', () => {
});
});
+ describe('onDirectoriesChanged', () => {
+ it('should call listener when adding a directory', () => {
+ const workspaceContext = new WorkspaceContext(cwd);
+ const listener = vi.fn();
+ workspaceContext.onDirectoriesChanged(listener);
+
+ workspaceContext.addDirectory(otherDir);
+
+ expect(listener).toHaveBeenCalledOnce();
+ });
+
+ it('should not call listener when adding a duplicate directory', () => {
+ const workspaceContext = new WorkspaceContext(cwd);
+ workspaceContext.addDirectory(otherDir);
+ const listener = vi.fn();
+ workspaceContext.onDirectoriesChanged(listener);
+
+ workspaceContext.addDirectory(otherDir);
+
+ expect(listener).not.toHaveBeenCalled();
+ });
+
+ it('should call listener when setting different directories', () => {
+ const workspaceContext = new WorkspaceContext(cwd);
+ const listener = vi.fn();
+ workspaceContext.onDirectoriesChanged(listener);
+
+ workspaceContext.setDirectories([otherDir]);
+
+ expect(listener).toHaveBeenCalledOnce();
+ });
+
+ it('should not call listener when setting same directories', () => {
+ const workspaceContext = new WorkspaceContext(cwd);
+ const listener = vi.fn();
+ workspaceContext.onDirectoriesChanged(listener);
+
+ workspaceContext.setDirectories([cwd]);
+
+ expect(listener).not.toHaveBeenCalled();
+ });
+
+ it('should support multiple listeners', () => {
+ const workspaceContext = new WorkspaceContext(cwd);
+ const listener1 = vi.fn();
+ const listener2 = vi.fn();
+ workspaceContext.onDirectoriesChanged(listener1);
+ workspaceContext.onDirectoriesChanged(listener2);
+
+ workspaceContext.addDirectory(otherDir);
+
+ expect(listener1).toHaveBeenCalledOnce();
+ expect(listener2).toHaveBeenCalledOnce();
+ });
+
+ it('should allow unsubscribing a listener', () => {
+ const workspaceContext = new WorkspaceContext(cwd);
+ const listener = vi.fn();
+ const unsubscribe = workspaceContext.onDirectoriesChanged(listener);
+
+ unsubscribe();
+ workspaceContext.addDirectory(otherDir);
+
+ expect(listener).not.toHaveBeenCalled();
+ });
+
+ it('should not fail if a listener throws an error', () => {
+ const workspaceContext = new WorkspaceContext(cwd);
+ const errorListener = () => {
+ throw new Error('test error');
+ };
+ const listener = vi.fn();
+ workspaceContext.onDirectoriesChanged(errorListener);
+ workspaceContext.onDirectoriesChanged(listener);
+
+ expect(() => {
+ workspaceContext.addDirectory(otherDir);
+ }).not.toThrow();
+ expect(listener).toHaveBeenCalledOnce();
+ });
+ });
+
describe('getDirectories', () => {
it('should return a copy of directories array', () => {
const workspaceContext = new WorkspaceContext(cwd);
diff --git a/packages/core/src/utils/workspaceContext.ts b/packages/core/src/utils/workspaceContext.ts
index be99a83c..924cd601 100644
--- a/packages/core/src/utils/workspaceContext.ts
+++ b/packages/core/src/utils/workspaceContext.ts
@@ -8,6 +8,8 @@ import { isNodeError } from '../utils/errors.js';
import * as fs from 'fs';
import * as path from 'path';
+export type Unsubscribe = () => void;
+
/**
* WorkspaceContext manages multiple workspace directories and validates paths
* against them. This allows the CLI to operate on files from multiple directories
@@ -16,6 +18,7 @@ import * as path from 'path';
export class WorkspaceContext {
private directories = new Set<string>();
private initialDirectories: Set<string>;
+ private onDirectoriesChangedListeners = new Set<() => void>();
/**
* Creates a new WorkspaceContext with the given initial directory and optional additional directories.
@@ -32,12 +35,41 @@ export class WorkspaceContext {
}
/**
+ * Registers a listener that is called when the workspace directories change.
+ * @param listener The listener to call.
+ * @returns A function to unsubscribe the listener.
+ */
+ onDirectoriesChanged(listener: () => void): Unsubscribe {
+ this.onDirectoriesChangedListeners.add(listener);
+ return () => {
+ this.onDirectoriesChangedListeners.delete(listener);
+ };
+ }
+
+ private notifyDirectoriesChanged() {
+ // Iterate over a copy of the set in case a listener unsubscribes itself or others.
+ for (const listener of [...this.onDirectoriesChangedListeners]) {
+ try {
+ listener();
+ } catch (e) {
+ // Don't let one listener break others.
+ console.error('Error in WorkspaceContext listener:', e);
+ }
+ }
+ }
+
+ /**
* Adds a directory to the workspace.
* @param directory The directory path to add (can be relative or absolute)
* @param basePath Optional base path for resolving relative paths (defaults to cwd)
*/
addDirectory(directory: string, basePath: string = process.cwd()): void {
- this.directories.add(this.resolveAndValidateDir(directory, basePath));
+ const resolved = this.resolveAndValidateDir(directory, basePath);
+ if (this.directories.has(resolved)) {
+ return;
+ }
+ this.directories.add(resolved);
+ this.notifyDirectoriesChanged();
}
private resolveAndValidateDir(
@@ -72,9 +104,17 @@ export class WorkspaceContext {
}
setDirectories(directories: readonly string[]): void {
- this.directories.clear();
+ const newDirectories = new Set<string>();
for (const dir of directories) {
- this.addDirectory(dir);
+ newDirectories.add(this.resolveAndValidateDir(dir));
+ }
+
+ if (
+ newDirectories.size !== this.directories.size ||
+ ![...newDirectories].every((d) => this.directories.has(d))
+ ) {
+ this.directories = newDirectories;
+ this.notifyDirectoriesChanged();
}
}