@immediately-run/sdk 0.43.1 → 0.45.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 (157) hide show
  1. package/dist/ambient.d.ts +47 -0
  2. package/dist/auth.cjs +3 -2
  3. package/dist/auth.cjs.map +1 -1
  4. package/dist/auth.js +3 -2
  5. package/dist/auth.js.map +1 -1
  6. package/dist/boot.cjs +3 -2
  7. package/dist/boot.cjs.map +1 -1
  8. package/dist/boot.js +3 -2
  9. package/dist/boot.js.map +1 -1
  10. package/dist/catalog.cjs +3 -2
  11. package/dist/catalog.cjs.map +1 -1
  12. package/dist/catalog.js +3 -2
  13. package/dist/catalog.js.map +1 -1
  14. package/dist/components/MDXComponents.cjs +14 -1
  15. package/dist/components/MDXComponents.cjs.map +1 -1
  16. package/dist/components/MDXComponents.js +14 -1
  17. package/dist/components/MDXComponents.js.map +1 -1
  18. package/dist/components/WikiLink.cjs +19 -17
  19. package/dist/components/WikiLink.cjs.map +1 -1
  20. package/dist/components/WikiLink.js +19 -17
  21. package/dist/components/WikiLink.js.map +1 -1
  22. package/dist/contribute.cjs +2 -1
  23. package/dist/contribute.cjs.map +1 -1
  24. package/dist/contribute.js +2 -1
  25. package/dist/contribute.js.map +1 -1
  26. package/dist/debug.cjs +6 -5
  27. package/dist/debug.cjs.map +1 -1
  28. package/dist/debug.js +12 -5
  29. package/dist/debug.js.map +1 -1
  30. package/dist/diagnostics.cjs +3 -2
  31. package/dist/diagnostics.cjs.map +1 -1
  32. package/dist/diagnostics.js +3 -2
  33. package/dist/diagnostics.js.map +1 -1
  34. package/dist/dnd.cjs +5 -3
  35. package/dist/dnd.cjs.map +1 -1
  36. package/dist/dnd.js +5 -3
  37. package/dist/dnd.js.map +1 -1
  38. package/dist/editor.cjs +3 -1
  39. package/dist/editor.cjs.map +1 -1
  40. package/dist/editor.js +3 -1
  41. package/dist/editor.js.map +1 -1
  42. package/dist/editorContext.cjs +3 -2
  43. package/dist/editorContext.cjs.map +1 -1
  44. package/dist/editorContext.js +3 -2
  45. package/dist/editorContext.js.map +1 -1
  46. package/dist/formFactor.cjs +3 -2
  47. package/dist/formFactor.cjs.map +1 -1
  48. package/dist/formFactor.js +3 -2
  49. package/dist/formFactor.js.map +1 -1
  50. package/dist/generated/protocol.cjs +23 -0
  51. package/dist/generated/protocol.cjs.map +1 -0
  52. package/dist/generated/protocol.d.cts +1 -0
  53. package/dist/generated/protocol.d.ts +1 -0
  54. package/dist/generated/protocol.js +2 -0
  55. package/dist/generated/protocol.js.map +1 -0
  56. package/dist/hooks.cjs +22 -14
  57. package/dist/hooks.cjs.map +1 -1
  58. package/dist/hooks.d.cts +26 -4
  59. package/dist/hooks.d.ts +26 -4
  60. package/dist/hooks.js +23 -15
  61. package/dist/hooks.js.map +1 -1
  62. package/dist/index.cjs +4 -0
  63. package/dist/index.cjs.map +1 -1
  64. package/dist/index.d.cts +3 -1
  65. package/dist/index.d.ts +3 -1
  66. package/dist/index.js +2 -0
  67. package/dist/index.js.map +1 -1
  68. package/dist/ipc.cjs +5 -3
  69. package/dist/ipc.cjs.map +1 -1
  70. package/dist/ipc.js +5 -3
  71. package/dist/ipc.js.map +1 -1
  72. package/dist/launch.cjs +5 -3
  73. package/dist/launch.cjs.map +1 -1
  74. package/dist/launch.js +5 -3
  75. package/dist/launch.js.map +1 -1
  76. package/dist/linkSpace.cjs +63 -0
  77. package/dist/linkSpace.cjs.map +1 -0
  78. package/dist/linkSpace.d.cts +44 -0
  79. package/dist/linkSpace.d.ts +44 -0
  80. package/dist/linkSpace.js +37 -0
  81. package/dist/linkSpace.js.map +1 -0
  82. package/dist/llm.cjs +3 -2
  83. package/dist/llm.cjs.map +1 -1
  84. package/dist/llm.js +3 -2
  85. package/dist/llm.js.map +1 -1
  86. package/dist/metadataSource.cjs +53 -0
  87. package/dist/metadataSource.cjs.map +1 -0
  88. package/dist/metadataSource.d.cts +51 -0
  89. package/dist/metadataSource.d.ts +51 -0
  90. package/dist/metadataSource.js +29 -0
  91. package/dist/metadataSource.js.map +1 -0
  92. package/dist/moduleCache.cjs +2 -1
  93. package/dist/moduleCache.cjs.map +1 -1
  94. package/dist/moduleCache.js +2 -1
  95. package/dist/moduleCache.js.map +1 -1
  96. package/dist/mounts.cjs +13 -10
  97. package/dist/mounts.cjs.map +1 -1
  98. package/dist/mounts.js +23 -10
  99. package/dist/mounts.js.map +1 -1
  100. package/dist/netFetch.cjs +4 -2
  101. package/dist/netFetch.cjs.map +1 -1
  102. package/dist/netFetch.js +4 -2
  103. package/dist/netFetch.js.map +1 -1
  104. package/dist/onFsChange.cjs +2 -1
  105. package/dist/onFsChange.cjs.map +1 -1
  106. package/dist/onFsChange.js +2 -1
  107. package/dist/onFsChange.js.map +1 -1
  108. package/dist/protocolSchemes.cjs +46 -0
  109. package/dist/protocolSchemes.cjs.map +1 -0
  110. package/dist/protocolSchemes.d.cts +18 -0
  111. package/dist/protocolSchemes.d.ts +18 -0
  112. package/dist/protocolSchemes.js +37 -0
  113. package/dist/protocolSchemes.js.map +1 -0
  114. package/dist/routing.cjs +2 -1
  115. package/dist/routing.cjs.map +1 -1
  116. package/dist/routing.js +2 -1
  117. package/dist/routing.js.map +1 -1
  118. package/dist/runtime.cjs +4 -2
  119. package/dist/runtime.cjs.map +1 -1
  120. package/dist/runtime.js +4 -2
  121. package/dist/runtime.js.map +1 -1
  122. package/dist/safeContent/renderMdast.cjs +2 -0
  123. package/dist/safeContent/renderMdast.cjs.map +1 -1
  124. package/dist/safeContent/renderMdast.js +2 -0
  125. package/dist/safeContent/renderMdast.js.map +1 -1
  126. package/dist/sandboxTypes.cjs.map +1 -1
  127. package/dist/sandboxTypes.d.cts +40 -6
  128. package/dist/sandboxTypes.d.ts +40 -6
  129. package/dist/secrets.cjs +5 -3
  130. package/dist/secrets.cjs.map +1 -1
  131. package/dist/secrets.js +5 -3
  132. package/dist/secrets.js.map +1 -1
  133. package/dist/tasks.cjs +6 -4
  134. package/dist/tasks.cjs.map +1 -1
  135. package/dist/tasks.js +6 -4
  136. package/dist/tasks.js.map +1 -1
  137. package/dist/theme.cjs +5 -3
  138. package/dist/theme.cjs.map +1 -1
  139. package/dist/theme.js +5 -3
  140. package/dist/theme.js.map +1 -1
  141. package/dist/urlUtils.cjs +3 -4
  142. package/dist/urlUtils.cjs.map +1 -1
  143. package/dist/urlUtils.d.cts +3 -10
  144. package/dist/urlUtils.d.ts +3 -10
  145. package/dist/urlUtils.js +1 -2
  146. package/dist/urlUtils.js.map +1 -1
  147. package/dist/vcs.cjs +5 -3
  148. package/dist/vcs.cjs.map +1 -1
  149. package/dist/vcs.js +5 -3
  150. package/dist/vcs.js.map +1 -1
  151. package/dist/version.cjs +1 -1
  152. package/dist/version.cjs.map +1 -1
  153. package/dist/version.d.cts +1 -1
  154. package/dist/version.d.ts +1 -1
  155. package/dist/version.js +1 -1
  156. package/dist/version.js.map +1 -1
  157. package/package.json +11 -4
package/dist/dnd.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/dnd.ts"],"sourcesContent":["// Cross-app drag-out (FILE_EXPLORER_SPEC §7; UI_AS_APPS_SPEC §2 host-mediated).\n//\n// Native HTML5 drag-and-drop does NOT cross the sandboxed cross-origin iframe\n// boundary, and pointer events inside one app's iframe never reach the host or a\n// sibling. So a drag that STARTS in one app (the file explorer) and ENDS over\n// another (the previewed app) can only be mediated by the host (the TCB). This\n// module is the SDK surface for that one net-new platform primitive:\n//\n// - SOURCE side (the file explorer, needs the first-party `dnd:source` cap):\n// `startItemDrag(item)` asks the host to begin a host-mediated drag carrying a\n// file/dir reference (+ optional inlined bytes for a small file). The host draws\n// the trusted drag ghost, tracks the pointer across regions, and on drop over the\n// preview delivers the item to that app. `cancelItemDrag()` aborts.\n// - RECEIVER side (the previewed app, NO new grant — it opts in by subscribing):\n// `onItemDrop(cb)` / `useDroppedItem()` deliver the dropped item with host-attached,\n// unspoofable provenance (`from` = source region id) and the drop position.\n//\n// The payload is UNTRUSTED app data (CLAUDE.md §5): the host attaches `from`; the app\n// validates everything else. v1 inlines bytes only for small files (the source can\n// only relay data it can already read — no new read authority is minted).\nimport { useEffect, useState } from 'react';\nimport { protocolRequest, sendMessage, addListener } from './sandboxUtils';\n\n/** A file/dir being dragged out of an app. `bytes` is present only for a small file\n * the source chose to inline (transferred zero-copy); a dir or an over-cap file\n * carries the reference only (`kind`/`name`/`mountId`/`relPath`). */\nexport interface DraggableItem {\n kind: 'file' | 'dir';\n /** Basename — display only. */\n name: string;\n /** Which mounted filesystem the item lives in. */\n mountId: string;\n /** Path within that mount (leading slash, no `..`). */\n relPath: string;\n /** Optional inlined content for a small file. */\n bytes?: Uint8Array;\n}\n\n/** An item dropped onto THIS app by a host-mediated cross-app drag. */\nexport interface DroppedItem {\n /** The dragged item (`bytes` present iff the source inlined them). */\n item: DraggableItem;\n /** Host-attached source region id — unspoofable (T19), like an `ipc` `from`. */\n from: string;\n /** Drop point in this app's viewport. */\n position: { x: number; y: number };\n}\n\n/** An error from {@link startItemDrag}, carrying a machine-readable `.code`. */\nexport interface ItemDragError extends Error {\n code:\n | 'forbidden' // the frame lacks the first-party `dnd:source` capability\n | 'invalid-params' // the item was malformed (empty path / `..` / URI / bad kind)\n | 'too-large' // inlined `bytes` exceed the host's size limit\n | 'rate-limited' // too many drags started too fast (capacity-class, fail-open)\n | 'unknown';\n}\n\n/**\n * Begin a host-mediated drag of `item` out of this app. Resolves once the host has\n * taken over the drag (drawn the ghost, installed the pointer-capture layer); rejects\n * with an {@link ItemDragError} if this app may not initiate drags (`forbidden`) or the\n * item is invalid. Only a first-party chrome app holding `dnd:source` may call this — a\n * previewed/third-party app is refused at the gate (it must not synthesize drags into\n * sibling apps).\n */\nexport const startItemDrag = async (item: DraggableItem): Promise<void> => {\n const res = (await protocolRequest('dnd', 'startDrag', [item])) as\n | { ok: true }\n | { ok: false; code?: string; message?: string }\n | undefined;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? 'dnd startDrag failed') as ItemDragError;\n err.code = (res?.code as ItemDragError['code']) ?? 'unknown';\n throw err;\n }\n};\n\n/** Abort an in-progress host-mediated drag this app started (e.g. the user pressed\n * Escape, or the gesture was cancelled). Best-effort and fire-and-forget. */\nexport const cancelItemDrag = (): void => {\n sendMessage('dnd-cancel', {});\n};\n\n/** Subscribe to items dropped onto this app by a host-mediated cross-app drag.\n * Returns an unsubscribe fn. Subscribing is the opt-in: an app that never subscribes\n * receives nothing (the host shows a \"not accepted\" cue and the drop is a no-op). */\nexport const onItemDrop = (listener: (d: DroppedItem) => void): (() => void) =>\n addListener('dropped-item', (m: { item: DraggableItem; from: string; position: { x: number; y: number } }) =>\n listener({ item: m.item, from: m.from, position: m.position }),\n );\n\n/** React hook: the most recently dropped item (or `null`). */\nexport const useDroppedItem = (): DroppedItem | null => {\n const [dropped, setDropped] = useState<DroppedItem | null>(null);\n useEffect(() => onItemDrop(setDropped), []);\n return dropped;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAoBA,mBAAoC;AACpC,0BAA0D;AA6CnD,MAAM,gBAAgB,OAAO,SAAuC;AACzE,QAAM,MAAO,UAAM,qCAAgB,OAAO,aAAa,CAAC,IAAI,CAAC;AAI7D,MAAI,CAAC,OAAO,IAAI,OAAO,MAAM;AAC3B,UAAM,MAAM,IAAI,MAAM,KAAK,WAAW,sBAAsB;AAC5D,QAAI,OAAQ,KAAK,QAAkC;AACnD,UAAM;AAAA,EACR;AACF;AAIO,MAAM,iBAAiB,MAAY;AACxC,uCAAY,cAAc,CAAC,CAAC;AAC9B;AAKO,MAAM,aAAa,CAAC,iBACzB;AAAA,EAAY;AAAA,EAAgB,CAAC,MAC3B,SAAS,EAAE,MAAM,EAAE,MAAM,MAAM,EAAE,MAAM,UAAU,EAAE,SAAS,CAAC;AAC/D;AAGK,MAAM,iBAAiB,MAA0B;AACtD,QAAM,CAAC,SAAS,UAAU,QAAI,uBAA6B,IAAI;AAC/D,8BAAU,MAAM,WAAW,UAAU,GAAG,CAAC,CAAC;AAC1C,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../src/dnd.ts"],"sourcesContent":["// Cross-app drag-out (FILE_EXPLORER_SPEC §7; UI_AS_APPS_SPEC §2 host-mediated).\n//\n// Native HTML5 drag-and-drop does NOT cross the sandboxed cross-origin iframe\n// boundary, and pointer events inside one app's iframe never reach the host or a\n// sibling. So a drag that STARTS in one app (the file explorer) and ENDS over\n// another (the previewed app) can only be mediated by the host (the TCB). This\n// module is the SDK surface for that one net-new platform primitive:\n//\n// - SOURCE side (the file explorer, needs the first-party `dnd:source` cap):\n// `startItemDrag(item)` asks the host to begin a host-mediated drag carrying a\n// file/dir reference (+ optional inlined bytes for a small file). The host draws\n// the trusted drag ghost, tracks the pointer across regions, and on drop over the\n// preview delivers the item to that app. `cancelItemDrag()` aborts.\n// - RECEIVER side (the previewed app, NO new grant — it opts in by subscribing):\n// `onItemDrop(cb)` / `useDroppedItem()` deliver the dropped item with host-attached,\n// unspoofable provenance (`from` = source region id) and the drop position.\n//\n// The payload is UNTRUSTED app data (CLAUDE.md §5): the host attaches `from`; the app\n// validates everything else. v1 inlines bytes only for small files (the source can\n// only relay data it can already read — no new read authority is minted).\nimport { useEffect, useState } from 'react';\nimport { protocolRequest, sendMessage, addListener } from './sandboxUtils';\nimport { DND_CANCEL, DROPPED_ITEM, PROTOCOL_DND } from './generated/protocol';\nimport { SCHEMES } from './protocolSchemes';\n\n/** A file/dir being dragged out of an app. `bytes` is present only for a small file\n * the source chose to inline (transferred zero-copy); a dir or an over-cap file\n * carries the reference only (`kind`/`name`/`mountId`/`relPath`). */\nexport interface DraggableItem {\n kind: 'file' | 'dir';\n /** Basename — display only. */\n name: string;\n /** Which mounted filesystem the item lives in. */\n mountId: string;\n /** Path within that mount (leading slash, no `..`). */\n relPath: string;\n /** Optional inlined content for a small file. */\n bytes?: Uint8Array;\n}\n\n/** An item dropped onto THIS app by a host-mediated cross-app drag. */\nexport interface DroppedItem {\n /** The dragged item (`bytes` present iff the source inlined them). */\n item: DraggableItem;\n /** Host-attached source region id — unspoofable (T19), like an `ipc` `from`. */\n from: string;\n /** Drop point in this app's viewport. */\n position: { x: number; y: number };\n}\n\n/** An error from {@link startItemDrag}, carrying a machine-readable `.code`. */\nexport interface ItemDragError extends Error {\n code:\n | 'forbidden' // the frame lacks the first-party `dnd:source` capability\n | 'invalid-params' // the item was malformed (empty path / `..` / URI / bad kind)\n | 'too-large' // inlined `bytes` exceed the host's size limit\n | 'rate-limited' // too many drags started too fast (capacity-class, fail-open)\n | 'unknown';\n}\n\n/**\n * Begin a host-mediated drag of `item` out of this app. Resolves once the host has\n * taken over the drag (drawn the ghost, installed the pointer-capture layer); rejects\n * with an {@link ItemDragError} if this app may not initiate drags (`forbidden`) or the\n * item is invalid. Only a first-party chrome app holding `dnd:source` may call this — a\n * previewed/third-party app is refused at the gate (it must not synthesize drags into\n * sibling apps).\n */\nexport const startItemDrag = async (item: DraggableItem): Promise<void> => {\n const res = (await protocolRequest(SCHEMES[PROTOCOL_DND], 'startDrag', [item])) as\n | { ok: true }\n | { ok: false; code?: string; message?: string }\n | undefined;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? 'dnd startDrag failed') as ItemDragError;\n err.code = (res?.code as ItemDragError['code']) ?? 'unknown';\n throw err;\n }\n};\n\n/** Abort an in-progress host-mediated drag this app started (e.g. the user pressed\n * Escape, or the gesture was cancelled). Best-effort and fire-and-forget. */\nexport const cancelItemDrag = (): void => {\n sendMessage(DND_CANCEL, {});\n};\n\n/** Subscribe to items dropped onto this app by a host-mediated cross-app drag.\n * Returns an unsubscribe fn. Subscribing is the opt-in: an app that never subscribes\n * receives nothing (the host shows a \"not accepted\" cue and the drop is a no-op). */\nexport const onItemDrop = (listener: (d: DroppedItem) => void): (() => void) =>\n addListener(DROPPED_ITEM, (m: { item: DraggableItem; from: string; position: { x: number; y: number } }) =>\n listener({ item: m.item, from: m.from, position: m.position }),\n );\n\n/** React hook: the most recently dropped item (or `null`). */\nexport const useDroppedItem = (): DroppedItem | null => {\n const [dropped, setDropped] = useState<DroppedItem | null>(null);\n useEffect(() => onItemDrop(setDropped), []);\n return dropped;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAoBA,mBAAoC;AACpC,0BAA0D;AAC1D,sBAAuD;AACvD,6BAAwB;AA6CjB,MAAM,gBAAgB,OAAO,SAAuC;AACzE,QAAM,MAAO,UAAM,qCAAgB,+BAAQ,4BAAY,GAAG,aAAa,CAAC,IAAI,CAAC;AAI7E,MAAI,CAAC,OAAO,IAAI,OAAO,MAAM;AAC3B,UAAM,MAAM,IAAI,MAAM,KAAK,WAAW,sBAAsB;AAC5D,QAAI,OAAQ,KAAK,QAAkC;AACnD,UAAM;AAAA,EACR;AACF;AAIO,MAAM,iBAAiB,MAAY;AACxC,uCAAY,4BAAY,CAAC,CAAC;AAC5B;AAKO,MAAM,aAAa,CAAC,iBACzB;AAAA,EAAY;AAAA,EAAc,CAAC,MACzB,SAAS,EAAE,MAAM,EAAE,MAAM,MAAM,EAAE,MAAM,UAAU,EAAE,SAAS,CAAC;AAC/D;AAGK,MAAM,iBAAiB,MAA0B;AACtD,QAAM,CAAC,SAAS,UAAU,QAAI,uBAA6B,IAAI;AAC/D,8BAAU,MAAM,WAAW,UAAU,GAAG,CAAC,CAAC;AAC1C,SAAO;AACT;","names":[]}
package/dist/dnd.js CHANGED
@@ -1,8 +1,10 @@
1
1
  import "./chunk-VHAA22YE.js";
2
2
  import { useEffect, useState } from "react";
3
3
  import { protocolRequest, sendMessage, addListener } from "./sandboxUtils";
4
+ import { DND_CANCEL, DROPPED_ITEM, PROTOCOL_DND } from "./generated/protocol";
5
+ import { SCHEMES } from "./protocolSchemes";
4
6
  const startItemDrag = async (item) => {
5
- const res = await protocolRequest("dnd", "startDrag", [item]);
7
+ const res = await protocolRequest(SCHEMES[PROTOCOL_DND], "startDrag", [item]);
6
8
  if (!res || res.ok !== true) {
7
9
  const err = new Error(res?.message ?? "dnd startDrag failed");
8
10
  err.code = res?.code ?? "unknown";
@@ -10,10 +12,10 @@ const startItemDrag = async (item) => {
10
12
  }
11
13
  };
12
14
  const cancelItemDrag = () => {
13
- sendMessage("dnd-cancel", {});
15
+ sendMessage(DND_CANCEL, {});
14
16
  };
15
17
  const onItemDrop = (listener) => addListener(
16
- "dropped-item",
18
+ DROPPED_ITEM,
17
19
  (m) => listener({ item: m.item, from: m.from, position: m.position })
18
20
  );
19
21
  const useDroppedItem = () => {
package/dist/dnd.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/dnd.ts"],"sourcesContent":["// Cross-app drag-out (FILE_EXPLORER_SPEC §7; UI_AS_APPS_SPEC §2 host-mediated).\n//\n// Native HTML5 drag-and-drop does NOT cross the sandboxed cross-origin iframe\n// boundary, and pointer events inside one app's iframe never reach the host or a\n// sibling. So a drag that STARTS in one app (the file explorer) and ENDS over\n// another (the previewed app) can only be mediated by the host (the TCB). This\n// module is the SDK surface for that one net-new platform primitive:\n//\n// - SOURCE side (the file explorer, needs the first-party `dnd:source` cap):\n// `startItemDrag(item)` asks the host to begin a host-mediated drag carrying a\n// file/dir reference (+ optional inlined bytes for a small file). The host draws\n// the trusted drag ghost, tracks the pointer across regions, and on drop over the\n// preview delivers the item to that app. `cancelItemDrag()` aborts.\n// - RECEIVER side (the previewed app, NO new grant — it opts in by subscribing):\n// `onItemDrop(cb)` / `useDroppedItem()` deliver the dropped item with host-attached,\n// unspoofable provenance (`from` = source region id) and the drop position.\n//\n// The payload is UNTRUSTED app data (CLAUDE.md §5): the host attaches `from`; the app\n// validates everything else. v1 inlines bytes only for small files (the source can\n// only relay data it can already read — no new read authority is minted).\nimport { useEffect, useState } from 'react';\nimport { protocolRequest, sendMessage, addListener } from './sandboxUtils';\n\n/** A file/dir being dragged out of an app. `bytes` is present only for a small file\n * the source chose to inline (transferred zero-copy); a dir or an over-cap file\n * carries the reference only (`kind`/`name`/`mountId`/`relPath`). */\nexport interface DraggableItem {\n kind: 'file' | 'dir';\n /** Basename — display only. */\n name: string;\n /** Which mounted filesystem the item lives in. */\n mountId: string;\n /** Path within that mount (leading slash, no `..`). */\n relPath: string;\n /** Optional inlined content for a small file. */\n bytes?: Uint8Array;\n}\n\n/** An item dropped onto THIS app by a host-mediated cross-app drag. */\nexport interface DroppedItem {\n /** The dragged item (`bytes` present iff the source inlined them). */\n item: DraggableItem;\n /** Host-attached source region id — unspoofable (T19), like an `ipc` `from`. */\n from: string;\n /** Drop point in this app's viewport. */\n position: { x: number; y: number };\n}\n\n/** An error from {@link startItemDrag}, carrying a machine-readable `.code`. */\nexport interface ItemDragError extends Error {\n code:\n | 'forbidden' // the frame lacks the first-party `dnd:source` capability\n | 'invalid-params' // the item was malformed (empty path / `..` / URI / bad kind)\n | 'too-large' // inlined `bytes` exceed the host's size limit\n | 'rate-limited' // too many drags started too fast (capacity-class, fail-open)\n | 'unknown';\n}\n\n/**\n * Begin a host-mediated drag of `item` out of this app. Resolves once the host has\n * taken over the drag (drawn the ghost, installed the pointer-capture layer); rejects\n * with an {@link ItemDragError} if this app may not initiate drags (`forbidden`) or the\n * item is invalid. Only a first-party chrome app holding `dnd:source` may call this — a\n * previewed/third-party app is refused at the gate (it must not synthesize drags into\n * sibling apps).\n */\nexport const startItemDrag = async (item: DraggableItem): Promise<void> => {\n const res = (await protocolRequest('dnd', 'startDrag', [item])) as\n | { ok: true }\n | { ok: false; code?: string; message?: string }\n | undefined;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? 'dnd startDrag failed') as ItemDragError;\n err.code = (res?.code as ItemDragError['code']) ?? 'unknown';\n throw err;\n }\n};\n\n/** Abort an in-progress host-mediated drag this app started (e.g. the user pressed\n * Escape, or the gesture was cancelled). Best-effort and fire-and-forget. */\nexport const cancelItemDrag = (): void => {\n sendMessage('dnd-cancel', {});\n};\n\n/** Subscribe to items dropped onto this app by a host-mediated cross-app drag.\n * Returns an unsubscribe fn. Subscribing is the opt-in: an app that never subscribes\n * receives nothing (the host shows a \"not accepted\" cue and the drop is a no-op). */\nexport const onItemDrop = (listener: (d: DroppedItem) => void): (() => void) =>\n addListener('dropped-item', (m: { item: DraggableItem; from: string; position: { x: number; y: number } }) =>\n listener({ item: m.item, from: m.from, position: m.position }),\n );\n\n/** React hook: the most recently dropped item (or `null`). */\nexport const useDroppedItem = (): DroppedItem | null => {\n const [dropped, setDropped] = useState<DroppedItem | null>(null);\n useEffect(() => onItemDrop(setDropped), []);\n return dropped;\n};\n"],"mappings":";AAoBA,SAAS,WAAW,gBAAgB;AACpC,SAAS,iBAAiB,aAAa,mBAAmB;AA6CnD,MAAM,gBAAgB,OAAO,SAAuC;AACzE,QAAM,MAAO,MAAM,gBAAgB,OAAO,aAAa,CAAC,IAAI,CAAC;AAI7D,MAAI,CAAC,OAAO,IAAI,OAAO,MAAM;AAC3B,UAAM,MAAM,IAAI,MAAM,KAAK,WAAW,sBAAsB;AAC5D,QAAI,OAAQ,KAAK,QAAkC;AACnD,UAAM;AAAA,EACR;AACF;AAIO,MAAM,iBAAiB,MAAY;AACxC,cAAY,cAAc,CAAC,CAAC;AAC9B;AAKO,MAAM,aAAa,CAAC,aACzB;AAAA,EAAY;AAAA,EAAgB,CAAC,MAC3B,SAAS,EAAE,MAAM,EAAE,MAAM,MAAM,EAAE,MAAM,UAAU,EAAE,SAAS,CAAC;AAC/D;AAGK,MAAM,iBAAiB,MAA0B;AACtD,QAAM,CAAC,SAAS,UAAU,IAAI,SAA6B,IAAI;AAC/D,YAAU,MAAM,WAAW,UAAU,GAAG,CAAC,CAAC;AAC1C,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../src/dnd.ts"],"sourcesContent":["// Cross-app drag-out (FILE_EXPLORER_SPEC §7; UI_AS_APPS_SPEC §2 host-mediated).\n//\n// Native HTML5 drag-and-drop does NOT cross the sandboxed cross-origin iframe\n// boundary, and pointer events inside one app's iframe never reach the host or a\n// sibling. So a drag that STARTS in one app (the file explorer) and ENDS over\n// another (the previewed app) can only be mediated by the host (the TCB). This\n// module is the SDK surface for that one net-new platform primitive:\n//\n// - SOURCE side (the file explorer, needs the first-party `dnd:source` cap):\n// `startItemDrag(item)` asks the host to begin a host-mediated drag carrying a\n// file/dir reference (+ optional inlined bytes for a small file). The host draws\n// the trusted drag ghost, tracks the pointer across regions, and on drop over the\n// preview delivers the item to that app. `cancelItemDrag()` aborts.\n// - RECEIVER side (the previewed app, NO new grant — it opts in by subscribing):\n// `onItemDrop(cb)` / `useDroppedItem()` deliver the dropped item with host-attached,\n// unspoofable provenance (`from` = source region id) and the drop position.\n//\n// The payload is UNTRUSTED app data (CLAUDE.md §5): the host attaches `from`; the app\n// validates everything else. v1 inlines bytes only for small files (the source can\n// only relay data it can already read — no new read authority is minted).\nimport { useEffect, useState } from 'react';\nimport { protocolRequest, sendMessage, addListener } from './sandboxUtils';\nimport { DND_CANCEL, DROPPED_ITEM, PROTOCOL_DND } from './generated/protocol';\nimport { SCHEMES } from './protocolSchemes';\n\n/** A file/dir being dragged out of an app. `bytes` is present only for a small file\n * the source chose to inline (transferred zero-copy); a dir or an over-cap file\n * carries the reference only (`kind`/`name`/`mountId`/`relPath`). */\nexport interface DraggableItem {\n kind: 'file' | 'dir';\n /** Basename — display only. */\n name: string;\n /** Which mounted filesystem the item lives in. */\n mountId: string;\n /** Path within that mount (leading slash, no `..`). */\n relPath: string;\n /** Optional inlined content for a small file. */\n bytes?: Uint8Array;\n}\n\n/** An item dropped onto THIS app by a host-mediated cross-app drag. */\nexport interface DroppedItem {\n /** The dragged item (`bytes` present iff the source inlined them). */\n item: DraggableItem;\n /** Host-attached source region id — unspoofable (T19), like an `ipc` `from`. */\n from: string;\n /** Drop point in this app's viewport. */\n position: { x: number; y: number };\n}\n\n/** An error from {@link startItemDrag}, carrying a machine-readable `.code`. */\nexport interface ItemDragError extends Error {\n code:\n | 'forbidden' // the frame lacks the first-party `dnd:source` capability\n | 'invalid-params' // the item was malformed (empty path / `..` / URI / bad kind)\n | 'too-large' // inlined `bytes` exceed the host's size limit\n | 'rate-limited' // too many drags started too fast (capacity-class, fail-open)\n | 'unknown';\n}\n\n/**\n * Begin a host-mediated drag of `item` out of this app. Resolves once the host has\n * taken over the drag (drawn the ghost, installed the pointer-capture layer); rejects\n * with an {@link ItemDragError} if this app may not initiate drags (`forbidden`) or the\n * item is invalid. Only a first-party chrome app holding `dnd:source` may call this — a\n * previewed/third-party app is refused at the gate (it must not synthesize drags into\n * sibling apps).\n */\nexport const startItemDrag = async (item: DraggableItem): Promise<void> => {\n const res = (await protocolRequest(SCHEMES[PROTOCOL_DND], 'startDrag', [item])) as\n | { ok: true }\n | { ok: false; code?: string; message?: string }\n | undefined;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? 'dnd startDrag failed') as ItemDragError;\n err.code = (res?.code as ItemDragError['code']) ?? 'unknown';\n throw err;\n }\n};\n\n/** Abort an in-progress host-mediated drag this app started (e.g. the user pressed\n * Escape, or the gesture was cancelled). Best-effort and fire-and-forget. */\nexport const cancelItemDrag = (): void => {\n sendMessage(DND_CANCEL, {});\n};\n\n/** Subscribe to items dropped onto this app by a host-mediated cross-app drag.\n * Returns an unsubscribe fn. Subscribing is the opt-in: an app that never subscribes\n * receives nothing (the host shows a \"not accepted\" cue and the drop is a no-op). */\nexport const onItemDrop = (listener: (d: DroppedItem) => void): (() => void) =>\n addListener(DROPPED_ITEM, (m: { item: DraggableItem; from: string; position: { x: number; y: number } }) =>\n listener({ item: m.item, from: m.from, position: m.position }),\n );\n\n/** React hook: the most recently dropped item (or `null`). */\nexport const useDroppedItem = (): DroppedItem | null => {\n const [dropped, setDropped] = useState<DroppedItem | null>(null);\n useEffect(() => onItemDrop(setDropped), []);\n return dropped;\n};\n"],"mappings":";AAoBA,SAAS,WAAW,gBAAgB;AACpC,SAAS,iBAAiB,aAAa,mBAAmB;AAC1D,SAAS,YAAY,cAAc,oBAAoB;AACvD,SAAS,eAAe;AA6CjB,MAAM,gBAAgB,OAAO,SAAuC;AACzE,QAAM,MAAO,MAAM,gBAAgB,QAAQ,YAAY,GAAG,aAAa,CAAC,IAAI,CAAC;AAI7E,MAAI,CAAC,OAAO,IAAI,OAAO,MAAM;AAC3B,UAAM,MAAM,IAAI,MAAM,KAAK,WAAW,sBAAsB;AAC5D,QAAI,OAAQ,KAAK,QAAkC;AACnD,UAAM;AAAA,EACR;AACF;AAIO,MAAM,iBAAiB,MAAY;AACxC,cAAY,YAAY,CAAC,CAAC;AAC5B;AAKO,MAAM,aAAa,CAAC,aACzB;AAAA,EAAY;AAAA,EAAc,CAAC,MACzB,SAAS,EAAE,MAAM,EAAE,MAAM,MAAM,EAAE,MAAM,UAAU,EAAE,SAAS,CAAC;AAC/D;AAGK,MAAM,iBAAiB,MAA0B;AACtD,QAAM,CAAC,SAAS,UAAU,IAAI,SAA6B,IAAI;AAC/D,YAAU,MAAM,WAAW,UAAU,GAAG,CAAC,CAAC;AAC1C,SAAO;AACT;","names":[]}
package/dist/editor.cjs CHANGED
@@ -30,8 +30,10 @@ __export(editor_exports, {
30
30
  });
31
31
  module.exports = __toCommonJS(editor_exports);
32
32
  var import_sandboxUtils = require("./sandboxUtils");
33
+ var import_protocolSchemes = require("./protocolSchemes");
34
+ var import_protocol = require("./generated/protocol");
33
35
  const editorRequest = async (method, arg) => {
34
- const res = await (0, import_sandboxUtils.protocolRequest)("editor", method, [arg]);
36
+ const res = await (0, import_sandboxUtils.protocolRequest)(import_protocolSchemes.SCHEMES[import_protocol.PROTOCOL_EDITOR], method, [arg]);
35
37
  if (!res || res.ok !== true) {
36
38
  const err = new Error(res?.message ?? `editor ${method} failed`);
37
39
  err.code = res?.code ?? "unknown";
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/editor.ts"],"sourcesContent":["import { protocolRequest } from './sandboxUtils';\n\n/**\n * Open a working-tree file in the immediately.run host editor (UI_AS_APPS_SPEC §4 —\n * the file explorer's click-to-open). This is an INTENT: the app asks, the HOST\n * validates the path and drives the CodeMirror editor — the editor itself stays\n * host-owned (§2 recursion boundary), so an app can never own or script it beyond\n * \"please show this file\".\n *\n * Requires the elevated `editor:open` capability — a previewed app does not hold it\n * (it must not move the host's focus), so only a system app whose binding grants it\n * (the file explorer) can call this; anyone else is refused at the gate.\n */\n\n/** An error from {@link openInEditor}, carrying a machine-readable `.code`. */\nexport interface EditorOpenError extends Error {\n code:\n | 'forbidden' // the frame lacks `editor:open`\n | 'not-found' // no such file in the live working tree (the host never creates)\n | 'invalid-params' // the path was empty / contained `..` / looked like a URI\n | 'no-target' // there is no host editor session to open files in\n | 'unknown';\n}\n\ntype EditorResult =\n | { ok: true; data: unknown }\n | { ok: false; code: string; message: string };\n\nconst editorRequest = async (\n method: string,\n arg: Record<string, unknown>,\n): Promise<void> => {\n const res = (await protocolRequest('editor', method, [arg])) as EditorResult;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? `editor ${method} failed`) as EditorWriteError;\n err.code = (res?.code as EditorWriteError['code']) ?? 'unknown';\n throw err;\n }\n};\n\n/**\n * Ask the host to open `path` (a repo-relative working-tree path, e.g. `src/App.tsx`\n * or `/src/App.tsx`) in the editor. Resolves once the editor switches to it; rejects\n * with an {@link EditorOpenError} (`.code`) if the path is invalid, missing, or this\n * app may not open files.\n */\nexport const openInEditor = (path: string): Promise<void> => editorRequest('open', { path });\n\n/**\n * Where to land when entering the edit experience (EDITOR_FIRST_EDITING_SPEC §6\n * Delta A). v1 supports only an optional repo-relative `path` in the CURRENT repo\n * (self-scoped — the app you are already running; the host navigates within the\n * current route, never to another repo). A URI or `..` path is refused\n * `invalid-params`. Editing a file in one of your *mounts* (a space) is the\n * `edit-file` task, not this.\n */\nexport interface EditTarget {\n /** A repo-relative working-tree path in the current repo to focus once in edit\n * mode (e.g. `src/App.tsx`). Omit to edit the current route's entry. */\n path?: string;\n}\n\n/** An error from {@link requestEdit}, carrying a machine-readable `.code`. */\nexport interface RequestEditError extends Error {\n code:\n | 'read-only' // editing isn't possible here (a `ro` mount / anonymous viewer) — HIDE the affordance\n | 'forbidden' // the host refuses (e.g. a cross-repo / out-of-scope target)\n | 'invalid-params' // the target was malformed (URI / `..` / a non-current repo)\n | 'no-target' // there is no host editor session to enter\n | 'unknown';\n}\n\n/**\n * Ask the host to enter the **edit experience** for the app you are running —\n * the present→edit transition (`/present/...` → `/edit/...`) an app cannot make\n * itself. This is an INTENT (§2 recursion boundary): the app asks, the HOST\n * performs the visible, user-observable navigation and draws all editor chrome;\n * the app never navigates or paints chrome.\n *\n * Use it to offer an \"edit this\" affordance from a run/present-mode app that opens\n * the app's own source in the platform editor — instead of shipping a bespoke\n * in-app editor (EDITOR_FIRST_EDITING_SPEC §1).\n *\n * Resolves once the host begins the transition; rejects with a\n * {@link RequestEditError} (`.code`). Treat `read-only`/`forbidden` as \"editing is\n * not available — hide the affordance,\" never as an error to surface to the user.\n */\nexport const requestEdit = (target?: EditTarget): Promise<void> =>\n editorRequest('requestEdit', target ? { ...target } : {});\n\n// ---------------------------------------------------------------------------\n// Editor SESSION management (EDITOR_AS_APP_SPEC §5.1; editor-as-app plan Phase\n// 03). Unlike `openInEditor` (the explorer's cross-app intent, `editor:open`),\n// these drive the editor's OWN open-tab set + active file, so they are gated by\n// the editor app's `editor:document` capability — a file explorer holding only\n// `editor:open` cannot call them. The host re-validates the path against the live\n// working tree; the editor itself stays host-owned (§2 recursion boundary).\n// ---------------------------------------------------------------------------\n\n/** An error from a session intent ({@link setActiveFile} / {@link closeFile}),\n * carrying a machine-readable `.code`. */\nexport interface EditorSessionError extends Error {\n code:\n | 'forbidden' // the frame lacks `editor:document`\n | 'not-found' // no such file in the live working tree\n | 'invalid-params' // the path was empty / contained `..` / looked like a URI\n | 'no-target' // there is no host editor session\n | 'unknown';\n}\n\n/** Switch the editor's active file to `path`, opening it (adding a tab) if it is\n * not already open — native `setActiveFile` parity. Rejects with an\n * {@link EditorSessionError} (`.code`) if the path is missing/invalid or this app\n * lacks `editor:document`. */\nexport const setActiveFile = (path: string): Promise<void> =>\n editorRequest('setActive', { path });\n\n/** Close `path`'s tab in the editor (remove it from the open set) — native\n * `closeFile` parity. Rejects with an {@link EditorSessionError} (`.code`). */\nexport const closeFile = (path: string): Promise<void> => editorRequest('close', { path });\n\n// ---------------------------------------------------------------------------\n// Working-tree mutation (UI_AS_APPS_SPEC §4 / EDITOR_AS_APP_SPEC §5.2). The file\n// explorer NAMES a working-tree path and the HOST performs the COW write (and\n// refreshes the preview) — the app holds no write port; it asks. Gated by the\n// first-party `editor:write` capability, so only a first-party chrome app (the\n// file explorer) can call these; anyone else is refused at the gate.\n// ---------------------------------------------------------------------------\n\n/** An error from a working-tree mutation, carrying a machine-readable `.code`. */\nexport interface EditorWriteError extends Error {\n code:\n | 'forbidden' // the frame lacks `editor:write` (first-party-only)\n | 'not-found' // the target file/folder does not exist (delete/rename)\n | 'exists' // the target already exists (create/rename would clobber)\n | 'protected' // the host refuses to delete this file (e.g. package.json)\n | 'too-large' // an upload exceeds the host's size limit\n | 'invalid-params' // a path was empty / contained `..` / looked like a URI\n | 'no-target' // there is no host editor session\n | 'unknown';\n}\n\n/** Create an empty working-tree file at `path` and open it. Rejects `exists` if a\n * file is already there. */\nexport const createFile = (path: string): Promise<void> => editorRequest('createFile', { path });\n\n/** Create a working-tree folder at `path` (materialised with a `.gitkeep`). */\nexport const createFolder = (path: string): Promise<void> =>\n editorRequest('createFolder', { path });\n\n/** Delete a working-tree file, or a folder and everything under it. Rejects\n * `protected` for files the host won't remove, `not-found` if absent. */\nexport const deleteEntry = (path: string): Promise<void> => editorRequest('deleteEntry', { path });\n\n/** Rename/move a working-tree file from `from` to `to`. Rejects `exists` if `to`\n * is taken, `not-found` if `from` is absent. */\nexport const renameEntry = (from: string, to: string): Promise<void> =>\n editorRequest('rename', { from, to });\n\n/** Upload binary/text `bytes` to a working-tree file at `path`. Rejects\n * `too-large` past the host's size limit. The bytes are transferred (zero-copy). */\nexport const uploadFile = (path: string, bytes: Uint8Array): Promise<void> =>\n editorRequest('upload', { path, bytes });\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,0BAAgC;AA4BhC,MAAM,gBAAgB,OACpB,QACA,QACkB;AAClB,QAAM,MAAO,UAAM,qCAAgB,UAAU,QAAQ,CAAC,GAAG,CAAC;AAC1D,MAAI,CAAC,OAAO,IAAI,OAAO,MAAM;AAC3B,UAAM,MAAM,IAAI,MAAM,KAAK,WAAW,UAAU,MAAM,SAAS;AAC/D,QAAI,OAAQ,KAAK,QAAqC;AACtD,UAAM;AAAA,EACR;AACF;AAQO,MAAM,eAAe,CAAC,SAAgC,cAAc,QAAQ,EAAE,KAAK,CAAC;AAyCpF,MAAM,cAAc,CAAC,WAC1B,cAAc,eAAe,SAAS,EAAE,GAAG,OAAO,IAAI,CAAC,CAAC;AA0BnD,MAAM,gBAAgB,CAAC,SAC5B,cAAc,aAAa,EAAE,KAAK,CAAC;AAI9B,MAAM,YAAY,CAAC,SAAgC,cAAc,SAAS,EAAE,KAAK,CAAC;AAyBlF,MAAM,aAAa,CAAC,SAAgC,cAAc,cAAc,EAAE,KAAK,CAAC;AAGxF,MAAM,eAAe,CAAC,SAC3B,cAAc,gBAAgB,EAAE,KAAK,CAAC;AAIjC,MAAM,cAAc,CAAC,SAAgC,cAAc,eAAe,EAAE,KAAK,CAAC;AAI1F,MAAM,cAAc,CAAC,MAAc,OACxC,cAAc,UAAU,EAAE,MAAM,GAAG,CAAC;AAI/B,MAAM,aAAa,CAAC,MAAc,UACvC,cAAc,UAAU,EAAE,MAAM,MAAM,CAAC;","names":[]}
1
+ {"version":3,"sources":["../src/editor.ts"],"sourcesContent":["import { protocolRequest } from './sandboxUtils';\nimport { SCHEMES } from './protocolSchemes';\nimport { PROTOCOL_EDITOR } from './generated/protocol';\n\n/**\n * Open a working-tree file in the immediately.run host editor (UI_AS_APPS_SPEC §4 —\n * the file explorer's click-to-open). This is an INTENT: the app asks, the HOST\n * validates the path and drives the CodeMirror editor — the editor itself stays\n * host-owned (§2 recursion boundary), so an app can never own or script it beyond\n * \"please show this file\".\n *\n * Requires the elevated `editor:open` capability — a previewed app does not hold it\n * (it must not move the host's focus), so only a system app whose binding grants it\n * (the file explorer) can call this; anyone else is refused at the gate.\n */\n\n/** An error from {@link openInEditor}, carrying a machine-readable `.code`. */\nexport interface EditorOpenError extends Error {\n code:\n | 'forbidden' // the frame lacks `editor:open`\n | 'not-found' // no such file in the live working tree (the host never creates)\n | 'invalid-params' // the path was empty / contained `..` / looked like a URI\n | 'no-target' // there is no host editor session to open files in\n | 'unknown';\n}\n\ntype EditorResult =\n | { ok: true; data: unknown }\n | { ok: false; code: string; message: string };\n\nconst editorRequest = async (\n method: string,\n arg: Record<string, unknown>,\n): Promise<void> => {\n const res = (await protocolRequest(SCHEMES[PROTOCOL_EDITOR], method, [arg])) as EditorResult;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? `editor ${method} failed`) as EditorWriteError;\n err.code = (res?.code as EditorWriteError['code']) ?? 'unknown';\n throw err;\n }\n};\n\n/**\n * Ask the host to open `path` (a repo-relative working-tree path, e.g. `src/App.tsx`\n * or `/src/App.tsx`) in the editor. Resolves once the editor switches to it; rejects\n * with an {@link EditorOpenError} (`.code`) if the path is invalid, missing, or this\n * app may not open files.\n */\nexport const openInEditor = (path: string): Promise<void> => editorRequest('open', { path });\n\n/**\n * Where to land when entering the edit experience (EDITOR_FIRST_EDITING_SPEC §6\n * Delta A). v1 supports only an optional repo-relative `path` in the CURRENT repo\n * (self-scoped — the app you are already running; the host navigates within the\n * current route, never to another repo). A URI or `..` path is refused\n * `invalid-params`. Editing a file in one of your *mounts* (a space) is the\n * `edit-file` task, not this.\n */\nexport interface EditTarget {\n /** A repo-relative working-tree path in the current repo to focus once in edit\n * mode (e.g. `src/App.tsx`). Omit to edit the current route's entry. */\n path?: string;\n}\n\n/** An error from {@link requestEdit}, carrying a machine-readable `.code`. */\nexport interface RequestEditError extends Error {\n code:\n | 'read-only' // editing isn't possible here (a `ro` mount / anonymous viewer) — HIDE the affordance\n | 'forbidden' // the host refuses (e.g. a cross-repo / out-of-scope target)\n | 'invalid-params' // the target was malformed (URI / `..` / a non-current repo)\n | 'no-target' // there is no host editor session to enter\n | 'unknown';\n}\n\n/**\n * Ask the host to enter the **edit experience** for the app you are running —\n * the present→edit transition (`/present/...` → `/edit/...`) an app cannot make\n * itself. This is an INTENT (§2 recursion boundary): the app asks, the HOST\n * performs the visible, user-observable navigation and draws all editor chrome;\n * the app never navigates or paints chrome.\n *\n * Use it to offer an \"edit this\" affordance from a run/present-mode app that opens\n * the app's own source in the platform editor — instead of shipping a bespoke\n * in-app editor (EDITOR_FIRST_EDITING_SPEC §1).\n *\n * Resolves once the host begins the transition; rejects with a\n * {@link RequestEditError} (`.code`). Treat `read-only`/`forbidden` as \"editing is\n * not available — hide the affordance,\" never as an error to surface to the user.\n */\nexport const requestEdit = (target?: EditTarget): Promise<void> =>\n editorRequest('requestEdit', target ? { ...target } : {});\n\n// ---------------------------------------------------------------------------\n// Editor SESSION management (EDITOR_AS_APP_SPEC §5.1; editor-as-app plan Phase\n// 03). Unlike `openInEditor` (the explorer's cross-app intent, `editor:open`),\n// these drive the editor's OWN open-tab set + active file, so they are gated by\n// the editor app's `editor:document` capability — a file explorer holding only\n// `editor:open` cannot call them. The host re-validates the path against the live\n// working tree; the editor itself stays host-owned (§2 recursion boundary).\n// ---------------------------------------------------------------------------\n\n/** An error from a session intent ({@link setActiveFile} / {@link closeFile}),\n * carrying a machine-readable `.code`. */\nexport interface EditorSessionError extends Error {\n code:\n | 'forbidden' // the frame lacks `editor:document`\n | 'not-found' // no such file in the live working tree\n | 'invalid-params' // the path was empty / contained `..` / looked like a URI\n | 'no-target' // there is no host editor session\n | 'unknown';\n}\n\n/** Switch the editor's active file to `path`, opening it (adding a tab) if it is\n * not already open — native `setActiveFile` parity. Rejects with an\n * {@link EditorSessionError} (`.code`) if the path is missing/invalid or this app\n * lacks `editor:document`. */\nexport const setActiveFile = (path: string): Promise<void> =>\n editorRequest('setActive', { path });\n\n/** Close `path`'s tab in the editor (remove it from the open set) — native\n * `closeFile` parity. Rejects with an {@link EditorSessionError} (`.code`). */\nexport const closeFile = (path: string): Promise<void> => editorRequest('close', { path });\n\n// ---------------------------------------------------------------------------\n// Working-tree mutation (UI_AS_APPS_SPEC §4 / EDITOR_AS_APP_SPEC §5.2). The file\n// explorer NAMES a working-tree path and the HOST performs the COW write (and\n// refreshes the preview) — the app holds no write port; it asks. Gated by the\n// first-party `editor:write` capability, so only a first-party chrome app (the\n// file explorer) can call these; anyone else is refused at the gate.\n// ---------------------------------------------------------------------------\n\n/** An error from a working-tree mutation, carrying a machine-readable `.code`. */\nexport interface EditorWriteError extends Error {\n code:\n | 'forbidden' // the frame lacks `editor:write` (first-party-only)\n | 'not-found' // the target file/folder does not exist (delete/rename)\n | 'exists' // the target already exists (create/rename would clobber)\n | 'protected' // the host refuses to delete this file (e.g. package.json)\n | 'too-large' // an upload exceeds the host's size limit\n | 'invalid-params' // a path was empty / contained `..` / looked like a URI\n | 'no-target' // there is no host editor session\n | 'unknown';\n}\n\n/** Create an empty working-tree file at `path` and open it. Rejects `exists` if a\n * file is already there. */\nexport const createFile = (path: string): Promise<void> => editorRequest('createFile', { path });\n\n/** Create a working-tree folder at `path` (materialised with a `.gitkeep`). */\nexport const createFolder = (path: string): Promise<void> =>\n editorRequest('createFolder', { path });\n\n/** Delete a working-tree file, or a folder and everything under it. Rejects\n * `protected` for files the host won't remove, `not-found` if absent. */\nexport const deleteEntry = (path: string): Promise<void> => editorRequest('deleteEntry', { path });\n\n/** Rename/move a working-tree file from `from` to `to`. Rejects `exists` if `to`\n * is taken, `not-found` if `from` is absent. */\nexport const renameEntry = (from: string, to: string): Promise<void> =>\n editorRequest('rename', { from, to });\n\n/** Upload binary/text `bytes` to a working-tree file at `path`. Rejects\n * `too-large` past the host's size limit. The bytes are transferred (zero-copy). */\nexport const uploadFile = (path: string, bytes: Uint8Array): Promise<void> =>\n editorRequest('upload', { path, bytes });\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,0BAAgC;AAChC,6BAAwB;AACxB,sBAAgC;AA4BhC,MAAM,gBAAgB,OACpB,QACA,QACkB;AAClB,QAAM,MAAO,UAAM,qCAAgB,+BAAQ,+BAAe,GAAG,QAAQ,CAAC,GAAG,CAAC;AAC1E,MAAI,CAAC,OAAO,IAAI,OAAO,MAAM;AAC3B,UAAM,MAAM,IAAI,MAAM,KAAK,WAAW,UAAU,MAAM,SAAS;AAC/D,QAAI,OAAQ,KAAK,QAAqC;AACtD,UAAM;AAAA,EACR;AACF;AAQO,MAAM,eAAe,CAAC,SAAgC,cAAc,QAAQ,EAAE,KAAK,CAAC;AAyCpF,MAAM,cAAc,CAAC,WAC1B,cAAc,eAAe,SAAS,EAAE,GAAG,OAAO,IAAI,CAAC,CAAC;AA0BnD,MAAM,gBAAgB,CAAC,SAC5B,cAAc,aAAa,EAAE,KAAK,CAAC;AAI9B,MAAM,YAAY,CAAC,SAAgC,cAAc,SAAS,EAAE,KAAK,CAAC;AAyBlF,MAAM,aAAa,CAAC,SAAgC,cAAc,cAAc,EAAE,KAAK,CAAC;AAGxF,MAAM,eAAe,CAAC,SAC3B,cAAc,gBAAgB,EAAE,KAAK,CAAC;AAIjC,MAAM,cAAc,CAAC,SAAgC,cAAc,eAAe,EAAE,KAAK,CAAC;AAI1F,MAAM,cAAc,CAAC,MAAc,OACxC,cAAc,UAAU,EAAE,MAAM,GAAG,CAAC;AAI/B,MAAM,aAAa,CAAC,MAAc,UACvC,cAAc,UAAU,EAAE,MAAM,MAAM,CAAC;","names":[]}
package/dist/editor.js CHANGED
@@ -1,7 +1,9 @@
1
1
  import "./chunk-VHAA22YE.js";
2
2
  import { protocolRequest } from "./sandboxUtils";
3
+ import { SCHEMES } from "./protocolSchemes";
4
+ import { PROTOCOL_EDITOR } from "./generated/protocol";
3
5
  const editorRequest = async (method, arg) => {
4
- const res = await protocolRequest("editor", method, [arg]);
6
+ const res = await protocolRequest(SCHEMES[PROTOCOL_EDITOR], method, [arg]);
5
7
  if (!res || res.ok !== true) {
6
8
  const err = new Error(res?.message ?? `editor ${method} failed`);
7
9
  err.code = res?.code ?? "unknown";
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/editor.ts"],"sourcesContent":["import { protocolRequest } from './sandboxUtils';\n\n/**\n * Open a working-tree file in the immediately.run host editor (UI_AS_APPS_SPEC §4 —\n * the file explorer's click-to-open). This is an INTENT: the app asks, the HOST\n * validates the path and drives the CodeMirror editor — the editor itself stays\n * host-owned (§2 recursion boundary), so an app can never own or script it beyond\n * \"please show this file\".\n *\n * Requires the elevated `editor:open` capability — a previewed app does not hold it\n * (it must not move the host's focus), so only a system app whose binding grants it\n * (the file explorer) can call this; anyone else is refused at the gate.\n */\n\n/** An error from {@link openInEditor}, carrying a machine-readable `.code`. */\nexport interface EditorOpenError extends Error {\n code:\n | 'forbidden' // the frame lacks `editor:open`\n | 'not-found' // no such file in the live working tree (the host never creates)\n | 'invalid-params' // the path was empty / contained `..` / looked like a URI\n | 'no-target' // there is no host editor session to open files in\n | 'unknown';\n}\n\ntype EditorResult =\n | { ok: true; data: unknown }\n | { ok: false; code: string; message: string };\n\nconst editorRequest = async (\n method: string,\n arg: Record<string, unknown>,\n): Promise<void> => {\n const res = (await protocolRequest('editor', method, [arg])) as EditorResult;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? `editor ${method} failed`) as EditorWriteError;\n err.code = (res?.code as EditorWriteError['code']) ?? 'unknown';\n throw err;\n }\n};\n\n/**\n * Ask the host to open `path` (a repo-relative working-tree path, e.g. `src/App.tsx`\n * or `/src/App.tsx`) in the editor. Resolves once the editor switches to it; rejects\n * with an {@link EditorOpenError} (`.code`) if the path is invalid, missing, or this\n * app may not open files.\n */\nexport const openInEditor = (path: string): Promise<void> => editorRequest('open', { path });\n\n/**\n * Where to land when entering the edit experience (EDITOR_FIRST_EDITING_SPEC §6\n * Delta A). v1 supports only an optional repo-relative `path` in the CURRENT repo\n * (self-scoped — the app you are already running; the host navigates within the\n * current route, never to another repo). A URI or `..` path is refused\n * `invalid-params`. Editing a file in one of your *mounts* (a space) is the\n * `edit-file` task, not this.\n */\nexport interface EditTarget {\n /** A repo-relative working-tree path in the current repo to focus once in edit\n * mode (e.g. `src/App.tsx`). Omit to edit the current route's entry. */\n path?: string;\n}\n\n/** An error from {@link requestEdit}, carrying a machine-readable `.code`. */\nexport interface RequestEditError extends Error {\n code:\n | 'read-only' // editing isn't possible here (a `ro` mount / anonymous viewer) — HIDE the affordance\n | 'forbidden' // the host refuses (e.g. a cross-repo / out-of-scope target)\n | 'invalid-params' // the target was malformed (URI / `..` / a non-current repo)\n | 'no-target' // there is no host editor session to enter\n | 'unknown';\n}\n\n/**\n * Ask the host to enter the **edit experience** for the app you are running —\n * the present→edit transition (`/present/...` → `/edit/...`) an app cannot make\n * itself. This is an INTENT (§2 recursion boundary): the app asks, the HOST\n * performs the visible, user-observable navigation and draws all editor chrome;\n * the app never navigates or paints chrome.\n *\n * Use it to offer an \"edit this\" affordance from a run/present-mode app that opens\n * the app's own source in the platform editor — instead of shipping a bespoke\n * in-app editor (EDITOR_FIRST_EDITING_SPEC §1).\n *\n * Resolves once the host begins the transition; rejects with a\n * {@link RequestEditError} (`.code`). Treat `read-only`/`forbidden` as \"editing is\n * not available — hide the affordance,\" never as an error to surface to the user.\n */\nexport const requestEdit = (target?: EditTarget): Promise<void> =>\n editorRequest('requestEdit', target ? { ...target } : {});\n\n// ---------------------------------------------------------------------------\n// Editor SESSION management (EDITOR_AS_APP_SPEC §5.1; editor-as-app plan Phase\n// 03). Unlike `openInEditor` (the explorer's cross-app intent, `editor:open`),\n// these drive the editor's OWN open-tab set + active file, so they are gated by\n// the editor app's `editor:document` capability — a file explorer holding only\n// `editor:open` cannot call them. The host re-validates the path against the live\n// working tree; the editor itself stays host-owned (§2 recursion boundary).\n// ---------------------------------------------------------------------------\n\n/** An error from a session intent ({@link setActiveFile} / {@link closeFile}),\n * carrying a machine-readable `.code`. */\nexport interface EditorSessionError extends Error {\n code:\n | 'forbidden' // the frame lacks `editor:document`\n | 'not-found' // no such file in the live working tree\n | 'invalid-params' // the path was empty / contained `..` / looked like a URI\n | 'no-target' // there is no host editor session\n | 'unknown';\n}\n\n/** Switch the editor's active file to `path`, opening it (adding a tab) if it is\n * not already open — native `setActiveFile` parity. Rejects with an\n * {@link EditorSessionError} (`.code`) if the path is missing/invalid or this app\n * lacks `editor:document`. */\nexport const setActiveFile = (path: string): Promise<void> =>\n editorRequest('setActive', { path });\n\n/** Close `path`'s tab in the editor (remove it from the open set) — native\n * `closeFile` parity. Rejects with an {@link EditorSessionError} (`.code`). */\nexport const closeFile = (path: string): Promise<void> => editorRequest('close', { path });\n\n// ---------------------------------------------------------------------------\n// Working-tree mutation (UI_AS_APPS_SPEC §4 / EDITOR_AS_APP_SPEC §5.2). The file\n// explorer NAMES a working-tree path and the HOST performs the COW write (and\n// refreshes the preview) — the app holds no write port; it asks. Gated by the\n// first-party `editor:write` capability, so only a first-party chrome app (the\n// file explorer) can call these; anyone else is refused at the gate.\n// ---------------------------------------------------------------------------\n\n/** An error from a working-tree mutation, carrying a machine-readable `.code`. */\nexport interface EditorWriteError extends Error {\n code:\n | 'forbidden' // the frame lacks `editor:write` (first-party-only)\n | 'not-found' // the target file/folder does not exist (delete/rename)\n | 'exists' // the target already exists (create/rename would clobber)\n | 'protected' // the host refuses to delete this file (e.g. package.json)\n | 'too-large' // an upload exceeds the host's size limit\n | 'invalid-params' // a path was empty / contained `..` / looked like a URI\n | 'no-target' // there is no host editor session\n | 'unknown';\n}\n\n/** Create an empty working-tree file at `path` and open it. Rejects `exists` if a\n * file is already there. */\nexport const createFile = (path: string): Promise<void> => editorRequest('createFile', { path });\n\n/** Create a working-tree folder at `path` (materialised with a `.gitkeep`). */\nexport const createFolder = (path: string): Promise<void> =>\n editorRequest('createFolder', { path });\n\n/** Delete a working-tree file, or a folder and everything under it. Rejects\n * `protected` for files the host won't remove, `not-found` if absent. */\nexport const deleteEntry = (path: string): Promise<void> => editorRequest('deleteEntry', { path });\n\n/** Rename/move a working-tree file from `from` to `to`. Rejects `exists` if `to`\n * is taken, `not-found` if `from` is absent. */\nexport const renameEntry = (from: string, to: string): Promise<void> =>\n editorRequest('rename', { from, to });\n\n/** Upload binary/text `bytes` to a working-tree file at `path`. Rejects\n * `too-large` past the host's size limit. The bytes are transferred (zero-copy). */\nexport const uploadFile = (path: string, bytes: Uint8Array): Promise<void> =>\n editorRequest('upload', { path, bytes });\n"],"mappings":";AAAA,SAAS,uBAAuB;AA4BhC,MAAM,gBAAgB,OACpB,QACA,QACkB;AAClB,QAAM,MAAO,MAAM,gBAAgB,UAAU,QAAQ,CAAC,GAAG,CAAC;AAC1D,MAAI,CAAC,OAAO,IAAI,OAAO,MAAM;AAC3B,UAAM,MAAM,IAAI,MAAM,KAAK,WAAW,UAAU,MAAM,SAAS;AAC/D,QAAI,OAAQ,KAAK,QAAqC;AACtD,UAAM;AAAA,EACR;AACF;AAQO,MAAM,eAAe,CAAC,SAAgC,cAAc,QAAQ,EAAE,KAAK,CAAC;AAyCpF,MAAM,cAAc,CAAC,WAC1B,cAAc,eAAe,SAAS,EAAE,GAAG,OAAO,IAAI,CAAC,CAAC;AA0BnD,MAAM,gBAAgB,CAAC,SAC5B,cAAc,aAAa,EAAE,KAAK,CAAC;AAI9B,MAAM,YAAY,CAAC,SAAgC,cAAc,SAAS,EAAE,KAAK,CAAC;AAyBlF,MAAM,aAAa,CAAC,SAAgC,cAAc,cAAc,EAAE,KAAK,CAAC;AAGxF,MAAM,eAAe,CAAC,SAC3B,cAAc,gBAAgB,EAAE,KAAK,CAAC;AAIjC,MAAM,cAAc,CAAC,SAAgC,cAAc,eAAe,EAAE,KAAK,CAAC;AAI1F,MAAM,cAAc,CAAC,MAAc,OACxC,cAAc,UAAU,EAAE,MAAM,GAAG,CAAC;AAI/B,MAAM,aAAa,CAAC,MAAc,UACvC,cAAc,UAAU,EAAE,MAAM,MAAM,CAAC;","names":[]}
1
+ {"version":3,"sources":["../src/editor.ts"],"sourcesContent":["import { protocolRequest } from './sandboxUtils';\nimport { SCHEMES } from './protocolSchemes';\nimport { PROTOCOL_EDITOR } from './generated/protocol';\n\n/**\n * Open a working-tree file in the immediately.run host editor (UI_AS_APPS_SPEC §4 —\n * the file explorer's click-to-open). This is an INTENT: the app asks, the HOST\n * validates the path and drives the CodeMirror editor — the editor itself stays\n * host-owned (§2 recursion boundary), so an app can never own or script it beyond\n * \"please show this file\".\n *\n * Requires the elevated `editor:open` capability — a previewed app does not hold it\n * (it must not move the host's focus), so only a system app whose binding grants it\n * (the file explorer) can call this; anyone else is refused at the gate.\n */\n\n/** An error from {@link openInEditor}, carrying a machine-readable `.code`. */\nexport interface EditorOpenError extends Error {\n code:\n | 'forbidden' // the frame lacks `editor:open`\n | 'not-found' // no such file in the live working tree (the host never creates)\n | 'invalid-params' // the path was empty / contained `..` / looked like a URI\n | 'no-target' // there is no host editor session to open files in\n | 'unknown';\n}\n\ntype EditorResult =\n | { ok: true; data: unknown }\n | { ok: false; code: string; message: string };\n\nconst editorRequest = async (\n method: string,\n arg: Record<string, unknown>,\n): Promise<void> => {\n const res = (await protocolRequest(SCHEMES[PROTOCOL_EDITOR], method, [arg])) as EditorResult;\n if (!res || res.ok !== true) {\n const err = new Error(res?.message ?? `editor ${method} failed`) as EditorWriteError;\n err.code = (res?.code as EditorWriteError['code']) ?? 'unknown';\n throw err;\n }\n};\n\n/**\n * Ask the host to open `path` (a repo-relative working-tree path, e.g. `src/App.tsx`\n * or `/src/App.tsx`) in the editor. Resolves once the editor switches to it; rejects\n * with an {@link EditorOpenError} (`.code`) if the path is invalid, missing, or this\n * app may not open files.\n */\nexport const openInEditor = (path: string): Promise<void> => editorRequest('open', { path });\n\n/**\n * Where to land when entering the edit experience (EDITOR_FIRST_EDITING_SPEC §6\n * Delta A). v1 supports only an optional repo-relative `path` in the CURRENT repo\n * (self-scoped — the app you are already running; the host navigates within the\n * current route, never to another repo). A URI or `..` path is refused\n * `invalid-params`. Editing a file in one of your *mounts* (a space) is the\n * `edit-file` task, not this.\n */\nexport interface EditTarget {\n /** A repo-relative working-tree path in the current repo to focus once in edit\n * mode (e.g. `src/App.tsx`). Omit to edit the current route's entry. */\n path?: string;\n}\n\n/** An error from {@link requestEdit}, carrying a machine-readable `.code`. */\nexport interface RequestEditError extends Error {\n code:\n | 'read-only' // editing isn't possible here (a `ro` mount / anonymous viewer) — HIDE the affordance\n | 'forbidden' // the host refuses (e.g. a cross-repo / out-of-scope target)\n | 'invalid-params' // the target was malformed (URI / `..` / a non-current repo)\n | 'no-target' // there is no host editor session to enter\n | 'unknown';\n}\n\n/**\n * Ask the host to enter the **edit experience** for the app you are running —\n * the present→edit transition (`/present/...` → `/edit/...`) an app cannot make\n * itself. This is an INTENT (§2 recursion boundary): the app asks, the HOST\n * performs the visible, user-observable navigation and draws all editor chrome;\n * the app never navigates or paints chrome.\n *\n * Use it to offer an \"edit this\" affordance from a run/present-mode app that opens\n * the app's own source in the platform editor — instead of shipping a bespoke\n * in-app editor (EDITOR_FIRST_EDITING_SPEC §1).\n *\n * Resolves once the host begins the transition; rejects with a\n * {@link RequestEditError} (`.code`). Treat `read-only`/`forbidden` as \"editing is\n * not available — hide the affordance,\" never as an error to surface to the user.\n */\nexport const requestEdit = (target?: EditTarget): Promise<void> =>\n editorRequest('requestEdit', target ? { ...target } : {});\n\n// ---------------------------------------------------------------------------\n// Editor SESSION management (EDITOR_AS_APP_SPEC §5.1; editor-as-app plan Phase\n// 03). Unlike `openInEditor` (the explorer's cross-app intent, `editor:open`),\n// these drive the editor's OWN open-tab set + active file, so they are gated by\n// the editor app's `editor:document` capability — a file explorer holding only\n// `editor:open` cannot call them. The host re-validates the path against the live\n// working tree; the editor itself stays host-owned (§2 recursion boundary).\n// ---------------------------------------------------------------------------\n\n/** An error from a session intent ({@link setActiveFile} / {@link closeFile}),\n * carrying a machine-readable `.code`. */\nexport interface EditorSessionError extends Error {\n code:\n | 'forbidden' // the frame lacks `editor:document`\n | 'not-found' // no such file in the live working tree\n | 'invalid-params' // the path was empty / contained `..` / looked like a URI\n | 'no-target' // there is no host editor session\n | 'unknown';\n}\n\n/** Switch the editor's active file to `path`, opening it (adding a tab) if it is\n * not already open — native `setActiveFile` parity. Rejects with an\n * {@link EditorSessionError} (`.code`) if the path is missing/invalid or this app\n * lacks `editor:document`. */\nexport const setActiveFile = (path: string): Promise<void> =>\n editorRequest('setActive', { path });\n\n/** Close `path`'s tab in the editor (remove it from the open set) — native\n * `closeFile` parity. Rejects with an {@link EditorSessionError} (`.code`). */\nexport const closeFile = (path: string): Promise<void> => editorRequest('close', { path });\n\n// ---------------------------------------------------------------------------\n// Working-tree mutation (UI_AS_APPS_SPEC §4 / EDITOR_AS_APP_SPEC §5.2). The file\n// explorer NAMES a working-tree path and the HOST performs the COW write (and\n// refreshes the preview) — the app holds no write port; it asks. Gated by the\n// first-party `editor:write` capability, so only a first-party chrome app (the\n// file explorer) can call these; anyone else is refused at the gate.\n// ---------------------------------------------------------------------------\n\n/** An error from a working-tree mutation, carrying a machine-readable `.code`. */\nexport interface EditorWriteError extends Error {\n code:\n | 'forbidden' // the frame lacks `editor:write` (first-party-only)\n | 'not-found' // the target file/folder does not exist (delete/rename)\n | 'exists' // the target already exists (create/rename would clobber)\n | 'protected' // the host refuses to delete this file (e.g. package.json)\n | 'too-large' // an upload exceeds the host's size limit\n | 'invalid-params' // a path was empty / contained `..` / looked like a URI\n | 'no-target' // there is no host editor session\n | 'unknown';\n}\n\n/** Create an empty working-tree file at `path` and open it. Rejects `exists` if a\n * file is already there. */\nexport const createFile = (path: string): Promise<void> => editorRequest('createFile', { path });\n\n/** Create a working-tree folder at `path` (materialised with a `.gitkeep`). */\nexport const createFolder = (path: string): Promise<void> =>\n editorRequest('createFolder', { path });\n\n/** Delete a working-tree file, or a folder and everything under it. Rejects\n * `protected` for files the host won't remove, `not-found` if absent. */\nexport const deleteEntry = (path: string): Promise<void> => editorRequest('deleteEntry', { path });\n\n/** Rename/move a working-tree file from `from` to `to`. Rejects `exists` if `to`\n * is taken, `not-found` if `from` is absent. */\nexport const renameEntry = (from: string, to: string): Promise<void> =>\n editorRequest('rename', { from, to });\n\n/** Upload binary/text `bytes` to a working-tree file at `path`. Rejects\n * `too-large` past the host's size limit. The bytes are transferred (zero-copy). */\nexport const uploadFile = (path: string, bytes: Uint8Array): Promise<void> =>\n editorRequest('upload', { path, bytes });\n"],"mappings":";AAAA,SAAS,uBAAuB;AAChC,SAAS,eAAe;AACxB,SAAS,uBAAuB;AA4BhC,MAAM,gBAAgB,OACpB,QACA,QACkB;AAClB,QAAM,MAAO,MAAM,gBAAgB,QAAQ,eAAe,GAAG,QAAQ,CAAC,GAAG,CAAC;AAC1E,MAAI,CAAC,OAAO,IAAI,OAAO,MAAM;AAC3B,UAAM,MAAM,IAAI,MAAM,KAAK,WAAW,UAAU,MAAM,SAAS;AAC/D,QAAI,OAAQ,KAAK,QAAqC;AACtD,UAAM;AAAA,EACR;AACF;AAQO,MAAM,eAAe,CAAC,SAAgC,cAAc,QAAQ,EAAE,KAAK,CAAC;AAyCpF,MAAM,cAAc,CAAC,WAC1B,cAAc,eAAe,SAAS,EAAE,GAAG,OAAO,IAAI,CAAC,CAAC;AA0BnD,MAAM,gBAAgB,CAAC,SAC5B,cAAc,aAAa,EAAE,KAAK,CAAC;AAI9B,MAAM,YAAY,CAAC,SAAgC,cAAc,SAAS,EAAE,KAAK,CAAC;AAyBlF,MAAM,aAAa,CAAC,SAAgC,cAAc,cAAc,EAAE,KAAK,CAAC;AAGxF,MAAM,eAAe,CAAC,SAC3B,cAAc,gBAAgB,EAAE,KAAK,CAAC;AAIjC,MAAM,cAAc,CAAC,SAAgC,cAAc,eAAe,EAAE,KAAK,CAAC;AAI1F,MAAM,cAAc,CAAC,MAAc,OACxC,cAAc,UAAU,EAAE,MAAM,GAAG,CAAC;AAI/B,MAAM,aAAa,CAAC,MAAc,UACvC,cAAc,UAAU,EAAE,MAAM,MAAM,CAAC;","names":[]}
@@ -24,10 +24,11 @@ __export(editorContext_exports, {
24
24
  });
25
25
  module.exports = __toCommonJS(editorContext_exports);
26
26
  var import_pushChannel = require("./pushChannel");
27
+ var import_protocol = require("./generated/protocol");
27
28
  const isStringArray = (v) => Array.isArray(v) && v.every((p) => typeof p === "string");
28
29
  const channel = (0, import_pushChannel.createPushChannel)({
29
- pushType: "editor-context",
30
- requestType: "request-editor-context",
30
+ pushType: import_protocol.EDITOR_CONTEXT,
31
+ requestType: import_protocol.REQUEST_EDITOR_CONTEXT,
31
32
  initial: { dirtyPaths: [], openFiles: [], activeFile: null, viewedFile: null },
32
33
  parse: (msg) => isStringArray(msg.dirtyPaths) ? {
33
34
  dirtyPaths: msg.dirtyPaths,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/editorContext.ts"],"sourcesContent":["import { createPushChannel } from './pushChannel';\n\n/**\n * The editor \"dirty set\" mirrored from the immediately.run host into the sandbox\n * (UI_AS_APPS_SPEC §5.3): which files the user has changed but not yet saved.\n *\n * This is the ELEVATED `editor:read` capability — only a system app whose binding\n * grants it (e.g. the contribute dialog) receives it. The active file and ref are\n * already available to every app via routing (`useNavigationState`), so this\n * channel carries only the genuine delta: the unsaved paths. An app without\n * `editor:read` simply sees an empty dirty set.\n */\nexport interface EditorContext {\n /** Repo-relative paths the user has modified but not yet saved. */\n dirtyPaths: string[];\n /** Repo-relative paths currently open as tabs in the host editor (§4.2). */\n openFiles: string[];\n /**\n * The one file currently focused in the host editor (repo-relative, leading\n * slash — e.g. `/src/index.ts`), or `null` when no file is open. Distinct from\n * {@link dirtyPaths} (the unsaved SET) and {@link openFiles} (the open-tab set):\n * this is the single ACTIVE file. A baseline app reads its own active file from\n * the route, but a self-routed system panel (`drivesHostRoute=false`, e.g. the\n * file explorer) does not see the host editor's route, so it receives the active\n * file here instead (UI_AS_APPS_SPEC §5.3 self-routed-panel refinement).\n */\n activeFile: string | null;\n /**\n * The viewed-document HINT (R3-268): the working-tree file the STAGE app is\n * currently rendering (leading slash), or `null`. Distinct from\n * {@link activeFile} (the editor's focus): this is what is ON STAGE. It is an\n * app-supplied claim the host validated for existence only — consume it as a\n * highlight and nothing more (never scroll, move focus, or switch files on\n * it; that contract is what keeps the signal activation-free).\n */\n viewedFile: string | null;\n}\n\n// Read over the transport (SDK_PACKAGING_SPEC §4): the host pushes `editor-context`\n// and answers `request-editor-context` — but only for a frame holding `editor:read`\n// (gated by the channel router). An app without it gets no reply, so the empty\n// default below stands. Wire format: site-main channelBridge.ts.\nconst isStringArray = (v: unknown): v is string[] =>\n Array.isArray(v) && v.every((p) => typeof p === 'string');\n\nconst channel = createPushChannel<EditorContext>({\n pushType: 'editor-context',\n requestType: 'request-editor-context',\n initial: { dirtyPaths: [], openFiles: [], activeFile: null, viewedFile: null },\n parse: (msg) =>\n isStringArray(msg.dirtyPaths)\n ? {\n dirtyPaths: msg.dirtyPaths,\n // `openFiles` is newer than `dirtyPaths`; tolerate an older host that\n // omits it (defensive SDK — defaults to empty rather than rejecting).\n openFiles: isStringArray(msg.openFiles) ? msg.openFiles : [],\n // `activeFile` is newer still; an older host that omits it (or sends a\n // non-string) reads as `null` — no file highlighted, never a throw.\n activeFile: typeof msg.activeFile === 'string' ? msg.activeFile : null,\n // `viewedFile` is the newest (R3-268); same tolerance — an older host\n // that omits it reads as `null` (no stage highlight), never a throw.\n viewedFile: typeof msg.viewedFile === 'string' ? msg.viewedFile : null,\n }\n : undefined,\n});\n\n/**\n * Returns the current editor context (dirty set). Poll this for a one-off read;\n * use {@link onEditorContextChange} or {@link useEditorContext} to react.\n */\nexport const getEditorContext = (): EditorContext => channel.get();\n\n/**\n * Subscribe to editor-context changes. The listener is invoked immediately with\n * the current context, then again on every change. Returns an unsubscribe fn.\n */\nexport const onEditorContextChange = (listener: (context: EditorContext) => void): (() => void) =>\n channel.onChange(listener);\n\n/**\n * React hook returning the current editor context (dirty set), re-rendering when\n * it changes. Handy for a contribute dialog: `const { dirtyPaths } =\n * useEditorContext()` to show \"you'll save N files\" before calling `contribute()`.\n */\nexport const useEditorContext = (): EditorContext => channel.use();\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,yBAAkC;AA0ClC,MAAM,gBAAgB,CAAC,MACrB,MAAM,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,OAAO,MAAM,QAAQ;AAE1D,MAAM,cAAU,sCAAiC;AAAA,EAC/C,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS,EAAE,YAAY,CAAC,GAAG,WAAW,CAAC,GAAG,YAAY,MAAM,YAAY,KAAK;AAAA,EAC7E,OAAO,CAAC,QACN,cAAc,IAAI,UAAU,IACxB;AAAA,IACE,YAAY,IAAI;AAAA;AAAA;AAAA,IAGhB,WAAW,cAAc,IAAI,SAAS,IAAI,IAAI,YAAY,CAAC;AAAA;AAAA;AAAA,IAG3D,YAAY,OAAO,IAAI,eAAe,WAAW,IAAI,aAAa;AAAA;AAAA;AAAA,IAGlE,YAAY,OAAO,IAAI,eAAe,WAAW,IAAI,aAAa;AAAA,EACpE,IACA;AACR,CAAC;AAMM,MAAM,mBAAmB,MAAqB,QAAQ,IAAI;AAM1D,MAAM,wBAAwB,CAAC,aACpC,QAAQ,SAAS,QAAQ;AAOpB,MAAM,mBAAmB,MAAqB,QAAQ,IAAI;","names":[]}
1
+ {"version":3,"sources":["../src/editorContext.ts"],"sourcesContent":["import { createPushChannel } from './pushChannel';\nimport { EDITOR_CONTEXT, REQUEST_EDITOR_CONTEXT } from './generated/protocol';\n\n/**\n * The editor \"dirty set\" mirrored from the immediately.run host into the sandbox\n * (UI_AS_APPS_SPEC §5.3): which files the user has changed but not yet saved.\n *\n * This is the ELEVATED `editor:read` capability — only a system app whose binding\n * grants it (e.g. the contribute dialog) receives it. The active file and ref are\n * already available to every app via routing (`useNavigationState`), so this\n * channel carries only the genuine delta: the unsaved paths. An app without\n * `editor:read` simply sees an empty dirty set.\n */\nexport interface EditorContext {\n /** Repo-relative paths the user has modified but not yet saved. */\n dirtyPaths: string[];\n /** Repo-relative paths currently open as tabs in the host editor (§4.2). */\n openFiles: string[];\n /**\n * The one file currently focused in the host editor (repo-relative, leading\n * slash — e.g. `/src/index.ts`), or `null` when no file is open. Distinct from\n * {@link dirtyPaths} (the unsaved SET) and {@link openFiles} (the open-tab set):\n * this is the single ACTIVE file. A baseline app reads its own active file from\n * the route, but a self-routed system panel (`drivesHostRoute=false`, e.g. the\n * file explorer) does not see the host editor's route, so it receives the active\n * file here instead (UI_AS_APPS_SPEC §5.3 self-routed-panel refinement).\n */\n activeFile: string | null;\n /**\n * The viewed-document HINT (R3-268): the working-tree file the STAGE app is\n * currently rendering (leading slash), or `null`. Distinct from\n * {@link activeFile} (the editor's focus): this is what is ON STAGE. It is an\n * app-supplied claim the host validated for existence only — consume it as a\n * highlight and nothing more (never scroll, move focus, or switch files on\n * it; that contract is what keeps the signal activation-free).\n */\n viewedFile: string | null;\n}\n\n// Read over the transport (SDK_PACKAGING_SPEC §4): the host pushes `editor-context`\n// and answers `request-editor-context` — but only for a frame holding `editor:read`\n// (gated by the channel router). An app without it gets no reply, so the empty\n// default below stands. Wire format: site-main channelBridge.ts.\nconst isStringArray = (v: unknown): v is string[] =>\n Array.isArray(v) && v.every((p) => typeof p === 'string');\n\nconst channel = createPushChannel<EditorContext>({\n pushType: EDITOR_CONTEXT,\n requestType: REQUEST_EDITOR_CONTEXT,\n initial: { dirtyPaths: [], openFiles: [], activeFile: null, viewedFile: null },\n parse: (msg) =>\n isStringArray(msg.dirtyPaths)\n ? {\n dirtyPaths: msg.dirtyPaths,\n // `openFiles` is newer than `dirtyPaths`; tolerate an older host that\n // omits it (defensive SDK — defaults to empty rather than rejecting).\n openFiles: isStringArray(msg.openFiles) ? msg.openFiles : [],\n // `activeFile` is newer still; an older host that omits it (or sends a\n // non-string) reads as `null` — no file highlighted, never a throw.\n activeFile: typeof msg.activeFile === 'string' ? msg.activeFile : null,\n // `viewedFile` is the newest (R3-268); same tolerance — an older host\n // that omits it reads as `null` (no stage highlight), never a throw.\n viewedFile: typeof msg.viewedFile === 'string' ? msg.viewedFile : null,\n }\n : undefined,\n});\n\n/**\n * Returns the current editor context (dirty set). Poll this for a one-off read;\n * use {@link onEditorContextChange} or {@link useEditorContext} to react.\n */\nexport const getEditorContext = (): EditorContext => channel.get();\n\n/**\n * Subscribe to editor-context changes. The listener is invoked immediately with\n * the current context, then again on every change. Returns an unsubscribe fn.\n */\nexport const onEditorContextChange = (listener: (context: EditorContext) => void): (() => void) =>\n channel.onChange(listener);\n\n/**\n * React hook returning the current editor context (dirty set), re-rendering when\n * it changes. Handy for a contribute dialog: `const { dirtyPaths } =\n * useEditorContext()` to show \"you'll save N files\" before calling `contribute()`.\n */\nexport const useEditorContext = (): EditorContext => channel.use();\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,yBAAkC;AAClC,sBAAuD;AA0CvD,MAAM,gBAAgB,CAAC,MACrB,MAAM,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,OAAO,MAAM,QAAQ;AAE1D,MAAM,cAAU,sCAAiC;AAAA,EAC/C,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS,EAAE,YAAY,CAAC,GAAG,WAAW,CAAC,GAAG,YAAY,MAAM,YAAY,KAAK;AAAA,EAC7E,OAAO,CAAC,QACN,cAAc,IAAI,UAAU,IACxB;AAAA,IACE,YAAY,IAAI;AAAA;AAAA;AAAA,IAGhB,WAAW,cAAc,IAAI,SAAS,IAAI,IAAI,YAAY,CAAC;AAAA;AAAA;AAAA,IAG3D,YAAY,OAAO,IAAI,eAAe,WAAW,IAAI,aAAa;AAAA;AAAA;AAAA,IAGlE,YAAY,OAAO,IAAI,eAAe,WAAW,IAAI,aAAa;AAAA,EACpE,IACA;AACR,CAAC;AAMM,MAAM,mBAAmB,MAAqB,QAAQ,IAAI;AAM1D,MAAM,wBAAwB,CAAC,aACpC,QAAQ,SAAS,QAAQ;AAOpB,MAAM,mBAAmB,MAAqB,QAAQ,IAAI;","names":[]}
@@ -1,9 +1,10 @@
1
1
  import "./chunk-VHAA22YE.js";
2
2
  import { createPushChannel } from "./pushChannel";
3
+ import { EDITOR_CONTEXT, REQUEST_EDITOR_CONTEXT } from "./generated/protocol";
3
4
  const isStringArray = (v) => Array.isArray(v) && v.every((p) => typeof p === "string");
4
5
  const channel = createPushChannel({
5
- pushType: "editor-context",
6
- requestType: "request-editor-context",
6
+ pushType: EDITOR_CONTEXT,
7
+ requestType: REQUEST_EDITOR_CONTEXT,
7
8
  initial: { dirtyPaths: [], openFiles: [], activeFile: null, viewedFile: null },
8
9
  parse: (msg) => isStringArray(msg.dirtyPaths) ? {
9
10
  dirtyPaths: msg.dirtyPaths,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/editorContext.ts"],"sourcesContent":["import { createPushChannel } from './pushChannel';\n\n/**\n * The editor \"dirty set\" mirrored from the immediately.run host into the sandbox\n * (UI_AS_APPS_SPEC §5.3): which files the user has changed but not yet saved.\n *\n * This is the ELEVATED `editor:read` capability — only a system app whose binding\n * grants it (e.g. the contribute dialog) receives it. The active file and ref are\n * already available to every app via routing (`useNavigationState`), so this\n * channel carries only the genuine delta: the unsaved paths. An app without\n * `editor:read` simply sees an empty dirty set.\n */\nexport interface EditorContext {\n /** Repo-relative paths the user has modified but not yet saved. */\n dirtyPaths: string[];\n /** Repo-relative paths currently open as tabs in the host editor (§4.2). */\n openFiles: string[];\n /**\n * The one file currently focused in the host editor (repo-relative, leading\n * slash — e.g. `/src/index.ts`), or `null` when no file is open. Distinct from\n * {@link dirtyPaths} (the unsaved SET) and {@link openFiles} (the open-tab set):\n * this is the single ACTIVE file. A baseline app reads its own active file from\n * the route, but a self-routed system panel (`drivesHostRoute=false`, e.g. the\n * file explorer) does not see the host editor's route, so it receives the active\n * file here instead (UI_AS_APPS_SPEC §5.3 self-routed-panel refinement).\n */\n activeFile: string | null;\n /**\n * The viewed-document HINT (R3-268): the working-tree file the STAGE app is\n * currently rendering (leading slash), or `null`. Distinct from\n * {@link activeFile} (the editor's focus): this is what is ON STAGE. It is an\n * app-supplied claim the host validated for existence only — consume it as a\n * highlight and nothing more (never scroll, move focus, or switch files on\n * it; that contract is what keeps the signal activation-free).\n */\n viewedFile: string | null;\n}\n\n// Read over the transport (SDK_PACKAGING_SPEC §4): the host pushes `editor-context`\n// and answers `request-editor-context` — but only for a frame holding `editor:read`\n// (gated by the channel router). An app without it gets no reply, so the empty\n// default below stands. Wire format: site-main channelBridge.ts.\nconst isStringArray = (v: unknown): v is string[] =>\n Array.isArray(v) && v.every((p) => typeof p === 'string');\n\nconst channel = createPushChannel<EditorContext>({\n pushType: 'editor-context',\n requestType: 'request-editor-context',\n initial: { dirtyPaths: [], openFiles: [], activeFile: null, viewedFile: null },\n parse: (msg) =>\n isStringArray(msg.dirtyPaths)\n ? {\n dirtyPaths: msg.dirtyPaths,\n // `openFiles` is newer than `dirtyPaths`; tolerate an older host that\n // omits it (defensive SDK — defaults to empty rather than rejecting).\n openFiles: isStringArray(msg.openFiles) ? msg.openFiles : [],\n // `activeFile` is newer still; an older host that omits it (or sends a\n // non-string) reads as `null` — no file highlighted, never a throw.\n activeFile: typeof msg.activeFile === 'string' ? msg.activeFile : null,\n // `viewedFile` is the newest (R3-268); same tolerance — an older host\n // that omits it reads as `null` (no stage highlight), never a throw.\n viewedFile: typeof msg.viewedFile === 'string' ? msg.viewedFile : null,\n }\n : undefined,\n});\n\n/**\n * Returns the current editor context (dirty set). Poll this for a one-off read;\n * use {@link onEditorContextChange} or {@link useEditorContext} to react.\n */\nexport const getEditorContext = (): EditorContext => channel.get();\n\n/**\n * Subscribe to editor-context changes. The listener is invoked immediately with\n * the current context, then again on every change. Returns an unsubscribe fn.\n */\nexport const onEditorContextChange = (listener: (context: EditorContext) => void): (() => void) =>\n channel.onChange(listener);\n\n/**\n * React hook returning the current editor context (dirty set), re-rendering when\n * it changes. Handy for a contribute dialog: `const { dirtyPaths } =\n * useEditorContext()` to show \"you'll save N files\" before calling `contribute()`.\n */\nexport const useEditorContext = (): EditorContext => channel.use();\n"],"mappings":";AAAA,SAAS,yBAAyB;AA0ClC,MAAM,gBAAgB,CAAC,MACrB,MAAM,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,OAAO,MAAM,QAAQ;AAE1D,MAAM,UAAU,kBAAiC;AAAA,EAC/C,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS,EAAE,YAAY,CAAC,GAAG,WAAW,CAAC,GAAG,YAAY,MAAM,YAAY,KAAK;AAAA,EAC7E,OAAO,CAAC,QACN,cAAc,IAAI,UAAU,IACxB;AAAA,IACE,YAAY,IAAI;AAAA;AAAA;AAAA,IAGhB,WAAW,cAAc,IAAI,SAAS,IAAI,IAAI,YAAY,CAAC;AAAA;AAAA;AAAA,IAG3D,YAAY,OAAO,IAAI,eAAe,WAAW,IAAI,aAAa;AAAA;AAAA;AAAA,IAGlE,YAAY,OAAO,IAAI,eAAe,WAAW,IAAI,aAAa;AAAA,EACpE,IACA;AACR,CAAC;AAMM,MAAM,mBAAmB,MAAqB,QAAQ,IAAI;AAM1D,MAAM,wBAAwB,CAAC,aACpC,QAAQ,SAAS,QAAQ;AAOpB,MAAM,mBAAmB,MAAqB,QAAQ,IAAI;","names":[]}
1
+ {"version":3,"sources":["../src/editorContext.ts"],"sourcesContent":["import { createPushChannel } from './pushChannel';\nimport { EDITOR_CONTEXT, REQUEST_EDITOR_CONTEXT } from './generated/protocol';\n\n/**\n * The editor \"dirty set\" mirrored from the immediately.run host into the sandbox\n * (UI_AS_APPS_SPEC §5.3): which files the user has changed but not yet saved.\n *\n * This is the ELEVATED `editor:read` capability — only a system app whose binding\n * grants it (e.g. the contribute dialog) receives it. The active file and ref are\n * already available to every app via routing (`useNavigationState`), so this\n * channel carries only the genuine delta: the unsaved paths. An app without\n * `editor:read` simply sees an empty dirty set.\n */\nexport interface EditorContext {\n /** Repo-relative paths the user has modified but not yet saved. */\n dirtyPaths: string[];\n /** Repo-relative paths currently open as tabs in the host editor (§4.2). */\n openFiles: string[];\n /**\n * The one file currently focused in the host editor (repo-relative, leading\n * slash — e.g. `/src/index.ts`), or `null` when no file is open. Distinct from\n * {@link dirtyPaths} (the unsaved SET) and {@link openFiles} (the open-tab set):\n * this is the single ACTIVE file. A baseline app reads its own active file from\n * the route, but a self-routed system panel (`drivesHostRoute=false`, e.g. the\n * file explorer) does not see the host editor's route, so it receives the active\n * file here instead (UI_AS_APPS_SPEC §5.3 self-routed-panel refinement).\n */\n activeFile: string | null;\n /**\n * The viewed-document HINT (R3-268): the working-tree file the STAGE app is\n * currently rendering (leading slash), or `null`. Distinct from\n * {@link activeFile} (the editor's focus): this is what is ON STAGE. It is an\n * app-supplied claim the host validated for existence only — consume it as a\n * highlight and nothing more (never scroll, move focus, or switch files on\n * it; that contract is what keeps the signal activation-free).\n */\n viewedFile: string | null;\n}\n\n// Read over the transport (SDK_PACKAGING_SPEC §4): the host pushes `editor-context`\n// and answers `request-editor-context` — but only for a frame holding `editor:read`\n// (gated by the channel router). An app without it gets no reply, so the empty\n// default below stands. Wire format: site-main channelBridge.ts.\nconst isStringArray = (v: unknown): v is string[] =>\n Array.isArray(v) && v.every((p) => typeof p === 'string');\n\nconst channel = createPushChannel<EditorContext>({\n pushType: EDITOR_CONTEXT,\n requestType: REQUEST_EDITOR_CONTEXT,\n initial: { dirtyPaths: [], openFiles: [], activeFile: null, viewedFile: null },\n parse: (msg) =>\n isStringArray(msg.dirtyPaths)\n ? {\n dirtyPaths: msg.dirtyPaths,\n // `openFiles` is newer than `dirtyPaths`; tolerate an older host that\n // omits it (defensive SDK — defaults to empty rather than rejecting).\n openFiles: isStringArray(msg.openFiles) ? msg.openFiles : [],\n // `activeFile` is newer still; an older host that omits it (or sends a\n // non-string) reads as `null` — no file highlighted, never a throw.\n activeFile: typeof msg.activeFile === 'string' ? msg.activeFile : null,\n // `viewedFile` is the newest (R3-268); same tolerance — an older host\n // that omits it reads as `null` (no stage highlight), never a throw.\n viewedFile: typeof msg.viewedFile === 'string' ? msg.viewedFile : null,\n }\n : undefined,\n});\n\n/**\n * Returns the current editor context (dirty set). Poll this for a one-off read;\n * use {@link onEditorContextChange} or {@link useEditorContext} to react.\n */\nexport const getEditorContext = (): EditorContext => channel.get();\n\n/**\n * Subscribe to editor-context changes. The listener is invoked immediately with\n * the current context, then again on every change. Returns an unsubscribe fn.\n */\nexport const onEditorContextChange = (listener: (context: EditorContext) => void): (() => void) =>\n channel.onChange(listener);\n\n/**\n * React hook returning the current editor context (dirty set), re-rendering when\n * it changes. Handy for a contribute dialog: `const { dirtyPaths } =\n * useEditorContext()` to show \"you'll save N files\" before calling `contribute()`.\n */\nexport const useEditorContext = (): EditorContext => channel.use();\n"],"mappings":";AAAA,SAAS,yBAAyB;AAClC,SAAS,gBAAgB,8BAA8B;AA0CvD,MAAM,gBAAgB,CAAC,MACrB,MAAM,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,OAAO,MAAM,QAAQ;AAE1D,MAAM,UAAU,kBAAiC;AAAA,EAC/C,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS,EAAE,YAAY,CAAC,GAAG,WAAW,CAAC,GAAG,YAAY,MAAM,YAAY,KAAK;AAAA,EAC7E,OAAO,CAAC,QACN,cAAc,IAAI,UAAU,IACxB;AAAA,IACE,YAAY,IAAI;AAAA;AAAA;AAAA,IAGhB,WAAW,cAAc,IAAI,SAAS,IAAI,IAAI,YAAY,CAAC;AAAA;AAAA;AAAA,IAG3D,YAAY,OAAO,IAAI,eAAe,WAAW,IAAI,aAAa;AAAA;AAAA;AAAA,IAGlE,YAAY,OAAO,IAAI,eAAe,WAAW,IAAI,aAAa;AAAA,EACpE,IACA;AACR,CAAC;AAMM,MAAM,mBAAmB,MAAqB,QAAQ,IAAI;AAM1D,MAAM,wBAAwB,CAAC,aACpC,QAAQ,SAAS,QAAQ;AAOpB,MAAM,mBAAmB,MAAqB,QAAQ,IAAI;","names":[]}
@@ -24,6 +24,7 @@ __export(formFactor_exports, {
24
24
  });
25
25
  module.exports = __toCommonJS(formFactor_exports);
26
26
  var import_pushChannel = require("./pushChannel");
27
+ var import_protocol = require("./generated/protocol");
27
28
  const DEFAULT_FORM_FACTOR = {
28
29
  class: "desktop",
29
30
  orientation: "landscape",
@@ -35,8 +36,8 @@ const isFormFactor = (v) => {
35
36
  return !!f && (f.class === "mobile" || f.class === "tablet" || f.class === "desktop") && (f.orientation === "portrait" || f.orientation === "landscape") && typeof f.width === "number" && typeof f.height === "number";
36
37
  };
37
38
  const channel = (0, import_pushChannel.createPushChannel)({
38
- pushType: "form-factor",
39
- requestType: "request-form-factor",
39
+ pushType: import_protocol.FORM_FACTOR,
40
+ requestType: import_protocol.REQUEST_FORM_FACTOR,
40
41
  initial: DEFAULT_FORM_FACTOR,
41
42
  parse: (msg) => isFormFactor(msg.formFactor) ? msg.formFactor : void 0
42
43
  });
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/formFactor.ts"],"sourcesContent":["import { createPushChannel } from './pushChannel';\n\n/**\n * The form factor of the surface your app is rendered into, mirrored from the\n * immediately.run host (UI_AS_APPS_SPEC §5.4.1). Read this to lay out\n * responsively — a narrow chrome panel, a full preview, or a mobile carousel\n * pane all report their box here. The host is the source of truth (it owns the\n * region); you cannot reliably measure your own viewport across the sandbox\n * boundary.\n *\n * Baseline capability `formFactor:read` — every app may read it.\n */\nexport type FormFactorClass = 'mobile' | 'tablet' | 'desktop';\n/** Whether the rendered surface is taller than wide (`portrait`) or wider (`landscape`). */\nexport type Orientation = 'portrait' | 'landscape';\n\n/** The host-reported size class, orientation, and pixel box of your app's surface. */\nexport interface FormFactor {\n class: FormFactorClass;\n orientation: Orientation;\n width: number;\n height: number;\n}\n\n/** Assumed before the host reports — a reasonable desktop default. */\nconst DEFAULT_FORM_FACTOR: FormFactor = {\n class: 'desktop',\n orientation: 'landscape',\n width: 1280,\n height: 800,\n};\n\nconst isFormFactor = (v: unknown): v is FormFactor => {\n const f = v as Partial<FormFactor> | null;\n return (\n !!f &&\n (f.class === 'mobile' || f.class === 'tablet' || f.class === 'desktop') &&\n (f.orientation === 'portrait' || f.orientation === 'landscape') &&\n typeof f.width === 'number' &&\n typeof f.height === 'number'\n );\n};\n\n// Read over the transport (SDK_PACKAGING_SPEC §4): the host pushes `form-factor`\n// and answers `request-form-factor` (wire format: site-main channelBridge.ts).\nconst channel = createPushChannel<FormFactor>({\n pushType: 'form-factor',\n requestType: 'request-form-factor',\n initial: DEFAULT_FORM_FACTOR,\n parse: (msg) => (isFormFactor(msg.formFactor) ? (msg.formFactor as FormFactor) : undefined),\n});\n\n/** Returns the current form factor. Poll for a one-off read. */\nexport const getFormFactor = (): FormFactor => channel.get();\n\n/**\n * Subscribe to form-factor changes. The listener is invoked immediately with\n * the current value, then again on every change. Returns an unsubscribe fn.\n */\nexport const onFormFactorChange = (listener: (formFactor: FormFactor) => void): (() => void) =>\n channel.onChange(listener);\n\n/** React hook returning the current form factor, re-rendering on change. */\nexport const useFormFactor = (): FormFactor => channel.use();\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,yBAAkC;AAyBlC,MAAM,sBAAkC;AAAA,EACtC,OAAO;AAAA,EACP,aAAa;AAAA,EACb,OAAO;AAAA,EACP,QAAQ;AACV;AAEA,MAAM,eAAe,CAAC,MAAgC;AACpD,QAAM,IAAI;AACV,SACE,CAAC,CAAC,MACD,EAAE,UAAU,YAAY,EAAE,UAAU,YAAY,EAAE,UAAU,eAC5D,EAAE,gBAAgB,cAAc,EAAE,gBAAgB,gBACnD,OAAO,EAAE,UAAU,YACnB,OAAO,EAAE,WAAW;AAExB;AAIA,MAAM,cAAU,sCAA8B;AAAA,EAC5C,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS;AAAA,EACT,OAAO,CAAC,QAAS,aAAa,IAAI,UAAU,IAAK,IAAI,aAA4B;AACnF,CAAC;AAGM,MAAM,gBAAgB,MAAkB,QAAQ,IAAI;AAMpD,MAAM,qBAAqB,CAAC,aACjC,QAAQ,SAAS,QAAQ;AAGpB,MAAM,gBAAgB,MAAkB,QAAQ,IAAI;","names":[]}
1
+ {"version":3,"sources":["../src/formFactor.ts"],"sourcesContent":["import { createPushChannel } from './pushChannel';\nimport { FORM_FACTOR, REQUEST_FORM_FACTOR } from './generated/protocol';\n\n/**\n * The form factor of the surface your app is rendered into, mirrored from the\n * immediately.run host (UI_AS_APPS_SPEC §5.4.1). Read this to lay out\n * responsively — a narrow chrome panel, a full preview, or a mobile carousel\n * pane all report their box here. The host is the source of truth (it owns the\n * region); you cannot reliably measure your own viewport across the sandbox\n * boundary.\n *\n * Baseline capability `formFactor:read` — every app may read it.\n */\nexport type FormFactorClass = 'mobile' | 'tablet' | 'desktop';\n/** Whether the rendered surface is taller than wide (`portrait`) or wider (`landscape`). */\nexport type Orientation = 'portrait' | 'landscape';\n\n/** The host-reported size class, orientation, and pixel box of your app's surface. */\nexport interface FormFactor {\n class: FormFactorClass;\n orientation: Orientation;\n width: number;\n height: number;\n}\n\n/** Assumed before the host reports — a reasonable desktop default. */\nconst DEFAULT_FORM_FACTOR: FormFactor = {\n class: 'desktop',\n orientation: 'landscape',\n width: 1280,\n height: 800,\n};\n\nconst isFormFactor = (v: unknown): v is FormFactor => {\n const f = v as Partial<FormFactor> | null;\n return (\n !!f &&\n (f.class === 'mobile' || f.class === 'tablet' || f.class === 'desktop') &&\n (f.orientation === 'portrait' || f.orientation === 'landscape') &&\n typeof f.width === 'number' &&\n typeof f.height === 'number'\n );\n};\n\n// Read over the transport (SDK_PACKAGING_SPEC §4): the host pushes `form-factor`\n// and answers `request-form-factor` (wire format: site-main channelBridge.ts).\nconst channel = createPushChannel<FormFactor>({\n pushType: FORM_FACTOR,\n requestType: REQUEST_FORM_FACTOR,\n initial: DEFAULT_FORM_FACTOR,\n parse: (msg) => (isFormFactor(msg.formFactor) ? (msg.formFactor as FormFactor) : undefined),\n});\n\n/** Returns the current form factor. Poll for a one-off read. */\nexport const getFormFactor = (): FormFactor => channel.get();\n\n/**\n * Subscribe to form-factor changes. The listener is invoked immediately with\n * the current value, then again on every change. Returns an unsubscribe fn.\n */\nexport const onFormFactorChange = (listener: (formFactor: FormFactor) => void): (() => void) =>\n channel.onChange(listener);\n\n/** React hook returning the current form factor, re-rendering on change. */\nexport const useFormFactor = (): FormFactor => channel.use();\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,yBAAkC;AAClC,sBAAiD;AAyBjD,MAAM,sBAAkC;AAAA,EACtC,OAAO;AAAA,EACP,aAAa;AAAA,EACb,OAAO;AAAA,EACP,QAAQ;AACV;AAEA,MAAM,eAAe,CAAC,MAAgC;AACpD,QAAM,IAAI;AACV,SACE,CAAC,CAAC,MACD,EAAE,UAAU,YAAY,EAAE,UAAU,YAAY,EAAE,UAAU,eAC5D,EAAE,gBAAgB,cAAc,EAAE,gBAAgB,gBACnD,OAAO,EAAE,UAAU,YACnB,OAAO,EAAE,WAAW;AAExB;AAIA,MAAM,cAAU,sCAA8B;AAAA,EAC5C,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS;AAAA,EACT,OAAO,CAAC,QAAS,aAAa,IAAI,UAAU,IAAK,IAAI,aAA4B;AACnF,CAAC;AAGM,MAAM,gBAAgB,MAAkB,QAAQ,IAAI;AAMpD,MAAM,qBAAqB,CAAC,aACjC,QAAQ,SAAS,QAAQ;AAGpB,MAAM,gBAAgB,MAAkB,QAAQ,IAAI;","names":[]}
@@ -1,5 +1,6 @@
1
1
  import "./chunk-VHAA22YE.js";
2
2
  import { createPushChannel } from "./pushChannel";
3
+ import { FORM_FACTOR, REQUEST_FORM_FACTOR } from "./generated/protocol";
3
4
  const DEFAULT_FORM_FACTOR = {
4
5
  class: "desktop",
5
6
  orientation: "landscape",
@@ -11,8 +12,8 @@ const isFormFactor = (v) => {
11
12
  return !!f && (f.class === "mobile" || f.class === "tablet" || f.class === "desktop") && (f.orientation === "portrait" || f.orientation === "landscape") && typeof f.width === "number" && typeof f.height === "number";
12
13
  };
13
14
  const channel = createPushChannel({
14
- pushType: "form-factor",
15
- requestType: "request-form-factor",
15
+ pushType: FORM_FACTOR,
16
+ requestType: REQUEST_FORM_FACTOR,
16
17
  initial: DEFAULT_FORM_FACTOR,
17
18
  parse: (msg) => isFormFactor(msg.formFactor) ? msg.formFactor : void 0
18
19
  });
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/formFactor.ts"],"sourcesContent":["import { createPushChannel } from './pushChannel';\n\n/**\n * The form factor of the surface your app is rendered into, mirrored from the\n * immediately.run host (UI_AS_APPS_SPEC §5.4.1). Read this to lay out\n * responsively — a narrow chrome panel, a full preview, or a mobile carousel\n * pane all report their box here. The host is the source of truth (it owns the\n * region); you cannot reliably measure your own viewport across the sandbox\n * boundary.\n *\n * Baseline capability `formFactor:read` — every app may read it.\n */\nexport type FormFactorClass = 'mobile' | 'tablet' | 'desktop';\n/** Whether the rendered surface is taller than wide (`portrait`) or wider (`landscape`). */\nexport type Orientation = 'portrait' | 'landscape';\n\n/** The host-reported size class, orientation, and pixel box of your app's surface. */\nexport interface FormFactor {\n class: FormFactorClass;\n orientation: Orientation;\n width: number;\n height: number;\n}\n\n/** Assumed before the host reports — a reasonable desktop default. */\nconst DEFAULT_FORM_FACTOR: FormFactor = {\n class: 'desktop',\n orientation: 'landscape',\n width: 1280,\n height: 800,\n};\n\nconst isFormFactor = (v: unknown): v is FormFactor => {\n const f = v as Partial<FormFactor> | null;\n return (\n !!f &&\n (f.class === 'mobile' || f.class === 'tablet' || f.class === 'desktop') &&\n (f.orientation === 'portrait' || f.orientation === 'landscape') &&\n typeof f.width === 'number' &&\n typeof f.height === 'number'\n );\n};\n\n// Read over the transport (SDK_PACKAGING_SPEC §4): the host pushes `form-factor`\n// and answers `request-form-factor` (wire format: site-main channelBridge.ts).\nconst channel = createPushChannel<FormFactor>({\n pushType: 'form-factor',\n requestType: 'request-form-factor',\n initial: DEFAULT_FORM_FACTOR,\n parse: (msg) => (isFormFactor(msg.formFactor) ? (msg.formFactor as FormFactor) : undefined),\n});\n\n/** Returns the current form factor. Poll for a one-off read. */\nexport const getFormFactor = (): FormFactor => channel.get();\n\n/**\n * Subscribe to form-factor changes. The listener is invoked immediately with\n * the current value, then again on every change. Returns an unsubscribe fn.\n */\nexport const onFormFactorChange = (listener: (formFactor: FormFactor) => void): (() => void) =>\n channel.onChange(listener);\n\n/** React hook returning the current form factor, re-rendering on change. */\nexport const useFormFactor = (): FormFactor => channel.use();\n"],"mappings":";AAAA,SAAS,yBAAyB;AAyBlC,MAAM,sBAAkC;AAAA,EACtC,OAAO;AAAA,EACP,aAAa;AAAA,EACb,OAAO;AAAA,EACP,QAAQ;AACV;AAEA,MAAM,eAAe,CAAC,MAAgC;AACpD,QAAM,IAAI;AACV,SACE,CAAC,CAAC,MACD,EAAE,UAAU,YAAY,EAAE,UAAU,YAAY,EAAE,UAAU,eAC5D,EAAE,gBAAgB,cAAc,EAAE,gBAAgB,gBACnD,OAAO,EAAE,UAAU,YACnB,OAAO,EAAE,WAAW;AAExB;AAIA,MAAM,UAAU,kBAA8B;AAAA,EAC5C,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS;AAAA,EACT,OAAO,CAAC,QAAS,aAAa,IAAI,UAAU,IAAK,IAAI,aAA4B;AACnF,CAAC;AAGM,MAAM,gBAAgB,MAAkB,QAAQ,IAAI;AAMpD,MAAM,qBAAqB,CAAC,aACjC,QAAQ,SAAS,QAAQ;AAGpB,MAAM,gBAAgB,MAAkB,QAAQ,IAAI;","names":[]}
1
+ {"version":3,"sources":["../src/formFactor.ts"],"sourcesContent":["import { createPushChannel } from './pushChannel';\nimport { FORM_FACTOR, REQUEST_FORM_FACTOR } from './generated/protocol';\n\n/**\n * The form factor of the surface your app is rendered into, mirrored from the\n * immediately.run host (UI_AS_APPS_SPEC §5.4.1). Read this to lay out\n * responsively — a narrow chrome panel, a full preview, or a mobile carousel\n * pane all report their box here. The host is the source of truth (it owns the\n * region); you cannot reliably measure your own viewport across the sandbox\n * boundary.\n *\n * Baseline capability `formFactor:read` — every app may read it.\n */\nexport type FormFactorClass = 'mobile' | 'tablet' | 'desktop';\n/** Whether the rendered surface is taller than wide (`portrait`) or wider (`landscape`). */\nexport type Orientation = 'portrait' | 'landscape';\n\n/** The host-reported size class, orientation, and pixel box of your app's surface. */\nexport interface FormFactor {\n class: FormFactorClass;\n orientation: Orientation;\n width: number;\n height: number;\n}\n\n/** Assumed before the host reports — a reasonable desktop default. */\nconst DEFAULT_FORM_FACTOR: FormFactor = {\n class: 'desktop',\n orientation: 'landscape',\n width: 1280,\n height: 800,\n};\n\nconst isFormFactor = (v: unknown): v is FormFactor => {\n const f = v as Partial<FormFactor> | null;\n return (\n !!f &&\n (f.class === 'mobile' || f.class === 'tablet' || f.class === 'desktop') &&\n (f.orientation === 'portrait' || f.orientation === 'landscape') &&\n typeof f.width === 'number' &&\n typeof f.height === 'number'\n );\n};\n\n// Read over the transport (SDK_PACKAGING_SPEC §4): the host pushes `form-factor`\n// and answers `request-form-factor` (wire format: site-main channelBridge.ts).\nconst channel = createPushChannel<FormFactor>({\n pushType: FORM_FACTOR,\n requestType: REQUEST_FORM_FACTOR,\n initial: DEFAULT_FORM_FACTOR,\n parse: (msg) => (isFormFactor(msg.formFactor) ? (msg.formFactor as FormFactor) : undefined),\n});\n\n/** Returns the current form factor. Poll for a one-off read. */\nexport const getFormFactor = (): FormFactor => channel.get();\n\n/**\n * Subscribe to form-factor changes. The listener is invoked immediately with\n * the current value, then again on every change. Returns an unsubscribe fn.\n */\nexport const onFormFactorChange = (listener: (formFactor: FormFactor) => void): (() => void) =>\n channel.onChange(listener);\n\n/** React hook returning the current form factor, re-rendering on change. */\nexport const useFormFactor = (): FormFactor => channel.use();\n"],"mappings":";AAAA,SAAS,yBAAyB;AAClC,SAAS,aAAa,2BAA2B;AAyBjD,MAAM,sBAAkC;AAAA,EACtC,OAAO;AAAA,EACP,aAAa;AAAA,EACb,OAAO;AAAA,EACP,QAAQ;AACV;AAEA,MAAM,eAAe,CAAC,MAAgC;AACpD,QAAM,IAAI;AACV,SACE,CAAC,CAAC,MACD,EAAE,UAAU,YAAY,EAAE,UAAU,YAAY,EAAE,UAAU,eAC5D,EAAE,gBAAgB,cAAc,EAAE,gBAAgB,gBACnD,OAAO,EAAE,UAAU,YACnB,OAAO,EAAE,WAAW;AAExB;AAIA,MAAM,UAAU,kBAA8B;AAAA,EAC5C,UAAU;AAAA,EACV,aAAa;AAAA,EACb,SAAS;AAAA,EACT,OAAO,CAAC,QAAS,aAAa,IAAI,UAAU,IAAK,IAAI,aAA4B;AACnF,CAAC;AAGM,MAAM,gBAAgB,MAAkB,QAAQ,IAAI;AAMpD,MAAM,qBAAqB,CAAC,aACjC,QAAQ,SAAS,QAAQ;AAGpB,MAAM,gBAAgB,MAAkB,QAAQ,IAAI;","names":[]}
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __copyProps = (to, from, except, desc) => {
7
+ if (from && typeof from === "object" || typeof from === "function") {
8
+ for (let key of __getOwnPropNames(from))
9
+ if (!__hasOwnProp.call(to, key) && key !== except)
10
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
11
+ }
12
+ return to;
13
+ };
14
+ var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
15
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
16
+ var protocol_exports = {};
17
+ module.exports = __toCommonJS(protocol_exports);
18
+ __reExport(protocol_exports, require("@immediately-run/sandbox-protocol/sdk"), module.exports);
19
+ // Annotate the CommonJS export names for ESM import in node:
20
+ 0 && (module.exports = {
21
+ ...require("@immediately-run/sandbox-protocol/sdk")
22
+ });
23
+ //# sourceMappingURL=protocol.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/generated/protocol.ts"],"sourcesContent":["// The sandbox↔SDK wire vocabulary — re-exported from the published contract.\n//\n// Until R3-274b1 this file was a COPY of a module generated in the sandbox repo,\n// carried here by hand because this package does not depend on that one and a build\n// that reads a sibling checkout is the coupling R3-274d removed. It now comes from\n// `@immediately-run/sandbox-protocol`, which owns the descriptors and publishes a\n// module per side (PLATFORM_LAYERING_SPEC §2 / S1 target 1).\n//\n// The file stays at this path because its exports are public API — `api-snapshot.json`\n// pins them, and a fork pinned to an older SDK imports them from here.\n//\n// To change the wire: edit the descriptors in that package, publish, bump the pin.\n// `npm run protocol:check` fails until this repo's source and the pinned contract\n// agree.\nexport * from '@immediately-run/sandbox-protocol/sdk';\n"],"mappings":";;;;;;;;;;;;;;;AAAA;AAAA;AAcA,6BAAc,kDAdd;","names":[]}
@@ -0,0 +1 @@
1
+ export * from '@immediately-run/sandbox-protocol/sdk';
@@ -0,0 +1 @@
1
+ export * from '@immediately-run/sandbox-protocol/sdk';
@@ -0,0 +1,2 @@
1
+ export * from "@immediately-run/sandbox-protocol/sdk";
2
+ //# sourceMappingURL=protocol.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/generated/protocol.ts"],"sourcesContent":["// The sandbox↔SDK wire vocabulary — re-exported from the published contract.\n//\n// Until R3-274b1 this file was a COPY of a module generated in the sandbox repo,\n// carried here by hand because this package does not depend on that one and a build\n// that reads a sibling checkout is the coupling R3-274d removed. It now comes from\n// `@immediately-run/sandbox-protocol`, which owns the descriptors and publishes a\n// module per side (PLATFORM_LAYERING_SPEC §2 / S1 target 1).\n//\n// The file stays at this path because its exports are public API — `api-snapshot.json`\n// pins them, and a fork pinned to an older SDK imports them from here.\n//\n// To change the wire: edit the descriptors in that package, publish, bump the pin.\n// `npm run protocol:check` fails until this repo's source and the pinned contract\n// agree.\nexport * from '@immediately-run/sandbox-protocol/sdk';\n"],"mappings":"AAcA,cAAc;","names":[]}
package/dist/hooks.cjs CHANGED
@@ -24,9 +24,10 @@ __export(hooks_exports, {
24
24
  useObjectUrl: () => useObjectUrl
25
25
  });
26
26
  module.exports = __toCommonJS(hooks_exports);
27
+ var import_platform_constants = require("@immediately-run/platform-constants");
27
28
  var import_react = require("react");
28
- var import_TinkerableContext = require("./TinkerableContext");
29
29
  var import_fs = require("./fs");
30
+ var import_metadataSource = require("./metadataSource");
30
31
  const entriesEqual = (a, b) => {
31
32
  if (a.length !== b.length) {
32
33
  return false;
@@ -35,17 +36,25 @@ const entriesEqual = (a, b) => {
35
36
  if (a[i].path !== b[i].path || a[i].meta !== b[i].meta) {
36
37
  return false;
37
38
  }
39
+ const ax = a[i];
40
+ const bx = b[i];
41
+ const keys = /* @__PURE__ */ new Set([...Object.keys(ax), ...Object.keys(bx)]);
42
+ for (const k of keys) {
43
+ if (k === "path" || k === "meta") continue;
44
+ if (ax[k] !== bx[k]) return false;
45
+ }
38
46
  }
39
47
  return true;
40
48
  };
41
49
  const useMetadataQuery = (queryFunction) => {
42
- const { filesMetadata } = (0, import_react.use)(import_TinkerableContext.TinkerableContext);
50
+ const files = (0, import_metadataSource.useMetadataStore)();
43
51
  const previous = (0, import_react.useRef)([]);
44
52
  return (0, import_react.useMemo)(() => {
45
- const files = filesMetadata ?? {};
46
53
  let entries;
47
54
  try {
48
- entries = queryFunction(files).map((path) => ({ path, meta: files[path] }));
55
+ entries = queryFunction(files).map(
56
+ (selected) => typeof selected === "string" ? { path: selected, meta: files[selected] } : { ...selected, path: selected.path, meta: files[selected.path] }
57
+ );
49
58
  } catch (error) {
50
59
  return { error };
51
60
  }
@@ -54,19 +63,18 @@ const useMetadataQuery = (queryFunction) => {
54
63
  }
55
64
  previous.current = entries;
56
65
  return entries;
57
- }, [filesMetadata, queryFunction]);
66
+ }, [files, queryFunction]);
58
67
  };
59
68
  const useFileMetadata = (path) => {
60
- const { filesMetadata } = (0, import_react.use)(import_TinkerableContext.TinkerableContext);
61
- return (0, import_react.useMemo)(
62
- () => (filesMetadata ?? {})[path],
63
- [path, filesMetadata]
64
- );
65
- };
66
- const useAllMetadata = () => {
67
- const { filesMetadata } = (0, import_react.use)(import_TinkerableContext.TinkerableContext);
68
- return filesMetadata ?? {};
69
+ const files = (0, import_metadataSource.useMetadataStore)();
70
+ return (0, import_react.useMemo)(() => {
71
+ const direct = files[path];
72
+ if (direct !== void 0) return direct;
73
+ if (path === import_platform_constants.APP_ROOT || path.startsWith(`${import_platform_constants.APP_ROOT}/`)) return void 0;
74
+ return files[(0, import_platform_constants.underAppRoot)(path)];
75
+ }, [path, files]);
69
76
  };
77
+ const useAllMetadata = () => (0, import_metadataSource.useMetadataStore)();
70
78
  const useObjectUrl = (mount, relPath, opts) => {
71
79
  const [state, setState] = (0, import_react.useState)({
72
80
  url: null,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/hooks.ts"],"sourcesContent":["import { use, useEffect, useMemo, useRef, useState } from 'react';\nimport { TinkerableContext } from './TinkerableContext';\nimport { openFs, type FsError } from './fs';\nimport type { SandboxMount } from './mounts';\nimport {\n FilesMetadata,\n Metadata,\n MetadataQueryEntry,\n MetadataQueryFunction,\n MetadataQueryResult,\n} from './sandboxTypes';\n\nconst entriesEqual = <T>(a: MetadataQueryEntry<T>[], b: MetadataQueryEntry<T>[]): boolean => {\n if (a.length !== b.length) {\n return false;\n }\n for (let i = 0; i < a.length; i++) {\n // Unchanged frontmatter keeps its object identity across `metadata-update`\n // merges, so an identity check on `meta` is a sound \"did this match change?\".\n if (a[i].path !== b[i].path || a[i].meta !== b[i].meta) {\n return false;\n }\n }\n return true;\n};\n\n/**\n * Query the file metadata store (MDX frontmatter) with a plain JS function.\n *\n * The query receives every file's frontmatter keyed by path and returns the paths\n * that match; the hook resolves each path back to its frontmatter and returns an\n * array of `{ path, meta }` entries — so a single call gives you everything you\n * filtered on, with no second lookup. A throwing query is reported as `{ error }`\n * rather than crashing the render.\n *\n * The query runs synchronously during render (no empty first frame), and the\n * returned array keeps its identity while the matches are unchanged, so it is safe\n * to use directly in downstream `useMemo`/`useEffect` dependency arrays.\n *\n * Pass a type parameter to get typed frontmatter throughout:\n * ```ts\n * interface PostMeta { title: string; date: string; draft?: boolean }\n * const posts = useMetadataQuery<PostMeta>((files) =>\n * Object.entries(files)\n * .filter(([, m]) => !m.draft)\n * .sort(([, a], [, b]) => b.date.localeCompare(a.date))\n * .map(([path]) => path),\n * );\n * ```\n */\nexport const useMetadataQuery = <T = Metadata>(\n queryFunction: MetadataQueryFunction<T>,\n): MetadataQueryResult<T> => {\n const { filesMetadata } = use(TinkerableContext);\n const previous = useRef<MetadataQueryEntry<T>[]>([]);\n return useMemo<MetadataQueryResult<T>>(() => {\n const files = (filesMetadata ?? {}) as FilesMetadata<T>;\n let entries: MetadataQueryEntry<T>[];\n try {\n entries = queryFunction(files).map((path) => ({ path, meta: files[path] }));\n } catch (error) {\n return { error };\n }\n // Preserve the prior array reference when nothing matched differently.\n if (entriesEqual(entries, previous.current)) {\n return previous.current;\n }\n previous.current = entries;\n return entries;\n }, [filesMetadata, queryFunction]);\n};\n\n/**\n * Read one file's metadata (MDX frontmatter) by repo-relative path. Returns\n * `undefined` when the path has no metadata. Pass a type parameter for typed\n * field access.\n */\nexport const useFileMetadata = <T = Metadata>(path: string): T | undefined => {\n const { filesMetadata } = use(TinkerableContext);\n return useMemo(\n () => (filesMetadata ?? {})[path] as T | undefined,\n [path, filesMetadata],\n );\n};\n\n/**\n * The raw, reactive metadata store: a map from file path to frontmatter. The\n * escape hatch for apps that want to render their own index rather than express it\n * as a path-returning query. Pass a type parameter for typed frontmatter values.\n */\nexport const useAllMetadata = <T = Metadata>(): FilesMetadata<T> => {\n const { filesMetadata } = use(TinkerableContext);\n return (filesMetadata ?? {}) as FilesMetadata<T>;\n};\n\n/** The reactive state returned by {@link useObjectUrl}. */\nexport interface ObjectUrlState {\n /** The object URL once the bytes have loaded; `null` while loading or on error. */\n url: string | null;\n /** True while the file is being read. */\n loading: boolean;\n /** The {@link FsError} if the read failed (`not-found`, `unavailable`, …), else `null`. */\n error: FsError | null;\n}\n\n/**\n * Read a file from a mount into an **object URL** for `<img src>`, revoking it\n * automatically on unmount or when `mount`/`relPath` changes. This is the React\n * answer to \"an opaque-origin iframe can't fetch a mount path\": it reads the bytes\n * off the sandbox ZenFS ({@link openFs}) and hands you a URL to drop into an\n * `<img>`, and it owns the create/revoke lifecycle so you never leak a URL.\n *\n * Pass `null`/`undefined` for `mount` or `relPath` to mean \"nothing to load yet\"\n * (idle state, no read). For a ready-made element use `MountImage`.\n *\n * ```tsx\n * const { url, loading, error } = useObjectUrl(mount, 'photos/cat.png');\n * if (loading) return <Spinner />;\n * if (error || !url) return <span>missing</span>;\n * return <img src={url} alt=\"cat\" />;\n * ```\n */\nexport const useObjectUrl = (\n mount: SandboxMount | null | undefined,\n relPath: string | null | undefined,\n opts?: { type?: string },\n): ObjectUrlState => {\n const [state, setState] = useState<ObjectUrlState>({\n url: null,\n loading: Boolean(mount && relPath),\n error: null,\n });\n const type = opts?.type;\n // Key the effect on the mount's stable `path` (its object identity churns as the\n // mount set re-announces) plus the relPath and the optional type override.\n const mountPath = mount?.path;\n useEffect(() => {\n if (!mount || !relPath) {\n setState({ url: null, loading: false, error: null });\n return;\n }\n let alive = true;\n let revoke: (() => void) | null = null;\n setState({ url: null, loading: true, error: null });\n openFs(mount)\n .readObjectUrl(relPath, type ? { type } : undefined)\n .then((res) => {\n // Lost the race (unmounted / prop changed): revoke immediately, don't set.\n if (!alive) {\n res.revoke();\n return;\n }\n revoke = res.revoke;\n setState({ url: res.url, loading: false, error: null });\n })\n .catch((e) => {\n if (alive) setState({ url: null, loading: false, error: e as FsError });\n });\n return () => {\n alive = false;\n if (revoke) revoke();\n };\n // `mount` is intentionally tracked via its stable `mountPath`.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [mountPath, relPath, type]);\n return state;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAA0D;AAC1D,+BAAkC;AAClC,gBAAqC;AAUrC,MAAM,eAAe,CAAI,GAA4B,MAAwC;AAC3F,MAAI,EAAE,WAAW,EAAE,QAAQ;AACzB,WAAO;AAAA,EACT;AACA,WAAS,IAAI,GAAG,IAAI,EAAE,QAAQ,KAAK;AAGjC,QAAI,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,MAAM;AACtD,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AA0BO,MAAM,mBAAmB,CAC9B,kBAC2B;AAC3B,QAAM,EAAE,cAAc,QAAI,kBAAI,0CAAiB;AAC/C,QAAM,eAAW,qBAAgC,CAAC,CAAC;AACnD,aAAO,sBAAgC,MAAM;AAC3C,UAAM,QAAS,iBAAiB,CAAC;AACjC,QAAI;AACJ,QAAI;AACF,gBAAU,cAAc,KAAK,EAAE,IAAI,CAAC,UAAU,EAAE,MAAM,MAAM,MAAM,IAAI,EAAE,EAAE;AAAA,IAC5E,SAAS,OAAO;AACd,aAAO,EAAE,MAAM;AAAA,IACjB;AAEA,QAAI,aAAa,SAAS,SAAS,OAAO,GAAG;AAC3C,aAAO,SAAS;AAAA,IAClB;AACA,aAAS,UAAU;AACnB,WAAO;AAAA,EACT,GAAG,CAAC,eAAe,aAAa,CAAC;AACnC;AAOO,MAAM,kBAAkB,CAAe,SAAgC;AAC5E,QAAM,EAAE,cAAc,QAAI,kBAAI,0CAAiB;AAC/C,aAAO;AAAA,IACL,OAAO,iBAAiB,CAAC,GAAG,IAAI;AAAA,IAChC,CAAC,MAAM,aAAa;AAAA,EACtB;AACF;AAOO,MAAM,iBAAiB,MAAsC;AAClE,QAAM,EAAE,cAAc,QAAI,kBAAI,0CAAiB;AAC/C,SAAQ,iBAAiB,CAAC;AAC5B;AA6BO,MAAM,eAAe,CAC1B,OACA,SACA,SACmB;AACnB,QAAM,CAAC,OAAO,QAAQ,QAAI,uBAAyB;AAAA,IACjD,KAAK;AAAA,IACL,SAAS,QAAQ,SAAS,OAAO;AAAA,IACjC,OAAO;AAAA,EACT,CAAC;AACD,QAAM,OAAO,MAAM;AAGnB,QAAM,YAAY,OAAO;AACzB,8BAAU,MAAM;AACd,QAAI,CAAC,SAAS,CAAC,SAAS;AACtB,eAAS,EAAE,KAAK,MAAM,SAAS,OAAO,OAAO,KAAK,CAAC;AACnD;AAAA,IACF;AACA,QAAI,QAAQ;AACZ,QAAI,SAA8B;AAClC,aAAS,EAAE,KAAK,MAAM,SAAS,MAAM,OAAO,KAAK,CAAC;AAClD,0BAAO,KAAK,EACT,cAAc,SAAS,OAAO,EAAE,KAAK,IAAI,MAAS,EAClD,KAAK,CAAC,QAAQ;AAEb,UAAI,CAAC,OAAO;AACV,YAAI,OAAO;AACX;AAAA,MACF;AACA,eAAS,IAAI;AACb,eAAS,EAAE,KAAK,IAAI,KAAK,SAAS,OAAO,OAAO,KAAK,CAAC;AAAA,IACxD,CAAC,EACA,MAAM,CAAC,MAAM;AACZ,UAAI,MAAO,UAAS,EAAE,KAAK,MAAM,SAAS,OAAO,OAAO,EAAa,CAAC;AAAA,IACxE,CAAC;AACH,WAAO,MAAM;AACX,cAAQ;AACR,UAAI,OAAQ,QAAO;AAAA,IACrB;AAAA,EAGF,GAAG,CAAC,WAAW,SAAS,IAAI,CAAC;AAC7B,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../src/hooks.ts"],"sourcesContent":["import { APP_ROOT, underAppRoot } from '@immediately-run/platform-constants';\nimport { useEffect, useMemo, useRef, useState } from 'react';\nimport { openFs, type FsError } from './fs';\nimport { useMetadataStore } from './metadataSource';\nimport type { SandboxMount } from './mounts';\nimport {\n FilesMetadata,\n Metadata,\n MetadataQueryEntry,\n MetadataQueryFunction,\n MetadataQueryResult,\n} from './sandboxTypes';\n\nconst entriesEqual = <T>(\n a: MetadataQueryEntry<T, Record<string, unknown>>[],\n b: MetadataQueryEntry<T, Record<string, unknown>>[],\n): boolean => {\n if (a.length !== b.length) {\n return false;\n }\n for (let i = 0; i < a.length; i++) {\n // Unchanged frontmatter keeps its object identity across `metadata-update`\n // merges, so an identity check on `meta` is a sound \"did this match change?\".\n if (a[i].path !== b[i].path || a[i].meta !== b[i].meta) {\n return false;\n }\n // Extra fields a record-returning query computed (R3-276) are part of the\n // result, so a change in them is a change in the result — compared by value,\n // one level, because they are derived scalars, not the store's shared objects.\n const ax = a[i] as Record<string, unknown>;\n const bx = b[i] as Record<string, unknown>;\n const keys = new Set([...Object.keys(ax), ...Object.keys(bx)]);\n for (const k of keys) {\n if (k === 'path' || k === 'meta') continue;\n if (ax[k] !== bx[k]) return false;\n }\n }\n return true;\n};\n\n/**\n * Query the file metadata store (MDX frontmatter) with a plain JS function.\n *\n * The query receives every file's frontmatter keyed by path and returns the paths\n * that match; the hook resolves each path back to its frontmatter and returns an\n * array of `{ path, meta }` entries — so a single call gives you everything you\n * filtered on, with no second lookup. A throwing query is reported as `{ error }`\n * rather than crashing the render.\n *\n * The query runs synchronously during render (no empty first frame), and the\n * returned array keeps its identity while the matches are unchanged, so it is safe\n * to use directly in downstream `useMemo`/`useEffect` dependency arrays.\n *\n * A query may return RECORDS instead of bare paths (R3-276) — `{ path, ...extra }`\n * — and the extra fields ride along on each entry, so something derived while\n * selecting does not have to be recomputed downstream:\n * ```ts\n * const posts = useMetadataQuery<PostMeta, { year: string }>((files) =>\n * Object.entries(files).map(([path, m]) => ({ path, year: m.date.slice(0, 4) })),\n * );\n * ```\n * The store it queries is the nearest {@link MetadataSource}, else the host's.\n *\n * Pass a type parameter to get typed frontmatter throughout:\n * ```ts\n * interface PostMeta { title: string; date: string; draft?: boolean }\n * const posts = useMetadataQuery<PostMeta>((files) =>\n * Object.entries(files)\n * .filter(([, m]) => !m.draft)\n * .sort(([, a], [, b]) => b.date.localeCompare(a.date))\n * .map(([path]) => path),\n * );\n * ```\n */\nexport const useMetadataQuery = <T = Metadata, E extends object = {}>(\n queryFunction: MetadataQueryFunction<T>,\n): MetadataQueryResult<T, E> => {\n const files = useMetadataStore<T>();\n const previous = useRef<MetadataQueryEntry<T, Record<string, unknown>>[]>([]);\n return useMemo<MetadataQueryResult<T, E>>(() => {\n let entries: MetadataQueryEntry<T, Record<string, unknown>>[];\n try {\n // `path` and `meta` are applied AFTER the record's own fields: a query cannot\n // shadow the two the hook is responsible for, whatever it happened to name.\n entries = queryFunction(files).map((selected) =>\n typeof selected === 'string'\n ? { path: selected, meta: files[selected] }\n : { ...selected, path: selected.path, meta: files[selected.path] },\n );\n } catch (error) {\n return { error };\n }\n // Preserve the prior array reference when nothing matched differently.\n if (entriesEqual(entries, previous.current)) {\n return previous.current as MetadataQueryEntry<T, E>[];\n }\n previous.current = entries;\n return entries as MetadataQueryEntry<T, E>[];\n }, [files, queryFunction]);\n};\n\n/**\n * Read one file's metadata (MDX frontmatter) by path. Returns `undefined` when the\n * path has no metadata. Pass a type parameter for typed field access.\n *\n * **The store is keyed by ABSOLUTE module path** — `/app/content/post.mdx`, the same\n * identifier `fs`, `module.dynamicImport` and `<Include>` use — not by the\n * repo-relative path this doc claimed until R3-276. Keeping metadata in the file\n * space is what lets an app read a file's metadata and render that same file by the\n * same path (`sandbox/src/bundler/metadataKey.test.ts` pins it).\n *\n * A repo-relative path (`/content/post.mdx`) is accepted as a fallback: if the path\n * is not a key, it is retried under the app root via the shared\n * `underAppRoot` helper (R3-275). That is additive — it only turns a previous\n * `undefined` into a value — and it exists because the old doc told people to pass\n * exactly that form. A path a {@link MetadataSource} provided in some other key\n * space is looked up as given, unchanged.\n */\nexport const useFileMetadata = <T = Metadata>(path: string): T | undefined => {\n const files = useMetadataStore<T>();\n return useMemo(() => {\n const direct = files[path];\n if (direct !== undefined) return direct;\n // The fallback is for a path in the REPO-RELATIVE space. An already-app-rooted\n // path that missed is simply a miss: retrying it would consult `/app/app/…`,\n // which is not a key space anything writes — it would only ever hit by accident.\n if (path === APP_ROOT || path.startsWith(`${APP_ROOT}/`)) return undefined;\n return files[underAppRoot(path)];\n }, [path, files]);\n};\n\n/**\n * The raw, reactive metadata store: a map from file path to frontmatter. The\n * escape hatch for apps that want to render their own index rather than express it\n * as a path-returning query. Pass a type parameter for typed frontmatter values.\n */\nexport const useAllMetadata = <T = Metadata>(): FilesMetadata<T> => useMetadataStore<T>();\n\n/** The reactive state returned by {@link useObjectUrl}. */\nexport interface ObjectUrlState {\n /** The object URL once the bytes have loaded; `null` while loading or on error. */\n url: string | null;\n /** True while the file is being read. */\n loading: boolean;\n /** The {@link FsError} if the read failed (`not-found`, `unavailable`, …), else `null`. */\n error: FsError | null;\n}\n\n/**\n * Read a file from a mount into an **object URL** for `<img src>`, revoking it\n * automatically on unmount or when `mount`/`relPath` changes. This is the React\n * answer to \"an opaque-origin iframe can't fetch a mount path\": it reads the bytes\n * off the sandbox ZenFS ({@link openFs}) and hands you a URL to drop into an\n * `<img>`, and it owns the create/revoke lifecycle so you never leak a URL.\n *\n * Pass `null`/`undefined` for `mount` or `relPath` to mean \"nothing to load yet\"\n * (idle state, no read). For a ready-made element use `MountImage`.\n *\n * ```tsx\n * const { url, loading, error } = useObjectUrl(mount, 'photos/cat.png');\n * if (loading) return <Spinner />;\n * if (error || !url) return <span>missing</span>;\n * return <img src={url} alt=\"cat\" />;\n * ```\n */\nexport const useObjectUrl = (\n mount: SandboxMount | null | undefined,\n relPath: string | null | undefined,\n opts?: { type?: string },\n): ObjectUrlState => {\n const [state, setState] = useState<ObjectUrlState>({\n url: null,\n loading: Boolean(mount && relPath),\n error: null,\n });\n const type = opts?.type;\n // Key the effect on the mount's stable `path` (its object identity churns as the\n // mount set re-announces) plus the relPath and the optional type override.\n const mountPath = mount?.path;\n useEffect(() => {\n if (!mount || !relPath) {\n setState({ url: null, loading: false, error: null });\n return;\n }\n let alive = true;\n let revoke: (() => void) | null = null;\n setState({ url: null, loading: true, error: null });\n openFs(mount)\n .readObjectUrl(relPath, type ? { type } : undefined)\n .then((res) => {\n // Lost the race (unmounted / prop changed): revoke immediately, don't set.\n if (!alive) {\n res.revoke();\n return;\n }\n revoke = res.revoke;\n setState({ url: res.url, loading: false, error: null });\n })\n .catch((e) => {\n if (alive) setState({ url: null, loading: false, error: e as FsError });\n });\n return () => {\n alive = false;\n if (revoke) revoke();\n };\n // `mount` is intentionally tracked via its stable `mountPath`.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [mountPath, relPath, type]);\n return state;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,gCAAuC;AACvC,mBAAqD;AACrD,gBAAqC;AACrC,4BAAiC;AAUjC,MAAM,eAAe,CACnB,GACA,MACY;AACZ,MAAI,EAAE,WAAW,EAAE,QAAQ;AACzB,WAAO;AAAA,EACT;AACA,WAAS,IAAI,GAAG,IAAI,EAAE,QAAQ,KAAK;AAGjC,QAAI,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,MAAM;AACtD,aAAO;AAAA,IACT;AAIA,UAAM,KAAK,EAAE,CAAC;AACd,UAAM,KAAK,EAAE,CAAC;AACd,UAAM,OAAO,oBAAI,IAAI,CAAC,GAAG,OAAO,KAAK,EAAE,GAAG,GAAG,OAAO,KAAK,EAAE,CAAC,CAAC;AAC7D,eAAW,KAAK,MAAM;AACpB,UAAI,MAAM,UAAU,MAAM,OAAQ;AAClC,UAAI,GAAG,CAAC,MAAM,GAAG,CAAC,EAAG,QAAO;AAAA,IAC9B;AAAA,EACF;AACA,SAAO;AACT;AAoCO,MAAM,mBAAmB,CAC9B,kBAC8B;AAC9B,QAAM,YAAQ,wCAAoB;AAClC,QAAM,eAAW,qBAAyD,CAAC,CAAC;AAC5E,aAAO,sBAAmC,MAAM;AAC9C,QAAI;AACJ,QAAI;AAGF,gBAAU,cAAc,KAAK,EAAE;AAAA,QAAI,CAAC,aAClC,OAAO,aAAa,WAChB,EAAE,MAAM,UAAU,MAAM,MAAM,QAAQ,EAAE,IACxC,EAAE,GAAG,UAAU,MAAM,SAAS,MAAM,MAAM,MAAM,SAAS,IAAI,EAAE;AAAA,MACrE;AAAA,IACF,SAAS,OAAO;AACd,aAAO,EAAE,MAAM;AAAA,IACjB;AAEA,QAAI,aAAa,SAAS,SAAS,OAAO,GAAG;AAC3C,aAAO,SAAS;AAAA,IAClB;AACA,aAAS,UAAU;AACnB,WAAO;AAAA,EACT,GAAG,CAAC,OAAO,aAAa,CAAC;AAC3B;AAmBO,MAAM,kBAAkB,CAAe,SAAgC;AAC5E,QAAM,YAAQ,wCAAoB;AAClC,aAAO,sBAAQ,MAAM;AACnB,UAAM,SAAS,MAAM,IAAI;AACzB,QAAI,WAAW,OAAW,QAAO;AAIjC,QAAI,SAAS,sCAAY,KAAK,WAAW,GAAG,kCAAQ,GAAG,EAAG,QAAO;AACjE,WAAO,UAAM,wCAAa,IAAI,CAAC;AAAA,EACjC,GAAG,CAAC,MAAM,KAAK,CAAC;AAClB;AAOO,MAAM,iBAAiB,UAAsC,wCAAoB;AA6BjF,MAAM,eAAe,CAC1B,OACA,SACA,SACmB;AACnB,QAAM,CAAC,OAAO,QAAQ,QAAI,uBAAyB;AAAA,IACjD,KAAK;AAAA,IACL,SAAS,QAAQ,SAAS,OAAO;AAAA,IACjC,OAAO;AAAA,EACT,CAAC;AACD,QAAM,OAAO,MAAM;AAGnB,QAAM,YAAY,OAAO;AACzB,8BAAU,MAAM;AACd,QAAI,CAAC,SAAS,CAAC,SAAS;AACtB,eAAS,EAAE,KAAK,MAAM,SAAS,OAAO,OAAO,KAAK,CAAC;AACnD;AAAA,IACF;AACA,QAAI,QAAQ;AACZ,QAAI,SAA8B;AAClC,aAAS,EAAE,KAAK,MAAM,SAAS,MAAM,OAAO,KAAK,CAAC;AAClD,0BAAO,KAAK,EACT,cAAc,SAAS,OAAO,EAAE,KAAK,IAAI,MAAS,EAClD,KAAK,CAAC,QAAQ;AAEb,UAAI,CAAC,OAAO;AACV,YAAI,OAAO;AACX;AAAA,MACF;AACA,eAAS,IAAI;AACb,eAAS,EAAE,KAAK,IAAI,KAAK,SAAS,OAAO,OAAO,KAAK,CAAC;AAAA,IACxD,CAAC,EACA,MAAM,CAAC,MAAM;AACZ,UAAI,MAAO,UAAS,EAAE,KAAK,MAAM,SAAS,OAAO,OAAO,EAAa,CAAC;AAAA,IACxE,CAAC;AACH,WAAO,MAAM;AACX,cAAQ;AACR,UAAI,OAAQ,QAAO;AAAA,IACrB;AAAA,EAGF,GAAG,CAAC,WAAW,SAAS,IAAI,CAAC;AAC7B,SAAO;AACT;","names":[]}
package/dist/hooks.d.cts CHANGED
@@ -17,6 +17,16 @@ import './tasks.cjs';
17
17
  * returned array keeps its identity while the matches are unchanged, so it is safe
18
18
  * to use directly in downstream `useMemo`/`useEffect` dependency arrays.
19
19
  *
20
+ * A query may return RECORDS instead of bare paths (R3-276) — `{ path, ...extra }`
21
+ * — and the extra fields ride along on each entry, so something derived while
22
+ * selecting does not have to be recomputed downstream:
23
+ * ```ts
24
+ * const posts = useMetadataQuery<PostMeta, { year: string }>((files) =>
25
+ * Object.entries(files).map(([path, m]) => ({ path, year: m.date.slice(0, 4) })),
26
+ * );
27
+ * ```
28
+ * The store it queries is the nearest {@link MetadataSource}, else the host's.
29
+ *
20
30
  * Pass a type parameter to get typed frontmatter throughout:
21
31
  * ```ts
22
32
  * interface PostMeta { title: string; date: string; draft?: boolean }
@@ -28,11 +38,23 @@ import './tasks.cjs';
28
38
  * );
29
39
  * ```
30
40
  */
31
- declare const useMetadataQuery: <T = Metadata>(queryFunction: MetadataQueryFunction<T>) => MetadataQueryResult<T>;
41
+ declare const useMetadataQuery: <T = Metadata, E extends object = {}>(queryFunction: MetadataQueryFunction<T>) => MetadataQueryResult<T, E>;
32
42
  /**
33
- * Read one file's metadata (MDX frontmatter) by repo-relative path. Returns
34
- * `undefined` when the path has no metadata. Pass a type parameter for typed
35
- * field access.
43
+ * Read one file's metadata (MDX frontmatter) by path. Returns `undefined` when the
44
+ * path has no metadata. Pass a type parameter for typed field access.
45
+ *
46
+ * **The store is keyed by ABSOLUTE module path** — `/app/content/post.mdx`, the same
47
+ * identifier `fs`, `module.dynamicImport` and `<Include>` use — not by the
48
+ * repo-relative path this doc claimed until R3-276. Keeping metadata in the file
49
+ * space is what lets an app read a file's metadata and render that same file by the
50
+ * same path (`sandbox/src/bundler/metadataKey.test.ts` pins it).
51
+ *
52
+ * A repo-relative path (`/content/post.mdx`) is accepted as a fallback: if the path
53
+ * is not a key, it is retried under the app root via the shared
54
+ * `underAppRoot` helper (R3-275). That is additive — it only turns a previous
55
+ * `undefined` into a value — and it exists because the old doc told people to pass
56
+ * exactly that form. A path a {@link MetadataSource} provided in some other key
57
+ * space is looked up as given, unchanged.
36
58
  */
37
59
  declare const useFileMetadata: <T = Metadata>(path: string) => T | undefined;
38
60
  /**