@notis_ai/cli 0.2.17 → 0.2.19

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 (395) hide show
  1. package/README.md +542 -146
  2. package/bin/check-runtime.js +15 -0
  3. package/bin/notis.js +2 -0
  4. package/config/notis_app_design_rules.json +320 -0
  5. package/config/portal-route-roots.json +45 -0
  6. package/dist/agent-hooks/notis-agent-hook.mjs +42323 -18258
  7. package/dist/base-skills/notis-cli/.notis-base-skill.json +1 -0
  8. package/dist/base-skills/notis-cli/SKILL.md +51 -280
  9. package/dist/base-skills/notis-cli/references/app-delivery.md +17 -0
  10. package/dist/base-skills/notis-cli/references/intelligence.md +94 -0
  11. package/dist/base-skills/notis-cli/references/native-databases.md +21 -0
  12. package/dist/base-skills/notis-cli/references/tool-examples.md +56 -0
  13. package/dist/base-skills/notis-cli/references/troubleshooting.md +67 -0
  14. package/dist/base-skills/notis-query/.notis-base-skill.json +1 -0
  15. package/dist/base-skills/notis-query/SKILL.md +35 -699
  16. package/dist/base-skills/notis-query/references/database-discovery.md +59 -0
  17. package/dist/base-skills/notis-query/references/documents.md +74 -0
  18. package/dist/base-skills/notis-query/references/query.md +76 -0
  19. package/dist/base-skills/notis-spaces/.notis-base-skill.json +1 -0
  20. package/dist/base-skills/notis-spaces/SKILL.md +218 -0
  21. package/dist/base-skills/notis-spaces/references/architecture.md +116 -0
  22. package/dist/base-skills/notis-spaces/references/context.md +81 -0
  23. package/dist/base-skills/notis-spaces/references/design.md +141 -0
  24. package/dist/base-skills/notis-spaces/references/fixtures.md +29 -0
  25. package/dist/base-skills/notis-spaces/references/memory.md +58 -0
  26. package/dist/base-skills/notis-spaces/references/reading.md +73 -0
  27. package/dist/base-skills/notis-spaces/references/release.md +120 -0
  28. package/dist/base-skills/notis-spaces/references/reports.md +77 -0
  29. package/dist/base-skills/notis-spaces/references/sdk.md +273 -0
  30. package/dist/base-skills/notis-spaces/references/store.md +118 -0
  31. package/dist/base-skills/notis-spaces/references/troubleshooting.md +28 -0
  32. package/dist/base-skills/notis-spaces/references/views.md +229 -0
  33. package/{template/packages → dist}/sdk/package.json +9 -10
  34. package/dist/sdk/src/agentContext.ts +36 -0
  35. package/dist/sdk/src/bundleModule.ts +175 -0
  36. package/dist/sdk/src/components/Dialog.tsx +103 -0
  37. package/dist/sdk/src/components/DocumentEditor.tsx +113 -0
  38. package/dist/sdk/src/components/DocumentPage.tsx +184 -0
  39. package/{template/packages → dist}/sdk/src/components/Markdown.tsx +9 -3
  40. package/{template/packages → dist}/sdk/src/components/MarkdownEditor.tsx +26 -5
  41. package/{template/packages → dist}/sdk/src/components/MultiSelectActionBar.tsx +95 -12
  42. package/{template/packages → dist}/sdk/src/components/MultiSelectCheckbox.tsx +4 -2
  43. package/dist/sdk/src/components/NotisCommentBoundary.tsx +172 -0
  44. package/{template/packages → dist}/sdk/src/components/NotisSelectionBoundary.tsx +9 -5
  45. package/dist/sdk/src/components/RecordComponents.tsx +111 -0
  46. package/dist/sdk/src/components/Skeleton.tsx +38 -0
  47. package/dist/sdk/src/config.ts +31 -0
  48. package/{template/packages → dist}/sdk/src/documents.ts +21 -6
  49. package/dist/sdk/src/hooks/useAction.ts +28 -0
  50. package/dist/sdk/src/hooks/useAgentContext.ts +23 -0
  51. package/{template/packages → dist}/sdk/src/hooks/useBackend.ts +3 -2
  52. package/dist/sdk/src/hooks/useCloudComputer.ts +94 -0
  53. package/dist/sdk/src/hooks/useCloudComputerShell.ts +114 -0
  54. package/{template/packages → dist}/sdk/src/hooks/useCollectionInteractions.ts +157 -147
  55. package/dist/sdk/src/hooks/useDatabaseSchema.ts +50 -0
  56. package/{template/packages → dist}/sdk/src/hooks/useDatabaseSubscription.ts +2 -2
  57. package/dist/sdk/src/hooks/useDocument.ts +51 -0
  58. package/{template/packages → dist}/sdk/src/hooks/useDocuments.ts +25 -59
  59. package/{template/packages → dist}/sdk/src/hooks/useHandover.ts +7 -7
  60. package/dist/sdk/src/hooks/useLongPressSelection.ts +79 -0
  61. package/dist/sdk/src/hooks/useMarqueeSelection.ts +194 -0
  62. package/{template/packages → dist}/sdk/src/hooks/useMultiSelect.ts +2 -8
  63. package/dist/sdk/src/hooks/useNativeDocumentBody.ts +50 -0
  64. package/dist/sdk/src/hooks/useNativeDocuments.ts +143 -0
  65. package/{template/packages → dist}/sdk/src/hooks/useNotis.ts +6 -1
  66. package/dist/sdk/src/hooks/useNotisNavigation.ts +70 -0
  67. package/dist/sdk/src/hooks/useQuery.ts +81 -0
  68. package/dist/sdk/src/hooks/useToolQuery.ts +12 -0
  69. package/dist/sdk/src/hooks/useTopBarSearch.ts +102 -0
  70. package/dist/sdk/src/hooks/useTopBarTitleLeaf.ts +75 -0
  71. package/{template/packages → dist}/sdk/src/hooks/useUpsertDocument.ts +1 -1
  72. package/dist/sdk/src/hooks/useViewParams.ts +195 -0
  73. package/dist/sdk/src/hooks/useViewerReads.ts +129 -0
  74. package/{template/packages → dist}/sdk/src/index.ts +64 -1
  75. package/{template/packages → dist}/sdk/src/interactions/actions.ts +14 -1
  76. package/{template/packages → dist}/sdk/src/interactions/shortcuts.tsx +129 -23
  77. package/dist/sdk/src/interactions/visibility.ts +13 -0
  78. package/{template/packages → dist}/sdk/src/interactions.ts +5 -1
  79. package/dist/sdk/src/nativeData.ts +39 -0
  80. package/{template/packages → dist}/sdk/src/provider.tsx +9 -4
  81. package/dist/sdk/src/queryCache.ts +292 -0
  82. package/dist/sdk/src/queryErrors.ts +51 -0
  83. package/dist/sdk/src/runtime.ts +675 -0
  84. package/dist/sdk/src/runtimeModules.ts +33 -0
  85. package/dist/sdk/src/space.ts +442 -0
  86. package/dist/sdk/src/standardViewParams.ts +36 -0
  87. package/{template/packages → dist}/sdk/src/styles.css +81 -16
  88. package/dist/sdk/src/tailwind.ts +66 -0
  89. package/dist/sdk/src/ui/charts/RenderChartData.tsx +25 -0
  90. package/dist/sdk/src/ui/engine/core.ts +392 -0
  91. package/dist/sdk/src/ui/engine/dom.ts +183 -0
  92. package/dist/sdk/src/ui/engine/hooks.ts +195 -0
  93. package/dist/sdk/src/ui/engine/instance.ts +63 -0
  94. package/dist/sdk/src/ui/engine/keycapContext.ts +2 -0
  95. package/dist/sdk/src/ui/engine/registry.ts +262 -0
  96. package/dist/sdk/src/ui/engine/types.ts +62 -0
  97. package/dist/sdk/src/ui/icons.tsx +91 -0
  98. package/dist/sdk/src/ui/util.ts +83 -0
  99. package/dist/sdk/src/viewPeriod.ts +26 -0
  100. package/dist/sdk/src/vite.ts +92 -0
  101. package/dist/sdk/src/words/actions.ts +295 -0
  102. package/dist/sdk/src/words/clashes.ts +158 -0
  103. package/dist/sdk/src/words/format.ts +136 -0
  104. package/dist/sdk/src/words/index.ts +13 -0
  105. package/dist/sdk/src/words/keymap.ts +72 -0
  106. package/dist/sdk/src/words/keys.ts +297 -0
  107. package/dist/sdk/src/words/shell.ts +46 -0
  108. package/dist/sdk/src/words/types.ts +65 -0
  109. package/dist/skill-sync/index.js +832 -151
  110. package/dist/skill-sync/index.js.map +4 -4
  111. package/dist/skill-sync-worker.mjs +3941 -0
  112. package/dist/space-harness/host.js +1460 -0
  113. package/dist/space-sdk/dist/chunk-ZSN57ATH.js +5 -0
  114. package/dist/space-sdk/dist/index.js +15 -0
  115. package/dist/space-sdk/dist/ui.js +18 -0
  116. package/dist/space-sdk/package.json +33 -0
  117. package/dist/space-sdk/src/agentContext.d.ts +36 -0
  118. package/dist/space-sdk/src/components/Dialog.d.ts +20 -0
  119. package/dist/space-sdk/src/components/DocumentEditor.d.ts +46 -0
  120. package/dist/space-sdk/src/components/DocumentPage.d.ts +53 -0
  121. package/dist/space-sdk/src/components/Markdown.d.ts +18 -0
  122. package/dist/space-sdk/src/components/MarkdownEditor.d.ts +11 -0
  123. package/dist/space-sdk/src/components/MultiSelectActionBar.d.ts +31 -0
  124. package/dist/space-sdk/src/components/MultiSelectCheckbox.d.ts +14 -0
  125. package/dist/space-sdk/src/components/MultiSelectDragOverlay.d.ts +8 -0
  126. package/dist/space-sdk/src/components/NotisCommentBoundary.d.ts +36 -0
  127. package/dist/space-sdk/src/components/NotisSelectionBoundary.d.ts +16 -0
  128. package/dist/space-sdk/src/components/RecordComponents.d.ts +64 -0
  129. package/dist/space-sdk/src/components/ShortcutHints.d.ts +12 -0
  130. package/dist/space-sdk/src/components/Skeleton.d.ts +23 -0
  131. package/dist/space-sdk/src/config.ts +31 -0
  132. package/dist/space-sdk/src/documents.d.ts +59 -0
  133. package/dist/space-sdk/src/hooks/useAction.d.ts +8 -0
  134. package/dist/space-sdk/src/hooks/useActiveResource.d.ts +3 -0
  135. package/dist/space-sdk/src/hooks/useAgentContext.d.ts +7 -0
  136. package/dist/space-sdk/src/hooks/useBackend.d.ts +23 -0
  137. package/dist/space-sdk/src/hooks/useCloudComputer.d.ts +45 -0
  138. package/dist/space-sdk/src/hooks/useCloudComputerShell.d.ts +49 -0
  139. package/dist/space-sdk/src/hooks/useCollectionInteractions.d.ts +118 -0
  140. package/dist/space-sdk/src/hooks/useDatabaseSchema.d.ts +14 -0
  141. package/dist/space-sdk/src/hooks/useDatabaseSubscription.d.ts +26 -0
  142. package/dist/space-sdk/src/hooks/useDocument.d.ts +22 -0
  143. package/dist/space-sdk/src/hooks/useDocuments.d.ts +31 -0
  144. package/dist/space-sdk/src/hooks/useHandover.d.ts +43 -0
  145. package/dist/space-sdk/src/hooks/useLongPressSelection.d.ts +9 -0
  146. package/dist/space-sdk/src/hooks/useMarqueeSelection.d.ts +57 -0
  147. package/dist/space-sdk/src/hooks/useMultiSelect.d.ts +51 -0
  148. package/dist/space-sdk/src/hooks/useNativeDocumentBody.d.ts +24 -0
  149. package/dist/space-sdk/src/hooks/useNativeDocuments.d.ts +55 -0
  150. package/dist/space-sdk/src/hooks/useNotis.d.ts +25 -0
  151. package/dist/space-sdk/src/hooks/useNotisNavigation.d.ts +33 -0
  152. package/dist/space-sdk/src/hooks/useQuery.d.ts +22 -0
  153. package/dist/space-sdk/src/hooks/useTool.d.ts +20 -0
  154. package/dist/space-sdk/src/hooks/useToolQuery.d.ts +3 -0
  155. package/dist/space-sdk/src/hooks/useTools.d.ts +15 -0
  156. package/dist/space-sdk/src/hooks/useTopBarSearch.d.ts +48 -0
  157. package/dist/space-sdk/src/hooks/useTopBarTitleLeaf.d.ts +24 -0
  158. package/dist/space-sdk/src/hooks/useUpsertDocument.d.ts +27 -0
  159. package/dist/space-sdk/src/hooks/useViewParams.d.ts +76 -0
  160. package/dist/space-sdk/src/hooks/useViewerReads.d.ts +38 -0
  161. package/dist/space-sdk/src/index.d.ts +98 -0
  162. package/dist/space-sdk/src/interactions/actions.d.ts +33 -0
  163. package/dist/space-sdk/src/interactions/shortcuts.d.ts +42 -0
  164. package/dist/space-sdk/src/interactions/visibility.d.ts +2 -0
  165. package/dist/space-sdk/src/nativeData.d.ts +66 -0
  166. package/dist/space-sdk/src/provider.d.ts +17 -0
  167. package/dist/space-sdk/src/queryCache.d.ts +45 -0
  168. package/dist/space-sdk/src/queryErrors.ts +51 -0
  169. package/dist/space-sdk/src/runtime.d.ts +615 -0
  170. package/dist/space-sdk/src/runtimeModules.ts +33 -0
  171. package/dist/space-sdk/src/space.ts +442 -0
  172. package/dist/space-sdk/src/standardViewParams.d.ts +15 -0
  173. package/dist/space-sdk/src/tailwind-candidates.d.ts +3418 -0
  174. package/dist/space-sdk/src/tailwind.ts +66 -0
  175. package/dist/space-sdk/src/ui/actions/ActionToast.d.ts +15 -0
  176. package/dist/space-sdk/src/ui/actions/actionWords.d.ts +16 -0
  177. package/dist/space-sdk/src/ui/actions/index.d.ts +7 -0
  178. package/dist/space-sdk/src/ui/actions/useActions.d.ts +69 -0
  179. package/dist/space-sdk/src/ui/actions/useToast.d.ts +41 -0
  180. package/dist/space-sdk/src/ui/blocks/Tabs.d.ts +139 -0
  181. package/dist/space-sdk/src/ui/blocks/blocks.d.ts +217 -0
  182. package/dist/space-sdk/src/ui/blocks/index.d.ts +4 -0
  183. package/dist/space-sdk/src/ui/board/BoardView.d.ts +36 -0
  184. package/dist/space-sdk/src/ui/board/boardModel.d.ts +46 -0
  185. package/dist/space-sdk/src/ui/board/boardWords.d.ts +9 -0
  186. package/dist/space-sdk/src/ui/board/index.d.ts +5 -0
  187. package/dist/space-sdk/src/ui/calendar/CalendarView.d.ts +49 -0
  188. package/dist/space-sdk/src/ui/calendar/DayPicker.d.ts +23 -0
  189. package/dist/space-sdk/src/ui/calendar/calendarMath.d.ts +81 -0
  190. package/dist/space-sdk/src/ui/charts/Chart.d.ts +203 -0
  191. package/dist/space-sdk/src/ui/charts/ChartTable.d.ts +37 -0
  192. package/dist/space-sdk/src/ui/charts/RenderChartData.d.ts +16 -0
  193. package/dist/space-sdk/src/ui/charts/StatRow.d.ts +59 -0
  194. package/dist/space-sdk/src/ui/charts/chartData.d.ts +84 -0
  195. package/dist/space-sdk/src/ui/comments/CommentPane.d.ts +58 -0
  196. package/dist/space-sdk/src/ui/comments/commentModel.d.ts +157 -0
  197. package/dist/space-sdk/src/ui/comments/commentWords.d.ts +60 -0
  198. package/dist/space-sdk/src/ui/comments/index.d.ts +4 -0
  199. package/dist/space-sdk/src/ui/comments/useComments.d.ts +146 -0
  200. package/dist/space-sdk/src/ui/engine/HelpSheet.d.ts +12 -0
  201. package/dist/space-sdk/src/ui/engine/KeyboardEngine.d.ts +27 -0
  202. package/dist/space-sdk/src/ui/engine/core.d.ts +73 -0
  203. package/dist/space-sdk/src/ui/engine/hooks.d.ts +64 -0
  204. package/dist/space-sdk/src/ui/engine/index.d.ts +14 -0
  205. package/dist/space-sdk/src/ui/engine/instance.d.ts +21 -0
  206. package/dist/space-sdk/src/ui/engine/keycapContext.d.ts +2 -0
  207. package/dist/space-sdk/src/ui/engine/registry.d.ts +77 -0
  208. package/dist/space-sdk/src/ui/engine/step.d.ts +11 -0
  209. package/dist/space-sdk/src/ui/engine/types.d.ts +54 -0
  210. package/dist/space-sdk/src/ui/fields/fieldWords.d.ts +56 -0
  211. package/dist/space-sdk/src/ui/fields/fields.d.ts +162 -0
  212. package/dist/space-sdk/src/ui/fields/index.d.ts +5 -0
  213. package/dist/space-sdk/src/ui/fields/useFieldDraft.d.ts +22 -0
  214. package/dist/space-sdk/src/ui/gallery/AttachmentGallery.d.ts +53 -0
  215. package/dist/space-sdk/src/ui/gallery/ImageViewer.d.ts +27 -0
  216. package/dist/space-sdk/src/ui/gallery/galleryWords.d.ts +95 -0
  217. package/dist/space-sdk/src/ui/gallery/index.d.ts +5 -0
  218. package/dist/space-sdk/src/ui/icons.d.ts +13 -0
  219. package/dist/space-sdk/src/ui/index.d.ts +54 -0
  220. package/dist/space-sdk/src/ui/keycap/Keycap.d.ts +22 -0
  221. package/dist/space-sdk/src/ui/keycap/index.d.ts +2 -0
  222. package/dist/space-sdk/src/ui/layout/Panels.d.ts +50 -0
  223. package/dist/space-sdk/src/ui/layout/chatPanelEdge.d.ts +77 -0
  224. package/dist/space-sdk/src/ui/layout/index.d.ts +5 -0
  225. package/dist/space-sdk/src/ui/layout/panelMath.d.ts +91 -0
  226. package/dist/space-sdk/src/ui/list/ListReader.d.ts +380 -0
  227. package/dist/space-sdk/src/ui/list/Row.d.ts +145 -0
  228. package/dist/space-sdk/src/ui/list/SelectionBar.d.ts +65 -0
  229. package/dist/space-sdk/src/ui/list/index.d.ts +14 -0
  230. package/dist/space-sdk/src/ui/list/rowState.d.ts +22 -0
  231. package/dist/space-sdk/src/ui/list/useListKeys.d.ts +89 -0
  232. package/dist/space-sdk/src/ui/list/useReorder.d.ts +86 -0
  233. package/dist/space-sdk/src/ui/list/useSelection.d.ts +90 -0
  234. package/dist/space-sdk/src/ui/open/OpenAsMenu.d.ts +15 -0
  235. package/dist/space-sdk/src/ui/open/index.d.ts +6 -0
  236. package/dist/space-sdk/src/ui/open/openModeStore.d.ts +39 -0
  237. package/dist/space-sdk/src/ui/open/useOpenMode.d.ts +17 -0
  238. package/dist/space-sdk/src/ui/overlay/Menu.d.ts +88 -0
  239. package/dist/space-sdk/src/ui/overlay/Popover.d.ts +28 -0
  240. package/dist/space-sdk/src/ui/overlay/Tooltip.d.ts +19 -0
  241. package/dist/space-sdk/src/ui/overlay/index.d.ts +9 -0
  242. package/dist/space-sdk/src/ui/overlay/layerRoot.d.ts +24 -0
  243. package/dist/space-sdk/src/ui/overlay/position.d.ts +31 -0
  244. package/dist/space-sdk/src/ui/period/PeriodPicker.d.ts +30 -0
  245. package/dist/space-sdk/src/ui/period/dateFormat.d.ts +50 -0
  246. package/dist/space-sdk/src/ui/period/dateWords.d.ts +88 -0
  247. package/dist/space-sdk/src/ui/period/periods.d.ts +70 -0
  248. package/dist/space-sdk/src/ui/period/usePeriod.d.ts +23 -0
  249. package/dist/space-sdk/src/ui/present/index.d.ts +3 -0
  250. package/dist/space-sdk/src/ui/present/presentWords.d.ts +27 -0
  251. package/dist/space-sdk/src/ui/present/screens.d.ts +36 -0
  252. package/dist/space-sdk/src/ui/present/useAudienceScreen.d.ts +38 -0
  253. package/dist/space-sdk/src/ui/styles.d.ts +21 -0
  254. package/dist/space-sdk/src/ui/timeline/TimelineView.d.ts +44 -0
  255. package/dist/space-sdk/src/ui/timeline/timelineMath.d.ts +123 -0
  256. package/dist/space-sdk/src/viewPeriod.d.ts +8 -0
  257. package/dist/space-sdk/src/vite.ts +92 -0
  258. package/dist/space-sdk/src/words/actions.ts +295 -0
  259. package/dist/space-sdk/src/words/clashes.ts +158 -0
  260. package/dist/space-sdk/src/words/format.ts +136 -0
  261. package/dist/space-sdk/src/words/index.ts +13 -0
  262. package/dist/space-sdk/src/words/keymap.ts +72 -0
  263. package/dist/space-sdk/src/words/keys.ts +297 -0
  264. package/dist/space-sdk/src/words/shell.ts +46 -0
  265. package/dist/space-sdk/src/words/types.ts +65 -0
  266. package/dist/view-renderer/dist/host.css +1 -0
  267. package/dist/view-renderer/dist/host.js +2374 -0
  268. package/dist/view-renderer/dist/theme.css +73 -0
  269. package/dist/view-renderer/src/dom.js +212 -0
  270. package/dist/view-renderer/src/errors.js +8 -0
  271. package/dist/view-renderer/src/host.tsx +250 -0
  272. package/dist/view-renderer/src/index.js +4 -0
  273. package/dist/view-renderer/src/png.js +213 -0
  274. package/dist/view-renderer/src/pool.js +110 -0
  275. package/dist/view-renderer/src/remote-broker.js +59 -0
  276. package/dist/view-renderer/src/renderer.js +271 -0
  277. package/package.json +21 -11
  278. package/skills/notis-cli/AGENT_INSTRUCTIONS.md +63 -12
  279. package/skills/notis-onboarding/BRIEF.md +14 -9
  280. package/skills/notis-query/cli.md +1 -1
  281. package/skills/notis-spaces/cli.md +540 -0
  282. package/src/cli.js +8 -8
  283. package/src/command-specs/agents.js +121 -8
  284. package/src/command-specs/auth.js +3 -3
  285. package/src/command-specs/diagnostics.js +97 -20
  286. package/src/command-specs/documents.js +158 -0
  287. package/src/command-specs/files.js +29 -0
  288. package/src/command-specs/handover.js +331 -79
  289. package/src/command-specs/helpers.js +50 -40
  290. package/src/command-specs/index.js +14 -5
  291. package/src/command-specs/meta.js +38 -9
  292. package/src/command-specs/onboarding.js +1 -1
  293. package/src/command-specs/profile.js +42 -1
  294. package/src/command-specs/skills.js +314 -9
  295. package/src/command-specs/smoke.js +16 -4
  296. package/src/command-specs/spaces.js +783 -0
  297. package/src/command-specs/tools.js +118 -21
  298. package/src/command-specs/version-history.js +20 -0
  299. package/src/command-specs/views.js +69 -0
  300. package/src/runtime/agent-browser.js +178 -267
  301. package/src/runtime/agent-setup.js +172 -13
  302. package/src/runtime/app-boundary-validator.js +338 -2
  303. package/src/runtime/app-data-literals.js +82 -0
  304. package/src/runtime/app-platform.js +750 -1568
  305. package/src/runtime/auth-recovery.js +17 -0
  306. package/src/runtime/base-skills.d.ts +11 -0
  307. package/src/runtime/base-skills.js +100 -3
  308. package/src/runtime/computer-identity.d.ts +30 -0
  309. package/src/runtime/computer-identity.js +151 -0
  310. package/src/runtime/config-lock.js +135 -0
  311. package/src/runtime/confined-space-build.js +74 -0
  312. package/src/runtime/confined-space-verify.js +49 -0
  313. package/src/runtime/delegated-context.js +5 -21
  314. package/src/runtime/native-record-files.js +47 -0
  315. package/src/runtime/oauth.js +342 -31
  316. package/src/runtime/profiles.js +19 -112
  317. package/src/runtime/retired-sdk-files.js +24 -0
  318. package/src/runtime/retry.js +150 -0
  319. package/src/runtime/skill-sync/cloud-client.ts +33 -8
  320. package/src/runtime/skill-sync/index.ts +443 -106
  321. package/src/runtime/skill-sync/local-scanner.ts +248 -32
  322. package/src/runtime/skill-sync/native-identity.ts +102 -0
  323. package/src/runtime/skill-sync/native-plan.ts +70 -0
  324. package/src/runtime/skill-sync/symlink-manager.ts +66 -16
  325. package/src/runtime/skill-sync/sync-plan.ts +34 -2
  326. package/src/runtime/skill-sync/types.ts +54 -0
  327. package/src/runtime/skill-sync/write-cloud-skill.ts +4 -4
  328. package/src/runtime/skill-sync-service.js +109 -0
  329. package/src/runtime/source-root.js +32 -0
  330. package/src/runtime/space-collection-scaffold.js +107 -0
  331. package/src/runtime/space-database-structures.js +229 -0
  332. package/src/runtime/space-fixture-broker.js +68 -0
  333. package/src/runtime/space-lock.js +337 -0
  334. package/src/runtime/space-platform.js +133 -0
  335. package/src/runtime/space-source.js +290 -0
  336. package/src/runtime/space-test-server.js +354 -0
  337. package/src/runtime/space-verification.js +94 -0
  338. package/src/runtime/space-verify-browser-worker.js +51 -0
  339. package/src/runtime/space-view-manifest.js +368 -0
  340. package/src/runtime/sync-skills.d.ts +5 -1
  341. package/src/runtime/sync-skills.js +11 -2
  342. package/src/runtime/tool-confirmation.js +138 -0
  343. package/src/runtime/transport.js +118 -24
  344. package/src/runtime/upsell.js +219 -0
  345. package/src/runtime/view-render.js +49 -0
  346. package/src/skill-sync-worker-entry.js +2 -0
  347. package/src/skill-sync-worker.js +114 -0
  348. package/src/space-harness-host.ts +122 -0
  349. package/template/.harness/index.html.tmpl +9 -352
  350. package/template/app/globals.css +29 -3
  351. package/template/app/layout.tsx +2 -6
  352. package/template/components/ui/badge.tsx +7 -4
  353. package/template/components/ui/button.tsx +1 -1
  354. package/template/components/ui/native-select.tsx +24 -0
  355. package/template/notis.config.ts +4 -36
  356. package/template/package-lock.json +1800 -1867
  357. package/template/package.json +16 -14
  358. package/template/postcss.config.mjs +1 -1
  359. package/template/tailwind.config.ts +1 -6
  360. package/template/tsconfig.json +1 -0
  361. package/template/vite.config.ts +2 -2
  362. package/dist/base-skills/notis-apps/SKILL.md +0 -613
  363. package/skills/notis-apps/cli.md +0 -285
  364. package/src/command-specs/apps.js +0 -3000
  365. package/src/runtime/app-changelog.js +0 -79
  366. package/src/runtime/app-dev-consumers.js +0 -154
  367. package/src/runtime/app-dev-host-lock.js +0 -80
  368. package/src/runtime/app-dev-process-identity.js +0 -111
  369. package/src/runtime/app-dev-roots.js +0 -284
  370. package/src/runtime/app-dev-server.js +0 -1140
  371. package/src/runtime/app-dev-sessions.js +0 -185
  372. package/src/runtime/app-registry-scaffolds.js +0 -367
  373. package/src/runtime/assets/store-screenshot-dark.png +0 -0
  374. package/src/runtime/cli-mode.generated.js +0 -5
  375. package/src/runtime/cli-mode.js +0 -34
  376. package/src/runtime/store-screenshot.js +0 -143
  377. package/template/CHANGELOG.md +0 -5
  378. package/template/app/page.tsx +0 -60
  379. package/template/components/ui/card.tsx +0 -56
  380. package/template/packages/sdk/src/components/DocumentEditor.tsx +0 -103
  381. package/template/packages/sdk/src/config.ts +0 -259
  382. package/template/packages/sdk/src/hooks/useCloudComputer.ts +0 -97
  383. package/template/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
  384. package/template/packages/sdk/src/hooks/useDocument.ts +0 -78
  385. package/template/packages/sdk/src/hooks/useNotisNavigation.ts +0 -52
  386. package/template/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
  387. package/template/packages/sdk/src/runtime.ts +0 -439
  388. package/template/packages/sdk/src/vite.ts +0 -56
  389. package/template/packages/sdk/tsconfig.json +0 -15
  390. /package/{template/packages → dist}/sdk/src/components/MultiSelectDragOverlay.tsx +0 -0
  391. /package/{template/packages → dist}/sdk/src/components/ShortcutHints.tsx +0 -0
  392. /package/{template/packages → dist}/sdk/src/hooks/useActiveResource.ts +0 -0
  393. /package/{template/packages → dist}/sdk/src/hooks/useTool.ts +0 -0
  394. /package/{template/packages → dist}/sdk/src/hooks/useTools.ts +0 -0
  395. /package/{template/packages → dist}/sdk/src/ui.ts +0 -0
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import type { AnchorHTMLAttributes, ReactElement } from 'react';
3
+ import type { AnchorHTMLAttributes, ReactElement, TableHTMLAttributes } from 'react';
4
4
  import ReactMarkdown, { type Components } from 'react-markdown';
5
5
  import remarkGfm from 'remark-gfm';
6
6
 
@@ -32,14 +32,20 @@ function MarkdownLink({
32
32
  );
33
33
  }
34
34
 
35
+ /** A table in a scroll box of its own, so a wide table scrolls sideways on a phone instead of the page (C17). */
36
+ function MarkdownTable({ node: _node, ...rest }: TableHTMLAttributes<HTMLTableElement> & { node?: unknown }) {
37
+ return <div className="notis-markdown-table"><table {...rest} /></div>;
38
+ }
39
+
35
40
  const defaultComponents: Components = {
36
41
  a: MarkdownLink,
42
+ table: MarkdownTable,
37
43
  };
38
44
 
39
45
  /**
40
46
  * Renders a markdown value with Notis typography. Markdown is a value type:
41
- * use this anywhere markdown appears — document content, database fields,
42
- * generated text. Styling comes from `.notis-markdown` rules in the SDK
47
+ * use this anywhere markdown appears (document content, database fields,
48
+ * generated text). Styling comes from `.notis-markdown` rules in the SDK
43
49
  * stylesheet and follows the portal theme variables.
44
50
  */
45
51
  export function Markdown({ value, size = 'base', className, components }: MarkdownProps): ReactElement {
@@ -1,20 +1,39 @@
1
- import type { ChangeEvent, KeyboardEvent, ReactElement } from 'react';
1
+ import type { ChangeEvent, CSSProperties, KeyboardEvent, ReactElement } from 'react';
2
2
  import { useCallback, useEffect, useRef, useState } from 'react';
3
3
  import { useNotisRuntime } from '../provider';
4
4
  import type { NotisMarkdownEditorProps } from '../runtime';
5
+ import { pick, type Words } from '../words/types';
6
+
7
+ /** The words of the fallback Markdown editor, in English and French (C9). */
8
+ export const MARKDOWN_EDITOR_WORDS = Object.freeze({
9
+ placeholder: { en: 'Write in Markdown', fr: 'Écris en Markdown' },
10
+ }) satisfies Readonly<Record<string, Words>>;
11
+
12
+ const editableStyle: CSSProperties = {
13
+ minHeight: 360, width: '100%', resize: 'vertical', boxSizing: 'border-box', padding: '12px 16px', borderRadius: 8,
14
+ border: '1px solid hsl(var(--border))', background: 'hsl(var(--background))', color: 'hsl(var(--foreground))',
15
+ font: 'inherit', fontSize: 14, lineHeight: '22px',
16
+ };
17
+
18
+ // Read-only reads as text, not a field (C9): no border, no resize, no caret, a muted tint.
19
+ const readOnlyStyle: CSSProperties = {
20
+ ...editableStyle, resize: 'none', border: '1px solid transparent', background: 'hsl(var(--muted) / .4)',
21
+ cursor: 'default', outline: 'none',
22
+ };
5
23
 
6
24
  function MarkdownEditorFallback({
7
25
  value,
8
26
  revision,
9
27
  readOnly,
10
28
  autosaveMs = 1500,
11
- placeholder = 'Write in Markdown…',
29
+ placeholder,
12
30
  className,
13
31
  onChange,
14
32
  onSave,
15
33
  onDirtyChange,
16
34
  onSavingChange,
17
35
  }: NotisMarkdownEditorProps) {
36
+ const locale = useNotisRuntime()?.context?.locale === 'fr' ? 'fr' : 'en';
18
37
  const [draft, setDraft] = useState(value);
19
38
  const [saveAttempt, setSaveAttempt] = useState(0);
20
39
  const draftRef = useRef(value);
@@ -102,16 +121,18 @@ function MarkdownEditorFallback({
102
121
  <textarea
103
122
  value={draft}
104
123
  readOnly={readOnly}
105
- placeholder={placeholder}
124
+ aria-readonly={readOnly || undefined}
125
+ data-readonly={readOnly ? '' : undefined}
126
+ placeholder={readOnly ? undefined : placeholder ?? pick(MARKDOWN_EDITOR_WORDS.placeholder, locale)}
106
127
  className={className}
107
128
  onChange={handleChange}
108
129
  onKeyDown={handleKeyDown}
109
- style={{ minHeight: 360, width: '100%', resize: 'vertical' }}
130
+ style={readOnly ? readOnlyStyle : editableStyle}
110
131
  />
111
132
  );
112
133
  }
113
134
 
114
- /** Use Notis' editor while leaving persistence entirely under app control. */
135
+ /** Use Notis' editor while leaving persistence entirely under app control; outside Notis a plain EN/FR textarea stands in. */
115
136
  export function MarkdownEditor(props: NotisMarkdownEditorProps): ReactElement {
116
137
  const runtime = useNotisRuntime();
117
138
  const HostEditor = runtime?.ui?.MarkdownEditor;
@@ -1,17 +1,27 @@
1
1
  'use client';
2
2
 
3
3
  import React, {
4
+ useEffect,
4
5
  useMemo,
6
+ useRef,
5
7
  useState,
6
8
  type CSSProperties,
7
9
  type ReactElement,
8
10
  } from 'react';
9
11
  import type { ResolvedCollectionAction } from '../interactions/actions';
10
- import { shortcutDisplay, useShortcuts, type ShortcutDefinition } from '../interactions/shortcuts';
12
+ import { activateShortcutCollection, shortcutDisplay, useShortcuts, type ShortcutDefinition } from '../interactions/shortcuts';
13
+ import { isInteractionElementVisible } from '../interactions/visibility';
11
14
  import type { ShortcutScope } from '../interactions/shortcuts';
12
15
 
13
16
  export type MultiSelectAction = ResolvedCollectionAction;
14
17
 
18
+ /** The Portal owns global launcher positioning; app code only reports its own bar. */
19
+ export const BULK_ACTION_LAYOUT_EVENT = 'notis:bulk-actions-layout';
20
+ export interface BulkActionLayoutDetail {
21
+ bar: HTMLElement;
22
+ active: boolean;
23
+ }
24
+
15
25
  export interface MultiSelectActionBarProps {
16
26
  selectedCount: number;
17
27
  actions: MultiSelectAction[];
@@ -25,11 +35,14 @@ export interface MultiSelectActionBarProps {
25
35
  shortcutsEnabled?: boolean;
26
36
  /** Override the default collection shortcut scope. */
27
37
  shortcutScope?: ShortcutScope;
38
+ collectionOwnerId?: string;
39
+ isAvailable?: () => boolean;
40
+ onClearSelection?: () => void;
28
41
  }
29
42
 
30
43
  const containerBaseStyle: CSSProperties = {
31
44
  position: 'fixed',
32
- bottom: '1rem',
45
+ bottom: 'calc(var(--notis-viewport-bottom, 0px) + max(1rem, var(--notis-safe-area-bottom, env(safe-area-inset-bottom, 0px))))',
33
46
  left: '50%',
34
47
  transform: 'translateX(-50%)',
35
48
  zIndex: 60,
@@ -46,6 +59,8 @@ const containerBaseStyle: CSSProperties = {
46
59
  pointerEvents: 'auto',
47
60
  fontSize: '13px',
48
61
  lineHeight: 1.2,
62
+ width: 'max-content',
63
+ maxWidth: 'calc(100% - 2rem)',
49
64
  };
50
65
 
51
66
  const countStyle: CSSProperties = {
@@ -103,7 +118,7 @@ const iconSlotStyle: CSSProperties = {
103
118
  display: 'inline-flex',
104
119
  alignItems: 'center',
105
120
  justifyContent: 'center',
106
- opacity: 0.8,
121
+ color: 'inherit',
107
122
  };
108
123
 
109
124
  export function MultiSelectActionBar({
@@ -114,15 +129,55 @@ export function MultiSelectActionBar({
114
129
  style,
115
130
  shortcutsEnabled = true,
116
131
  shortcutScope = 'collection',
132
+ collectionOwnerId,
133
+ isAvailable,
134
+ onClearSelection,
117
135
  }: MultiSelectActionBarProps): ReactElement | null {
136
+ const barRef = useRef<HTMLDivElement>(null);
137
+ const visible = selectedCount > 0;
138
+ useEffect(() => {
139
+ const bar = barRef.current;
140
+ if (!visible || !bar) return;
141
+ // Notify the owning document, including from a shadow root or after unmount.
142
+ // Only the Portal host may measure this bar and change global layout styles.
143
+ const report = (active: boolean) => {
144
+ const owner = bar.ownerDocument;
145
+ const LayoutEvent = owner.defaultView?.CustomEvent;
146
+ if (LayoutEvent) {
147
+ owner.dispatchEvent(new LayoutEvent<BulkActionLayoutDetail>(BULK_ACTION_LAYOUT_EVENT, {
148
+ detail: { bar, active },
149
+ }));
150
+ }
151
+ };
152
+ const update = () => report(isAvailable?.() !== false && isInteractionElementVisible(bar));
153
+ update();
154
+ const observer = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(update);
155
+ observer?.observe(bar);
156
+ const VisibilityObserver = bar.ownerDocument.defaultView?.MutationObserver;
157
+ const visibilityObserver = VisibilityObserver ? new VisibilityObserver(update) : null;
158
+ let element: Element | null = bar;
159
+ while (element) {
160
+ visibilityObserver?.observe(element, { attributes: true, attributeFilter: element === bar.ownerDocument.documentElement ? ['hidden', 'inert', 'aria-hidden', 'class'] : ['hidden', 'inert', 'aria-hidden', 'style', 'class'] });
161
+ const root: Node = element.getRootNode();
162
+ element = element.parentElement ?? ('host' in root ? (root as ShadowRoot).host : null);
163
+ }
164
+ return () => {
165
+ observer?.disconnect();
166
+ visibilityObserver?.disconnect();
167
+ report(false);
168
+ };
169
+ }, [isAvailable, visible]);
170
+
118
171
  const actionShortcuts = useMemo<ShortcutDefinition[]>(() => {
119
172
  return actions.flatMap((action): ShortcutDefinition[] => {
120
- if (!action.shortcut || action.disabled || action.pending) return [];
173
+ if (!action.shortcut) return [];
121
174
  return [{
122
175
  id: `collection.action.${action.id}`,
123
176
  keys: action.shortcut,
124
177
  label: action.label,
125
- onTrigger: () => action.onRun(),
178
+ allowRepeat: Boolean(action.disabled || action.pending),
179
+ // Keep advertised keys owned while disabled/pending; never fall through to another action.
180
+ onTrigger: () => { if (!action.disabled && !action.pending) action.onRun(); },
126
181
  }];
127
182
  });
128
183
  }, [actions]);
@@ -130,6 +185,8 @@ export function MultiSelectActionBar({
130
185
  enabled: shortcutsEnabled && selectedCount > 0,
131
186
  scope: shortcutScope,
132
187
  priority: 25,
188
+ collectionOwnerId,
189
+ isAvailable: () => isAvailable?.() !== false && isInteractionElementVisible(barRef.current),
133
190
  });
134
191
 
135
192
  if (selectedCount === 0) return null;
@@ -147,16 +204,42 @@ export function MultiSelectActionBar({
147
204
 
148
205
  return (
149
206
  <div
207
+ ref={barRef}
208
+ data-notis-bulk-actions
209
+ onFocusCapture={() => { if (collectionOwnerId) activateShortcutCollection(collectionOwnerId); }}
210
+ onMouseDownCapture={() => { if (collectionOwnerId) activateShortcutCollection(collectionOwnerId); }}
211
+ onKeyDown={(event) => {
212
+ if (event.key !== 'Escape' || !shortcutsEnabled || !onClearSelection || isAvailable?.() === false) return;
213
+ event.preventDefault();
214
+ event.stopPropagation();
215
+ onClearSelection();
216
+ }}
150
217
  role="toolbar"
151
218
  aria-label={`Bulk actions for ${selectedCount} selected ${countWord}`}
152
219
  className={className}
153
220
  style={composedStyle}
154
221
  >
155
- <span style={countStyle}>{countLabel}</span>
156
- {actions.length > 0 ? <span aria-hidden style={dividerStyle} /> : null}
157
- {actions.map((action) => (
158
- <ActionButton key={action.id} action={action} />
159
- ))}
222
+ <style>{`
223
+ [data-notis-bulk-action-icon][data-has-shortcut] { display: none !important; }
224
+ @media (max-width: 639px) {
225
+ [data-notis-bulk-actions] { flex-wrap: wrap; width: calc(100% - 2rem) !important; }
226
+ [data-notis-bulk-count] { flex-basis: 100%; padding: 0.375rem 0.5rem !important; font-size: 14px !important; }
227
+ [data-notis-bulk-divider] { display: none; }
228
+ [data-notis-bulk-action-list] { width: 100%; }
229
+ [data-notis-bulk-actions] button { min-height: 48px; font-size: 16px !important; }
230
+ }
231
+ @media (hover: none) and (pointer: coarse) {
232
+ [data-notis-bulk-actions] kbd { display: none !important; }
233
+ [data-notis-bulk-action-icon][data-has-shortcut] { display: inline-flex !important; }
234
+ }
235
+ `}</style>
236
+ <span data-notis-bulk-count style={countStyle}>{countLabel}</span>
237
+ {actions.length > 0 ? <span data-notis-bulk-divider aria-hidden style={dividerStyle} /> : null}
238
+ <div data-notis-bulk-action-list style={{ display: 'flex', minWidth: 0, overflowX: 'auto', overscrollBehaviorX: 'contain', gap: '0.25rem' }}>
239
+ {actions.map((action) => (
240
+ <ActionButton key={action.id} action={action} />
241
+ ))}
242
+ </div>
160
243
  </div>
161
244
  );
162
245
  }
@@ -192,8 +275,8 @@ function ActionButton({ action }: { action: MultiSelectAction }) {
192
275
  onBlur={() => setHover(false)}
193
276
  style={buttonStyle}
194
277
  >
195
- {!action.shortcut && action.icon ? (
196
- <span aria-hidden style={iconSlotStyle}>{action.icon}</span>
278
+ {action.icon ? (
279
+ <span data-notis-bulk-action-icon data-has-shortcut={action.shortcut ? '' : undefined} aria-hidden style={iconSlotStyle}>{action.icon}</span>
197
280
  ) : null}
198
281
  {display ? <kbd aria-hidden style={keycapStyle}>{display}</kbd> : null}
199
282
  <span>{action.pending ? `${action.label}…` : action.label}</span>
@@ -23,7 +23,9 @@ const baseStyle: CSSProperties = {
23
23
  width: '16px',
24
24
  height: '16px',
25
25
  borderRadius: '4px',
26
- border: '1px solid hsl(var(--border))',
26
+ borderWidth: '1px',
27
+ borderStyle: 'solid',
28
+ borderColor: 'hsl(var(--border))',
27
29
  background: 'transparent',
28
30
  color: 'hsl(var(--primary-foreground))',
29
31
  cursor: 'pointer',
@@ -56,7 +58,7 @@ export function MultiSelectCheckbox({
56
58
  };
57
59
 
58
60
  // When alwaysVisible is false and the row isn't selected, defer the
59
- // hover-reveal to the parent — consumers wrap the checkbox in a `.group`
61
+ // hover-reveal to the parent: consumers wrap the checkbox in a `.group`
60
62
  // hover scope and pass `data-notis-hover-reveal` styles via className.
61
63
  // Out of the box, we render the checkbox at full opacity if selected,
62
64
  // otherwise inherit the parent's reveal state.
@@ -0,0 +1,172 @@
1
+ 'use client';
2
+ import React, { useEffect, useRef, useState, type CSSProperties, type ReactNode } from 'react';
3
+ import type { ContextResource } from '../runtime';
4
+ import { NotisSelectionBoundary } from './NotisSelectionBoundary';
5
+ import { useAgentContext } from '../hooks/useAgentContext';
6
+
7
+ export interface NotisCommentBoxProps {
8
+ quote?: string;
9
+ value: string;
10
+ onChange(value: string): void;
11
+ onSubmit(): void;
12
+ onCancel(): void;
13
+ pending?: boolean;
14
+ error?: string;
15
+ className?: string;
16
+ }
17
+
18
+ /** Compact, chrome-themed presentation. Apps retain ownership of comment state. */
19
+ export function NotisCommentBox({ quote, value, onChange, onSubmit, onCancel, pending, error, className = '' }: NotisCommentBoxProps) {
20
+ return <div data-notis-comment-ui role="dialog" aria-label="Comment on selection"
21
+ style={{ colorScheme: 'dark', background: 'hsl(var(--sidebar-background, 0 0% 7.5%))', color: 'hsl(var(--sidebar-foreground, 240 4.8% 95.9%))', borderColor: 'hsl(var(--sidebar-border, 0 0% 23.5%))' }}
22
+ className={`flex min-w-0 flex-col gap-2 rounded-xl border p-2.5 text-sm shadow-lg ${className}`}>
23
+ <div className="flex min-w-0 items-center gap-2">
24
+ {quote && <blockquote title={quote} className="min-w-0 flex-1 truncate border-l-2 border-current/40 pl-2 opacity-70">{quote}</blockquote>}
25
+ <button type="button" aria-label="Cancel comment" onClick={onCancel} disabled={pending} className="ml-auto flex size-7 shrink-0 items-center justify-center rounded-lg opacity-70 hover:bg-white/10 hover:opacity-100 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current disabled:opacity-40 [@media(pointer:coarse)]:size-9">
26
+ <svg aria-hidden="true" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"><path d="m6 6 12 12M18 6 6 18" /></svg>
27
+ </button>
28
+ </div>
29
+ <textarea autoFocus rows={2} name="selection-comment" aria-label="Your comment" placeholder="Add a comment…" value={value} onChange={event => onChange(event.target.value)} disabled={pending}
30
+ onKeyDown={event => { if (event.nativeEvent.isComposing || pending) return; if (event.key === 'Escape') { event.preventDefault(); onCancel(); } if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) { event.preventDefault(); if (value.trim()) onSubmit(); } }}
31
+ className="w-full min-w-0 resize-none rounded bg-transparent px-0.5 py-1 text-base text-inherit outline-none placeholder:text-current placeholder:opacity-60 sm:text-sm" />
32
+ {error && <p role="alert" className="text-sm text-red-300">{error}</p>}
33
+ <div className="flex items-center justify-between gap-3">
34
+ <span className="text-xs opacity-60">⌘ / Ctrl ↵ to add</span>
35
+ <button type="button" disabled={pending || !value.trim()} onClick={onSubmit}
36
+ style={{ background: 'hsl(var(--sidebar-foreground, 240 4.8% 95.9%))', color: 'hsl(var(--sidebar-background, 0 0% 7.5%))' }}
37
+ className="min-h-8 shrink-0 rounded-lg px-2.5 text-sm font-medium hover:opacity-90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current disabled:opacity-40 [@media(pointer:coarse)]:min-h-9">{pending ? 'Adding…' : 'Add to chat'}</button>
38
+ </div>
39
+ </div>;
40
+ }
41
+
42
+ export interface NotisCommentBoundaryProps {
43
+ resource?: ContextResource | null;
44
+ children: ReactNode;
45
+ className?: string;
46
+ commentClassName?: string;
47
+ /** Replace the optional standard editor while retaining selection/context behavior. */
48
+ renderComment?: (props: NotisCommentBoxProps) => ReactNode;
49
+ }
50
+
51
+ type SelectionRect = Pick<DOMRect, 'left' | 'right' | 'top' | 'bottom'>;
52
+
53
+ /** Place the tile after the final selected line, otherwise below (or above near the viewport bottom). */
54
+ export function getCommentActionPosition(rects: SelectionRect[], viewport: { width: number; height: number }, rightBoundary = viewport.width, trailing: SelectionRect[] = []) {
55
+ const last = rects[rects.length - 1];
56
+ const size = 32, gap = 8;
57
+ const clampX = (x: number) => Math.max(gap, Math.min(viewport.width - size - gap, x));
58
+ const clampY = (y: number) => Math.max(gap, Math.min(viewport.height - size - gap, y));
59
+ const beside = { left: last.right + gap, top: clampY((last.top + last.bottom - size) / 2) };
60
+ const overlapsText = trailing.some(rect => rect.left < beside.left + size && rect.right > beside.left && rect.top < beside.top + size && rect.bottom > beside.top);
61
+ if (beside.left + size <= Math.min(viewport.width, rightBoundary) - gap && !overlapsText) return beside;
62
+ return { left: clampX(last.right - size), top: clampY(last.bottom + gap + size <= viewport.height - gap ? last.bottom + gap : last.top - size - gap) };
63
+ }
64
+
65
+ /** Browser paragraph selection may include an adjacent block's empty starting boundary. */
66
+ export function getOwnedCommentRange(selected: Range, content: Element): Range | null {
67
+ const owned = content.ownerDocument.createRange();
68
+ owned.selectNodeContents(content);
69
+ const range = selected.cloneRange();
70
+ // Only clip whitespace outside the resource; actual neighboring content must
71
+ // never be silently attached to this resource's comment.
72
+ if (range.compareBoundaryPoints(Range.START_TO_START, owned) < 0) {
73
+ const before = range.cloneRange();
74
+ before.setEnd(owned.startContainer, owned.startOffset);
75
+ if (before.toString().trim()) return null;
76
+ range.setStart(owned.startContainer, owned.startOffset);
77
+ }
78
+ if (range.compareBoundaryPoints(Range.END_TO_END, owned) > 0) {
79
+ const after = range.cloneRange();
80
+ after.setStart(owned.endContainer, owned.endOffset);
81
+ if (after.toString().trim()) return null;
82
+ range.setEnd(owned.endContainer, owned.endOffset);
83
+ }
84
+ if (range.collapsed || !content.contains(range.startContainer) || !content.contains(range.endContainer)) return null;
85
+ return range;
86
+ }
87
+
88
+ /** Selection-to-chat convenience. No annotation database, polling or background attachment. */
89
+ export function NotisCommentBoundary({ resource, children, className, commentClassName, renderComment }: NotisCommentBoundaryProps) {
90
+ const context = useAgentContext();
91
+ const root = useRef<HTMLDivElement>(null);
92
+ const [selection, setSelection] = useState<{ id: string; text: string; resource?: ContextResource | null; top: number; left: number } | null>(null);
93
+ const [editing, setEditing] = useState(false);
94
+ const editingRef = useRef(false);
95
+ const [comment, setComment] = useState('');
96
+ const [pending, setPending] = useState(false);
97
+ const [error, setError] = useState('');
98
+ useEffect(() => {
99
+ const node = root.current;
100
+ if (!node) return;
101
+ const document = node.ownerDocument;
102
+ const scope = node.getRootNode() as ShadowRoot & { getSelection?: () => Selection | null };
103
+ const clearInvalidSelection = () => {
104
+ // Opening the editor focuses its input and collapses the browser range.
105
+ // Keep the captured quote/editor; only the unopened action follows live selection.
106
+ if (editingRef.current) return;
107
+ const selected = scope.getSelection?.() || document.defaultView?.getSelection();
108
+ const content = node.firstElementChild;
109
+ if (!selected?.rangeCount || selected.isCollapsed || !selected.toString().trim()
110
+ || !content || !getOwnedCommentRange(selected.getRangeAt(0), content)) setSelection(null);
111
+ };
112
+ document.addEventListener('selectionchange', clearInvalidSelection);
113
+ if (scope !== (document as unknown as ShadowRoot)) scope.addEventListener('selectionchange', clearInvalidSelection);
114
+ return () => {
115
+ document.removeEventListener('selectionchange', clearInvalidSelection);
116
+ if (scope !== (document as unknown as ShadowRoot)) scope.removeEventListener('selectionchange', clearInvalidSelection);
117
+ };
118
+ }, []);
119
+ const open = () => { editingRef.current = true; setEditing(true); };
120
+ const capture = (target: EventTarget | null) => {
121
+ // Clicking the action/editor must not unmount it before its click runs.
122
+ if (editing || (target instanceof Element && target.closest('[data-notis-comment-ui]'))) return;
123
+ const node = root.current;
124
+ const scope = node?.getRootNode() as ShadowRoot & { getSelection?: () => Selection | null };
125
+ const selected = scope?.getSelection?.() || window.getSelection();
126
+ if (!node || !selected?.rangeCount || selected.isCollapsed) { setSelection(null); return; }
127
+ const content = node.firstElementChild;
128
+ const range = content ? getOwnedCommentRange(selected.getRangeAt(0), content) : null;
129
+ if (!range) { setSelection(null); return; }
130
+ const start = range.startContainer.nodeType === Node.ELEMENT_NODE ? range.startContainer as Element : range.startContainer.parentElement;
131
+ const end = range.endContainer.nodeType === Node.ELEMENT_NODE ? range.endContainer as Element : range.endContainer.parentElement;
132
+ if (!node.contains(range.startContainer) || !node.contains(range.endContainer)
133
+ || start?.closest('input,textarea,button,[contenteditable],[data-notis-comment-ui]')
134
+ || end?.closest('input,textarea,button,[contenteditable],[data-notis-comment-ui]')) { setSelection(null); return; }
135
+ const rect = range.getBoundingClientRect();
136
+ const lines = Array.from(range.getClientRects?.() || []).filter(line => line.width > 0 && line.height > 0);
137
+ const boundary = node.firstElementChild?.getBoundingClientRect();
138
+ const trailing: DOMRect[] = [];
139
+ const endBlock = end?.closest('p,li,h1,h2,h3,h4,h5,h6,td,th,div');
140
+ if (endBlock && node.contains(endBlock)) {
141
+ const remainder = range.cloneRange();
142
+ remainder.selectNodeContents(endBlock);
143
+ remainder.setStart(range.endContainer, range.endOffset);
144
+ trailing.push(...Array.from(remainder.getClientRects?.() || []).filter(line => line.width > 0 && line.height > 0));
145
+ }
146
+ const position = getCommentActionPosition(lines.length ? lines : [rect], { width: window.innerWidth, height: window.innerHeight }, boundary?.width ? boundary.right : window.innerWidth, trailing);
147
+ setSelection({ id: crypto.randomUUID(), text: selected.toString().trim(), resource: resource ? JSON.parse(JSON.stringify(resource)) : null, ...position });
148
+ };
149
+ const close = () => { editingRef.current = false; setEditing(false); setSelection(null); setComment(''); setError(''); };
150
+ const submit = async () => {
151
+ if (!selection || !comment.trim() || pending) return;
152
+ setPending(true); setError('');
153
+ try {
154
+ const added = await context.add({ id: selection.id, kind: 'comment', title: selection.resource?.label || 'Comment', icon: 'phosphor:chat-text', text: selection.text, comment, resource: selection.resource });
155
+ if (!added) throw new Error('The comment could not be added to chat.');
156
+ close();
157
+ } catch (reason) { setError(reason instanceof Error ? reason.message : 'Could not add this comment.'); }
158
+ finally { setPending(false); }
159
+ };
160
+ const editorProps: NotisCommentBoxProps = { quote: selection?.text, value: comment, onChange: setComment, onSubmit: () => void submit(), onCancel: close, pending, error, className: commentClassName };
161
+ return <div ref={root} className="contents" onMouseUp={event => capture(event.target)} onDoubleClick={event => capture(event.target)} onKeyUp={event => { if (!editing && (event.shiftKey || event.key === 'Shift' || ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'a'))) capture(event.target); }}>
162
+ <NotisSelectionBoundary resource={resource} className={className} style={className ? undefined : { display: 'contents' }}>{children}</NotisSelectionBoundary>
163
+ {selection && <div data-notis-comment-ui className="fixed z-50 max-h-[calc(100dvh-1rem)]" style={{ overflow: editing ? 'auto' : 'visible', top: editing ? Math.min(selection.top, Math.max(8, window.innerHeight - 320)) : selection.top, left: `clamp(8px, ${selection.left}px, calc(100vw - ${editing ? 'min(20rem, calc(100vw - 1rem))' : '2rem'} - 8px))`, width: editing ? 'min(20rem, calc(100vw - 1rem))' : undefined } as CSSProperties}>
164
+ {editing ? (renderComment ? renderComment(editorProps) : <NotisCommentBox {...editorProps} />) : <button type="button" aria-label="Comment" title="Comment" onMouseDown={event => event.preventDefault()} onClick={open}
165
+ style={{ background: 'hsl(var(--sidebar-background, 0 0% 7.5%))', color: 'hsl(var(--sidebar-foreground, 240 4.8% 95.9%))' }}
166
+ className="relative flex size-8 items-center justify-center rounded-lg ring-1 ring-white/15 hover:opacity-90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current">
167
+ <svg aria-hidden="true" width="18" height="18" viewBox="0 0 256 256" fill="currentColor"><path d="M216,48H40A16,16,0,0,0,24,64V224a15.85,15.85,0,0,0,9.24,14.5A16.13,16.13,0,0,0,40,240a15.89,15.89,0,0,0,10.25-3.78l.09-.07L83,208H216a16,16,0,0,0,16-16V64A16,16,0,0,0,216,48ZM40,224h0ZM216,192H80a8,8,0,0,0-5.23,1.95L40,224V64H216ZM88,112a8,8,0,0,1,8-8h64a8,8,0,0,1,0,16H96A8,8,0,0,1,88,112Zm0,32a8,8,0,0,1,8-8h64a8,8,0,1,1,0,16H96A8,8,0,0,1,88,144Z" /></svg>
168
+ <span aria-hidden="true" className="absolute left-1/2 top-1/2 hidden size-12 -translate-x-1/2 -translate-y-1/2 [@media(pointer:coarse)]:block" />
169
+ </button>}
170
+ </div>}
171
+ </div>;
172
+ }
@@ -1,5 +1,5 @@
1
1
  import type { CSSProperties, ClipboardEvent, ReactNode } from 'react';
2
- import { useCallback } from 'react';
2
+ import React, { useCallback } from 'react';
3
3
  import { useNotisRuntime } from '../provider';
4
4
  import type { ContextResource, ContextSelection } from '../runtime';
5
5
 
@@ -31,10 +31,13 @@ export function NotisSelectionBoundary({
31
31
  }: NotisSelectionBoundaryProps) {
32
32
  const runtime = useNotisRuntime();
33
33
  const onCopy = useCallback((event: ClipboardEvent<HTMLDivElement>) => {
34
- const selection = window.getSelection();
35
- const text = selection?.toString().trim() ?? '';
34
+ const target = event.target as Element | null;
35
+ if (target?.closest?.('input,textarea,[contenteditable="true"]')) return;
36
+ const scope = event.currentTarget.getRootNode() as ShadowRoot & { getSelection?: () => Selection | null };
37
+ const selection = scope?.getSelection?.() || window.getSelection();
38
+ const text = selection?.toString() ?? '';
36
39
  if (
37
- !text
40
+ !text.trim()
38
41
  || !selection
39
42
  || !event.currentTarget.contains(selection.anchorNode)
40
43
  || !event.currentTarget.contains(selection.focusNode)
@@ -42,7 +45,8 @@ export function NotisSelectionBoundary({
42
45
 
43
46
  const payload: ContextSelection = {
44
47
  id: selectionId(),
45
- text: text.slice(0, 12_000),
48
+ text,
49
+ ...(runtime?.contextSource ? { source: runtime.contextSource } : {}),
46
50
  ...(resource ? { resource: { ...resource, snapshot: undefined } } : {}),
47
51
  };
48
52
  event.clipboardData.setData('text/plain', text);
@@ -0,0 +1,111 @@
1
+ "use client";
2
+
3
+ import type { ReactElement } from 'react';
4
+ import { useNotisRuntime } from '../provider';
5
+ import type {
6
+ NotisHtmlFrameProps, NotisRecordPropertiesProps, NotisReportFrameProps, NotisShareControlProps, NotisSpacePageFrameProps,
7
+ } from '../runtime';
8
+ import { pick, type Locale, type Words } from '../words/types';
9
+
10
+ /** The words a record component says outside Notis, in English and French. */
11
+ export const RECORD_COMPONENT_WORDS = Object.freeze({
12
+ unavailable: { en: 'Open this record in Notis to use this component.', fr: 'Ouvre cet enregistrement dans Notis pour utiliser ce composant.' },
13
+ /** Short enough for a toolbar (C11); the full sentence is its tooltip. */
14
+ shareUnavailable: { en: 'Share unavailable', fr: 'Partage indisponible' },
15
+ shareUnavailableHint: { en: 'Open this record in Notis to share it.', fr: 'Ouvre cet enregistrement dans Notis pour le partager.' },
16
+ spacePageTitle: { en: 'Space page', fr: 'Page du Space' },
17
+ spacePagePreview: { en: 'This page shows here live when the Space opens in Notis.', fr: 'Cette page s’affiche ici en direct quand le Space est ouvert dans Notis.' },
18
+ spacePageOpen: { en: 'Open page', fr: 'Ouvrir la page' },
19
+ }) satisfies Readonly<Record<string, Words>>;
20
+
21
+ function useLocale(): Locale {
22
+ return useNotisRuntime()?.context?.locale === 'fr' ? 'fr' : 'en';
23
+ }
24
+
25
+ /** What a record component shows when the host does not draw it: one sentence, in the Space's language. */
26
+ export function RecordComponentUnavailable({ className }: { className?: string }): ReactElement {
27
+ return <div role="status" className={className}>{pick(RECORD_COMPONENT_WORDS.unavailable, useLocale())}</div>;
28
+ }
29
+ /** The record's properties, drawn by the host. */
30
+ export function RecordProperties(props: NotisRecordPropertiesProps): ReactElement {
31
+ const Host = useNotisRuntime()?.ui?.RecordProperties;
32
+ return Host ? <Host {...props} /> : <RecordComponentUnavailable className={props.className} />;
33
+ }
34
+ /** The host applies its viewer CSP and an opaque-origin sandbox; source cannot relax either. */
35
+ export function HtmlFrame(props: NotisHtmlFrameProps): ReactElement {
36
+ const Host = useNotisRuntime()?.ui?.HtmlFrame;
37
+ return Host ? <Host {...props} /> : <RecordComponentUnavailable className={props.className} />;
38
+ }
39
+ /** A saved report's frame, drawn by the host. */
40
+ export function ReportFrame(props: NotisReportFrameProps): ReactElement {
41
+ const Host = useNotisRuntime()?.ui?.ReportFrame;
42
+ return Host ? <Host {...props} /> : <RecordComponentUnavailable className={props.className} />;
43
+ }
44
+ /**
45
+ * Private/shared state, copy link and revoke, backed by this record's Site. Outside Notis it sits in a toolbar as a
46
+ * short muted label whose tooltip says where sharing works (C11), never a sentence that fills the bar.
47
+ */
48
+ export function ShareControl(props: NotisShareControlProps): ReactElement {
49
+ const Host = useNotisRuntime()?.ui?.ShareControl;
50
+ const locale = useLocale();
51
+ if (Host) return <Host {...props} />;
52
+ const hint = pick(RECORD_COMPONENT_WORDS.shareUnavailableHint, locale);
53
+ return <span role="status" className={props.className} title={hint} aria-label={hint} data-notis-share-unavailable=""
54
+ style={{ display: 'inline-flex', alignItems: 'center', height: 28, padding: '0 8px', fontSize: 13, whiteSpace: 'nowrap',
55
+ color: 'hsl(var(--muted-foreground))' }}>
56
+ {pick(RECORD_COMPONENT_WORDS.shareUnavailable, locale)}
57
+ </span>;
58
+ }
59
+
60
+ /**
61
+ * The path a Space page link names and the link the fallback card may open: a site-relative path, or an http(s) URL
62
+ * without credentials. Anything else (script URLs, protocol-relative links) opens nothing.
63
+ */
64
+ export function spacePageLink(href: string): { path: string; openHref: string | null } {
65
+ const value = typeof href === 'string' ? href.trim() : '';
66
+ // Control characters and backslashes change how browsers read a URL; such a link never opens.
67
+ if (!value || /[\u0000-\u001f\u007f\\]/.test(value)) return { path: value, openHref: null };
68
+ if (value.startsWith('/') && !value.startsWith('//')) {
69
+ const path = value.split(/[?#]/, 1)[0] || '/';
70
+ return { path, openHref: value };
71
+ }
72
+ try {
73
+ const url = new URL(value);
74
+ if ((url.protocol === 'https:' || url.protocol === 'http:') && !url.username && !url.password) {
75
+ return { path: url.pathname, openHref: url.href };
76
+ }
77
+ } catch { /* not a URL */ }
78
+ return { path: value, openHref: null };
79
+ }
80
+
81
+ /** What a Space page frame shows when the host does not draw it (previews, verify): its title, its path and Open page. */
82
+ export function SpacePageCard({ href, title, className, notice }: Pick<NotisSpacePageFrameProps, 'href' | 'title' | 'className'> & {
83
+ /** One sentence under the path; default: the page shows live once the Space opens in Notis. */
84
+ notice?: string;
85
+ }): ReactElement {
86
+ const locale = useLocale();
87
+ const { path, openHref } = spacePageLink(href);
88
+ const muted = 'hsl(var(--muted-foreground))';
89
+ return <div role="group" aria-label={title || pick(RECORD_COMPONENT_WORDS.spacePageTitle, locale)} className={className}
90
+ data-notis-space-page-card=""
91
+ style={{ display: 'grid', alignContent: 'start', gap: 8, padding: 16, borderRadius: 10, minWidth: 0,
92
+ background: 'hsl(var(--muted) / .5)', color: 'hsl(var(--foreground))' }}>
93
+ <div style={{ fontSize: 14, fontWeight: 500, overflowWrap: 'anywhere' }}>{title || pick(RECORD_COMPONENT_WORDS.spacePageTitle, locale)}</div>
94
+ {path ? <div style={{ fontSize: 12, color: muted, overflowWrap: 'anywhere',
95
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace' }}>{path}</div> : null}
96
+ <div style={{ fontSize: 13, color: muted }}>{notice ?? pick(RECORD_COMPONENT_WORDS.spacePagePreview, locale)}</div>
97
+ {openHref ? <div><a className="nui-btn" href={openHref} style={{ textDecoration: 'none' }}>
98
+ {pick(RECORD_COMPONENT_WORDS.spacePageOpen, locale)}
99
+ </a></div> : null}
100
+ </div>;
101
+ }
102
+
103
+ /**
104
+ * Another Space page shown live inside this view. The host loads only a Space page of its own Notis origin, with the
105
+ * viewer's own access; any other link (and every host without the frame) shows a card with the title, the path and
106
+ * Open page. Not interactive by default, so a deck around it keeps its keys and clicks.
107
+ */
108
+ export function SpacePageFrame(props: NotisSpacePageFrameProps): ReactElement {
109
+ const Host = useNotisRuntime()?.ui?.SpacePageFrame;
110
+ return Host ? <Host {...props} /> : <SpacePageCard href={props.href} title={props.title} className={props.className} />;
111
+ }