@notis_ai/cli 0.2.0-beta.137.1 → 0.2.0-beta.144.1

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 (294) hide show
  1. package/README.md +82 -20
  2. package/dist/agent-hooks/notis-agent-hook.mjs +19750 -0
  3. package/{skills → dist/base-skills}/notis-apps/SKILL.md +84 -53
  4. package/{skills → dist/base-skills}/notis-cli/SKILL.md +9 -9
  5. package/dist/base-skills/notis-query/SKILL.md +705 -0
  6. package/dist/skill-sync/index.js +1528 -0
  7. package/dist/skill-sync/index.js.map +7 -0
  8. package/package.json +5 -2
  9. package/skills/notis-apps/cli.md +38 -17
  10. package/skills/notis-cli/AGENT_INSTRUCTIONS.md +39 -0
  11. package/skills/notis-onboarding/BRIEF.md +16 -0
  12. package/src/agent-hook-entry.js +5 -0
  13. package/src/cli.js +23 -14
  14. package/src/command-specs/agents.js +392 -0
  15. package/src/command-specs/apps.js +871 -387
  16. package/src/command-specs/auth.js +63 -10
  17. package/src/command-specs/index.js +6 -0
  18. package/src/command-specs/onboarding.js +69 -5
  19. package/src/command-specs/skills.js +56 -0
  20. package/src/runtime/agent-memory-state.js +126 -0
  21. package/src/runtime/agent-setup.js +383 -0
  22. package/src/runtime/app-dev-consumers.js +154 -0
  23. package/src/runtime/app-dev-host-lock.js +80 -0
  24. package/src/runtime/app-dev-roots.js +284 -0
  25. package/src/runtime/app-dev-server.js +212 -22
  26. package/src/runtime/app-dev-sessions.js +105 -145
  27. package/src/runtime/app-platform.js +1291 -165
  28. package/src/runtime/app-registry-scaffolds.js +367 -0
  29. package/src/runtime/base-skills.d.ts +20 -0
  30. package/src/runtime/base-skills.js +167 -0
  31. package/src/runtime/login-listener.js +15 -0
  32. package/src/runtime/oauth.js +1506 -141
  33. package/src/runtime/output.js +7 -0
  34. package/src/runtime/ports.js +16 -0
  35. package/src/runtime/profiles.js +0 -5
  36. package/src/runtime/skill-sync/cloud-client.ts +96 -0
  37. package/src/runtime/skill-sync/index.ts +644 -0
  38. package/src/runtime/skill-sync/local-scanner.ts +1046 -0
  39. package/src/runtime/skill-sync/symlink-manager.ts +383 -0
  40. package/src/runtime/skill-sync/sync-plan.ts +22 -0
  41. package/src/runtime/skill-sync/types.ts +103 -0
  42. package/src/runtime/skill-sync/write-cloud-skill.ts +50 -0
  43. package/src/runtime/store-screenshot.js +6 -1
  44. package/src/runtime/sync-skills.d.ts +37 -0
  45. package/src/runtime/sync-skills.js +215 -0
  46. package/template/app/layout.tsx +5 -1
  47. package/template/notis.config.ts +1 -0
  48. package/template/packages/sdk/package.json +2 -1
  49. package/template/packages/sdk/src/components/MarkdownEditor.tsx +121 -0
  50. package/template/packages/sdk/src/components/MultiSelectActionBar.tsx +37 -119
  51. package/template/packages/sdk/src/components/MultiSelectCheckbox.tsx +5 -1
  52. package/template/packages/sdk/src/components/MultiSelectDragOverlay.tsx +1 -1
  53. package/template/packages/sdk/src/components/NotisSelectionBoundary.tsx +55 -0
  54. package/template/packages/sdk/src/components/ShortcutHints.tsx +56 -0
  55. package/template/packages/sdk/src/config.ts +19 -0
  56. package/template/packages/sdk/src/hooks/useActiveResource.ts +19 -0
  57. package/template/packages/sdk/src/hooks/useCollectionInteractions.ts +709 -0
  58. package/template/packages/sdk/src/hooks/useHandover.ts +3 -0
  59. package/template/packages/sdk/src/hooks/useMultiSelect.ts +44 -482
  60. package/template/packages/sdk/src/hooks/useTool.ts +5 -4
  61. package/template/packages/sdk/src/index.ts +47 -2
  62. package/template/packages/sdk/src/interactions/actions.ts +46 -0
  63. package/template/packages/sdk/src/interactions/shortcuts.tsx +628 -0
  64. package/template/packages/sdk/src/interactions.ts +41 -0
  65. package/template/packages/sdk/src/provider.tsx +2 -1
  66. package/template/packages/sdk/src/runtime.ts +85 -3
  67. package/dist/scaffolds/notis-database/CHANGELOG.md +0 -5
  68. package/dist/scaffolds/notis-database/app/globals.css +0 -44
  69. package/dist/scaffolds/notis-database/app/layout.tsx +0 -6
  70. package/dist/scaffolds/notis-database/app/page.tsx +0 -1088
  71. package/dist/scaffolds/notis-database/components/ui/badge.tsx +0 -28
  72. package/dist/scaffolds/notis-database/components/ui/button.tsx +0 -53
  73. package/dist/scaffolds/notis-database/components/ui/card.tsx +0 -56
  74. package/dist/scaffolds/notis-database/components/ui/table.tsx +0 -120
  75. package/dist/scaffolds/notis-database/components.json +0 -20
  76. package/dist/scaffolds/notis-database/index.html +0 -12
  77. package/dist/scaffolds/notis-database/lib/types.ts +0 -132
  78. package/dist/scaffolds/notis-database/lib/utils.ts +0 -6
  79. package/dist/scaffolds/notis-database/metadata/screenshot-1.png +0 -0
  80. package/dist/scaffolds/notis-database/metadata/screenshot-2.png +0 -0
  81. package/dist/scaffolds/notis-database/metadata/screenshot-3.png +0 -0
  82. package/dist/scaffolds/notis-database/metadata/screenshot-4.png +0 -0
  83. package/dist/scaffolds/notis-database/metadata/screenshot-5.png +0 -0
  84. package/dist/scaffolds/notis-database/metadata/screenshot-fixtures.json +0 -1839
  85. package/dist/scaffolds/notis-database/notis.config.ts +0 -73
  86. package/dist/scaffolds/notis-database/package-lock.json +0 -3935
  87. package/dist/scaffolds/notis-database/package.json +0 -32
  88. package/dist/scaffolds/notis-database/packages/sdk/package.json +0 -36
  89. package/dist/scaffolds/notis-database/packages/sdk/src/components/DocumentEditor.tsx +0 -93
  90. package/dist/scaffolds/notis-database/packages/sdk/src/components/Markdown.tsx +0 -60
  91. package/dist/scaffolds/notis-database/packages/sdk/src/components/MultiSelectActionBar.tsx +0 -278
  92. package/dist/scaffolds/notis-database/packages/sdk/src/components/MultiSelectCheckbox.tsx +0 -91
  93. package/dist/scaffolds/notis-database/packages/sdk/src/components/MultiSelectDragOverlay.tsx +0 -39
  94. package/dist/scaffolds/notis-database/packages/sdk/src/config.ts +0 -234
  95. package/dist/scaffolds/notis-database/packages/sdk/src/documents.ts +0 -250
  96. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useBackend.ts +0 -41
  97. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useCloudComputer.ts +0 -97
  98. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
  99. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDatabaseSubscription.ts +0 -76
  100. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDocument.ts +0 -78
  101. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDocuments.ts +0 -121
  102. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useHandover.ts +0 -75
  103. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useMultiSelect.ts +0 -539
  104. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useNotis.ts +0 -34
  105. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useNotisNavigation.ts +0 -49
  106. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useTool.ts +0 -64
  107. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useTools.ts +0 -56
  108. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
  109. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useUpsertDocument.ts +0 -95
  110. package/dist/scaffolds/notis-database/packages/sdk/src/index.ts +0 -100
  111. package/dist/scaffolds/notis-database/packages/sdk/src/provider.tsx +0 -43
  112. package/dist/scaffolds/notis-database/packages/sdk/src/runtime.ts +0 -351
  113. package/dist/scaffolds/notis-database/packages/sdk/src/styles.css +0 -186
  114. package/dist/scaffolds/notis-database/packages/sdk/src/ui.ts +0 -15
  115. package/dist/scaffolds/notis-database/packages/sdk/src/vite.ts +0 -56
  116. package/dist/scaffolds/notis-database/packages/sdk/tsconfig.json +0 -15
  117. package/dist/scaffolds/notis-database/postcss.config.mjs +0 -8
  118. package/dist/scaffolds/notis-database/src/dev-main.tsx +0 -23
  119. package/dist/scaffolds/notis-database/src/mock-runtime.ts +0 -561
  120. package/dist/scaffolds/notis-database/tailwind.config.ts +0 -59
  121. package/dist/scaffolds/notis-database/tsconfig.json +0 -23
  122. package/dist/scaffolds/notis-database/vite.config.ts +0 -22
  123. package/dist/scaffolds/notis-journal/CHANGELOG.md +0 -29
  124. package/dist/scaffolds/notis-journal/app/globals.css +0 -37
  125. package/dist/scaffolds/notis-journal/app/insights/page.tsx +0 -513
  126. package/dist/scaffolds/notis-journal/app/journal-core.tsx +0 -362
  127. package/dist/scaffolds/notis-journal/app/journal-ui.tsx +0 -337
  128. package/dist/scaffolds/notis-journal/app/layout.tsx +0 -6
  129. package/dist/scaffolds/notis-journal/app/page.tsx +0 -486
  130. package/dist/scaffolds/notis-journal/components/ui/badge.tsx +0 -28
  131. package/dist/scaffolds/notis-journal/components/ui/button.tsx +0 -53
  132. package/dist/scaffolds/notis-journal/components/ui/card.tsx +0 -56
  133. package/dist/scaffolds/notis-journal/components.json +0 -20
  134. package/dist/scaffolds/notis-journal/index.html +0 -12
  135. package/dist/scaffolds/notis-journal/lib/utils.ts +0 -6
  136. package/dist/scaffolds/notis-journal/metadata/screenshot-1.png +0 -0
  137. package/dist/scaffolds/notis-journal/metadata/screenshot-2.png +0 -0
  138. package/dist/scaffolds/notis-journal/metadata/screenshot-3.png +0 -0
  139. package/dist/scaffolds/notis-journal/metadata/screenshot-4.png +0 -0
  140. package/dist/scaffolds/notis-journal/metadata/screenshot-5.png +0 -0
  141. package/dist/scaffolds/notis-journal/metadata/screenshot-6.png +0 -0
  142. package/dist/scaffolds/notis-journal/metadata/screenshot-fixtures.json +0 -132
  143. package/dist/scaffolds/notis-journal/notis.config.ts +0 -93
  144. package/dist/scaffolds/notis-journal/package-lock.json +0 -4615
  145. package/dist/scaffolds/notis-journal/package.json +0 -34
  146. package/dist/scaffolds/notis-journal/packages/sdk/package.json +0 -36
  147. package/dist/scaffolds/notis-journal/packages/sdk/src/components/DocumentEditor.tsx +0 -93
  148. package/dist/scaffolds/notis-journal/packages/sdk/src/components/Markdown.tsx +0 -60
  149. package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectActionBar.tsx +0 -278
  150. package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectCheckbox.tsx +0 -91
  151. package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectDragOverlay.tsx +0 -39
  152. package/dist/scaffolds/notis-journal/packages/sdk/src/config.ts +0 -234
  153. package/dist/scaffolds/notis-journal/packages/sdk/src/documents.ts +0 -250
  154. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useBackend.ts +0 -41
  155. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useCloudComputer.ts +0 -97
  156. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
  157. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDatabaseSubscription.ts +0 -76
  158. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDocument.ts +0 -78
  159. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDocuments.ts +0 -121
  160. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useHandover.ts +0 -75
  161. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useMultiSelect.ts +0 -539
  162. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useNotis.ts +0 -34
  163. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useNotisNavigation.ts +0 -49
  164. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTool.ts +0 -64
  165. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTools.ts +0 -56
  166. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
  167. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useUpsertDocument.ts +0 -95
  168. package/dist/scaffolds/notis-journal/packages/sdk/src/index.ts +0 -100
  169. package/dist/scaffolds/notis-journal/packages/sdk/src/provider.tsx +0 -43
  170. package/dist/scaffolds/notis-journal/packages/sdk/src/runtime.ts +0 -351
  171. package/dist/scaffolds/notis-journal/packages/sdk/src/styles.css +0 -186
  172. package/dist/scaffolds/notis-journal/packages/sdk/src/ui.ts +0 -15
  173. package/dist/scaffolds/notis-journal/packages/sdk/src/vite.ts +0 -56
  174. package/dist/scaffolds/notis-journal/packages/sdk/tsconfig.json +0 -15
  175. package/dist/scaffolds/notis-journal/postcss.config.mjs +0 -8
  176. package/dist/scaffolds/notis-journal/skills/journal-onboarding/SKILL.md +0 -120
  177. package/dist/scaffolds/notis-journal/src/dev-main.tsx +0 -58
  178. package/dist/scaffolds/notis-journal/src/mock-runtime.ts +0 -199
  179. package/dist/scaffolds/notis-journal/tailwind.config.ts +0 -58
  180. package/dist/scaffolds/notis-journal/tsconfig.json +0 -23
  181. package/dist/scaffolds/notis-journal/vite.config.ts +0 -10
  182. package/dist/scaffolds/notis-notes/CHANGELOG.md +0 -5
  183. package/dist/scaffolds/notis-notes/app/globals.css +0 -3
  184. package/dist/scaffolds/notis-notes/app/layout.tsx +0 -6
  185. package/dist/scaffolds/notis-notes/app/page.tsx +0 -1943
  186. package/dist/scaffolds/notis-notes/app/phosphor-icons.ts +0 -596
  187. package/dist/scaffolds/notis-notes/components/ui/badge.tsx +0 -28
  188. package/dist/scaffolds/notis-notes/components/ui/button.tsx +0 -53
  189. package/dist/scaffolds/notis-notes/components/ui/card.tsx +0 -56
  190. package/dist/scaffolds/notis-notes/components.json +0 -20
  191. package/dist/scaffolds/notis-notes/lib/utils.ts +0 -6
  192. package/dist/scaffolds/notis-notes/lib/visible-properties.ts +0 -144
  193. package/dist/scaffolds/notis-notes/metadata/screenshot-1.png +0 -0
  194. package/dist/scaffolds/notis-notes/metadata/screenshot-2.png +0 -0
  195. package/dist/scaffolds/notis-notes/metadata/screenshot-3.png +0 -0
  196. package/dist/scaffolds/notis-notes/metadata/screenshot-4.png +0 -0
  197. package/dist/scaffolds/notis-notes/metadata/screenshot-5.png +0 -0
  198. package/dist/scaffolds/notis-notes/metadata/screenshot-fixtures.json +0 -752
  199. package/dist/scaffolds/notis-notes/notis.config.ts +0 -80
  200. package/dist/scaffolds/notis-notes/package-lock.json +0 -4636
  201. package/dist/scaffolds/notis-notes/package.json +0 -35
  202. package/dist/scaffolds/notis-notes/packages/sdk/package.json +0 -36
  203. package/dist/scaffolds/notis-notes/packages/sdk/src/components/DocumentEditor.tsx +0 -93
  204. package/dist/scaffolds/notis-notes/packages/sdk/src/components/Markdown.tsx +0 -60
  205. package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectActionBar.tsx +0 -278
  206. package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectCheckbox.tsx +0 -91
  207. package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectDragOverlay.tsx +0 -39
  208. package/dist/scaffolds/notis-notes/packages/sdk/src/config.ts +0 -234
  209. package/dist/scaffolds/notis-notes/packages/sdk/src/documents.ts +0 -250
  210. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useBackend.ts +0 -41
  211. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useCloudComputer.ts +0 -97
  212. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
  213. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDatabaseSubscription.ts +0 -76
  214. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDocument.ts +0 -78
  215. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDocuments.ts +0 -121
  216. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useHandover.ts +0 -75
  217. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useMultiSelect.ts +0 -539
  218. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useNotis.ts +0 -34
  219. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useNotisNavigation.ts +0 -49
  220. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTool.ts +0 -64
  221. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTools.ts +0 -56
  222. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
  223. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useUpsertDocument.ts +0 -95
  224. package/dist/scaffolds/notis-notes/packages/sdk/src/index.ts +0 -100
  225. package/dist/scaffolds/notis-notes/packages/sdk/src/provider.tsx +0 -43
  226. package/dist/scaffolds/notis-notes/packages/sdk/src/runtime.ts +0 -351
  227. package/dist/scaffolds/notis-notes/packages/sdk/src/styles.css +0 -186
  228. package/dist/scaffolds/notis-notes/packages/sdk/src/ui.ts +0 -15
  229. package/dist/scaffolds/notis-notes/packages/sdk/src/vite.ts +0 -56
  230. package/dist/scaffolds/notis-notes/packages/sdk/tsconfig.json +0 -15
  231. package/dist/scaffolds/notis-notes/postcss.config.mjs +0 -8
  232. package/dist/scaffolds/notis-notes/tailwind.config.ts +0 -58
  233. package/dist/scaffolds/notis-notes/tsconfig.json +0 -23
  234. package/dist/scaffolds/notis-notes/vite.config.ts +0 -10
  235. package/dist/scaffolds/notis-random/CHANGELOG.md +0 -15
  236. package/dist/scaffolds/notis-random/README.md +0 -33
  237. package/dist/scaffolds/notis-random/app/globals.css +0 -11
  238. package/dist/scaffolds/notis-random/app/history/page.tsx +0 -67
  239. package/dist/scaffolds/notis-random/app/layout.tsx +0 -7
  240. package/dist/scaffolds/notis-random/app/page.tsx +0 -289
  241. package/dist/scaffolds/notis-random/components/ui/button.tsx +0 -50
  242. package/dist/scaffolds/notis-random/components/ui/card.tsx +0 -16
  243. package/dist/scaffolds/notis-random/components/ui/input.tsx +0 -23
  244. package/dist/scaffolds/notis-random/components.json +0 -20
  245. package/dist/scaffolds/notis-random/index.html +0 -12
  246. package/dist/scaffolds/notis-random/lib/notis-tools.ts +0 -128
  247. package/dist/scaffolds/notis-random/lib/rng.ts +0 -202
  248. package/dist/scaffolds/notis-random/lib/roll-record.ts +0 -189
  249. package/dist/scaffolds/notis-random/lib/utils.ts +0 -25
  250. package/dist/scaffolds/notis-random/metadata/screenshot-1.png +0 -0
  251. package/dist/scaffolds/notis-random/metadata/screenshot-2.png +0 -0
  252. package/dist/scaffolds/notis-random/metadata/screenshot-3.png +0 -0
  253. package/dist/scaffolds/notis-random/metadata/screenshot-4.png +0 -0
  254. package/dist/scaffolds/notis-random/metadata/screenshot-5.png +0 -0
  255. package/dist/scaffolds/notis-random/metadata/screenshot-fixtures.json +0 -753
  256. package/dist/scaffolds/notis-random/notis.config.ts +0 -86
  257. package/dist/scaffolds/notis-random/package-lock.json +0 -4513
  258. package/dist/scaffolds/notis-random/package.json +0 -36
  259. package/dist/scaffolds/notis-random/packages/sdk/package.json +0 -36
  260. package/dist/scaffolds/notis-random/packages/sdk/src/components/DocumentEditor.tsx +0 -93
  261. package/dist/scaffolds/notis-random/packages/sdk/src/components/Markdown.tsx +0 -60
  262. package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectActionBar.tsx +0 -278
  263. package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectCheckbox.tsx +0 -91
  264. package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectDragOverlay.tsx +0 -39
  265. package/dist/scaffolds/notis-random/packages/sdk/src/config.ts +0 -234
  266. package/dist/scaffolds/notis-random/packages/sdk/src/documents.ts +0 -250
  267. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useBackend.ts +0 -41
  268. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useCloudComputer.ts +0 -97
  269. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
  270. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDatabaseSubscription.ts +0 -76
  271. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDocument.ts +0 -78
  272. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDocuments.ts +0 -121
  273. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useHandover.ts +0 -75
  274. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useMultiSelect.ts +0 -539
  275. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useNotis.ts +0 -34
  276. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useNotisNavigation.ts +0 -49
  277. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTool.ts +0 -64
  278. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTools.ts +0 -56
  279. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
  280. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useUpsertDocument.ts +0 -95
  281. package/dist/scaffolds/notis-random/packages/sdk/src/index.ts +0 -100
  282. package/dist/scaffolds/notis-random/packages/sdk/src/provider.tsx +0 -43
  283. package/dist/scaffolds/notis-random/packages/sdk/src/runtime.ts +0 -351
  284. package/dist/scaffolds/notis-random/packages/sdk/src/styles.css +0 -186
  285. package/dist/scaffolds/notis-random/packages/sdk/src/ui.ts +0 -15
  286. package/dist/scaffolds/notis-random/packages/sdk/src/vite.ts +0 -56
  287. package/dist/scaffolds/notis-random/packages/sdk/tsconfig.json +0 -15
  288. package/dist/scaffolds/notis-random/postcss.config.mjs +0 -6
  289. package/dist/scaffolds/notis-random/src/dev-main.tsx +0 -70
  290. package/dist/scaffolds/notis-random/src/mock-runtime.ts +0 -129
  291. package/dist/scaffolds/notis-random/tailwind.config.ts +0 -50
  292. package/dist/scaffolds/notis-random/tsconfig.json +0 -23
  293. package/dist/scaffolds/notis-random/vite.config.ts +0 -11
  294. package/dist/scaffolds.json +0 -49
@@ -0,0 +1,215 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { mkdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
3
+ import { homedir } from 'node:os';
4
+ import { dirname, join } from 'node:path';
5
+
6
+ import { reconcileBaseSkills } from './base-skills.js';
7
+
8
+ const DEFAULT_LOCK_TIMEOUT_MS = 30_000;
9
+ const DEFAULT_LOCK_STALE_MS = 10 * 60_000;
10
+ const DEFAULT_LOCK_POLL_MS = 50;
11
+
12
+ async function lockSnapshot(lockDirectory) {
13
+ try {
14
+ const [raw, metadata] = await Promise.all([
15
+ readFile(join(lockDirectory, 'owner'), 'utf8').catch(() => ''),
16
+ stat(lockDirectory),
17
+ ]);
18
+ let owner = {};
19
+ try {
20
+ owner = raw ? JSON.parse(raw) : {};
21
+ } catch {
22
+ // A process can die between mkdir and the atomic owner write. Preserve
23
+ // the directory mtime as a reclaimable ownerless lease.
24
+ }
25
+ return {
26
+ id: typeof owner.id === 'string' ? owner.id : null,
27
+ pid: Number.isInteger(Number(owner.pid)) ? Number(owner.pid) : null,
28
+ at: Number.isFinite(Number(owner.at)) ? Number(owner.at) : metadata.mtimeMs,
29
+ mtimeMs: metadata.mtimeMs,
30
+ };
31
+ } catch {
32
+ return null;
33
+ }
34
+ }
35
+
36
+ function sameLockSnapshot(left, right) {
37
+ return Boolean(
38
+ left
39
+ && right
40
+ && left.id === right.id
41
+ && left.pid === right.pid
42
+ && left.at === right.at
43
+ && left.mtimeMs === right.mtimeMs,
44
+ );
45
+ }
46
+
47
+ function processIsAlive(pid) {
48
+ if (!Number.isInteger(pid) || pid <= 0) return false;
49
+ try {
50
+ process.kill(pid, 0);
51
+ return true;
52
+ } catch (error) {
53
+ return error?.code === 'EPERM';
54
+ }
55
+ }
56
+
57
+ function delay(milliseconds) {
58
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
59
+ }
60
+
61
+ async function quarantineStaleLock(lockDirectory, snapshot) {
62
+ const quarantineRoot = join(dirname(lockDirectory), '.stale-operation-locks');
63
+ const snapshotToken = createHash('sha256')
64
+ .update(JSON.stringify(snapshot))
65
+ .digest('hex');
66
+ await mkdir(quarantineRoot, { recursive: true, mode: 0o700 });
67
+ try {
68
+ // The deterministic destination is the compare-and-swap guard. If two
69
+ // waiters observed the same stale owner, only one can move it here. The
70
+ // retained non-empty tombstone prevents the loser from ever moving a new
71
+ // live lock that appeared at the shared pathname in the meantime.
72
+ await rename(lockDirectory, join(quarantineRoot, snapshotToken));
73
+ return true;
74
+ } catch (error) {
75
+ if (['EEXIST', 'ENOTEMPTY', 'ENOENT'].includes(error?.code)) return false;
76
+ throw error;
77
+ }
78
+ }
79
+
80
+ async function writeLockOwnerAtomically(lockDirectory, owner) {
81
+ const temporaryOwnerPath = join(lockDirectory, `.owner.${owner.id}.tmp`);
82
+ const ownerPath = join(lockDirectory, 'owner');
83
+ await writeFile(temporaryOwnerPath, JSON.stringify(owner), { mode: 0o600 });
84
+ await rename(temporaryOwnerPath, ownerPath);
85
+ }
86
+
87
+ /** Serialize Desktop and terminal skill sync across processes on one Mac. */
88
+ export async function withSkillSyncLock(callback, {
89
+ home = homedir(),
90
+ timeoutMs = DEFAULT_LOCK_TIMEOUT_MS,
91
+ staleMs = DEFAULT_LOCK_STALE_MS,
92
+ pollMs = DEFAULT_LOCK_POLL_MS,
93
+ now = () => Date.now(),
94
+ } = {}) {
95
+ const lockDirectory = join(home, '.notis', 'skills', '.operation-lock');
96
+ const ownerId = `${process.pid}.${randomUUID()}`;
97
+ const deadline = now() + timeoutMs;
98
+ await mkdir(dirname(lockDirectory), { recursive: true, mode: 0o700 });
99
+
100
+ for (;;) {
101
+ try {
102
+ await mkdir(lockDirectory, { mode: 0o700 });
103
+ try {
104
+ await writeLockOwnerAtomically(lockDirectory, {
105
+ id: ownerId,
106
+ pid: process.pid,
107
+ at: now(),
108
+ });
109
+ } catch (error) {
110
+ // This process exclusively created the directory and has not yet
111
+ // published an owner, so it is safe to undo a failed acquisition.
112
+ await rm(lockDirectory, { recursive: true, force: true });
113
+ throw error;
114
+ }
115
+ break;
116
+ } catch (error) {
117
+ if (error?.code !== 'EEXIST') throw error;
118
+ }
119
+
120
+ const observed = await lockSnapshot(lockDirectory);
121
+ if (
122
+ observed
123
+ && now() - observed.at > staleMs
124
+ && !processIsAlive(observed.pid)
125
+ ) {
126
+ // Require an unchanged observation interval before stealing. This avoids
127
+ // racing a holder that is publishing or refreshing its owner lease.
128
+ await delay(Math.max(pollMs, 10));
129
+ const current = await lockSnapshot(lockDirectory);
130
+ if (sameLockSnapshot(observed, current) && !processIsAlive(current?.pid)) {
131
+ if (await quarantineStaleLock(lockDirectory, current)) continue;
132
+ }
133
+ }
134
+ if (now() >= deadline) {
135
+ throw new Error('Timed out waiting for another Notis skill sync to finish.');
136
+ }
137
+ await delay(pollMs);
138
+ }
139
+
140
+ const heartbeatMs = Math.max(10, Math.min(30_000, Math.floor(staleMs / 3)));
141
+ let heartbeatStopped = false;
142
+ let heartbeatInFlight = Promise.resolve();
143
+ const refreshHeartbeat = async () => {
144
+ if (heartbeatStopped) return;
145
+ const temporaryOwnerPath = join(lockDirectory, `.owner.${ownerId}.tmp`);
146
+ await writeFile(
147
+ temporaryOwnerPath,
148
+ JSON.stringify({ id: ownerId, pid: process.pid, at: now() }),
149
+ { mode: 0o600 },
150
+ );
151
+ const owner = await lockSnapshot(lockDirectory);
152
+ if (owner?.id !== ownerId) {
153
+ await rm(temporaryOwnerPath, { force: true });
154
+ return;
155
+ }
156
+ // If a stale-lock reclaimer moved this directory after the ownership
157
+ // check, the source path disappears and rename fails instead of
158
+ // overwriting the owner of a newly acquired lock at the shared path.
159
+ await rename(temporaryOwnerPath, join(lockDirectory, 'owner'));
160
+ };
161
+ const heartbeat = setInterval(() => {
162
+ // Serialize refreshes and retain the active promise so cleanup cannot race
163
+ // a temporary owner write that was already in flight when the interval was
164
+ // cleared.
165
+ heartbeatInFlight = heartbeatInFlight
166
+ .then(refreshHeartbeat)
167
+ .catch(() => undefined);
168
+ }, heartbeatMs);
169
+ heartbeat.unref?.();
170
+
171
+ try {
172
+ return await callback();
173
+ } finally {
174
+ heartbeatStopped = true;
175
+ clearInterval(heartbeat);
176
+ await heartbeatInFlight;
177
+ const owner = await lockSnapshot(lockDirectory);
178
+ if (owner?.id === ownerId) {
179
+ await rm(lockDirectory, { recursive: true, force: true });
180
+ }
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Shared CLI-owned orchestration for base-skill installation plus account sync.
186
+ * Desktop injects the source engine that webpack bundles; the CLI command
187
+ * injects the generated dist engine used by the published package.
188
+ */
189
+ export async function reconcileAllSkills({
190
+ serverUrl,
191
+ jwt,
192
+ honorSyncEnabled,
193
+ userId = null,
194
+ home,
195
+ runAccountSync,
196
+ reconcileBase = reconcileBaseSkills,
197
+ lockOptions = {},
198
+ }) {
199
+ return withSkillSyncLock(async () => {
200
+ const base = reconcileBase({ userId, ...(home ? { home } : {}) });
201
+ const account = await runAccountSync(
202
+ serverUrl,
203
+ jwt,
204
+ {},
205
+ { honorSyncEnabled },
206
+ );
207
+ return {
208
+ ...account,
209
+ baseSkills: base.skills,
210
+ baseInstalled: base.installed,
211
+ baseLinked: base.linked,
212
+ baseBackups: base.backups,
213
+ };
214
+ }, { ...lockOptions, ...(home ? { home } : {}) });
215
+ }
@@ -1,6 +1,10 @@
1
1
  import '@notis/sdk/styles.css';
2
+ import { ShortcutProvider } from '@notis/sdk/interactions';
2
3
  import './globals.css';
3
4
 
4
5
  export default function AppShell({ children }: { children: React.ReactNode }) {
5
- return children;
6
+ // NotisProvider supplies this in the product host. Keeping the shell-level
7
+ // provider makes the same scope arbitration available in the local harness;
8
+ // ShortcutProvider is nesting-safe, so the hosted app still has one registry.
9
+ return <ShortcutProvider>{children}</ShortcutProvider>;
6
10
  }
@@ -2,6 +2,7 @@ import { defineNotisApp } from '@notis/sdk/config';
2
2
 
3
3
  export default defineNotisApp({
4
4
  name: 'my-notis-app',
5
+ devSlug: 'my-notis-app',
5
6
  title: 'My Notis App',
6
7
  description: 'A focused Notis app with one route and one database-backed workflow.',
7
8
  icon: 'phosphor:squares-four',
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@notis/sdk",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "exports": {
7
7
  ".": "./src/index.ts",
8
+ "./interactions": "./src/interactions.ts",
8
9
  "./config": "./src/config.ts",
9
10
  "./vite": "./src/vite.ts",
10
11
  "./ui": "./src/ui.ts",
@@ -0,0 +1,121 @@
1
+ import type { ChangeEvent, KeyboardEvent, ReactElement } from 'react';
2
+ import { useCallback, useEffect, useRef, useState } from 'react';
3
+ import { useNotisRuntime } from '../provider';
4
+ import type { NotisMarkdownEditorProps } from '../runtime';
5
+
6
+ function MarkdownEditorFallback({
7
+ value,
8
+ revision,
9
+ readOnly,
10
+ autosaveMs = 1500,
11
+ placeholder = 'Write in Markdown…',
12
+ className,
13
+ onChange,
14
+ onSave,
15
+ onDirtyChange,
16
+ onSavingChange,
17
+ }: NotisMarkdownEditorProps) {
18
+ const [draft, setDraft] = useState(value);
19
+ const [saveAttempt, setSaveAttempt] = useState(0);
20
+ const draftRef = useRef(value);
21
+ const dirtyRef = useRef(false);
22
+ const savingRef = useRef(false);
23
+ const revisionRef = useRef(revision);
24
+ const onSaveRef = useRef(onSave);
25
+ const readOnlyRef = useRef(readOnly);
26
+ const unmountedRef = useRef(false);
27
+ const saveRef = useRef<() => Promise<void>>(async () => undefined);
28
+
29
+ useEffect(() => {
30
+ onSaveRef.current = onSave;
31
+ readOnlyRef.current = readOnly;
32
+ }, [onSave, readOnly]);
33
+
34
+ useEffect(() => {
35
+ if (dirtyRef.current) return;
36
+ draftRef.current = value;
37
+ revisionRef.current = revision;
38
+ setDraft(value);
39
+ }, [revision, value]);
40
+
41
+ const save = useCallback(async () => {
42
+ if (!onSave || !dirtyRef.current || readOnly || savingRef.current) return;
43
+ const markdown = draftRef.current;
44
+ savingRef.current = true;
45
+ if (!unmountedRef.current) onSavingChange?.(true);
46
+ try {
47
+ const result = await onSave({ markdown, expectedRevision: revisionRef.current });
48
+ if (result?.revision) revisionRef.current = result.revision;
49
+ if (draftRef.current === markdown) {
50
+ dirtyRef.current = false;
51
+ if (!unmountedRef.current) onDirtyChange?.(false);
52
+ }
53
+ } finally {
54
+ savingRef.current = false;
55
+ if (!unmountedRef.current) onSavingChange?.(false);
56
+ if (dirtyRef.current && draftRef.current !== markdown) {
57
+ if (unmountedRef.current) {
58
+ void saveRef.current().catch((error) => console.error('Could not save Markdown on unmount', error));
59
+ } else {
60
+ setSaveAttempt((attempt) => attempt + 1);
61
+ }
62
+ }
63
+ }
64
+ }, [onDirtyChange, onSave, onSavingChange, readOnly]);
65
+ saveRef.current = save;
66
+
67
+ useEffect(() => {
68
+ unmountedRef.current = false;
69
+ return () => {
70
+ unmountedRef.current = true;
71
+ if (!savingRef.current) {
72
+ void saveRef.current().catch((error) => console.error('Could not save Markdown on unmount', error));
73
+ }
74
+ };
75
+ }, []);
76
+
77
+ useEffect(() => {
78
+ if (!dirtyRef.current || !onSave || readOnly) return;
79
+ const timer = window.setTimeout(() => void save(), Math.max(250, autosaveMs));
80
+ return () => window.clearTimeout(timer);
81
+ }, [autosaveMs, draft, onSave, readOnly, save, saveAttempt]);
82
+
83
+ const handleChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
84
+ const next = event.currentTarget.value;
85
+ draftRef.current = next;
86
+ setDraft(next);
87
+ if (!dirtyRef.current) {
88
+ dirtyRef.current = true;
89
+ onDirtyChange?.(true);
90
+ }
91
+ onChange?.(next);
92
+ };
93
+
94
+ const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
95
+ if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 's') {
96
+ event.preventDefault();
97
+ void save();
98
+ }
99
+ };
100
+
101
+ return (
102
+ <textarea
103
+ value={draft}
104
+ readOnly={readOnly}
105
+ placeholder={placeholder}
106
+ className={className}
107
+ onChange={handleChange}
108
+ onKeyDown={handleKeyDown}
109
+ style={{ minHeight: 360, width: '100%', resize: 'vertical' }}
110
+ />
111
+ );
112
+ }
113
+
114
+ /** Use Notis' editor while leaving persistence entirely under app control. */
115
+ export function MarkdownEditor(props: NotisMarkdownEditorProps): ReactElement {
116
+ const runtime = useNotisRuntime();
117
+ const HostEditor = runtime?.ui?.MarkdownEditor;
118
+ return HostEditor
119
+ ? <HostEditor key={props.resourceKey} {...props} />
120
+ : <MarkdownEditorFallback key={props.resourceKey} {...props} />;
121
+ }
@@ -1,25 +1,16 @@
1
1
  'use client';
2
2
 
3
- import {
4
- useEffect,
3
+ import React, {
5
4
  useMemo,
6
- useRef,
7
5
  useState,
8
6
  type CSSProperties,
9
7
  type ReactElement,
10
- type ReactNode,
11
8
  } from 'react';
9
+ import type { ResolvedCollectionAction } from '../interactions/actions';
10
+ import { shortcutDisplay, useShortcuts, type ShortcutDefinition } from '../interactions/shortcuts';
11
+ import type { ShortcutScope } from '../interactions/shortcuts';
12
12
 
13
- export interface MultiSelectAction {
14
- id: string;
15
- label: string;
16
- icon: ReactNode;
17
- /** Single-key (e.g. "E", "#") or "Mod+K" shortcut. Bound while the bar is mounted with count > 0. */
18
- shortcut?: string;
19
- onRun: () => void | Promise<void>;
20
- /** Hint for future destructive styling. Currently unused visually. */
21
- destructive?: boolean;
22
- }
13
+ export type MultiSelectAction = ResolvedCollectionAction;
23
14
 
24
15
  export interface MultiSelectActionBarProps {
25
16
  selectedCount: number;
@@ -30,73 +21,10 @@ export interface MultiSelectActionBarProps {
30
21
  className?: string;
31
22
  /** Optional override for the bar's positioning style. Defaults to bottom-center floating. */
32
23
  style?: CSSProperties;
33
- }
34
-
35
- interface ParsedShortcut {
36
- key: string;
37
- needsMod: boolean;
38
- needsShift: boolean;
39
- }
40
-
41
- function parseShortcut(raw: string): ParsedShortcut | null {
42
- const parts = raw.split('+').map((p) => p.trim()).filter(Boolean);
43
- if (parts.length === 0) return null;
44
- let needsMod = false;
45
- let needsShift = false;
46
- let key = '';
47
- for (const part of parts) {
48
- const lower = part.toLowerCase();
49
- if (lower === 'mod' || lower === 'cmd' || lower === 'ctrl' || lower === 'meta') {
50
- needsMod = true;
51
- } else if (lower === 'shift') {
52
- needsShift = true;
53
- } else {
54
- key = lower;
55
- }
56
- }
57
- if (!key) return null;
58
- return { key, needsMod, needsShift };
59
- }
60
-
61
- function shortcutMatches(shortcut: ParsedShortcut, event: KeyboardEvent): boolean {
62
- const eventKey = event.key.toLowerCase();
63
- // Special-case "#" so Shift+3 matches without the consumer specifying Shift.
64
- if (shortcut.key === '#') {
65
- if (eventKey === '#') return true;
66
- if (event.shiftKey && eventKey === '3') return true;
67
- return false;
68
- }
69
- if (eventKey !== shortcut.key) return false;
70
- if (shortcut.needsMod !== Boolean(event.metaKey || event.ctrlKey)) return false;
71
- if (shortcut.needsShift !== event.shiftKey) return false;
72
- if (event.altKey) return false;
73
- return true;
74
- }
75
-
76
- function isEditableEvent(event: Event): boolean {
77
- // Notis apps mount inside a Shadow DOM, so a document-level listener sees
78
- // `event.target` retargeted to the shadow host. `composedPath()[0]` pierces
79
- // the shadow boundary to the real focused element, so action-bar shortcuts
80
- // don't fire while the user is typing in an app input.
81
- const target = event.composedPath?.()[0] ?? event.target;
82
- if (!(target instanceof HTMLElement)) return false;
83
- if (target.isContentEditable) return true;
84
- const tag = target.tagName;
85
- return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT';
86
- }
87
-
88
- function shortcutDisplay(raw: string): string {
89
- const parts = raw.split('+').map((p) => p.trim()).filter(Boolean);
90
- return parts
91
- .map((part) => {
92
- const lower = part.toLowerCase();
93
- if (lower === 'mod' || lower === 'cmd' || lower === 'meta') return '⌘';
94
- if (lower === 'ctrl') return '⌃';
95
- if (lower === 'shift') return '⇧';
96
- if (lower === 'alt' || lower === 'option') return '⌥';
97
- return part.length === 1 ? part.toUpperCase() : part;
98
- })
99
- .join('');
24
+ /** Disable action key bindings while leaving the visible toolbar mounted. */
25
+ shortcutsEnabled?: boolean;
26
+ /** Override the default collection shortcut scope. */
27
+ shortcutScope?: ShortcutScope;
100
28
  }
101
29
 
102
30
  const containerBaseStyle: CSSProperties = {
@@ -180,43 +108,25 @@ export function MultiSelectActionBar({
180
108
  itemLabel,
181
109
  className,
182
110
  style,
111
+ shortcutsEnabled = true,
112
+ shortcutScope = 'collection',
183
113
  }: MultiSelectActionBarProps): ReactElement | null {
184
- const actionsRef = useRef(actions);
185
- actionsRef.current = actions;
186
-
187
- const parsedShortcuts = useMemo(() => {
188
- return actions
189
- .map((action) => {
190
- if (!action.shortcut) return null;
191
- const parsed = parseShortcut(action.shortcut);
192
- return parsed ? { action, parsed } : null;
193
- })
194
- .filter((entry): entry is { action: MultiSelectAction; parsed: ParsedShortcut } => entry !== null);
114
+ const actionShortcuts = useMemo<ShortcutDefinition[]>(() => {
115
+ return actions.flatMap((action): ShortcutDefinition[] => {
116
+ if (!action.shortcut || action.disabled || action.pending) return [];
117
+ return [{
118
+ id: `collection.action.${action.id}`,
119
+ keys: action.shortcut,
120
+ label: action.label,
121
+ onTrigger: () => action.onRun(),
122
+ }];
123
+ });
195
124
  }, [actions]);
196
-
197
- useEffect(() => {
198
- if (selectedCount === 0 || parsedShortcuts.length === 0) return;
199
-
200
- const handleKeyDown = (event: KeyboardEvent) => {
201
- if (isEditableEvent(event)) return;
202
- for (const { action, parsed } of parsedShortcuts) {
203
- if (shortcutMatches(parsed, event)) {
204
- event.preventDefault();
205
- event.stopPropagation();
206
- if (typeof event.stopImmediatePropagation === 'function') {
207
- event.stopImmediatePropagation();
208
- }
209
- void action.onRun();
210
- return;
211
- }
212
- }
213
- };
214
-
215
- document.addEventListener('keydown', handleKeyDown, true);
216
- return () => {
217
- document.removeEventListener('keydown', handleKeyDown, true);
218
- };
219
- }, [parsedShortcuts, selectedCount]);
125
+ useShortcuts(actionShortcuts, {
126
+ enabled: shortcutsEnabled && selectedCount > 0,
127
+ scope: shortcutScope,
128
+ priority: 25,
129
+ });
220
130
 
221
131
  if (selectedCount === 0) return null;
222
132
 
@@ -258,21 +168,29 @@ function ActionButton({ action }: { action: MultiSelectAction }) {
258
168
  color: hover
259
169
  ? 'hsl(var(--background))'
260
170
  : baseButtonStyle.color,
171
+ opacity: action.disabled ? 0.45 : 1,
172
+ cursor: action.disabled || action.pending ? 'not-allowed' : 'pointer',
261
173
  };
262
174
 
263
175
  return (
264
176
  <button
265
177
  type="button"
266
- onClick={() => void action.onRun()}
178
+ onClick={() => {
179
+ if (!action.disabled && !action.pending) void action.onRun();
180
+ }}
181
+ disabled={action.disabled || action.pending}
182
+ aria-busy={action.pending || undefined}
183
+ aria-keyshortcuts={action.shortcut || undefined}
184
+ data-destructive={action.destructive ? 'true' : 'false'}
267
185
  onMouseEnter={() => setHover(true)}
268
186
  onMouseLeave={() => setHover(false)}
269
187
  onFocus={() => setHover(true)}
270
188
  onBlur={() => setHover(false)}
271
189
  style={buttonStyle}
272
190
  >
273
- <span aria-hidden style={iconSlotStyle}>{action.icon}</span>
191
+ {action.icon ? <span aria-hidden style={iconSlotStyle}>{action.icon}</span> : null}
274
192
  {display ? <kbd aria-hidden style={keycapStyle}>{display}</kbd> : null}
275
- <span>{action.label}</span>
193
+ <span>{action.pending ? `${action.label}…` : action.label}</span>
276
194
  </button>
277
195
  );
278
196
  }
@@ -1,10 +1,11 @@
1
1
  'use client';
2
2
 
3
- import { type CSSProperties, type MouseEvent as ReactMouseEvent, type ReactElement } from 'react';
3
+ import React, { type CSSProperties, type MouseEvent as ReactMouseEvent, type ReactElement } from 'react';
4
4
 
5
5
  export interface MultiSelectCheckboxProps {
6
6
  isSelected: boolean;
7
7
  onClick: (event: ReactMouseEvent) => void;
8
+ disabled?: boolean;
8
9
  /** When true, the checkbox is always visible. Default false (hover/focus reveal via the parent's :hover state). */
9
10
  alwaysVisible?: boolean;
10
11
  /** Optional aria-label override. Defaults to "Select item" / "Deselect item". */
@@ -40,6 +41,7 @@ const checkPath = 'M3.5 7.5l2.5 2.5 6.5-6.5';
40
41
  export function MultiSelectCheckbox({
41
42
  isSelected,
42
43
  onClick,
44
+ disabled = false,
43
45
  alwaysVisible = false,
44
46
  ariaLabel,
45
47
  className,
@@ -49,6 +51,7 @@ export function MultiSelectCheckbox({
49
51
  ...baseStyle,
50
52
  ...(isSelected ? selectedStyle : null),
51
53
  ...(alwaysVisible || isSelected ? { opacity: 1 } : null),
54
+ ...(disabled ? { opacity: 0.45, cursor: 'not-allowed' } : null),
52
55
  ...(style || null),
53
56
  };
54
57
 
@@ -62,6 +65,7 @@ export function MultiSelectCheckbox({
62
65
  type="button"
63
66
  role="checkbox"
64
67
  aria-checked={isSelected}
68
+ disabled={disabled}
65
69
  aria-label={ariaLabel ?? (isSelected ? 'Deselect item' : 'Select item')}
66
70
  onClick={onClick}
67
71
  onMouseDown={(e) => e.stopPropagation()}
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { type CSSProperties, type ReactElement } from 'react';
3
+ import React, { type CSSProperties, type ReactElement } from 'react';
4
4
  import type { DragRect } from '../hooks/useMultiSelect';
5
5
 
6
6
  export interface MultiSelectDragOverlayProps {
@@ -0,0 +1,55 @@
1
+ import type { CSSProperties, ClipboardEvent, ReactNode } from 'react';
2
+ import { useCallback } from 'react';
3
+ import { useNotisRuntime } from '../provider';
4
+ import type { ContextResource, ContextSelection } from '../runtime';
5
+
6
+ export const NOTIS_CONTEXT_CLIPBOARD_TYPE = 'application/x-notis-context+json';
7
+
8
+ export interface NotisSelectionBoundaryProps {
9
+ resource?: ContextResource | null;
10
+ children: ReactNode;
11
+ className?: string;
12
+ style?: CSSProperties;
13
+ }
14
+
15
+ function selectionId(): string {
16
+ return typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
17
+ ? crypto.randomUUID()
18
+ : `selection-${Date.now()}-${Math.random().toString(36).slice(2)}`;
19
+ }
20
+
21
+ /**
22
+ * Adds structured provenance to ordinary copied text. Pasting still works in
23
+ * every app because `text/plain` is preserved; Notis chat additionally reads
24
+ * the custom MIME payload and renders a separate quote pill.
25
+ */
26
+ export function NotisSelectionBoundary({
27
+ resource,
28
+ children,
29
+ className,
30
+ style,
31
+ }: NotisSelectionBoundaryProps) {
32
+ const runtime = useNotisRuntime();
33
+ const onCopy = useCallback((event: ClipboardEvent<HTMLDivElement>) => {
34
+ const selection = window.getSelection();
35
+ const text = selection?.toString().trim() ?? '';
36
+ if (
37
+ !text
38
+ || !selection
39
+ || !event.currentTarget.contains(selection.anchorNode)
40
+ || !event.currentTarget.contains(selection.focusNode)
41
+ ) return;
42
+
43
+ const payload: ContextSelection = {
44
+ id: selectionId(),
45
+ text: text.slice(0, 12_000),
46
+ ...(resource ? { resource: { ...resource, snapshot: undefined } } : {}),
47
+ };
48
+ event.clipboardData.setData('text/plain', text);
49
+ event.clipboardData.setData(NOTIS_CONTEXT_CLIPBOARD_TYPE, JSON.stringify(payload));
50
+ event.preventDefault();
51
+ runtime?.captureContextSelection?.(payload);
52
+ }, [resource, runtime]);
53
+
54
+ return <div className={className} style={style} onCopy={onCopy}>{children}</div>;
55
+ }