@nimbalyst/extension-sdk 0.4.0 → 0.6.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 (48) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/__tests__/editorHostCapabilities.test.d.ts +2 -0
  3. package/dist/__tests__/editorHostCapabilities.test.d.ts.map +1 -0
  4. package/dist/__tests__/editorHostCapabilities.test.js +24 -0
  5. package/dist/__tests__/fileDirectoryTree.test.js +34 -0
  6. package/dist/__tests__/gitOperationLog.test.d.ts +2 -0
  7. package/dist/__tests__/gitOperationLog.test.d.ts.map +1 -0
  8. package/dist/__tests__/gitOperationLog.test.js +51 -0
  9. package/dist/__tests__/useCollaborativeEditor.offline.test.js +43 -1
  10. package/dist/agents/AgentProtocol.d.ts +20 -0
  11. package/dist/agents/AgentProtocol.d.ts.map +1 -1
  12. package/dist/agents/AgentProtocolHost.d.ts +1 -1
  13. package/dist/agents/AgentProtocolHost.d.ts.map +1 -1
  14. package/dist/createReadOnlyHost.d.ts +11 -1
  15. package/dist/createReadOnlyHost.d.ts.map +1 -1
  16. package/dist/createReadOnlyHost.js +3 -0
  17. package/dist/editorHostCapabilities.d.ts +31 -0
  18. package/dist/editorHostCapabilities.d.ts.map +1 -0
  19. package/dist/editorHostCapabilities.js +35 -0
  20. package/dist/externalsPlugin.d.ts.map +1 -1
  21. package/dist/externalsPlugin.js +1 -5
  22. package/dist/fileDirectoryTree.d.ts +9 -2
  23. package/dist/fileDirectoryTree.d.ts.map +1 -1
  24. package/dist/fileDirectoryTree.js +82 -14
  25. package/dist/gitOperationLog.d.ts +104 -0
  26. package/dist/gitOperationLog.d.ts.map +1 -0
  27. package/dist/gitOperationLog.js +88 -0
  28. package/dist/index.d.ts +3 -1
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +3 -0
  31. package/dist/types/collab.d.ts +31 -2
  32. package/dist/types/collab.d.ts.map +1 -1
  33. package/dist/types/comments.d.ts +104 -0
  34. package/dist/types/comments.d.ts.map +1 -0
  35. package/dist/types/comments.js +2 -0
  36. package/dist/types/editor.d.ts +201 -1
  37. package/dist/types/editor.d.ts.map +1 -1
  38. package/dist/types/extension.d.ts +49 -0
  39. package/dist/types/extension.d.ts.map +1 -1
  40. package/dist/types/index.d.ts +1 -0
  41. package/dist/types/index.d.ts.map +1 -1
  42. package/dist/types/index.js +1 -0
  43. package/dist/types/panel.d.ts +21 -0
  44. package/dist/types/panel.d.ts.map +1 -1
  45. package/dist/useCollaborativeEditor.d.ts +13 -8
  46. package/dist/useCollaborativeEditor.d.ts.map +1 -1
  47. package/dist/useCollaborativeEditor.js +92 -74
  48. package/package.json +14 -2
@@ -139,8 +139,10 @@ export function useCollaborativeEditor(host, config) {
139
139
  return;
140
140
  let cancelled = false;
141
141
  let handle = null;
142
+ let starting = false;
143
+ let unregisterContentFlush = null;
142
144
  const tryStart = async () => {
143
- if (cancelled || handle)
145
+ if (cancelled || handle || starting)
144
146
  return;
145
147
  const cfg = resolveConfig(configRef.current);
146
148
  const connected = collab.getStatus() === 'connected';
@@ -149,86 +151,100 @@ export function useCollaborativeEditor(host, config) {
149
151
  // can reopen offline; only first-open seeding requires a live transport.
150
152
  if (!connected && cfg.isEmpty(collab.yDoc))
151
153
  return;
152
- // NEVER seed when the transport skipped payloads it could not decode:
153
- // the Y.Doc looking empty then means "content exists but is unreadable
154
- // on this client", and seeding would write a default document over the
155
- // real content for every client (the "Untitled map" clobber).
156
- const undecoded = collab.hasUndecodedContent?.() === true;
157
- if (connected && !undecoded && cfg.isEmpty(collab.yDoc)) {
158
- try {
159
- const content = await collab.loadInitialContent();
160
- if (cancelled)
161
- return;
162
- // NEVER seed from empty content. A host that has no bytes for this
163
- // document (reopen of an already-shared doc: no in-memory share
164
- // payload, no file) returns ''/empty -- seeding from that writes a
165
- // default document into the shared room and clobbers whatever the
166
- // room's real content is for every client. Fall through to bind:
167
- // the room content (or a teammate's seed) will populate the doc.
168
- if (!hasSeedableContent(content)) {
169
- console.warn('[useCollaborativeEditor] Skipping first-open seed: host returned empty initial content.');
170
- }
171
- // Re-check emptiness in case another client seeded while we were
172
- // awaiting -- they would have raced through the WebSocket and
173
- // applied their update during our await gap. Avoid double-seeding
174
- // in that case; CRDT merge would otherwise insert duplicate
175
- // content unless the seed is fully deterministic.
176
- else if (cfg.isEmpty(collab.yDoc)) {
177
- collab.yDoc.transact(() => {
178
- cfg.seed(collab.yDoc, content);
179
- }, COLLAB_INIT_ORIGIN);
180
- // Durability: the seed the user sees locally must reach the server
181
- // before this provider can tear down. flushWithAck resolves only
182
- // after a server-persisted ack; flushLocalState is the deprecated
183
- // fire-and-forget fallback for older hosts.
184
- const flushed = collab.flushWithAck
185
- ? await collab.flushWithAck()
186
- : (await collab.flushLocalState?.(), true);
187
- if (!flushed) {
188
- const err = new Error('Seed flush was not confirmed by the server before timeout; content may not have persisted.');
189
- console.warn('[useCollaborativeEditor]', err.message);
190
- setSeedError(err);
191
- collab.reportSeedOutcome?.({ ok: false, error: err });
192
- cfg.onSeedOutcome?.({ ok: false, error: err });
154
+ starting = true;
155
+ try {
156
+ // NEVER seed when the transport skipped payloads it could not decode:
157
+ // the Y.Doc looking empty then means "content exists but is unreadable
158
+ // on this client", and seeding would write a default document over the
159
+ // real content for every client (the "Untitled map" clobber).
160
+ const undecoded = collab.hasUndecodedContent?.() === true;
161
+ if (connected && !undecoded && cfg.isEmpty(collab.yDoc)) {
162
+ try {
163
+ const content = await collab.loadInitialContent();
164
+ if (cancelled)
165
+ return;
166
+ // NEVER seed from empty content. A host that has no bytes for this
167
+ // document (reopen of an already-shared doc: no in-memory share
168
+ // payload, no file) returns ''/empty -- seeding from that writes a
169
+ // default document into the shared room and clobbers whatever the
170
+ // room's real content is for every client. Fall through to bind:
171
+ // the room content (or a teammate's seed) will populate the doc.
172
+ if (!hasSeedableContent(content)) {
173
+ console.warn('[useCollaborativeEditor] Skipping first-open seed: host returned empty initial content.');
193
174
  }
194
- else {
195
- setSeedError(null);
196
- collab.reportSeedOutcome?.({ ok: true });
197
- cfg.onSeedOutcome?.({ ok: true });
175
+ // Re-check emptiness in case another client seeded while we were
176
+ // awaiting -- they would have raced through the WebSocket and
177
+ // applied their update during our await gap. Avoid double-seeding
178
+ // in that case; CRDT merge would otherwise insert duplicate
179
+ // content unless the seed is fully deterministic.
180
+ else if (cfg.isEmpty(collab.yDoc)) {
181
+ collab.yDoc.transact(() => {
182
+ cfg.seed(collab.yDoc, content);
183
+ }, COLLAB_INIT_ORIGIN);
184
+ // Durability: the seed the user sees locally must reach the server
185
+ // before this provider can tear down. flushWithAck resolves only
186
+ // after a server-persisted ack; flushLocalState is the deprecated
187
+ // fire-and-forget fallback for older hosts.
188
+ const flushed = collab.flushWithAck
189
+ ? await collab.flushWithAck()
190
+ : (await collab.flushLocalState?.(), true);
191
+ if (!flushed) {
192
+ const err = new Error('Seed flush was not confirmed by the server before timeout; content may not have persisted.');
193
+ console.warn('[useCollaborativeEditor]', err.message);
194
+ setSeedError(err);
195
+ collab.reportSeedOutcome?.({ ok: false, error: err });
196
+ cfg.onSeedOutcome?.({ ok: false, error: err });
197
+ }
198
+ else {
199
+ setSeedError(null);
200
+ collab.reportSeedOutcome?.({ ok: true });
201
+ cfg.onSeedOutcome?.({ ok: true });
202
+ }
198
203
  }
199
204
  }
205
+ catch (err) {
206
+ console.error('[useCollaborativeEditor] Failed to load/seed initial content:', err);
207
+ setSeedError(err);
208
+ collab.reportSeedOutcome?.({ ok: false, error: err });
209
+ cfg.onSeedOutcome?.({ ok: false, error: err });
210
+ // Continue with bind -- the doc may still be usable once another
211
+ // client seeds it.
212
+ }
200
213
  }
201
- catch (err) {
202
- console.error('[useCollaborativeEditor] Failed to load/seed initial content:', err);
203
- setSeedError(err);
204
- collab.reportSeedOutcome?.({ ok: false, error: err });
205
- cfg.onSeedOutcome?.({ ok: false, error: err });
206
- // Continue with bind -- the doc may still be usable once another
207
- // client seeds it.
208
- }
209
- }
210
- if (cancelled)
211
- return;
212
- const created = cfg.bind({
213
- yDoc: collab.yDoc,
214
- awareness: collab.awareness,
215
- user: collab.user,
216
- });
217
- const resolved = created instanceof Promise ? await created : created;
218
- if (cancelled) {
219
- // Effect unmounted while we were awaiting the extension's
220
- // imperative API. Destroy the freshly-built handle so any
221
- // observers/awareness subscriptions inside it get cleaned up.
222
- try {
223
- resolved.destroy();
214
+ if (cancelled)
215
+ return;
216
+ const created = cfg.bind({
217
+ yDoc: collab.yDoc,
218
+ awareness: collab.awareness,
219
+ user: collab.user,
220
+ });
221
+ const resolved = created instanceof Promise ? await created : created;
222
+ if (cancelled) {
223
+ // Effect unmounted while we were awaiting the extension's
224
+ // imperative API. Destroy the freshly-built handle so any
225
+ // observers/awareness subscriptions inside it get cleaned up.
226
+ try {
227
+ resolved.destroy();
228
+ }
229
+ catch (err) {
230
+ console.error('[useCollaborativeEditor] post-cancel destroy failed:', err);
231
+ }
232
+ return;
224
233
  }
225
- catch (err) {
226
- console.error('[useCollaborativeEditor] post-cancel destroy failed:', err);
234
+ handle = resolved;
235
+ // A binding that debounces its local pushes holds the newest edit
236
+ // outside the CRDT. Hand the host a drain so it can complete a write
237
+ // before reporting it done -- notably after a mutating AI tool, which
238
+ // otherwise returns success while the edit is still in the debounce.
239
+ if (typeof handle.syncNow === 'function') {
240
+ const syncNow = handle.syncNow.bind(handle);
241
+ unregisterContentFlush = collab.registerContentFlush?.(syncNow) ?? null;
227
242
  }
228
- return;
243
+ setBinding(handle);
244
+ }
245
+ finally {
246
+ starting = false;
229
247
  }
230
- handle = resolved;
231
- setBinding(handle);
232
248
  };
233
249
  void tryStart();
234
250
  const unsubscribe = collab.onStatusChange(() => {
@@ -237,6 +253,8 @@ export function useCollaborativeEditor(host, config) {
237
253
  return () => {
238
254
  cancelled = true;
239
255
  unsubscribe();
256
+ unregisterContentFlush?.();
257
+ unregisterContentFlush = null;
240
258
  if (handle) {
241
259
  handle.destroy();
242
260
  handle = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nimbalyst/extension-sdk",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "SDK for building Nimbalyst extensions",
5
5
  "repository": {
6
6
  "type": "git",
@@ -17,7 +17,7 @@
17
17
  "./dist/validate.js": "./dist/validate.browser.js"
18
18
  },
19
19
  "nimbalyst": {
20
- "minAppVersion": "0.70.0"
20
+ "minAppVersion": "0.78.0"
21
21
  },
22
22
  "publishConfig": {
23
23
  "access": "public"
@@ -33,6 +33,14 @@
33
33
  "types": "./dist/collab/index.d.ts",
34
34
  "import": "./dist/collab/index.js"
35
35
  },
36
+ "./agents": {
37
+ "types": "./dist/agents/index.d.ts",
38
+ "import": "./dist/agents/index.js"
39
+ },
40
+ "./types/editor": {
41
+ "types": "./dist/types/editor.d.ts",
42
+ "import": "./dist/types/editor.js"
43
+ },
36
44
  "./vite": {
37
45
  "types": "./dist/vite.d.ts",
38
46
  "import": "./dist/vite.js"
@@ -52,6 +60,10 @@
52
60
  "./file-mask": {
53
61
  "types": "./dist/fileMask.d.ts",
54
62
  "import": "./dist/fileMask.js"
63
+ },
64
+ "./git-operation-log": {
65
+ "types": "./dist/gitOperationLog.d.ts",
66
+ "import": "./dist/gitOperationLog.js"
55
67
  }
56
68
  },
57
69
  "files": [