@pixerate/editor 0.22.0 → 0.24.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 (188) hide show
  1. package/README.md +175 -2
  2. package/THIRD_PARTY_NOTICES.md +139 -0
  3. package/dist/agent/index.cjs +87 -111
  4. package/dist/agent/index.cjs.map +1 -1
  5. package/dist/agent/index.d.cts +70 -211
  6. package/dist/agent/index.d.ts +70 -211
  7. package/dist/agent/index.js +78 -102
  8. package/dist/agent/index.js.map +1 -1
  9. package/dist/arrangeShortcuts-B8O3f8-P.d.cts +184 -0
  10. package/dist/arrangeShortcuts-sLlD2rSX.d.ts +184 -0
  11. package/dist/canvas/index.cjs +2075 -86
  12. package/dist/canvas/index.cjs.map +1 -1
  13. package/dist/canvas/index.d.cts +608 -186
  14. package/dist/canvas/index.d.ts +608 -186
  15. package/dist/canvas/index.js +2060 -71
  16. package/dist/canvas/index.js.map +1 -1
  17. package/dist/chunk-26BVFKHU.cjs +453 -0
  18. package/dist/chunk-26BVFKHU.cjs.map +1 -0
  19. package/dist/chunk-2H7ZHPEJ.js +762 -0
  20. package/dist/chunk-2H7ZHPEJ.js.map +1 -0
  21. package/dist/{chunk-DENSCKFC.js → chunk-3CN2WD3X.js} +1730 -189
  22. package/dist/chunk-3CN2WD3X.js.map +1 -0
  23. package/dist/{chunk-JJDLPEWL.js → chunk-3LV6RRPH.js} +2 -2
  24. package/dist/{chunk-MJZBNWG7.js → chunk-5S3Z4PPF.js} +78 -557
  25. package/dist/chunk-5S3Z4PPF.js.map +1 -0
  26. package/dist/chunk-664LQYFJ.js +115 -0
  27. package/dist/chunk-664LQYFJ.js.map +1 -0
  28. package/dist/chunk-7DWMJCAT.js +453 -0
  29. package/dist/chunk-7DWMJCAT.js.map +1 -0
  30. package/dist/chunk-ARRTP33I.js +101 -0
  31. package/dist/chunk-ARRTP33I.js.map +1 -0
  32. package/dist/chunk-CPOKFNBG.cjs +101 -0
  33. package/dist/chunk-CPOKFNBG.cjs.map +1 -0
  34. package/dist/{chunk-AA4ZEVCC.cjs → chunk-D27YFC46.cjs} +101 -580
  35. package/dist/chunk-D27YFC46.cjs.map +1 -0
  36. package/dist/chunk-D6FH2YAX.cjs +131 -0
  37. package/dist/chunk-D6FH2YAX.cjs.map +1 -0
  38. package/dist/{chunk-4SUNJZWB.cjs → chunk-FD4LPFAE.cjs} +48 -200
  39. package/dist/chunk-FD4LPFAE.cjs.map +1 -0
  40. package/dist/chunk-FODNLX5N.js +749 -0
  41. package/dist/chunk-FODNLX5N.js.map +1 -0
  42. package/dist/{chunk-OYXSKQFR.js → chunk-FPENXAEJ.js} +35 -187
  43. package/dist/chunk-FPENXAEJ.js.map +1 -0
  44. package/dist/chunk-G2I4QJOT.cjs +762 -0
  45. package/dist/chunk-G2I4QJOT.cjs.map +1 -0
  46. package/dist/chunk-HLIMTC6N.js +1 -0
  47. package/dist/chunk-HLIMTC6N.js.map +1 -0
  48. package/dist/{chunk-QGHGJ56O.js → chunk-ITBSKI26.js} +1483 -19
  49. package/dist/chunk-ITBSKI26.js.map +1 -0
  50. package/dist/{chunk-6M3BCTBD.cjs → chunk-MA2F5KHF.cjs} +470 -242
  51. package/dist/chunk-MA2F5KHF.cjs.map +1 -0
  52. package/dist/{chunk-SAZRENEZ.cjs → chunk-MCSJFWAO.cjs} +1733 -192
  53. package/dist/chunk-MCSJFWAO.cjs.map +1 -0
  54. package/dist/chunk-MKA3AHLV.cjs +256 -0
  55. package/dist/chunk-MKA3AHLV.cjs.map +1 -0
  56. package/dist/chunk-OLZJXL4R.cjs +749 -0
  57. package/dist/chunk-OLZJXL4R.cjs.map +1 -0
  58. package/dist/chunk-OXSV64JX.js +689 -0
  59. package/dist/chunk-OXSV64JX.js.map +1 -0
  60. package/dist/chunk-P3RQ5UJE.cjs +689 -0
  61. package/dist/chunk-P3RQ5UJE.cjs.map +1 -0
  62. package/dist/chunk-P6ODPZ4W.js +256 -0
  63. package/dist/chunk-P6ODPZ4W.js.map +1 -0
  64. package/dist/chunk-PGNZKFQS.js +685 -0
  65. package/dist/chunk-PGNZKFQS.js.map +1 -0
  66. package/dist/chunk-PL4JB3BF.cjs +1 -0
  67. package/dist/chunk-PL4JB3BF.cjs.map +1 -0
  68. package/dist/{chunk-GVSMRPHB.cjs → chunk-SBCZ6RN5.cjs} +1568 -104
  69. package/dist/chunk-SBCZ6RN5.cjs.map +1 -0
  70. package/dist/{chunk-CL37JGFW.cjs → chunk-SML64VJG.cjs} +4 -4
  71. package/dist/{chunk-CL37JGFW.cjs.map → chunk-SML64VJG.cjs.map} +1 -1
  72. package/dist/chunk-TNNJXF46.cjs +115 -0
  73. package/dist/chunk-TNNJXF46.cjs.map +1 -0
  74. package/dist/chunk-U5HZSFCS.js +131 -0
  75. package/dist/chunk-U5HZSFCS.js.map +1 -0
  76. package/dist/{chunk-RU52I7QO.js → chunk-U5KWZU4X.js} +388 -160
  77. package/dist/chunk-U5KWZU4X.js.map +1 -0
  78. package/dist/chunk-XNORY7M6.cjs +685 -0
  79. package/dist/chunk-XNORY7M6.cjs.map +1 -0
  80. package/dist/commands/index.cjs +860 -0
  81. package/dist/commands/index.cjs.map +1 -0
  82. package/dist/commands/index.d.cts +244 -0
  83. package/dist/commands/index.d.ts +244 -0
  84. package/dist/commands/index.js +860 -0
  85. package/dist/commands/index.js.map +1 -0
  86. package/dist/comment-store-BEMgVlAA.d.cts +135 -0
  87. package/dist/comment-store-BEMgVlAA.d.ts +135 -0
  88. package/dist/compare/index.cjs +15 -0
  89. package/dist/compare/index.cjs.map +1 -0
  90. package/dist/compare/index.d.cts +58 -0
  91. package/dist/compare/index.d.ts +58 -0
  92. package/dist/compare/index.js +15 -0
  93. package/dist/compare/index.js.map +1 -0
  94. package/dist/{controller-B7GIybc4.d.cts → controller-BhLLAoi8.d.cts} +119 -5
  95. package/dist/{controller-B7GIybc4.d.ts → controller-BhLLAoi8.d.ts} +119 -5
  96. package/dist/documents-BQjzJNe2.d.ts +76 -0
  97. package/dist/documents-KRTPhvst.d.cts +76 -0
  98. package/dist/extensions/index.cjs +53 -5
  99. package/dist/extensions/index.cjs.map +1 -1
  100. package/dist/extensions/index.d.cts +7 -3
  101. package/dist/extensions/index.d.ts +7 -3
  102. package/dist/extensions/index.js +52 -4
  103. package/dist/generation/index.cjs +1373 -0
  104. package/dist/generation/index.cjs.map +1 -0
  105. package/dist/generation/index.d.cts +554 -0
  106. package/dist/generation/index.d.ts +554 -0
  107. package/dist/generation/index.js +1373 -0
  108. package/dist/generation/index.js.map +1 -0
  109. package/dist/grammar/index.cjs +48 -3
  110. package/dist/grammar/index.cjs.map +1 -1
  111. package/dist/grammar/index.d.cts +322 -100
  112. package/dist/grammar/index.d.ts +322 -100
  113. package/dist/grammar/index.js +54 -9
  114. package/dist/history/index.cjs +12 -2
  115. package/dist/history/index.cjs.map +1 -1
  116. package/dist/history/index.d.cts +211 -7
  117. package/dist/history/index.d.ts +211 -7
  118. package/dist/history/index.js +13 -3
  119. package/dist/image-editor/index.cjs +2244 -38
  120. package/dist/image-editor/index.cjs.map +1 -1
  121. package/dist/image-editor/index.d.cts +742 -4
  122. package/dist/image-editor/index.d.ts +742 -4
  123. package/dist/image-editor/index.js +2222 -16
  124. package/dist/image-editor/index.js.map +1 -1
  125. package/dist/{index-CCr_z9kF.d.cts → index-BhJBSm2d.d.cts} +370 -4
  126. package/dist/{index-Ck6ajwXh.d.ts → index-fJrlkJCd.d.ts} +370 -4
  127. package/dist/index.cjs +193 -98
  128. package/dist/index.cjs.map +1 -1
  129. package/dist/index.d.cts +17 -48
  130. package/dist/index.d.ts +17 -48
  131. package/dist/index.js +198 -103
  132. package/dist/index.js.map +1 -1
  133. package/dist/migrate-iTlm3a8h.d.cts +112 -0
  134. package/dist/migrate-iTlm3a8h.d.ts +112 -0
  135. package/dist/persistence/index.cjs +676 -0
  136. package/dist/persistence/index.cjs.map +1 -0
  137. package/dist/persistence/index.d.cts +347 -0
  138. package/dist/persistence/index.d.ts +347 -0
  139. package/dist/persistence/index.js +676 -0
  140. package/dist/persistence/index.js.map +1 -0
  141. package/dist/registry-BDe-vlv3.d.ts +171 -0
  142. package/dist/registry-D32LpIc4.d.cts +354 -0
  143. package/dist/registry-DnzpgY27.d.cts +171 -0
  144. package/dist/registry-evKTP4ls.d.ts +354 -0
  145. package/dist/schema-DYDGdYL9.d.cts +41 -0
  146. package/dist/schema-DYDGdYL9.d.ts +41 -0
  147. package/dist/serializers/index.cjs +4 -3
  148. package/dist/serializers/index.cjs.map +1 -1
  149. package/dist/serializers/index.d.cts +2 -1
  150. package/dist/serializers/index.d.ts +2 -1
  151. package/dist/serializers/index.js +3 -2
  152. package/dist/sourcemap-C8AH3MBQ.d.cts +91 -0
  153. package/dist/sourcemap-DA1Evhrv.d.ts +91 -0
  154. package/dist/spreadsheet/index.cjs +53 -3
  155. package/dist/spreadsheet/index.cjs.map +1 -1
  156. package/dist/spreadsheet/index.d.cts +247 -18
  157. package/dist/spreadsheet/index.d.ts +247 -18
  158. package/dist/spreadsheet/index.js +58 -8
  159. package/dist/{tokens-CyLZmIei.d.cts → tokens-D7yXvUU6.d.cts} +15 -2
  160. package/dist/{tokens-CyLZmIei.d.ts → tokens-D7yXvUU6.d.ts} +15 -2
  161. package/dist/{track-changes-BSF58RK0.d.cts → track-changes-BK5sVf4_.d.cts} +18 -136
  162. package/dist/{track-changes-BSF58RK0.d.ts → track-changes-CDjCrwKe.d.ts} +18 -136
  163. package/dist/{types-CJ4U-HHI.d.ts → types-DX4Mh4SL.d.cts} +1 -1
  164. package/dist/{types-CJ4U-HHI.d.cts → types-DX4Mh4SL.d.ts} +1 -1
  165. package/dist/types-GT5a5YAX.d.cts +86 -0
  166. package/dist/types-GT5a5YAX.d.ts +86 -0
  167. package/package.json +23 -2
  168. package/dist/chunk-2TWGLC5K.cjs +0 -262
  169. package/dist/chunk-2TWGLC5K.cjs.map +0 -1
  170. package/dist/chunk-4SUNJZWB.cjs.map +0 -1
  171. package/dist/chunk-6M3BCTBD.cjs.map +0 -1
  172. package/dist/chunk-AA4ZEVCC.cjs.map +0 -1
  173. package/dist/chunk-AXV2ZW34.cjs +0 -118
  174. package/dist/chunk-AXV2ZW34.cjs.map +0 -1
  175. package/dist/chunk-B2ZRLN4S.js +0 -118
  176. package/dist/chunk-B2ZRLN4S.js.map +0 -1
  177. package/dist/chunk-DENSCKFC.js.map +0 -1
  178. package/dist/chunk-GVSMRPHB.cjs.map +0 -1
  179. package/dist/chunk-KAADNAPF.js +0 -262
  180. package/dist/chunk-KAADNAPF.js.map +0 -1
  181. package/dist/chunk-MJZBNWG7.js.map +0 -1
  182. package/dist/chunk-OYXSKQFR.js.map +0 -1
  183. package/dist/chunk-QGHGJ56O.js.map +0 -1
  184. package/dist/chunk-RU52I7QO.js.map +0 -1
  185. package/dist/chunk-SAZRENEZ.cjs.map +0 -1
  186. package/dist/types-BZgcDsC5.d.cts +0 -42
  187. package/dist/types-BZgcDsC5.d.ts +0 -42
  188. /package/dist/{chunk-JJDLPEWL.js.map → chunk-3LV6RRPH.js.map} +0 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/persistence/recoveryPolicy.ts","../../src/persistence/storage.ts","../../src/persistence/autosave.ts","../../src/persistence/revisions.ts"],"sourcesContent":["// Portions ported from filmcraft-main/apps/filmcraft-web/src/recovery_policy.rs (MIT OR Apache-2.0,\n// © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\n/**\n * What the caller should do now (see `RecoveryPolicy.tick`).\n *\n * - `writeSnapshot`: write the snapshot and a dirty meta for `revision`, then report with `written`.\n * - `writeClean`: write a clean meta (retire the snapshot), then report with `cleaned`.\n * - `wakeAfter`: nothing now; tick again after `delay` (same unit as `now`).\n */\nexport type RecoveryAction =\n | { type: \"nothing\" }\n | { type: \"writeSnapshot\"; revision: number }\n | { type: \"writeClean\" }\n | { type: \"wakeAfter\"; delay: number };\n\nexport interface RecoveryPolicyOptions {\n /**\n * Minimum time between snapshots of a changing document, in the unit of `now`\n * (default 5000, i.e. milliseconds).\n */\n interval?: number;\n /**\n * Wait before retrying after the `failures`-th consecutive failed write\n * (default: `interval`, as in FilmCraft). Use `exponentialBackoff` for doubling delays.\n */\n retryDelay?: (failures: number) => number;\n}\n\nexport interface RecoveryTickOptions {\n /** Ignore the interval and any retry wait (e.g. on `pagehide`). */\n force?: boolean;\n}\n\nexport interface RecoveryPolicy {\n /** Called whenever something may have changed, with the revisions and the current time. */\n tick(\n now: number,\n revision: number,\n savedRevision: number,\n options?: RecoveryTickOptions,\n ): RecoveryAction;\n /** The snapshot write for `revision` finished. */\n written(revision: number, ok: boolean, now: number): void;\n /** The clean-meta write finished. */\n cleaned(ok: boolean, now: number): void;\n /** Whether this session wrote a dirty snapshot that is not retired yet. */\n readonly ownsSnapshot: boolean;\n /** Consecutive failed writes (reset by a successful one). */\n readonly failures: number;\n}\n\n/**\n * Exponential backoff: `base`, `2·base`, `4·base`, … capped at `max`.\n * `failures` is 1 for the first failure.\n */\nexport function exponentialBackoff(\n failures: number,\n base = 1000,\n max = 30000,\n): number {\n const exp = Math.min(Math.max(failures - 1, 0), 16);\n return Math.min(base * 2 ** exp, max);\n}\n\n/**\n * Pure state machine deciding when a crash-recovery snapshot is written or\n * retired. Deterministic: time is always passed in.\n *\n * - A dirty document is snapshotted at most every `interval`, once per revision.\n * A write that fails is retried after `retryDelay`, without waiting for another edit.\n * - The snapshot is marked clean only if this session wrote it. A snapshot left by an\n * earlier session (that was not restored) stays dirty, so it can still be recovered.\n * - An edit made while the clean write is in flight keeps the snapshot owned.\n */\nexport function createRecoveryPolicy(\n options: RecoveryPolicyOptions = {},\n): RecoveryPolicy {\n const interval = options.interval ?? 5000;\n const retryDelay = options.retryDelay ?? (() => interval);\n const minWake = interval * 0.01;\n\n /** Revision whose snapshot was last written or is being written. */\n let lastRevision: number | null = null;\n let lastTime: number | null = null;\n /** Required gap after `lastTime`: the interval, or the retry delay after a failure. */\n let gap = interval;\n /** This session wrote a dirty meta that is not yet retired. */\n let ownsDirtyMeta = false;\n /** A clean-meta write is in flight. */\n let cleaning = false;\n /** Snapshot writes started so far, and how many had started when the clean write did. */\n let snapshots = 0;\n let snapshotsAtClean = 0;\n let failures = 0;\n\n const fail = (now: number) => {\n failures++;\n lastRevision = null;\n lastTime = now;\n gap = retryDelay(failures);\n };\n\n return {\n tick(now, revision, savedRevision, tickOptions) {\n const force = tickOptions?.force === true;\n const since = force || lastTime === null ? Infinity : now - lastTime;\n if (revision !== savedRevision) {\n if (lastRevision === revision) return { type: \"nothing\" };\n if (since >= gap) {\n lastRevision = revision;\n lastTime = now;\n ownsDirtyMeta = true;\n snapshots++;\n return { type: \"writeSnapshot\", revision };\n }\n return { type: \"wakeAfter\", delay: Math.max(gap - since, minWake) };\n }\n if (ownsDirtyMeta && !cleaning) {\n if (lastRevision !== null || since >= gap) {\n cleaning = true;\n snapshotsAtClean = snapshots;\n return { type: \"writeClean\" };\n }\n return { type: \"wakeAfter\", delay: Math.max(gap - since, minWake) };\n }\n return { type: \"nothing\" };\n },\n written(revision, ok, now) {\n if (ok) {\n if (lastRevision === revision) {\n failures = 0;\n gap = interval;\n }\n } else if (lastRevision === revision) {\n // retry after the delay, even if nothing else changes\n fail(now);\n }\n },\n cleaned(ok, now) {\n cleaning = false;\n if (ok) {\n ownsDirtyMeta = snapshots !== snapshotsAtClean;\n failures = 0;\n gap = interval;\n } else {\n fail(now);\n }\n },\n get ownsSnapshot() {\n return ownsDirtyMeta;\n },\n get failures() {\n return failures;\n },\n };\n}\n","/**\n * Key/value storage adapters for autosave and crash recovery. Every adapter is\n * async, stores strings, feature-detects its backend (an unavailable backend\n * rejects every call with `StorageUnavailableError` instead of throwing at\n * construction, so SSR and old browsers are safe) and accepts an injectable\n * failure mode for tests.\n */\n\nexport type StorageOperation = \"get\" | \"set\" | \"remove\";\n\n/**\n * Injected failure: return `true` (or an `Error`) to make that operation fail.\n * Example: `failure: (op) => op === \"set\"` simulates a full disk.\n */\nexport type StorageFailureMode = (\n operation: StorageOperation,\n key: string,\n) => boolean | Error | undefined | void;\n\nexport interface StorageAdapter {\n /** Short backend name, e.g. `\"memory\"`, `\"localStorage\"`, `\"indexedDB\"`, `\"opfs\"`. */\n readonly kind: string;\n /** Whether the backend was found. When `false`, every call rejects. */\n readonly available: boolean;\n get(key: string): Promise<string | null>;\n set(key: string, value: string): Promise<void>;\n remove(key: string): Promise<void>;\n}\n\nexport interface StorageAdapterOptions {\n failure?: StorageFailureMode;\n}\n\nexport class StorageUnavailableError extends Error {\n constructor(kind: string) {\n super(`${kind} storage is not available in this environment`);\n this.name = \"StorageUnavailableError\";\n }\n}\n\nexport class InjectedStorageError extends Error {\n constructor(operation: StorageOperation, key: string) {\n super(`injected ${operation} failure for \"${key}\"`);\n this.name = \"InjectedStorageError\";\n }\n}\n\nfunction checkFailure(\n failure: StorageFailureMode | undefined,\n operation: StorageOperation,\n key: string,\n): void {\n if (!failure) return;\n const result = failure(operation, key);\n if (result instanceof Error) throw result;\n if (result) throw new InjectedStorageError(operation, key);\n}\n\n/** Wraps an adapter so `failure` can make any of its operations fail. */\nexport function withStorageFailures(\n inner: StorageAdapter,\n failure: StorageFailureMode,\n): StorageAdapter {\n return {\n kind: inner.kind,\n get available() {\n return inner.available;\n },\n async get(key) {\n checkFailure(failure, \"get\", key);\n return inner.get(key);\n },\n async set(key, value) {\n checkFailure(failure, \"set\", key);\n return inner.set(key, value);\n },\n async remove(key) {\n checkFailure(failure, \"remove\", key);\n return inner.remove(key);\n },\n };\n}\n\nfunction guarded(\n kind: string,\n isAvailable: () => boolean,\n failure: StorageFailureMode | undefined,\n impl: Omit<StorageAdapter, \"kind\" | \"available\">,\n): StorageAdapter {\n const check = (operation: StorageOperation, key: string) => {\n if (!isAvailable()) throw new StorageUnavailableError(kind);\n checkFailure(failure, operation, key);\n };\n return {\n kind,\n get available() {\n return isAvailable();\n },\n async get(key) {\n check(\"get\", key);\n return impl.get(key);\n },\n async set(key, value) {\n check(\"set\", key);\n return impl.set(key, value);\n },\n async remove(key) {\n check(\"remove\", key);\n return impl.remove(key);\n },\n };\n}\n\n// ---------------------------------------------------------------- memory\n\nexport interface MemoryStorage extends StorageAdapter {\n /** The underlying map (inspect it in tests). */\n readonly entries: Map<string, string>;\n}\n\n/** In-memory storage (tests, SSR, or a session-only fallback). */\nexport function createMemoryStorage(\n options: StorageAdapterOptions & { initial?: Record<string, string> } = {},\n): MemoryStorage {\n const entries = new Map<string, string>(Object.entries(options.initial ?? {}));\n const adapter = guarded(\"memory\", () => true, options.failure, {\n async get(key) {\n return entries.has(key) ? (entries.get(key) as string) : null;\n },\n async set(key, value) {\n entries.set(key, value);\n },\n async remove(key) {\n entries.delete(key);\n },\n });\n return Object.assign(adapter, { entries });\n}\n\n// ---------------------------------------------------------------- localStorage\n\n/** Minimal `Storage` surface used by the localStorage adapter. */\nexport interface WebStorageLike {\n getItem(key: string): string | null;\n setItem(key: string, value: string): void;\n removeItem(key: string): void;\n}\n\nexport interface LocalStorageAdapterOptions extends StorageAdapterOptions {\n /** Storage to use (default `globalThis.localStorage`, e.g. pass `sessionStorage`). */\n storage?: WebStorageLike;\n}\n\nfunction detectLocalStorage(): WebStorageLike | undefined {\n try {\n const storage = (globalThis as { localStorage?: WebStorageLike }).localStorage;\n return storage && typeof storage.getItem === \"function\" ? storage : undefined;\n } catch {\n // Accessing localStorage throws when site data is blocked.\n return undefined;\n }\n}\n\n/** Whether `localStorage` can be used (it throws when site data is blocked). */\nexport function isLocalStorageAvailable(): boolean {\n return detectLocalStorage() !== undefined;\n}\n\n/**\n * `localStorage` adapter; every key is prefixed with `prefix`. Writes are\n * synchronous under the hood, so they complete even inside `pagehide`.\n * A full quota rejects the `set` (`QuotaExceededError`).\n */\nexport function createLocalStorageAdapter(\n prefix = \"\",\n options: LocalStorageAdapterOptions = {},\n): StorageAdapter {\n const storage = options.storage ?? detectLocalStorage();\n return guarded(\"localStorage\", () => storage !== undefined, options.failure, {\n async get(key) {\n return storage!.getItem(prefix + key);\n },\n async set(key, value) {\n storage!.setItem(prefix + key, value);\n },\n async remove(key) {\n storage!.removeItem(prefix + key);\n },\n });\n}\n\n// ---------------------------------------------------------------- IndexedDB\n\n/** Minimal `IDBFactory` surface (the real `indexedDB`, or a fake in tests). */\nexport interface IdbFactoryLike {\n open(name: string, version?: number): any;\n}\n\nexport interface IndexedDbAdapterOptions extends StorageAdapterOptions {\n /** Object store name (default `\"kv\"`). */\n store?: string;\n /** Factory to use (default `globalThis.indexedDB`). */\n indexedDB?: IdbFactoryLike;\n}\n\nfunction detectIndexedDb(): IdbFactoryLike | undefined {\n try {\n const factory = (globalThis as { indexedDB?: IdbFactoryLike }).indexedDB;\n return factory && typeof factory.open === \"function\" ? factory : undefined;\n } catch {\n return undefined;\n }\n}\n\n/** Whether `indexedDB` exists in this environment. */\nexport function isIndexedDbAvailable(): boolean {\n return detectIndexedDb() !== undefined;\n}\n\nconst requestToPromise = <T>(request: any): Promise<T> =>\n new Promise((resolve, reject) => {\n request.onsuccess = () => resolve(request.result as T);\n request.onerror = () => reject(request.error ?? new Error(\"IndexedDB request failed\"));\n });\n\n/**\n * IndexedDB adapter: one database `name` with a single key/value object store.\n * A `set` resolves when its transaction completes (not merely when the request\n * succeeds), so a resolved write is durable.\n */\nexport function createIndexedDbAdapter(\n name = \"pixerate-editor\",\n options: IndexedDbAdapterOptions = {},\n): StorageAdapter {\n const factory = options.indexedDB ?? detectIndexedDb();\n const storeName = options.store ?? \"kv\";\n let dbPromise: Promise<any> | null = null;\n\n const open = (): Promise<any> => {\n dbPromise ??= new Promise((resolve, reject) => {\n const request = factory!.open(name, 1);\n request.onupgradeneeded = () => {\n const db = request.result;\n if (!db.objectStoreNames.contains(storeName)) db.createObjectStore(storeName);\n };\n request.onsuccess = () => resolve(request.result);\n request.onerror = () => {\n dbPromise = null;\n reject(request.error ?? new Error(\"IndexedDB open failed\"));\n };\n });\n return dbPromise;\n };\n\n const write = async (fn: (store: any) => void): Promise<void> => {\n const db = await open();\n await new Promise<void>((resolve, reject) => {\n const tx = db.transaction(storeName, \"readwrite\");\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error ?? new Error(\"IndexedDB transaction failed\"));\n tx.onabort = () => reject(tx.error ?? new Error(\"IndexedDB transaction aborted\"));\n fn(tx.objectStore(storeName));\n });\n };\n\n return guarded(\"indexedDB\", () => factory !== undefined, options.failure, {\n async get(key) {\n const db = await open();\n const tx = db.transaction(storeName, \"readonly\");\n const value = await requestToPromise<unknown>(tx.objectStore(storeName).get(key));\n return typeof value === \"string\" ? value : null;\n },\n set: (key, value) => write((store) => store.put(value, key)),\n remove: (key) => write((store) => store.delete(key)),\n });\n}\n\n// ---------------------------------------------------------------- OPFS\n\n/** Minimal `FileSystemDirectoryHandle` surface used by the OPFS adapter. */\nexport interface OpfsDirectoryLike {\n getDirectoryHandle(name: string, options?: { create?: boolean }): Promise<OpfsDirectoryLike>;\n getFileHandle(name: string, options?: { create?: boolean }): Promise<OpfsFileLike>;\n removeEntry(name: string): Promise<void>;\n}\n\nexport interface OpfsFileLike {\n getFile(): Promise<{ text(): Promise<string> }>;\n createWritable?(): Promise<{ write(data: string): Promise<void>; close(): Promise<void> }>;\n /** Rename in place (Chromium, Safari). Used for temp-file + rename writes. */\n move?(name: string): Promise<void>;\n}\n\nexport interface OpfsAdapterOptions extends StorageAdapterOptions {\n /** Root directory (default `navigator.storage.getDirectory()`). */\n root?: () => Promise<OpfsDirectoryLike>;\n /** Force the write-then-pointer-swap strategy even when `move` exists. */\n disableRename?: boolean;\n}\n\nfunction detectOpfsRoot(): (() => Promise<OpfsDirectoryLike>) | undefined {\n try {\n const storage = (globalThis as any).navigator?.storage;\n if (storage && typeof storage.getDirectory === \"function\") {\n return () => storage.getDirectory();\n }\n } catch {\n // ignore\n }\n return undefined;\n}\n\n/** Whether the Origin Private File System exists (`navigator.storage.getDirectory`). */\nexport function isOpfsAvailable(): boolean {\n return detectOpfsRoot() !== undefined;\n}\n\nconst isNotFound = (error: unknown) =>\n !!error && typeof error === \"object\" && (error as { name?: string }).name === \"NotFoundError\";\n\n/**\n * Origin Private File System adapter. Values are files in `dir` (created on\n * demand), named after the URI-encoded key.\n *\n * Writes never leave a half-written value behind: where file handles support\n * `move`, the value is written to `<file>.tmp` and renamed over `<file>`;\n * otherwise it alternates between two slots (`<file>.a` / `<file>.b`) and then\n * swaps a pointer file (`<file>.ptr`), so a crash mid-write keeps the previous\n * value. Main-thread writes need `createWritable` (missing in some Safari\n * versions); without it, `set` rejects with `StorageUnavailableError`.\n */\nexport function createOpfsAdapter(\n dir = \"pixerate-editor\",\n options: OpfsAdapterOptions = {},\n): StorageAdapter {\n const getRoot = options.root ?? detectOpfsRoot();\n let dirPromise: Promise<OpfsDirectoryLike> | null = null;\n const directory = () => {\n dirPromise ??= getRoot!().then((root) => root.getDirectoryHandle(dir, { create: true }));\n dirPromise.catch(() => {\n dirPromise = null;\n });\n return dirPromise;\n };\n const fileName = (key: string) => encodeURIComponent(key) || \"_\";\n\n const readFile = async (d: OpfsDirectoryLike, name: string): Promise<string | null> => {\n try {\n const handle = await d.getFileHandle(name);\n return await (await handle.getFile()).text();\n } catch (error) {\n if (isNotFound(error)) return null;\n throw error;\n }\n };\n\n const writeFile = async (d: OpfsDirectoryLike, name: string, value: string) => {\n const handle = await d.getFileHandle(name, { create: true });\n if (typeof handle.createWritable !== \"function\") {\n throw new StorageUnavailableError(\"opfs (createWritable)\");\n }\n const writable = await handle.createWritable();\n await writable.write(value);\n await writable.close();\n return handle;\n };\n\n const removeFile = async (d: OpfsDirectoryLike, name: string) => {\n try {\n await d.removeEntry(name);\n } catch (error) {\n if (!isNotFound(error)) throw error;\n }\n };\n\n return guarded(\"opfs\", () => getRoot !== undefined, options.failure, {\n async get(key) {\n const d = await directory();\n const base = fileName(key);\n const pointer = await readFile(d, `${base}.ptr`);\n if (pointer === \"a\" || pointer === \"b\") return readFile(d, `${base}.${pointer}`);\n return readFile(d, base);\n },\n async set(key, value) {\n const d = await directory();\n const base = fileName(key);\n const pointer = await readFile(d, `${base}.ptr`);\n if (!options.disableRename && pointer === null) {\n const tmp = await writeFile(d, `${base}.tmp`, value);\n if (typeof tmp.move === \"function\") {\n await tmp.move(base);\n return;\n }\n await removeFile(d, `${base}.tmp`);\n }\n // Write-then-pointer swap: the pointer only moves once the new slot is complete.\n const next = pointer === \"a\" ? \"b\" : \"a\";\n await writeFile(d, `${base}.${next}`, value);\n await writeFile(d, `${base}.ptr`, next);\n },\n async remove(key) {\n const d = await directory();\n const base = fileName(key);\n for (const name of [`${base}.ptr`, base, `${base}.a`, `${base}.b`, `${base}.tmp`]) {\n await removeFile(d, name);\n }\n },\n });\n}\n","// Portions ported from filmcraft-main/apps/filmcraft-web/src/recovery.rs and\n// craft-libs-main/crates/settings/src/store.rs (MIT OR Apache-2.0, © ArtCraft Team and\n// contributors). See THIRD_PARTY_NOTICES.md.\n\nimport {\n createRecoveryPolicy,\n exponentialBackoff,\n type RecoveryAction,\n type RecoveryPolicy,\n} from \"./recoveryPolicy\";\nimport type { StorageAdapter } from \"./storage\";\n\nexport type AutosaveStatus = \"idle\" | \"dirty\" | \"saving\" | \"saved\" | \"error\";\n\nexport interface AutosaveStatusInfo {\n status: AutosaveStatus;\n /** Last write error (while `status` is `\"error\"`). */\n error: unknown;\n /** Consecutive failed writes. */\n failures: number;\n /** When the next attempt is due (`now()` units), while waiting. */\n nextAttemptAt: number | null;\n /** When the last snapshot was written (`now()` units). */\n lastSavedAt: number | null;\n /** Whether `ready()` was called. */\n ready: boolean;\n}\n\n/** Stored next to the snapshot (`<key>:meta`). */\nexport interface RecoveryMeta {\n dirty: boolean;\n revision?: number;\n /** `Date.now()` of the write. */\n time?: number;\n /** Document name, for the \"Restore unsaved changes from …?\" prompt. */\n name?: string;\n}\n\nexport interface AutosaveRecovery<T = unknown> {\n /** The parsed snapshot (`undefined` when `deserialize` threw; see `error`). */\n snapshot: T | undefined;\n /** The stored text. */\n text: string;\n meta: RecoveryMeta & { dirty: true };\n error?: unknown;\n}\n\nexport interface AutosaveControllerOptions<T> {\n storage: StorageAdapter;\n /** Snapshot key; the meta lives at `<key>:meta`. */\n key: string;\n /** Reads the current document. */\n getSnapshot: () => T;\n /** Default `JSON.stringify`. */\n serialize?: (snapshot: T) => string;\n /** Default `JSON.parse` (used by `checkRecovery`). */\n deserialize?: (text: string) => T;\n /** Current document revision; must change on every edit. */\n revision: () => number;\n /** Revision of the last explicit save (equal to `revision()` when clean). */\n savedRevision: () => number;\n /** Document name stored in the meta. */\n name?: string | (() => string | undefined);\n onStatus?: (info: AutosaveStatusInfo) => void;\n /** Minimum time between snapshots while edits keep coming (default 5000 ms). */\n interval?: number;\n /** Quiet time after the last edit before a snapshot is written (default 1000 ms). */\n debounceMs?: number;\n /** First retry delay after a failed write; doubles per failure (default 1000 ms). */\n retryBaseMs?: number;\n /** Retry delay cap (default 30000 ms). */\n retryMaxMs?: number;\n /** Clock for scheduling (default `Date.now`). */\n now?: () => number;\n /** Wall clock stored in the meta (default `Date.now`). */\n wallClock?: () => number;\n setTimeout?: (fn: () => void, ms: number) => unknown;\n clearTimeout?: (handle: any) => void;\n /** Start hydrated, i.e. as if `ready()` was already called (default `false`). */\n ready?: boolean;\n}\n\nexport interface AutosaveController<T = unknown> {\n /** Current status. */\n getStatus(): AutosaveStatusInfo;\n /** Listener is called on every status change (not immediately). */\n subscribe(listener: (info: AutosaveStatusInfo) => void): () => void;\n /** Call after every change (edit, undo, save). Cheap; the write is scheduled. */\n notify(): void;\n /** Lifts the hydration gate: no snapshot is written before this. */\n ready(): void;\n readonly isReady: boolean;\n /**\n * Writes a snapshot of unsaved changes right away, skipping the debounce, the\n * interval and retry waits. Use on `pagehide` / `visibilitychange`. Resolves\n * once the write settled.\n */\n flushNow(): Promise<void>;\n /**\n * Call after the document was saved (`savedRevision()` now equals\n * `revision()`): retires the snapshot only if this session wrote it.\n */\n markClean(): Promise<void>;\n /**\n * A dirty snapshot left by an earlier session, or `null`. Returns `null` once\n * this session has written its own snapshot.\n */\n checkRecovery(): Promise<AutosaveRecovery<T> | null>;\n /** Deletes an earlier session's snapshot. No-op once this session wrote its own. */\n discardRecovery(): Promise<boolean>;\n /** Runs one scheduling step now (normally driven by `notify` and timers). */\n tick(): void;\n /** Resolves when no write is in flight. */\n whenIdle(): Promise<void>;\n /** Stops all timers. Pending changes are not written; call `flushNow` first. */\n destroy(): void;\n}\n\nexport const recoveryMetaKey = (key: string) => `${key}:meta`;\n\n/**\n * Text for a recovery prompt: \"Restore unsaved changes from <time>?\" (the\n * React and Svelte `RecoveryPrompt` components use it).\n */\nexport function formatRecoveryMessage(\n meta: Pick<RecoveryMeta, \"time\">,\n formatTime: (time: number) => string = (time) => new Date(time).toLocaleString(),\n): string {\n return typeof meta.time === \"number\" && Number.isFinite(meta.time)\n ? `Restore unsaved changes from ${formatTime(meta.time)}?`\n : \"Restore unsaved changes?\";\n}\n\n/**\n * Debounced autosave of crash-recovery snapshots.\n *\n * - Hydration gate: nothing is written until `ready()`, so loading a document\n * (or deciding about a recovered one) never overwrites the previous session's snapshot.\n * - A snapshot is written `debounceMs` after the last edit, and at least every\n * `interval` while edits keep coming; never twice for one revision.\n * - Failed writes are retried with exponential backoff (`retryBaseMs` doubling up to\n * `retryMaxMs`) without waiting for another edit.\n * - The snapshot is written before its dirty meta, so the meta never points at a\n * missing snapshot. Once saved, the meta is marked clean only if this session\n * wrote the snapshot (`createRecoveryPolicy`).\n */\nexport function createAutosaveController<T>(\n options: AutosaveControllerOptions<T>,\n): AutosaveController<T> {\n const {\n storage,\n key,\n getSnapshot,\n revision,\n savedRevision,\n interval = 5000,\n debounceMs = 1000,\n retryBaseMs = 1000,\n retryMaxMs = 30000,\n } = options;\n const serialize = options.serialize ?? ((value: T) => JSON.stringify(value));\n const deserialize = options.deserialize ?? ((text: string) => JSON.parse(text) as T);\n const now = options.now ?? (() => Date.now());\n const wallClock = options.wallClock ?? (() => Date.now());\n const setTimer =\n options.setTimeout ?? ((fn: () => void, ms: number) => globalThis.setTimeout(fn, ms));\n const clearTimer = options.clearTimeout ?? ((handle: any) => globalThis.clearTimeout(handle));\n const metaKey = recoveryMetaKey(key);\n\n const policy: RecoveryPolicy = createRecoveryPolicy({\n interval,\n retryDelay: (failures) => exponentialBackoff(failures, retryBaseMs, retryMaxMs),\n });\n\n let hydrated = options.ready === true;\n let destroyed = false;\n let timer: unknown = null;\n let timerDue = Infinity;\n let inflight: Promise<void> | null = null;\n let observedRevision = revision();\n let lastChangeAt: number | null = null;\n let pendingSince: number | null = null;\n let forceNext = false;\n let info: AutosaveStatusInfo = {\n status: \"idle\",\n error: null,\n failures: 0,\n nextAttemptAt: null,\n lastSavedAt: null,\n ready: hydrated,\n };\n const listeners = new Set<(info: AutosaveStatusInfo) => void>();\n\n const setInfo = (patch: Partial<AutosaveStatusInfo>) => {\n const next = { ...info, ...patch, failures: policy.failures, ready: hydrated };\n if (\n next.status === info.status &&\n next.error === info.error &&\n next.failures === info.failures &&\n next.nextAttemptAt === info.nextAttemptAt &&\n next.lastSavedAt === info.lastSavedAt &&\n next.ready === info.ready\n ) {\n return;\n }\n info = next;\n options.onStatus?.(info);\n listeners.forEach((listener) => {\n try {\n listener(info);\n } catch (error) {\n console.error(\"[autosave] Error in status listener:\", error);\n }\n });\n };\n\n const cancelTimer = () => {\n if (timer !== null) clearTimer(timer);\n timer = null;\n timerDue = Infinity;\n };\n\n const schedule = (delay: number) => {\n if (destroyed) return;\n const due = now() + Math.max(0, delay);\n if (timer !== null && timerDue <= due) return;\n cancelTimer();\n timerDue = due;\n timer = setTimer(() => {\n timer = null;\n timerDue = Infinity;\n tick();\n }, Math.max(0, delay));\n };\n\n const observe = (t: number) => {\n const rev = revision();\n if (rev !== observedRevision) {\n observedRevision = rev;\n lastChangeAt = t;\n pendingSince ??= t;\n }\n return rev;\n };\n\n const docName = () =>\n typeof options.name === \"function\" ? options.name() : options.name;\n\n const run = (action: RecoveryAction, rev: number) => {\n if (action.type === \"writeSnapshot\") {\n setInfo({ status: \"saving\", nextAttemptAt: null });\n inflight = (async () => {\n let ok = false;\n let error: unknown = null;\n try {\n const text = serialize(getSnapshot());\n await storage.set(key, text);\n const meta: RecoveryMeta = {\n dirty: true,\n revision: action.revision,\n time: wallClock(),\n name: docName(),\n };\n await storage.set(metaKey, JSON.stringify(meta));\n ok = true;\n } catch (e) {\n error = e;\n }\n const t = now();\n policy.written(action.revision, ok, t);\n inflight = null;\n if (ok) {\n if (observedRevision === action.revision) pendingSince = null;\n setInfo({ status: \"saved\", error: null, lastSavedAt: t });\n } else {\n setInfo({ status: \"error\", error });\n }\n tick();\n })();\n } else if (action.type === \"writeClean\") {\n setInfo({ status: \"saving\", nextAttemptAt: null });\n inflight = (async () => {\n let ok = false;\n let error: unknown = null;\n try {\n await storage.set(metaKey, JSON.stringify({ dirty: false } satisfies RecoveryMeta));\n ok = true;\n } catch (e) {\n error = e;\n }\n policy.cleaned(ok, now());\n inflight = null;\n if (ok) {\n // Best effort: the clean meta already retired the snapshot.\n if (!policy.ownsSnapshot) await storage.remove(key).catch(() => undefined);\n setInfo({ status: \"saved\", error: null });\n } else {\n setInfo({ status: \"error\", error });\n }\n tick();\n })();\n } else if (action.type === \"wakeAfter\") {\n schedule(action.delay);\n setInfo(\n info.status === \"error\"\n ? { nextAttemptAt: now() + action.delay }\n : { status: rev !== savedRevision() ? \"dirty\" : info.status, nextAttemptAt: now() + action.delay },\n );\n } else if (rev === savedRevision() && info.status === \"dirty\") {\n setInfo({ status: policy.ownsSnapshot ? \"dirty\" : \"idle\", nextAttemptAt: null });\n } else if (rev !== savedRevision() && info.status !== \"saved\" && info.status !== \"error\") {\n setInfo({ status: \"dirty\" });\n }\n };\n\n function tick(): void {\n if (destroyed || inflight) return;\n const t = now();\n const rev = observe(t);\n const saved = savedRevision();\n if (!hydrated) {\n if (rev !== saved) setInfo({ status: \"dirty\" });\n return;\n }\n const force = forceNext;\n forceNext = false;\n if (rev !== saved && !force && info.status !== \"error\") {\n const changed = lastChangeAt ?? t;\n const due = Math.min(changed + debounceMs, (pendingSince ?? changed) + interval);\n if (t < due) {\n setInfo({ status: \"dirty\", nextAttemptAt: due });\n schedule(due - t);\n return;\n }\n }\n run(policy.tick(t, rev, saved, { force }), rev);\n }\n\n const whenIdle = async () => {\n while (inflight) await inflight;\n };\n\n return {\n getStatus: () => info,\n subscribe(listener) {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n notify() {\n if (destroyed) return;\n const t = now();\n const before = observedRevision;\n const rev = observe(t);\n const dirty = rev !== savedRevision();\n if (dirty && rev !== before && info.status !== \"error\" && !inflight) {\n setInfo({ status: \"dirty\" });\n }\n if (!hydrated) return;\n // Never synchronous: listeners that update `savedRevision` may run after us.\n const changed = lastChangeAt ?? t;\n const due = Math.min(changed + debounceMs, (pendingSince ?? changed) + interval);\n schedule(dirty ? due - t : 0);\n },\n ready() {\n if (hydrated || destroyed) return;\n hydrated = true;\n setInfo({});\n schedule(0);\n },\n get isReady() {\n return hydrated;\n },\n async flushNow() {\n if (destroyed || !hydrated) return;\n await whenIdle();\n cancelTimer();\n forceNext = true;\n tick();\n await whenIdle();\n },\n async markClean() {\n if (destroyed) return;\n await whenIdle();\n cancelTimer();\n tick();\n await whenIdle();\n },\n async checkRecovery() {\n if (policy.ownsSnapshot) return null;\n let meta: RecoveryMeta;\n try {\n const metaText = await storage.get(metaKey);\n if (!metaText) return null;\n meta = JSON.parse(metaText) as RecoveryMeta;\n } catch {\n return null;\n }\n if (!meta || meta.dirty !== true) return null;\n const text = await storage.get(key).catch(() => null);\n if (text === null) return null;\n try {\n return { snapshot: deserialize(text), text, meta: meta as RecoveryMeta & { dirty: true } };\n } catch (error) {\n return { snapshot: undefined, text, meta: meta as RecoveryMeta & { dirty: true }, error };\n }\n },\n async discardRecovery() {\n if (policy.ownsSnapshot) return false;\n await whenIdle();\n await storage.remove(metaKey);\n await storage.remove(key);\n return true;\n },\n tick,\n whenIdle,\n destroy() {\n destroyed = true;\n cancelTimer();\n listeners.clear();\n },\n };\n}\n","/** Anything that notifies on every document change (`HistoryManager` satisfies it). */\nexport interface RevisionChangeSource {\n subscribe(listener: (...args: any[]) => void): () => void;\n}\n\n/** Anything with a dirty flag (`DirtyTracker` and `HistoryDirtyBinding` satisfy it). */\nexport interface DirtyFlagSource {\n readonly dirty: boolean;\n subscribe(listener: (dirty: boolean) => void): () => void;\n}\n\nexport interface RevisionTrackerOptions {\n /** Change notifications (e.g. a `HistoryManager`). Every notification is a new revision. */\n history?: RevisionChangeSource;\n /**\n * Saved-vs-current flag (e.g. `bindDirtyToHistory(...)` or a `DirtyTracker`).\n * Whenever it reads clean, the current revision counts as saved, so undoing\n * back to the saved state needs no snapshot. Without it, call `markSaved()`.\n */\n dirty?: DirtyFlagSource;\n}\n\nexport interface RevisionTracker {\n /** Bumped on every change. */\n revision(): number;\n /** Revision of the last save; equals `revision()` while clean. */\n savedRevision(): number;\n /** Records a change made outside `history`. */\n bump(): void;\n /** Marks the current revision saved. */\n markSaved(): void;\n /** Called after every change, save or dirty flip. */\n subscribe(listener: () => void): () => void;\n destroy(): void;\n}\n\n/**\n * Derives the `revision` / `savedRevision` pair an autosave controller needs\n * from a history and a dirty flag (`bindDirtyToHistory` or `DirtyTracker`).\n * Structural types only, so this entry stays dependency-free.\n */\nexport function createRevisionTracker(options: RevisionTrackerOptions = {}): RevisionTracker {\n let revision = 0;\n let saved = 0;\n const listeners = new Set<() => void>();\n const emit = () =>\n listeners.forEach((listener) => {\n try {\n listener();\n } catch (error) {\n console.error(\"[autosave] Error in revision listener:\", error);\n }\n });\n const syncSaved = () => {\n // Read lazily: the dirty binding may update after our history listener ran.\n if (options.dirty && !options.dirty.dirty) saved = revision;\n };\n\n const unsubscribers: (() => void)[] = [];\n if (options.history) {\n unsubscribers.push(\n options.history.subscribe(() => {\n revision++;\n emit();\n }),\n );\n }\n if (options.dirty) {\n let first = true;\n unsubscribers.push(\n options.dirty.subscribe((dirty) => {\n if (first) {\n first = false;\n // DirtyTracker calls back immediately with the current value.\n if (dirty && revision === saved) revision++;\n return;\n }\n if (dirty && revision === saved) revision++;\n if (!dirty) saved = revision;\n emit();\n }),\n );\n }\n\n return {\n revision: () => revision,\n savedRevision: () => {\n syncSaved();\n return saved;\n },\n bump() {\n revision++;\n emit();\n },\n markSaved() {\n saved = revision;\n emit();\n },\n subscribe(listener) {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n destroy() {\n unsubscribers.forEach((fn) => fn());\n unsubscribers.length = 0;\n listeners.clear();\n },\n };\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,171 @@
1
+ import { J as JsonSchema, S as SchemaValidationError } from './schema-DYDGdYL9.js';
2
+
3
+ /**
4
+ * MCP-shaped tool definition, as returned by `tools/list`.
5
+ */
6
+ interface McpToolDefinition {
7
+ name: string;
8
+ title?: string;
9
+ description?: string;
10
+ inputSchema: JsonSchema & {
11
+ type: 'object';
12
+ };
13
+ outputSchema?: JsonSchema;
14
+ annotations?: {
15
+ title?: string;
16
+ readOnlyHint?: boolean;
17
+ destructiveHint?: boolean;
18
+ idempotentHint?: boolean;
19
+ openWorldHint?: boolean;
20
+ [key: string]: unknown;
21
+ };
22
+ }
23
+ type McpToolContent = {
24
+ type: 'text';
25
+ text: string;
26
+ } | {
27
+ type: 'image';
28
+ data: string;
29
+ mimeType: string;
30
+ } | {
31
+ type: string;
32
+ [key: string]: unknown;
33
+ };
34
+ /** MCP `tools/call` result. */
35
+ interface McpToolResult {
36
+ content: McpToolContent[];
37
+ structuredContent?: Record<string, unknown>;
38
+ isError?: boolean;
39
+ }
40
+ /** MCP `tools/list` result. */
41
+ interface McpListToolsResult {
42
+ tools: McpToolDefinition[];
43
+ }
44
+ interface ToolCallContext {
45
+ /** Name the tool was called by (possibly namespaced). */
46
+ name: string;
47
+ /** Id of the provider that owns the tool. */
48
+ providerId: string;
49
+ signal?: AbortSignal;
50
+ }
51
+ /**
52
+ * Handler return values are normalized into an {@link McpToolResult}: a full
53
+ * result (`{ content: [...] }`) is passed through, a string becomes a text
54
+ * block, `undefined` becomes an empty result, and anything else is returned
55
+ * as `structuredContent` (plain objects) plus a JSON text block.
56
+ */
57
+ type AgentToolHandler = (args: Record<string, any>, context: ToolCallContext) => unknown | Promise<unknown>;
58
+ interface AgentTool extends McpToolDefinition {
59
+ handler: AgentToolHandler;
60
+ }
61
+ /**
62
+ * A source of tools, typically one per mounted editor, spreadsheet or canvas.
63
+ */
64
+ interface AgentToolProvider {
65
+ /** Unique id within a registry, e.g. `doc-1`. Used as the namespace on collisions. */
66
+ id: string;
67
+ /** Always prefix this provider's tools with `<namespace>__`. */
68
+ namespace?: string;
69
+ /** Tools, or a function returning the current tools (re-read on every `listTools`). */
70
+ tools: readonly AgentTool[] | (() => readonly AgentTool[]);
71
+ /** Lets the provider signal that its tool list changed; returns an unsubscribe function. */
72
+ subscribe?: (notify: () => void) => () => void;
73
+ }
74
+ type ToolErrorCode = 'unknown_tool' | 'invalid_arguments' | 'tool_failed';
75
+ /** Structured error carried in `structuredContent.error` of a failed call. */
76
+ interface ToolCallError {
77
+ code: ToolErrorCode;
78
+ message: string;
79
+ tool: string;
80
+ details?: SchemaValidationError[];
81
+ }
82
+ declare class ToolRegistryError extends Error {
83
+ readonly code: 'duplicate_provider' | 'name_collision' | 'invalid_tool';
84
+ constructor(code: ToolRegistryError['code'], message: string);
85
+ }
86
+ interface ToolRegistryOptions {
87
+ /**
88
+ * What to do when two providers contribute the same tool name:
89
+ * `namespace` (default) exposes every colliding tool as `<providerId>__<name>`;
90
+ * `error` makes `register` throw.
91
+ */
92
+ onCollision?: 'namespace' | 'error';
93
+ /** Coalescing window for `listChanged` notifications in ms (default 0: same tick). */
94
+ listChangedDelayMs?: number;
95
+ /** Validate arguments against `inputSchema` before calling handlers (default `true`). */
96
+ validateArguments?: boolean;
97
+ }
98
+ /** Separator between namespace and tool name; valid in MCP and LLM tool-name charsets. */
99
+ declare const TOOL_NAMESPACE_SEPARATOR = "__";
100
+ /** Normalizes a handler's return value into an MCP `tools/call` result. */
101
+ declare function toToolResult(value: unknown): McpToolResult;
102
+ /**
103
+ * Transport-agnostic registry of agent tools contributed by live providers.
104
+ *
105
+ * `listTools()` and `callTool()` return MCP `tools/list` / `tools/call`
106
+ * result shapes, and `onListChanged` fires (coalesced) whenever the exposed
107
+ * tool list changes, mirroring MCP's `notifications/tools/list_changed`, so an
108
+ * MCP server can be a thin wrapper around it.
109
+ */
110
+ declare class ToolRegistry {
111
+ /** MCP server capabilities this registry supports. */
112
+ readonly capabilities: {
113
+ readonly tools: {
114
+ readonly listChanged: true;
115
+ };
116
+ };
117
+ private readonly entries;
118
+ private readonly listeners;
119
+ private readonly options;
120
+ private timer;
121
+ private lastSignature;
122
+ constructor(options?: ToolRegistryOptions);
123
+ /**
124
+ * Registers a provider and returns a function that unregisters it.
125
+ * Throws a {@link ToolRegistryError} for a duplicate provider id, an invalid
126
+ * tool name, or (with `onCollision: 'error'`) a tool name collision.
127
+ */
128
+ register(provider: AgentToolProvider): () => void;
129
+ /** Removes a provider by id or instance. Returns whether it was registered. */
130
+ unregister(providerOrId: string | AgentToolProvider): boolean;
131
+ /** Registered provider ids in registration order. */
132
+ providerIds(): string[];
133
+ /**
134
+ * Resolves exposed names. Tools are ordered by provider registration, then
135
+ * by their order within the provider.
136
+ */
137
+ private resolve;
138
+ /** MCP `tools/list` result for the current providers. */
139
+ listTools(): McpListToolsResult;
140
+ /** Whether a tool with this exposed name exists. */
141
+ hasTool(name: string): boolean;
142
+ /** Looks up a tool definition by exposed name. */
143
+ getTool(name: string): McpToolDefinition | undefined;
144
+ /**
145
+ * Calls a tool by exposed name and returns an MCP `tools/call` result.
146
+ * Never rejects: unknown tools, invalid arguments and handler errors come
147
+ * back as `isError: true` results with a {@link ToolCallError} in
148
+ * `structuredContent.error` (MCP transports may map `unknown_tool` to a
149
+ * JSON-RPC `-32602` error instead).
150
+ */
151
+ callTool(name: string, args?: Record<string, unknown>, options?: {
152
+ signal?: AbortSignal;
153
+ }): Promise<McpToolResult>;
154
+ /**
155
+ * Subscribes to tool-list changes. Notifications are coalesced: many
156
+ * registrations or provider signals within the delay window produce one
157
+ * call, and none if the exposed list ends up unchanged (e.g. a provider
158
+ * unmounted and remounted). Returns an unsubscribe function.
159
+ */
160
+ onListChanged(listener: (tools: McpToolDefinition[]) => void): () => void;
161
+ /** Schedules a coalesced `listChanged` check; providers may call it directly. */
162
+ notifyListChanged(): void;
163
+ /** Runs a pending `listChanged` check immediately. */
164
+ flush(): void;
165
+ private signature;
166
+ /** Unregisters every provider and listener and cancels pending notifications. */
167
+ dispose(): void;
168
+ }
169
+ declare function createToolRegistry(options?: ToolRegistryOptions): ToolRegistry;
170
+
171
+ export { type AgentToolProvider as A, type McpListToolsResult as M, TOOL_NAMESPACE_SEPARATOR as T, type AgentTool as a, type AgentToolHandler as b, type McpToolContent as c, type McpToolDefinition as d, type McpToolResult as e, type ToolCallContext as f, type ToolCallError as g, type ToolErrorCode as h, ToolRegistry as i, ToolRegistryError as j, type ToolRegistryOptions as k, createToolRegistry as l, toToolResult as t };
@@ -0,0 +1,354 @@
1
+ import { J as JsonSchema, S as SchemaValidationError } from './schema-DYDGdYL9.cjs';
2
+
3
+ /**
4
+ * Keyboard chords in a platform-neutral notation.
5
+ *
6
+ * | token | macOS | Windows / Linux |
7
+ * |---|---|---|
8
+ * | `Mod` | ⌘ Command | Ctrl |
9
+ * | `Ctrl` | ⌃ Control | Ctrl (same key as `Mod`: conflicts are reported) |
10
+ * | `Meta` / `Cmd` | ⌘ Command | Win / Super |
11
+ * | `Alt` / `Option` | ⌥ Option | Alt |
12
+ * | `Shift` | ⇧ Shift | Shift |
13
+ *
14
+ * Keys are stored as `KeyboardEvent.code` values (`KeyZ`, `Digit1`, `BracketRight`,
15
+ * `PageDown`), so Option/Alt chords work on macOS (where Option changes `event.key`).
16
+ */
17
+ type Platform = 'mac' | 'windows' | 'linux';
18
+ declare const PLATFORMS: readonly Platform[];
19
+ /** A parsed chord. `mod` is the platform's primary modifier. */
20
+ interface Chord {
21
+ /** `KeyboardEvent.code` of the key, e.g. `KeyZ`. */
22
+ key: string;
23
+ mod?: boolean;
24
+ ctrl?: boolean;
25
+ meta?: boolean;
26
+ alt?: boolean;
27
+ shift?: boolean;
28
+ }
29
+ /** The modifiers a chord needs on one platform (`mod` resolved to `meta` or `ctrl`). */
30
+ interface EffectiveChord {
31
+ key: string;
32
+ ctrl: boolean;
33
+ meta: boolean;
34
+ alt: boolean;
35
+ shift: boolean;
36
+ }
37
+ /** The current platform from `navigator` (`linux` when unknown, e.g. in Node). */
38
+ declare function detectPlatform(): Platform;
39
+ /** `KeyboardEvent.code` for a key token (`Z`, `1`, `]`, `PageDown`, `F5`, `KeyZ`), or `null`. */
40
+ declare function keyToCode(token: string): string | null;
41
+ /** Neutral text for a code: `KeyZ` → `Z`, `BracketRight` → `]`, `ArrowUp` → `Up`. */
42
+ declare function codeToKeyText(code: string): string;
43
+ /**
44
+ * Parses chord text (`Mod+Shift+Z`, `Alt+]`, `⌥⇧⌘K`, `CmdOrCtrl+K`). Throws on unknown
45
+ * modifiers or keys. The last `+`-separated token is the key (`+` itself is `Plus`).
46
+ */
47
+ declare function parseChord(text: string): Chord;
48
+ /** Canonical platform-neutral text: `Mod+Ctrl+Meta+Alt+Shift+<key>`. */
49
+ declare function chordToString(chord: Chord): string;
50
+ /** Canonical spelling of chord text (`shift+cmdorctrl+z` → `Mod+Shift+Z`). Throws when invalid. */
51
+ declare function normalizeChord(text: string): string;
52
+ /** The modifiers a chord needs on `platform`: `Mod` is ⌘ (meta) on macOS and Ctrl elsewhere. */
53
+ declare function effectiveChord(chord: string | Chord, platform: Platform): EffectiveChord;
54
+ /** Identity of an effective chord, for conflict detection (`Ctrl+Shift+KeyZ`). */
55
+ declare function effectiveChordKey(chord: string | Chord, platform: Platform): string;
56
+ /**
57
+ * Display text: `⌥⇧⌘K` on macOS, `Ctrl+Alt+Shift+K` on Windows / Linux (`Meta` shows as
58
+ * `Win` / `Super`). Invalid text is returned unchanged.
59
+ */
60
+ declare function formatShortcut(chord: string | Chord, platform?: Platform): string;
61
+ /** Minimal keyboard event surface (DOM `KeyboardEvent` satisfies it). */
62
+ interface KeyboardEventLike {
63
+ key?: string;
64
+ code?: string;
65
+ ctrlKey?: boolean;
66
+ metaKey?: boolean;
67
+ altKey?: boolean;
68
+ shiftKey?: boolean;
69
+ target?: EventTarget | null | unknown;
70
+ defaultPrevented?: boolean;
71
+ preventDefault?: () => void;
72
+ }
73
+ /** `event.code`, or one derived from `event.key` for synthetic events without a code. */
74
+ declare function eventCode(event: KeyboardEventLike): string;
75
+ /**
76
+ * The key code a shortcut event is matched by: `event.code` (layout-independent, so
77
+ * Option/Alt chords work on macOS and punctuation chords follow the physical key),
78
+ * except that a letter key producing a different Latin letter without Alt (AZERTY,
79
+ * QWERTZ, Dvorak) uses the produced letter, as browsers and operating systems do.
80
+ */
81
+ declare function shortcutCode(event: KeyboardEventLike): string;
82
+ /** The code a printable `event.key` names on a US layout (`z` → `KeyZ`, `]` → `BracketRight`). */
83
+ declare function codeFromKey(key: string): string | null;
84
+ /**
85
+ * Whether a keyboard event comes from a text-entry element: `<input>`, `<textarea>`,
86
+ * `<select>`, a `contenteditable` element (or a descendant) or a ProseMirror editor.
87
+ * Falls back to `document.activeElement` when the event has no element target.
88
+ */
89
+ declare function isEditableEventTarget(event: {
90
+ target?: unknown;
91
+ }): boolean;
92
+
93
+ /** One key binding: a chord that runs `command`, optionally only in one scope / on some platforms. */
94
+ interface ShortcutBinding {
95
+ command: string;
96
+ /** Chord text (`Mod+Shift+Z`) or a parsed chord. */
97
+ chord: string | Chord;
98
+ /** Scope (e.g. `canvas`, `spreadsheet`); scoped bindings win over global ones. Omit for global. */
99
+ scope?: string;
100
+ /** Platforms the binding exists on (default: all). E.g. `Mod+Y` redo only off macOS. */
101
+ platforms?: readonly Platform[];
102
+ /** Also fire while focus is in an input, textarea or contenteditable (default `false`). */
103
+ allowInEditable?: boolean;
104
+ }
105
+ /** Shortcut declaration on a command: chord text, or an object for platform / scope specifics. */
106
+ type ShortcutInput = string | Omit<ShortcutBinding, 'command'> | ReadonlyArray<string | Omit<ShortcutBinding, 'command'>>;
107
+ /** Expands a command's `shortcut` declaration into bindings. */
108
+ declare function toShortcutBindings(command: string, input: ShortcutInput | undefined, scope?: string): ShortcutBinding[];
109
+ interface ShortcutConflict {
110
+ /** Scope shared by the bindings (`undefined` = global). */
111
+ scope?: string;
112
+ /** Effective chord, e.g. `Ctrl+KeyZ`. */
113
+ chord: string;
114
+ /** Commands bound to it (two or more). */
115
+ commands: string[];
116
+ }
117
+ interface ShortcutMatch {
118
+ command: string;
119
+ binding: ShortcutBinding;
120
+ }
121
+ interface ShortcutResolveOptions {
122
+ /** Active scopes, most specific first. Bindings in these scopes win over global ones. */
123
+ scopes?: string | readonly string[];
124
+ /** Resolve even when the event comes from a text input (default: per binding). */
125
+ allowInEditable?: boolean;
126
+ /** Override the map's platform. */
127
+ platform?: Platform;
128
+ }
129
+ interface ShortcutMapOptions {
130
+ /** Platform used to resolve `Mod` (default: detected from `navigator` on every resolve). */
131
+ platform?: Platform;
132
+ /**
133
+ * `platform` (default): `Mod` means ⌘ on macOS and Ctrl elsewhere, exactly.
134
+ * `either`: `Mod` (and `Ctrl` / `Meta`) match ⌘ or Ctrl on every platform.
135
+ */
136
+ modifierMode?: 'platform' | 'either';
137
+ }
138
+ /**
139
+ * Keyboard shortcuts as data: default bindings plus user overrides, resolved from
140
+ * `KeyboardEvent`s by `event.code` (so Option/Alt chords work on macOS), with scopes
141
+ * (scoped bindings win), conflict detection per platform and display formatting.
142
+ */
143
+ declare class ShortcutMap {
144
+ private defaults;
145
+ private overrides;
146
+ private readonly listeners;
147
+ private readonly options;
148
+ constructor(bindings?: readonly ShortcutBinding[], options?: ShortcutMapOptions);
149
+ get platform(): Platform;
150
+ private entry;
151
+ private changed;
152
+ /** Subscribes to binding changes. Returns an unsubscribe function. */
153
+ subscribe(listener: () => void): () => void;
154
+ /** Adds default bindings (throws on invalid chord text). Returns a function removing them. */
155
+ bind(bindings: ShortcutBinding | readonly ShortcutBinding[]): () => void;
156
+ /**
157
+ * User override for a command: replaces all of its default bindings with `chords`
158
+ * (an empty list unbinds it). Throws on invalid chord text.
159
+ */
160
+ setOverride(command: string, chords: string | Chord | ReadonlyArray<string | Chord | Omit<ShortcutBinding, 'command'>>): void;
161
+ /** Removes a user override (the defaults apply again). */
162
+ clearOverride(command: string): void;
163
+ /** User overrides as plain data (`{ command: ['Mod+K'] }`), e.g. to persist them. */
164
+ getOverrides(): Record<string, string[]>;
165
+ /** Restores overrides saved with `getOverrides` (replacing the current ones). */
166
+ setOverrides(overrides: Record<string, readonly string[]>): void;
167
+ private active;
168
+ /** Effective bindings on `platform` (defaults without overridden commands, plus overrides). */
169
+ bindings(platform?: Platform): ShortcutBinding[];
170
+ /** Canonical chord text bound to `command` on `platform` (first = primary). */
171
+ forCommand(command: string, platform?: Platform): string[];
172
+ /**
173
+ * The command a keydown event runs, or `null`. Events from text inputs are ignored
174
+ * unless the binding (or `options`) allows them. Bindings in the active scopes
175
+ * (in order) win over global bindings; bindings in other scopes never match.
176
+ * Keys are matched by `event.code`, except that a letter key producing another Latin
177
+ * letter (AZERTY, QWERTZ, Dvorak) matches that letter when Alt is not held, so
178
+ * `Mod+Z` follows the Z label (see `shortcutCode`).
179
+ */
180
+ resolve(event: KeyboardEventLike, options?: ShortcutResolveOptions): ShortcutMatch | null;
181
+ private matches;
182
+ /**
183
+ * Groups of bindings that share an effective chord in one scope on `platform`
184
+ * (e.g. `Mod+Y` and `Ctrl+Y` collide off macOS). A scoped binding shadowing a
185
+ * global one is not a conflict.
186
+ */
187
+ findConflicts(platform?: Platform): ShortcutConflict[];
188
+ /** Conflicts on every platform, keyed by platform (only platforms with conflicts). */
189
+ findAllConflicts(): Partial<Record<Platform, ShortcutConflict[]>>;
190
+ }
191
+
192
+ /** Object JSON Schema describing a command's parameters. */
193
+ type CommandParamsSchema = JsonSchema & {
194
+ type: 'object';
195
+ };
196
+ /**
197
+ * Metadata and implementation of one command. Menus, shortcuts, the palette and agent
198
+ * tools all run commands by id, so every entry point shares enablement and validation.
199
+ */
200
+ interface CommandSpec<TContext = any, TParams = any, TResult = unknown> {
201
+ /** Unique id, e.g. `history.undo` or `canvas.alignLeft`. */
202
+ id: string;
203
+ label: string;
204
+ /** Group / menu name shown next to the label, e.g. `Edit` or `Arrange`. */
205
+ group?: string;
206
+ /** Extra search terms for the palette. */
207
+ keywords?: readonly string[];
208
+ description?: string;
209
+ /** Object JSON Schema for `params`. Unknown keys are rejected unless `additionalProperties` allows them. */
210
+ params?: CommandParamsSchema;
211
+ /** Default shortcut(s): platform-neutral chord text such as `Mod+Shift+Z`. */
212
+ shortcut?: ShortcutInput;
213
+ /** Scope the command (and its shortcuts) belong to, e.g. `canvas`. Omit for global commands. */
214
+ scope?: string;
215
+ /**
216
+ * Whether the command can run: `true` (or `undefined`), `false`, or a reason string
217
+ * ("Nothing to undo"). Receives the params a caller named, so a command acting on the
218
+ * selection can still run on targets passed explicitly (`params` is `undefined` for menus).
219
+ */
220
+ enabled?: (context: TContext, params: TParams | undefined) => boolean | string | undefined;
221
+ run: (context: TContext, params: TParams) => TResult;
222
+ /**
223
+ * Runs inside the registry's `history.batch(label, …)`, so all history entries the
224
+ * command records become one undo step and a throw rolls back its partial changes.
225
+ */
226
+ undoable?: boolean;
227
+ /** The command only reads state (agent tools mark it read-only). */
228
+ readOnly?: boolean;
229
+ /** Hide from the palette and `list()` unless `includeHidden` (still runnable by id). */
230
+ hidden?: boolean;
231
+ }
232
+ type CommandErrorCode = 'unknown_command' | 'disabled' | 'invalid_params' | 'failed';
233
+ type CommandResult<T = unknown> = {
234
+ ok: true;
235
+ id: string;
236
+ result: T;
237
+ } | {
238
+ ok: false;
239
+ id: string;
240
+ code: CommandErrorCode;
241
+ message: string;
242
+ details?: SchemaValidationError[];
243
+ };
244
+ /** A command as listed for menus, palettes and agents. */
245
+ interface CommandEntry {
246
+ id: string;
247
+ label: string;
248
+ group?: string;
249
+ keywords?: readonly string[];
250
+ description?: string;
251
+ params?: CommandParamsSchema;
252
+ scope?: string;
253
+ /** Canonical chord text bound on the registry's platform (first = primary). */
254
+ shortcuts: string[];
255
+ enabled: boolean;
256
+ /** Why the command is disabled (only when `enabled` is false). */
257
+ disabledReason?: string;
258
+ undoable: boolean;
259
+ readOnly: boolean;
260
+ }
261
+ interface CommandListOptions<TContext = any> {
262
+ /** Only global commands plus commands of these scopes. */
263
+ scope?: string | readonly string[];
264
+ enabledOnly?: boolean;
265
+ includeHidden?: boolean;
266
+ /** Context for the enablement check (default: the registry's context). */
267
+ context?: TContext;
268
+ }
269
+ /** Minimal history surface (`HistoryManager` satisfies it). */
270
+ interface CommandHistoryTarget {
271
+ batch(name: string, fn: () => void): void;
272
+ }
273
+ interface CommandRegistryOptions<TContext = any> {
274
+ /** Default context passed to `enabled` / `run` (a value or a getter). */
275
+ context?: TContext | (() => TContext);
276
+ /** Wraps `undoable` commands in one history step (see `CommandSpec.undoable`). */
277
+ history?: CommandHistoryTarget;
278
+ /** Options for the registry's `ShortcutMap`. */
279
+ shortcuts?: ShortcutMapOptions;
280
+ }
281
+ declare class CommandRegistryError extends Error {
282
+ readonly code: 'duplicate_command' | 'invalid_command';
283
+ constructor(code: CommandRegistryError['code'], message: string);
284
+ }
285
+ /**
286
+ * Registry of commands: one id per action, shared by shortcuts (`shortcuts`), the
287
+ * command palette (`list` + `rankCommands`) and agent tools (`createCommandToolProvider`).
288
+ */
289
+ declare class CommandRegistry<TContext = any> {
290
+ /** Shortcut bindings of the registered commands (plus any user overrides). */
291
+ readonly shortcuts: ShortcutMap;
292
+ private readonly commands;
293
+ private readonly listeners;
294
+ private readonly options;
295
+ private version;
296
+ private muted;
297
+ constructor(options?: CommandRegistryOptions<TContext>);
298
+ /** Monotonic counter bumped on every change (useful as a snapshot key). */
299
+ getVersion(): number;
300
+ /** The default context. */
301
+ get context(): TContext;
302
+ /**
303
+ * Registers a command (or several) and returns a function that unregisters it.
304
+ * Throws a `CommandRegistryError` for a duplicate or invalid id, or invalid chord text.
305
+ */
306
+ register(spec: CommandSpec<TContext> | readonly CommandSpec<TContext>[]): () => void;
307
+ /** Removes a command by id. Returns whether it was registered. */
308
+ unregister(id: string): boolean;
309
+ private unbindQuietly;
310
+ has(id: string): boolean;
311
+ get(id: string): CommandSpec<TContext> | undefined;
312
+ /** Registered ids in registration order. */
313
+ ids(): string[];
314
+ /** Whether `id` can run now (`{ enabled, reason }`); `params` are the targets a caller named. */
315
+ isEnabled(id: string, params?: unknown, context?: TContext): {
316
+ enabled: boolean;
317
+ reason?: string;
318
+ };
319
+ /** Commands for menus, palettes and agents, with their current enablement. */
320
+ list(options?: CommandListOptions<TContext>): CommandEntry[];
321
+ private check;
322
+ private invoke;
323
+ /**
324
+ * Runs a command synchronously and returns a structured result; never throws.
325
+ * A command whose `run` returns a promise yields `{ ok: true, result: promise }`
326
+ * (use `executeAsync` to await it and catch its rejection).
327
+ */
328
+ execute<T = unknown>(id: string, params?: unknown, context?: TContext): CommandResult<T>;
329
+ /** Like `execute`, but awaits async commands (rejections become `failed` results). */
330
+ executeAsync<T = unknown>(id: string, params?: unknown, context?: TContext): Promise<CommandResult<T>>;
331
+ /**
332
+ * Resolves a keydown event to a command (see `ShortcutMap.resolve`) and runs it when
333
+ * enabled, calling `preventDefault()`. Disabled commands leave the event alone, so the
334
+ * browser or a focused editor can still handle it. Returns the result, or `null`.
335
+ */
336
+ handleKeyDown(event: KeyboardEventLike, options?: ShortcutResolveOptions & {
337
+ context?: TContext;
338
+ }): CommandResult | null;
339
+ /** Display-ready primary shortcut of a command (`⇧⌘Z` / `Ctrl+Shift+Z`), or `undefined`. */
340
+ shortcutFor(id: string, platform?: Platform): string | undefined;
341
+ /**
342
+ * Subscribes to changes: commands registered or removed, shortcut overrides, executed
343
+ * commands and `invalidate()` calls (e.g. when enablement inputs change).
344
+ */
345
+ subscribe(listener: () => void): () => void;
346
+ /** Tells subscribers that enablement may have changed (e.g. the selection changed). */
347
+ invalidate(): void;
348
+ private notify;
349
+ }
350
+ declare function createCommandRegistry<TContext = any>(options?: CommandRegistryOptions<TContext>): CommandRegistry<TContext>;
351
+ /** Identity helper that types a command spec. */
352
+ declare function defineCommand<TContext = any, TParams = any, TResult = unknown>(spec: CommandSpec<TContext, TParams, TResult>): CommandSpec<TContext, TParams, TResult>;
353
+
354
+ export { formatShortcut as A, isEditableEventTarget as B, CommandRegistry as C, keyToCode as D, type EffectiveChord as E, normalizeChord as F, parseChord as G, shortcutCode as H, toShortcutBindings as I, type KeyboardEventLike as K, type Platform as P, type ShortcutBinding as S, type CommandResult as a, type CommandEntry as b, type CommandSpec as c, type Chord as d, type CommandErrorCode as e, type CommandHistoryTarget as f, type CommandListOptions as g, type CommandParamsSchema as h, CommandRegistryError as i, type CommandRegistryOptions as j, PLATFORMS as k, type ShortcutConflict as l, type ShortcutInput as m, ShortcutMap as n, type ShortcutMapOptions as o, type ShortcutMatch as p, type ShortcutResolveOptions as q, chordToString as r, codeFromKey as s, codeToKeyText as t, createCommandRegistry as u, defineCommand as v, detectPlatform as w, effectiveChord as x, effectiveChordKey as y, eventCode as z };