@nimbalyst/extension-sdk 0.2.2 → 0.4.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 (54) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/__tests__/buildEmitStability.test.d.ts +2 -0
  3. package/dist/__tests__/buildEmitStability.test.d.ts.map +1 -0
  4. package/dist/__tests__/buildEmitStability.test.js +33 -0
  5. package/dist/__tests__/createTextCollabContentAdapter.test.js +1 -0
  6. package/dist/__tests__/externalsPlugin.test.js +50 -0
  7. package/dist/__tests__/fileDirectoryTree.test.js +5 -0
  8. package/dist/__tests__/fileMask.test.d.ts +2 -0
  9. package/dist/__tests__/fileMask.test.d.ts.map +1 -0
  10. package/dist/__tests__/fileMask.test.js +33 -0
  11. package/dist/__tests__/manifestValidation.agentProviders.test.js +1 -0
  12. package/dist/__tests__/textReplacement.test.d.ts +2 -0
  13. package/dist/__tests__/textReplacement.test.d.ts.map +1 -0
  14. package/dist/__tests__/textReplacement.test.js +31 -0
  15. package/dist/__tests__/useCollaborativeEditor.offline.test.d.ts +2 -0
  16. package/dist/__tests__/useCollaborativeEditor.offline.test.d.ts.map +1 -0
  17. package/dist/__tests__/useCollaborativeEditor.offline.test.js +34 -0
  18. package/dist/__tests__/useEditorLifecycle.test.d.ts +2 -0
  19. package/dist/__tests__/useEditorLifecycle.test.d.ts.map +1 -0
  20. package/dist/__tests__/useEditorLifecycle.test.js +60 -0
  21. package/dist/collab/index.d.ts +14 -0
  22. package/dist/collab/index.d.ts.map +1 -0
  23. package/dist/collab/index.js +13 -0
  24. package/dist/collab/origins.d.ts +15 -0
  25. package/dist/collab/origins.d.ts.map +1 -0
  26. package/dist/collab/origins.js +14 -0
  27. package/dist/collab/textReplacement.d.ts +7 -0
  28. package/dist/collab/textReplacement.d.ts.map +1 -0
  29. package/dist/collab/textReplacement.js +27 -0
  30. package/dist/createReadOnlyHost.d.ts.map +1 -1
  31. package/dist/createReadOnlyHost.js +1 -0
  32. package/dist/electronMain.d.ts +12 -0
  33. package/dist/electronMain.d.ts.map +1 -0
  34. package/dist/electronMain.js +11 -0
  35. package/dist/externalsPlugin.js +7 -3
  36. package/dist/fileDirectoryTree.js +1 -1
  37. package/dist/fileMask.d.ts +17 -0
  38. package/dist/fileMask.d.ts.map +1 -0
  39. package/dist/fileMask.js +45 -0
  40. package/dist/index.d.ts +1 -0
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +1 -0
  43. package/dist/types/editor.d.ts +164 -1
  44. package/dist/types/editor.d.ts.map +1 -1
  45. package/dist/types/extension.d.ts +19 -7
  46. package/dist/types/extension.d.ts.map +1 -1
  47. package/dist/types/panel.d.ts +43 -0
  48. package/dist/types/panel.d.ts.map +1 -1
  49. package/dist/useCollaborativeEditor.d.ts +9 -21
  50. package/dist/useCollaborativeEditor.d.ts.map +1 -1
  51. package/dist/useCollaborativeEditor.js +14 -22
  52. package/dist/useEditorLifecycle.d.ts.map +1 -1
  53. package/dist/useEditorLifecycle.js +2 -0
  54. package/package.json +15 -6
package/CHANGELOG.md CHANGED
@@ -8,6 +8,8 @@ The SDK is versioned independently of the Nimbalyst app. Each release declares i
8
8
 
9
9
  | SDK version | Minimum Nimbalyst app version |
10
10
  | --- | --- |
11
+ | 0.4.0 | 0.70.0 |
12
+ | 0.3.0 | 0.70.0 |
11
13
  | 0.2.2 | 0.58.5 |
12
14
  | 0.2.1 | 0.58.5 |
13
15
  | 0.2.0 | 0.58.5 |
@@ -16,6 +18,26 @@ The SDK is versioned independently of the Nimbalyst app. Each release declares i
16
18
 
17
19
  ## [Unreleased]
18
20
 
21
+ ## [0.4.0]
22
+
23
+ ### Added
24
+
25
+ - `applyTextDiff()` and `replaceYText()` reduce a whole-string replacement to one contiguous `Y.Text` edit, so a binding no longer has to hand-write the prefix/suffix diff.
26
+ - `@nimbalyst/extension-sdk/collab` is a React-free entry point for the collaborative-document helpers, for codecs and codec tests that have no React installed.
27
+ - `host.onFindRequested()` routes the app's Find command (Cmd+F) to a custom editor's own find UI.
28
+
29
+ ## [0.3.0]
30
+
31
+ Requires Nimbalyst 0.70.0 for the new host-provided selection, filesystem, and visibility APIs.
32
+
33
+ ### Added
34
+
35
+ - `host.setEditorContextItems()` and `EditorContextItem` let node-like editors (diagrams, CAD, electronics) push a list of selected items to the chat as individually removable context chips, instead of a single `setEditorContext` blob.
36
+ - `host.fs` (`EditorHostFileSystem`) reads files with SHA-256 version tokens and applies labeled, compare-and-swap grouped writes recorded in document history.
37
+ - `host.openExternal()` opens a reviewed HTTPS URL in the operating system's browser without navigating the renderer.
38
+ - `host.visible` and `host.onVisibilityChanged()` let editors pause render loops and release GPU resources while mounted but hidden.
39
+ - `ExtensionFileSystemService.writeFile()` accepts a `Uint8Array` so extensions can write binary files without corrupting their contents.
40
+
19
41
  ## [0.2.2]
20
42
 
21
43
  ### Added
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=buildEmitStability.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"buildEmitStability.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/buildEmitStability.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,33 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { readFileSync } from 'fs';
3
+ import { resolve } from 'path';
4
+ /**
5
+ * The renderer resolves `@nimbalyst/extension-sdk` to `dist/`, so every file the
6
+ * SDK build emits lives in the dev server's module graph. When the build wiped
7
+ * dist and re-emitted all 114 files, the pre-push gate (`typecheck:prepush`,
8
+ * which builds the SDK on every push) made Vite full-reload every open Electron
9
+ * window -- several times an hour across parallel agent sessions, with no SDK
10
+ * source change involved. These assertions keep the emit stable.
11
+ */
12
+ const sdkRoot = resolve(__dirname, '../..');
13
+ function readJsonc(relativePath) {
14
+ const text = readFileSync(resolve(sdkRoot, relativePath), 'utf8');
15
+ return JSON.parse(text.replace(/^\s*\/\/.*$/gm, ''));
16
+ }
17
+ describe('extension-sdk build emit stability', () => {
18
+ it('does not wipe dist on the default build', () => {
19
+ const { scripts } = readJsonc('package.json');
20
+ expect(scripts.build).toBe('tsc');
21
+ expect(scripts.build).not.toMatch(/rmSync|rm -rf/);
22
+ });
23
+ it('emits incrementally so unchanged sources produce no file writes', () => {
24
+ const { compilerOptions } = readJsonc('tsconfig.json');
25
+ expect(compilerOptions.incremental).toBe(true);
26
+ expect(compilerOptions.tsBuildInfoFile).toBeTruthy();
27
+ });
28
+ it('keeps a clean build available for publishing', () => {
29
+ const { scripts } = readJsonc('package.json');
30
+ expect(scripts['build:clean']).toMatch(/rmSync/);
31
+ expect(scripts.prepublishOnly).toBe('npm run build:clean');
32
+ });
33
+ });
@@ -1,3 +1,4 @@
1
+ // @vitest-environment node
1
2
  import { describe, expect, it } from 'vitest';
2
3
  import * as Y from 'yjs';
3
4
  import { createTextCollabContentAdapter } from '../collab/createTextCollabContentAdapter';
@@ -13,4 +13,54 @@ describe('nimbalystExternalsPlugin runtime bridge', () => {
13
13
  expect(source).toContain('export const useResolvedTrackerReference');
14
14
  expect(source).toContain('export const navigateToTrackerReference');
15
15
  });
16
+ it('evaluates the react-dom bridge against the React 19 host export shape', async () => {
17
+ const plugin = nimbalystExternalsPlugin();
18
+ const resolveId = plugin.resolveId;
19
+ const load = plugin.load;
20
+ const id = await resolveId('react-dom', undefined, {});
21
+ expect(id).toBe('\0nimbalyst-external:react-dom');
22
+ const source = await load(id);
23
+ expect(typeof source).toBe('string');
24
+ expect(source).not.toContain('export const render');
25
+ expect(source).not.toContain('export const hydrate');
26
+ expect(source).not.toContain('export const findDOMNode');
27
+ expect(source).not.toContain('export const unmountComponentAtNode');
28
+ const createPortal = () => null;
29
+ const preload = () => undefined;
30
+ const hostReactDOM = {
31
+ createPortal,
32
+ flushSync: () => undefined,
33
+ preload,
34
+ version: '19.2.7',
35
+ };
36
+ const executableSource = String(source)
37
+ .replace('export default __nimbalyst_mod__;', 'const __default__ = __nimbalyst_mod__;')
38
+ .replace(/export const (\w+) =/g, 'const $1 =');
39
+ const evaluate = new Function('window', `${executableSource}
40
+ return {
41
+ default: __default__, createPortal, preload, preconnect, version,
42
+ render: typeof render === 'undefined' ? undefined : render,
43
+ hydrate: typeof hydrate === 'undefined' ? undefined : hydrate,
44
+ findDOMNode: typeof findDOMNode === 'undefined' ? undefined : findDOMNode,
45
+ unmountComponentAtNode: typeof unmountComponentAtNode === 'undefined'
46
+ ? undefined
47
+ : unmountComponentAtNode,
48
+ };`);
49
+ const evaluated = evaluate({
50
+ __nimbalyst_extensions: {
51
+ 'react-dom': hostReactDOM,
52
+ },
53
+ });
54
+ expect(evaluated).toBeTypeOf('object');
55
+ const exports = evaluated;
56
+ expect(exports.default).toBe(hostReactDOM);
57
+ expect(exports.createPortal).toBe(createPortal);
58
+ expect(exports.preload).toBe(preload);
59
+ expect(exports.preconnect).toBeUndefined();
60
+ expect(exports.version).toBe('19.2.7');
61
+ expect(exports.render).toBeUndefined();
62
+ expect(exports.hydrate).toBeUndefined();
63
+ expect(exports.findDOMNode).toBeUndefined();
64
+ expect(exports.unmountComponentAtNode).toBeUndefined();
65
+ });
16
66
  });
@@ -1,3 +1,4 @@
1
+ // @vitest-environment node
1
2
  import { describe, expect, it } from 'vitest';
2
3
  import { buildFileDirectoryTree, getFileDirectoryPaths, getFilePathBasename, getWorkspaceRelativeFilePath, } from '../fileDirectoryTree';
3
4
  const buildTree = (files, workspacePath) => buildFileDirectoryTree(files.map(filePath => ({ filePath })), file => file.filePath, workspacePath);
@@ -21,6 +22,10 @@ describe('file path helpers', () => {
21
22
  expect(getFilePathBasename('packages/runtime/index.ts')).toBe('index.ts');
22
23
  expect(getFilePathBasename('packages\\runtime\\index.ts')).toBe('index.ts');
23
24
  });
25
+ it('gets directory names from paths with trailing separators', () => {
26
+ expect(getFilePathBasename('docs/')).toBe('docs');
27
+ expect(getFilePathBasename('packages\\runtime\\')).toBe('runtime');
28
+ });
24
29
  });
25
30
  describe('buildFileDirectoryTree', () => {
26
31
  it('groups Windows files with duplicate basenames under their directories', () => {
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=fileMask.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fileMask.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/fileMask.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,33 @@
1
+ // @vitest-environment node
2
+ import { describe, it, expect } from 'vitest';
3
+ import { parseFileMask, matchesFileMask } from '../fileMask';
4
+ describe('fileMask', () => {
5
+ it('matches a name glob against Windows back-slashed paths', () => {
6
+ // #1196: main returns `path.normalize`d paths, so on Windows the mask sees
7
+ // `C:\...\Ch01.md`. Before the separator normalization only `*.md` matched
8
+ // (the whole path has no `/`), which read as "the mask filters by
9
+ // extension only".
10
+ const winPath = 'C:\\Users\\me\\book\\Ch01.md';
11
+ expect(matchesFileMask(winPath, parseFileMask('Ch0*.md'))).toBe(true);
12
+ expect(matchesFileMask(winPath, parseFileMask('*.md'))).toBe(true);
13
+ expect(matchesFileMask(winPath, parseFileMask('Notes*.md'))).toBe(false);
14
+ expect(matchesFileMask('C:\\Users\\me\\book\\notes.txt', parseFileMask('Ch0*.md'))).toBe(false);
15
+ });
16
+ it('matches names, directory globs and comma-separated lists on posix paths', () => {
17
+ const patterns = parseFileMask('Ch0*.md, *.ts');
18
+ expect(matchesFileMask('/Users/me/book/Ch01.md', patterns)).toBe(true);
19
+ expect(matchesFileMask('/Users/me/book/index.ts', patterns)).toBe(true);
20
+ expect(matchesFileMask('/Users/me/book/index.tsx', patterns)).toBe(false);
21
+ // `*` stops at a separator, `**` crosses them, `?` is a single character.
22
+ expect(matchesFileMask('src/a/b.test.ts', parseFileMask('src/*.test.ts'))).toBe(false);
23
+ expect(matchesFileMask('src/a/b.test.ts', parseFileMask('src/**/*.test.ts'))).toBe(true);
24
+ expect(matchesFileMask('src\\a\\b.test.ts', parseFileMask('src/**/*.test.ts'))).toBe(true);
25
+ expect(matchesFileMask('Ch1.md', parseFileMask('Ch?.md'))).toBe(true);
26
+ expect(matchesFileMask('Ch12.md', parseFileMask('Ch?.md'))).toBe(false);
27
+ });
28
+ it('treats an empty or absent mask as no filter', () => {
29
+ expect(parseFileMask(undefined)).toEqual([]);
30
+ expect(parseFileMask(' , ')).toEqual([]);
31
+ expect(matchesFileMask('anything.bin', [])).toBe(true);
32
+ });
33
+ });
@@ -1,3 +1,4 @@
1
+ // @vitest-environment node
1
2
  import { describe, it, expect } from 'vitest';
2
3
  import { validateAgentProviders } from '../manifestValidation';
3
4
  // The validator must agree with the AiAgentProviderContribution /
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=textReplacement.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"textReplacement.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/textReplacement.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,31 @@
1
+ // @vitest-environment node
2
+ import { describe, expect, it, vi } from 'vitest';
3
+ import * as Y from 'yjs';
4
+ import { applyTextDiff, replaceYText } from '../collab/textReplacement';
5
+ describe('text replacement', () => {
6
+ it('reduces whole-string replacements to one range edit', () => {
7
+ const cases = [
8
+ { name: 'insert-only', previous: 'abcd', next: 'abXcd', edit: [2, 2, 'X'] },
9
+ { name: 'delete-only', previous: 'abcd', next: 'acd', edit: [1, 2, ''] },
10
+ { name: 'replace-in-the-middle', previous: 'abcde', next: 'abXYde', edit: [2, 3, 'XY'] },
11
+ { name: 'identical', previous: 'same', next: 'same', edit: null },
12
+ { name: 'empty-to-nonempty', previous: '', next: 'new', edit: [0, 0, 'new'] },
13
+ ];
14
+ for (const { name, previous, next, edit } of cases) {
15
+ const applyEdit = vi.fn();
16
+ applyTextDiff(previous, next, applyEdit);
17
+ if (edit === null) {
18
+ expect(applyEdit, name).not.toHaveBeenCalled();
19
+ }
20
+ else {
21
+ expect(applyEdit, name).toHaveBeenCalledOnce();
22
+ expect(applyEdit, name).toHaveBeenCalledWith(...edit);
23
+ }
24
+ const yText = new Y.Text(previous);
25
+ const doc = new Y.Doc();
26
+ doc.getMap('root').set('text', yText);
27
+ replaceYText(yText, next);
28
+ expect(yText.toString(), name).toBe(next);
29
+ }
30
+ });
31
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=useCollaborativeEditor.offline.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useCollaborativeEditor.offline.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/useCollaborativeEditor.offline.test.tsx"],"names":[],"mappings":""}
@@ -0,0 +1,34 @@
1
+ import { renderHook, waitFor } from '@testing-library/react';
2
+ import { Awareness } from 'y-protocols/awareness';
3
+ import { describe, expect, it, vi } from 'vitest';
4
+ import * as Y from 'yjs';
5
+ import { useCollaborativeEditor } from '../useCollaborativeEditor.js';
6
+ describe('useCollaborativeEditor offline replica binding', () => {
7
+ it('binds an already-hydrated Y.Doc without waiting for the transport', async () => {
8
+ const yDoc = new Y.Doc();
9
+ yDoc.getMap('nodes').set('node_root', 'persisted');
10
+ const awareness = new Awareness(yDoc);
11
+ const loadInitialContent = vi.fn(async () => 'must not seed offline');
12
+ const bind = vi.fn(() => ({ destroy: vi.fn() }));
13
+ const host = {
14
+ collaboration: {
15
+ yDoc,
16
+ awareness,
17
+ user: { id: 'offline-user', name: 'Offline User', color: '#123456' },
18
+ getStatus: () => 'disconnected',
19
+ onStatusChange: () => () => { },
20
+ loadInitialContent,
21
+ },
22
+ };
23
+ const { unmount } = renderHook(() => useCollaborativeEditor(host, {
24
+ isEmpty: doc => doc.getMap('nodes').size === 0,
25
+ initializeFromContent: vi.fn(),
26
+ createBinding: bind,
27
+ }));
28
+ await waitFor(() => expect(bind).toHaveBeenCalledTimes(1));
29
+ expect(loadInitialContent).not.toHaveBeenCalled();
30
+ unmount();
31
+ awareness.destroy();
32
+ yDoc.destroy();
33
+ });
34
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=useEditorLifecycle.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useEditorLifecycle.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/useEditorLifecycle.test.tsx"],"names":[],"mappings":""}
@@ -0,0 +1,60 @@
1
+ import { act, renderHook, waitFor } from '@testing-library/react';
2
+ import { describe, expect, it, vi } from 'vitest';
3
+ import { useEditorLifecycle } from '../useEditorLifecycle.js';
4
+ function createHost(loadContent) {
5
+ let fileChanged;
6
+ const host = {
7
+ filePath: '/workspace/plans/example.mockup.html',
8
+ fileName: 'example.mockup.html',
9
+ isActive: true,
10
+ theme: 'dark',
11
+ loadContent,
12
+ loadBinaryContent: vi.fn(),
13
+ saveContent: vi.fn(),
14
+ setDirty: vi.fn(),
15
+ onSaveRequested: () => () => { },
16
+ onFileChanged: (callback) => {
17
+ fileChanged = callback;
18
+ return () => {
19
+ fileChanged = undefined;
20
+ };
21
+ },
22
+ onThemeChanged: () => () => { },
23
+ registerMenuItems: vi.fn(),
24
+ registerEditorAPI: vi.fn(),
25
+ setEditorContext: vi.fn(),
26
+ setEditorContextItems: vi.fn(),
27
+ storage: {
28
+ get: vi.fn(),
29
+ set: vi.fn(),
30
+ delete: vi.fn(),
31
+ getGlobal: vi.fn(),
32
+ setGlobal: vi.fn(),
33
+ deleteGlobal: vi.fn(),
34
+ getSecret: vi.fn(),
35
+ setSecret: vi.fn(),
36
+ deleteSecret: vi.fn(),
37
+ },
38
+ };
39
+ return {
40
+ host,
41
+ notifyFileChanged(content) {
42
+ fileChanged?.(content);
43
+ },
44
+ };
45
+ }
46
+ describe('useEditorLifecycle', () => {
47
+ it('recovers when a missing file appears after the initial load fails', async () => {
48
+ const applyContent = vi.fn();
49
+ const { host, notifyFileChanged } = createHost(() => Promise.reject(new Error('File not found')));
50
+ const { result } = renderHook(() => useEditorLifecycle(host, { applyContent }));
51
+ await waitFor(() => {
52
+ expect(result.current.error?.message).toBe('File not found');
53
+ });
54
+ act(() => {
55
+ notifyFileChanged('<html>created later</html>');
56
+ });
57
+ expect(applyContent).toHaveBeenCalledWith('<html>created later</html>');
58
+ expect(result.current.error).toBeNull();
59
+ });
60
+ });
@@ -0,0 +1,14 @@
1
+ /**
2
+ * React-free public entry for the collaborative-document helpers.
3
+ *
4
+ * The package root re-exports everything here, but the root barrel also pulls
5
+ * in `useEditorLifecycle` and therefore `react` — a peer dependency the host
6
+ * injects at runtime. A codec or a codec test that only touches `Y.Doc` has no
7
+ * React installed, so importing the root barrel fails to resolve for it.
8
+ * `@nimbalyst/extension-sdk/collab` is the entry those consumers import
9
+ * instead, so nobody has to reach into `dist/` by path.
10
+ */
11
+ export { COLLAB_INIT_ORIGIN } from './origins.js';
12
+ export { createTextCollabContentAdapter, reconstructCollabContentAdapterFromDescriptor, TEXT_COLLAB_DEFAULT_FIELD, type TextCollabContentAdapterOptions, } from './createTextCollabContentAdapter.js';
13
+ export { applyTextDiff, replaceYText, type ApplyTextEdit, } from './textReplacement.js';
14
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/collab/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAElD,OAAO,EACL,8BAA8B,EAC9B,6CAA6C,EAC7C,yBAAyB,EACzB,KAAK,+BAA+B,GACrC,MAAM,qCAAqC,CAAC;AAE7C,OAAO,EACL,aAAa,EACb,YAAY,EACZ,KAAK,aAAa,GACnB,MAAM,sBAAsB,CAAC"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * React-free public entry for the collaborative-document helpers.
3
+ *
4
+ * The package root re-exports everything here, but the root barrel also pulls
5
+ * in `useEditorLifecycle` and therefore `react` — a peer dependency the host
6
+ * injects at runtime. A codec or a codec test that only touches `Y.Doc` has no
7
+ * React installed, so importing the root barrel fails to resolve for it.
8
+ * `@nimbalyst/extension-sdk/collab` is the entry those consumers import
9
+ * instead, so nobody has to reach into `dist/` by path.
10
+ */
11
+ export { COLLAB_INIT_ORIGIN } from './origins.js';
12
+ export { createTextCollabContentAdapter, reconstructCollabContentAdapterFromDescriptor, TEXT_COLLAB_DEFAULT_FIELD, } from './createTextCollabContentAdapter.js';
13
+ export { applyTextDiff, replaceYText, } from './textReplacement.js';
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Origin tag used when the SDK wraps `initializeFromContent` in a Y.Doc
3
+ * transaction. Extension bindings can compare a transaction's origin
4
+ * against this to suppress their own change handlers during seeding
5
+ * (otherwise the binding would echo the seed back into local-edit state).
6
+ *
7
+ * ```ts
8
+ * yDoc.on('update', (update, origin) => {
9
+ * if (origin === COLLAB_INIT_ORIGIN) return;
10
+ * // ... apply remote change
11
+ * });
12
+ * ```
13
+ */
14
+ export declare const COLLAB_INIT_ORIGIN: unique symbol;
15
+ //# sourceMappingURL=origins.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"origins.d.ts","sourceRoot":"","sources":["../../src/collab/origins.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,kBAAkB,eAAkC,CAAC"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Origin tag used when the SDK wraps `initializeFromContent` in a Y.Doc
3
+ * transaction. Extension bindings can compare a transaction's origin
4
+ * against this to suppress their own change handlers during seeding
5
+ * (otherwise the binding would echo the seed back into local-edit state).
6
+ *
7
+ * ```ts
8
+ * yDoc.on('update', (update, origin) => {
9
+ * if (origin === COLLAB_INIT_ORIGIN) return;
10
+ * // ... apply remote change
11
+ * });
12
+ * ```
13
+ */
14
+ export const COLLAB_INIT_ORIGIN = Symbol('nimbalyst:collab-init');
@@ -0,0 +1,7 @@
1
+ import type * as Y from 'yjs';
2
+ export type ApplyTextEdit = (start: number, end: number, inserted: string) => void;
3
+ /** Reduce a whole-string replacement to one contiguous range edit. */
4
+ export declare function applyTextDiff(previous: string, next: string, applyEdit: ApplyTextEdit): void;
5
+ /** Apply a whole-string replacement to a Y.Text as one contiguous edit. */
6
+ export declare function replaceYText(target: Y.Text, next: string): void;
7
+ //# sourceMappingURL=textReplacement.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"textReplacement.d.ts","sourceRoot":"","sources":["../../src/collab/textReplacement.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,CAAC,MAAM,KAAK,CAAC;AAE9B,MAAM,MAAM,aAAa,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;AAEnF,sEAAsE;AACtE,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,aAAa,GACvB,IAAI,CAmBN;AAED,2EAA2E;AAC3E,wBAAgB,YAAY,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAM/D"}
@@ -0,0 +1,27 @@
1
+ /** Reduce a whole-string replacement to one contiguous range edit. */
2
+ export function applyTextDiff(previous, next, applyEdit) {
3
+ if (previous === next)
4
+ return;
5
+ let prefix = 0;
6
+ const prefixLimit = Math.min(previous.length, next.length);
7
+ while (prefix < prefixLimit && previous.charCodeAt(prefix) === next.charCodeAt(prefix)) {
8
+ prefix += 1;
9
+ }
10
+ let suffix = 0;
11
+ const suffixLimit = Math.min(previous.length - prefix, next.length - prefix);
12
+ while (suffix < suffixLimit
13
+ && previous.charCodeAt(previous.length - 1 - suffix) === next.charCodeAt(next.length - 1 - suffix)) {
14
+ suffix += 1;
15
+ }
16
+ applyEdit(prefix, previous.length - suffix, next.slice(prefix, next.length - suffix));
17
+ }
18
+ /** Apply a whole-string replacement to a Y.Text as one contiguous edit. */
19
+ export function replaceYText(target, next) {
20
+ applyTextDiff(target.toString(), next, (start, end, inserted) => {
21
+ const removeLength = end - start;
22
+ if (removeLength > 0)
23
+ target.delete(start, removeLength);
24
+ if (inserted.length > 0)
25
+ target.insert(start, inserted);
26
+ });
27
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"createReadOnlyHost.d.ts","sourceRoot":"","sources":["../src/createReadOnlyHost.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAiC,MAAM,mBAAmB,CAAC;AAEnF,MAAM,WAAW,mBAAmB;IAClC,iDAAiD;IACjD,KAAK,EAAE,MAAM,CAAC;IAEd,4BAA4B;IAC5B,QAAQ,EAAE,MAAM,CAAC;IAEjB,sEAAsE;IACtE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,YAAa,SAAQ,UAAU;IAC9C,gEAAgE;IAChE,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,mBAAmB,GACxB,YAAY,CA6Dd"}
1
+ {"version":3,"file":"createReadOnlyHost.d.ts","sourceRoot":"","sources":["../src/createReadOnlyHost.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAiC,MAAM,mBAAmB,CAAC;AAEnF,MAAM,WAAW,mBAAmB;IAClC,iDAAiD;IACjD,KAAK,EAAE,MAAM,CAAC;IAEd,4BAA4B;IAC5B,QAAQ,EAAE,MAAM,CAAC;IAEjB,sEAAsE;IACtE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,YAAa,SAAQ,UAAU;IAC9C,gEAAgE;IAChE,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,mBAAmB,GACxB,YAAY,CA8Dd"}
@@ -61,6 +61,7 @@ export function createReadOnlyHost(content, opts) {
61
61
  registerMenuItems: (_items) => { },
62
62
  registerEditorAPI: () => { },
63
63
  setEditorContext: (_context) => { },
64
+ setEditorContextItems: () => { },
64
65
  // -- Storage (in-memory, non-persistent) --
65
66
  storage: {
66
67
  get: (_key) => undefined,
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Narrow Extension SDK entry used by the Electron main-process bundle.
3
+ *
4
+ * The public SDK barrel includes renderer hooks and components. Main-process
5
+ * code only needs manifest validation, tracker-importer constants, and the
6
+ * serializable collaboration adapter factory.
7
+ */
8
+ export { effectiveModulePermissions, validateBackendModules, } from './manifestValidation';
9
+ export { TRACKER_IMPORTER_RPC_METHODS } from './types/trackerImporter';
10
+ export { createTextCollabContentAdapter, reconstructCollabContentAdapterFromDescriptor, } from './collab/createTextCollabContentAdapter';
11
+ export { COLLAB_INIT_ORIGIN } from './collab/origins';
12
+ //# sourceMappingURL=electronMain.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"electronMain.d.ts","sourceRoot":"","sources":["../src/electronMain.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EACL,0BAA0B,EAC1B,sBAAsB,GACvB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,4BAA4B,EAAE,MAAM,yBAAyB,CAAC;AACvE,OAAO,EACL,8BAA8B,EAC9B,6CAA6C,GAC9C,MAAM,yCAAyC,CAAC;AACjD,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Narrow Extension SDK entry used by the Electron main-process bundle.
3
+ *
4
+ * The public SDK barrel includes renderer hooks and components. Main-process
5
+ * code only needs manifest validation, tracker-importer constants, and the
6
+ * serializable collaboration adapter factory.
7
+ */
8
+ export { effectiveModulePermissions, validateBackendModules, } from './manifestValidation';
9
+ export { TRACKER_IMPORTER_RPC_METHODS } from './types/trackerImporter';
10
+ export { createTextCollabContentAdapter, reconstructCollabContentAdapterFromDescriptor, } from './collab/createTextCollabContentAdapter';
11
+ export { COLLAB_INIT_ORIGIN } from './collab/origins';
@@ -149,12 +149,16 @@ function generateCommonExports(externalKey) {
149
149
  'version', 'useInsertionEffect', 'cache', 'use', 'act',
150
150
  ];
151
151
  }
152
- else if (externalKey === 'react-dom' || externalKey === 'react-dom/client') {
152
+ else if (externalKey === 'react-dom') {
153
153
  exports = [
154
- 'createRoot', 'hydrateRoot', 'render', 'hydrate', 'createPortal', 'flushSync',
155
- 'unmountComponentAtNode', 'findDOMNode', 'unstable_batchedUpdates',
154
+ 'createPortal', 'flushSync', 'preconnect', 'prefetchDNS', 'preinit',
155
+ 'preinitModule', 'preload', 'preloadModule', 'requestFormReset',
156
+ 'unstable_batchedUpdates', 'useFormState', 'useFormStatus', 'version',
156
157
  ];
157
158
  }
159
+ else if (externalKey === 'react-dom/client') {
160
+ exports = ['createRoot', 'hydrateRoot', 'version'];
161
+ }
158
162
  else if (externalKey === 'react/jsx-runtime' || externalKey === 'react/jsx-dev-runtime') {
159
163
  exports = ['jsx', 'jsxs', 'jsxDEV', 'Fragment'];
160
164
  }
@@ -3,7 +3,7 @@ export function normalizeFilePath(filePath) {
3
3
  return filePath.replace(/[\\/]+/g, '/');
4
4
  }
5
5
  export function getFilePathBasename(filePath) {
6
- const normalizedPath = normalizeFilePath(filePath);
6
+ const normalizedPath = normalizeFilePath(filePath).replace(/\/+$/, '');
7
7
  return normalizedPath.slice(normalizedPath.lastIndexOf('/') + 1);
8
8
  }
9
9
  /**
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Comma-separated file mask matching, shared by Quick Open (renderer and main)
3
+ * and the git extension's Changes filter so the syntax means the same thing
4
+ * everywhere:
5
+ *
6
+ * "*.ts,*.tsx" — any .ts or .tsx file
7
+ * "src/** /*.test.ts" — recursive pattern
8
+ * "Ch0*.md" — case-insensitive filename match
9
+ *
10
+ * Glob semantics: `*` matches any run of non-slash characters, `**` matches
11
+ * anything (across slashes), `?` matches a single non-slash character. The
12
+ * pattern is matched against both the basename and the full path so users can
13
+ * write either `*.ts` or `src/*.ts` and get what they expect.
14
+ */
15
+ export declare function parseFileMask(mask: string | null | undefined): RegExp[];
16
+ export declare function matchesFileMask(filePath: string, patterns: RegExp[]): boolean;
17
+ //# sourceMappingURL=fileMask.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fileMask.d.ts","sourceRoot":"","sources":["../src/fileMask.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAcH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,EAAE,CAOvE;AAED,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,OAAO,CAS7E"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Comma-separated file mask matching, shared by Quick Open (renderer and main)
3
+ * and the git extension's Changes filter so the syntax means the same thing
4
+ * everywhere:
5
+ *
6
+ * "*.ts,*.tsx" — any .ts or .tsx file
7
+ * "src/** /*.test.ts" — recursive pattern
8
+ * "Ch0*.md" — case-insensitive filename match
9
+ *
10
+ * Glob semantics: `*` matches any run of non-slash characters, `**` matches
11
+ * anything (across slashes), `?` matches a single non-slash character. The
12
+ * pattern is matched against both the basename and the full path so users can
13
+ * write either `*.ts` or `src/*.ts` and get what they expect.
14
+ */
15
+ function globToRegex(glob) {
16
+ // Escape regex special chars except * and ?
17
+ const escaped = glob.replace(/[.+^${}()|[\]\\]/g, '\\$&');
18
+ // Convert glob wildcards: ** -> match any path, * -> match any non-slash, ? -> single non-slash
19
+ const pattern = escaped
20
+ .replace(/\*\*/g, '__DOUBLESTAR__')
21
+ .replace(/\*/g, '[^/]*')
22
+ .replace(/__DOUBLESTAR__/g, '.*')
23
+ .replace(/\?/g, '[^/]');
24
+ return new RegExp(`^${pattern}$`, 'i');
25
+ }
26
+ export function parseFileMask(mask) {
27
+ if (!mask)
28
+ return [];
29
+ return mask
30
+ .split(',')
31
+ .map((s) => s.trim())
32
+ .filter(Boolean)
33
+ .map(globToRegex);
34
+ }
35
+ export function matchesFileMask(filePath, patterns) {
36
+ if (patterns.length === 0)
37
+ return true;
38
+ // Windows paths arrive back-slashed (main runs `path.normalize`). Without
39
+ // this the basename split finds no separator, so the whole absolute path is
40
+ // treated as the filename: `*.md` still matches it but `Ch0*.md` never can,
41
+ // which is the extension-only filtering reported in #1196.
42
+ const normalizedPath = filePath.replace(/\\/g, '/');
43
+ const basename = normalizedPath.split('/').pop() ?? normalizedPath;
44
+ return patterns.some((re) => re.test(basename) || re.test(normalizedPath));
45
+ }
package/dist/index.d.ts CHANGED
@@ -27,6 +27,7 @@ export * from './types/index.js';
27
27
  export { useEditorLifecycle, type UseEditorLifecycleOptions, type UseEditorLifecycleResult, type DiffState, } from './useEditorLifecycle.js';
28
28
  export { useCollaborativeEditor, COLLAB_INIT_ORIGIN, type UseCollaborativeEditorConfig, type UseCollaborativeEditorResult, } from './useCollaborativeEditor.js';
29
29
  export { createTextCollabContentAdapter, reconstructCollabContentAdapterFromDescriptor, TEXT_COLLAB_DEFAULT_FIELD, type TextCollabContentAdapterOptions, } from './collab/createTextCollabContentAdapter.js';
30
+ export { applyTextDiff, replaceYText, type ApplyTextEdit, } from './collab/textReplacement.js';
30
31
  export { useDocumentPath, type DocumentPathContextValue, } from './documentPath.js';
31
32
  export { MaterialSymbol } from './MaterialSymbol.js';
32
33
  export { TrackerReferenceChip, TrackerReferencePicker, navigateToTrackerReference, useResolvedTrackerReference, type ResolvedTrackerReference, type TrackerReferenceChipProps, type TrackerReferencePickerProps, } from './trackerReferences.js';