@notis_ai/cli 0.2.18 → 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 +42325 -18273
  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 -1570
  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 -3011
  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
@@ -0,0 +1,136 @@
1
+ /**
2
+ * How keys read on screen (toolkit spec 2.2), matching the review deck:
3
+ * - Mac: glyphs in ctrl, alt, shift, cmd order, then the key ("⇧E", "⌘A").
4
+ * - Elsewhere: names joined by "+" ("Ctrl+A"); French says "Maj" for Shift.
5
+ * - Sequences join with "then" / "puis"; alternatives with "or" / "ou".
6
+ */
7
+
8
+ import { keyList, parseKeySpec, type Chord } from './keys';
9
+ import type { KeySpec, Locale, Platform, Words } from './types';
10
+
11
+ export interface FormatKeyOptions {
12
+ platform: Platform;
13
+ locale: Locale;
14
+ }
15
+
16
+ export const KEY_JOIN_WORDS: Readonly<{ then: Words; or: Words }> = Object.freeze({
17
+ then: { en: 'then', fr: 'puis' },
18
+ or: { en: 'or', fr: 'ou' },
19
+ });
20
+
21
+ const MAC_GLYPH = { ctrl: '⌃', alt: '⌥', shift: '⇧', mod: '⌘' } as const;
22
+
23
+ function keyName(key: string, mac: boolean, fr: boolean): string {
24
+ switch (key) {
25
+ case 'enter': return mac ? '↵' : fr ? 'Entrée' : 'Enter';
26
+ // French says Échap on every computer, as the Portal does.
27
+ case 'escape': return fr ? 'Échap' : 'Esc';
28
+ case 'space': return fr ? 'Espace' : 'Space';
29
+ case 'arrowdown': return '↓';
30
+ case 'arrowup': return '↑';
31
+ case 'arrowleft': return '←';
32
+ case 'arrowright': return '→';
33
+ case 'tab': return 'Tab';
34
+ case 'backspace': return mac ? '⌫' : fr ? 'Retour arrière' : 'Backspace';
35
+ case 'delete': return mac ? '⌦' : fr ? 'Suppr' : 'Delete';
36
+ case 'insert': return fr ? 'Inser' : 'Insert';
37
+ case 'home': return fr ? 'Début' : 'Home';
38
+ case 'end': return fr ? 'Fin' : 'End';
39
+ case 'pagedown': return fr ? 'Pg suiv' : 'PgDn';
40
+ case 'pageup': return fr ? 'Pg préc' : 'PgUp';
41
+ default:
42
+ if (key.length === 1) return key.toUpperCase();
43
+ return key[0]!.toUpperCase() + key.slice(1);
44
+ }
45
+ }
46
+
47
+ /** One chord as shown on a single keycap. */
48
+ export function formatChord(chord: Chord, { platform, locale }: FormatKeyOptions): string {
49
+ const mac = platform === 'mac';
50
+ const fr = locale === 'fr';
51
+ if (mac) {
52
+ const glyphs = (['ctrl', 'alt', 'shift', 'mod'] as const).filter((m) => chord[m]).map((m) => MAC_GLYPH[m]).join('');
53
+ return glyphs + keyName(chord.key, mac, fr);
54
+ }
55
+ const names: string[] = [];
56
+ if (chord.mod || chord.ctrl) names.push('Ctrl');
57
+ if (chord.alt) names.push('Alt');
58
+ if (chord.shift) names.push(fr ? 'Maj' : 'Shift');
59
+ return [...names, keyName(chord.key, mac, fr)].join('+');
60
+ }
61
+
62
+ /**
63
+ * Keycap parts: one entry per alternative, each a list of caps (one per sequence step).
64
+ * `[['⇧E']]`, `[['G', 'I']]`, `[['J'], ['↓']]`.
65
+ */
66
+ export function formatKeyParts(spec: KeySpec | readonly KeySpec[], options: FormatKeyOptions): string[][] {
67
+ return keyList(spec).map((one) => parseKeySpec(one).map((chord) => formatChord(chord, options)));
68
+ }
69
+
70
+ /** The whole spec as one string: "J or ↓", "G then I", "⇧E". */
71
+ export function formatKey(spec: KeySpec | readonly KeySpec[], options: FormatKeyOptions): string {
72
+ const then = ` ${KEY_JOIN_WORDS.then[options.locale]} `;
73
+ const or = ` ${KEY_JOIN_WORDS.or[options.locale]} `;
74
+ return formatKeyParts(spec, options).map((caps) => caps.join(then)).join(or);
75
+ }
76
+
77
+ const ARIA_KEY: Readonly<Record<string, string>> = {
78
+ escape: 'Escape', enter: 'Enter', tab: 'Tab', space: 'Space', backspace: 'Backspace', delete: 'Delete',
79
+ insert: 'Insert', arrowup: 'ArrowUp', arrowdown: 'ArrowDown', arrowleft: 'ArrowLeft', arrowright: 'ArrowRight',
80
+ home: 'Home', end: 'End', pageup: 'PageUp', pagedown: 'PageDown',
81
+ };
82
+
83
+ /** Value for the `aria-keyshortcuts` attribute ("Shift+E J ArrowDown"). Sequences are left out (ARIA has no syntax). */
84
+ export function toAriaKeyShortcuts(spec: KeySpec | readonly KeySpec[], platform: Platform): string {
85
+ return keyList(spec)
86
+ .map((one) => parseKeySpec(one))
87
+ .filter((chords) => chords.length === 1)
88
+ .map(([chord]) => {
89
+ const parts: string[] = [];
90
+ if (chord!.mod) parts.push(platform === 'mac' ? 'Meta' : 'Control');
91
+ if (chord!.ctrl && !(chord!.mod && platform !== 'mac')) parts.push('Control');
92
+ if (chord!.alt) parts.push('Alt');
93
+ if (chord!.shift) parts.push('Shift');
94
+ const key = chord!.key;
95
+ parts.push(ARIA_KEY[key] ?? (key.length === 1 ? key.toUpperCase() : key[0]!.toUpperCase() + key.slice(1)));
96
+ return parts.join('+');
97
+ })
98
+ .join(' ');
99
+ }
100
+
101
+ const SPOKEN_KEY: Readonly<Record<string, Words>> = Object.freeze({
102
+ enter: { en: 'Enter', fr: 'Entrée' },
103
+ escape: { en: 'Escape', fr: 'Échap' },
104
+ space: { en: 'Space', fr: 'Espace' },
105
+ arrowdown: { en: 'Down arrow', fr: 'Flèche bas' },
106
+ arrowup: { en: 'Up arrow', fr: 'Flèche haut' },
107
+ arrowleft: { en: 'Left arrow', fr: 'Flèche gauche' },
108
+ arrowright: { en: 'Right arrow', fr: 'Flèche droite' },
109
+ backspace: { en: 'Backspace', fr: 'Retour arrière' },
110
+ delete: { en: 'Delete', fr: 'Suppr' },
111
+ pagedown: { en: 'Page down', fr: 'Page suivante' },
112
+ pageup: { en: 'Page up', fr: 'Page précédente' },
113
+ });
114
+
115
+ const SPOKEN_MOD = Object.freeze({
116
+ ctrl: { en: 'Control', fr: 'Contrôle' },
117
+ alt: { mac: { en: 'Option', fr: 'Option' }, other: { en: 'Alt', fr: 'Alt' } },
118
+ shift: { en: 'Shift', fr: 'Maj' },
119
+ mod: { mac: { en: 'Command', fr: 'Commande' }, other: { en: 'Control', fr: 'Contrôle' } },
120
+ });
121
+
122
+ /** Words for screen readers: glyphs spelled out ("Command A", "G then I"). Used for the Keycap sr-only label. */
123
+ export function formatKeySpoken(spec: KeySpec | readonly KeySpec[], { platform, locale }: FormatKeyOptions): string {
124
+ const then = ` ${KEY_JOIN_WORDS.then[locale]} `;
125
+ const or = ` ${KEY_JOIN_WORDS.or[locale]} `;
126
+ const chordWords = (chord: Chord): string => {
127
+ const parts: string[] = [];
128
+ if (chord.ctrl && !(chord.mod && platform !== 'mac')) parts.push(SPOKEN_MOD.ctrl[locale]);
129
+ if (chord.alt) parts.push(SPOKEN_MOD.alt[platform][locale]);
130
+ if (chord.shift) parts.push(SPOKEN_MOD.shift[locale]);
131
+ if (chord.mod) parts.push(SPOKEN_MOD.mod[platform][locale]);
132
+ parts.push(SPOKEN_KEY[chord.key]?.[locale] ?? keyName(chord.key, false, locale === 'fr'));
133
+ return parts.join(' ');
134
+ };
135
+ return keyList(spec).map((one) => parseKeySpec(one).map(chordWords).join(then)).join(or);
136
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * @notis/sdk/words: pure toolkit vocabulary. No React, no DOM, no state; bundled into each Space and read by the
3
+ * CLI design check and the server. Action catalogue, shell words, key specs, the default key map, `formatKey`,
4
+ * host-reserved keys and `checkClashes`.
5
+ */
6
+
7
+ export * from './types';
8
+ export * from './keys';
9
+ export * from './format';
10
+ export * from './actions';
11
+ export * from './shell';
12
+ export * from './keymap';
13
+ export * from './clashes';
@@ -0,0 +1,72 @@
1
+ /**
2
+ * The default key map (toolkit spec 3). Pieces register these ids with these keys and words, so the ? sheet reads
3
+ * the same in every Space and on every Notis page. Action keys come from the catalogue (`STANDARD_ACTIONS`).
4
+ */
5
+
6
+ import type { KeySpec, Scope, Words } from './types';
7
+
8
+ export interface KeymapEntry {
9
+ id: string;
10
+ keys: readonly KeySpec[];
11
+ scope: Scope;
12
+ label: Words;
13
+ /** Registered by the Notis host, never by a Space. */
14
+ host?: boolean;
15
+ /** Manager only for now (spec 6.10). */
16
+ managerOnly?: boolean;
17
+ /** Fires while the key is held (J/K stepping). */
18
+ repeat?: boolean;
19
+ /** Not listed in the help sheet. */
20
+ hidden?: boolean;
21
+ }
22
+
23
+ const entry = (id: string, keys: KeySpec[], scope: Scope, en: string, fr: string, extra: Partial<KeymapEntry> = {}):
24
+ KeymapEntry => Object.freeze({ id, keys: Object.freeze(keys), scope, label: Object.freeze({ en, fr }), ...extra });
25
+
26
+ export const DEFAULT_KEYMAP = Object.freeze({
27
+ // Lists (collection scope).
28
+ 'list.next': entry('list.next', ['j', 'arrowdown'], 'collection', 'Next item', 'Élément suivant', { repeat: true }),
29
+ 'list.prev': entry('list.prev', ['k', 'arrowup'], 'collection', 'Previous item', 'Élément précédent', { repeat: true }),
30
+ 'list.extendDown': entry('list.extendDown', ['shift+j', 'shift+arrowdown'], 'collection',
31
+ 'Extend selection down', 'Étendre la sélection vers le bas', { repeat: true }),
32
+ 'list.extendUp': entry('list.extendUp', ['shift+k', 'shift+arrowup'], 'collection',
33
+ 'Extend selection up', 'Étendre la sélection vers le haut', { repeat: true }),
34
+ 'list.open': entry('list.open', ['enter', 'o'], 'collection', 'Open', 'Ouvrir'),
35
+ 'list.toggleSelect': entry('list.toggleSelect', ['x'], 'collection', 'Select', 'Sélectionner'),
36
+ 'list.selectAll': entry('list.selectAll', ['mod+a'], 'collection', 'Select all', 'Tout sélectionner'),
37
+ // A calendar or a timeline (collection scope): [ and ] step its period only while focus is inside the view, so
38
+ // elsewhere on the page they stay the Tabs' view keys; T works while the view shows.
39
+ 'period.prev': entry('period.prev', ['['], 'collection', 'Previous period', 'Période précédente'),
40
+ 'period.next': entry('period.next', [']'], 'collection', 'Next period', 'Période suivante'),
41
+ 'period.today': entry('period.today', ['t'], 'collection', 'Today', 'Aujourd’hui'),
42
+ // Route scope.
43
+ undo: entry('undo', ['z', 'mod+z'], 'route', 'Undo', 'Annuler'),
44
+ // Tabs: switch the whole content to the previous or next view.
45
+ 'view.prev': entry('view.prev', ['['], 'route', 'Previous view', 'Vue précédente'),
46
+ 'view.next': entry('view.next', [']'], 'route', 'Next view', 'Vue suivante'),
47
+ // Host (app scope).
48
+ 'host.help': entry('host.help', ['?'], 'app', 'Show keyboard shortcuts', 'Afficher les raccourcis clavier',
49
+ { host: true }),
50
+ 'host.escape': entry('host.escape', ['escape'], 'app', 'Close, then go back', 'Fermer, puis revenir',
51
+ { host: true }),
52
+ 'host.search': entry('host.search', ['/'], 'app', 'Search', 'Rechercher', { host: true }),
53
+ 'host.newChat': entry('host.newChat', ['c'], 'app', 'New conversation', 'Nouvelle conversation', { host: true }),
54
+ 'host.call': entry('host.call', ['shift+c'], 'app', 'Open voice call', 'Ouvrir l’appel vocal', { host: true }),
55
+ 'host.toggleSidebar': entry('host.toggleSidebar', ['mod+s'], 'app', 'Toggle sidebar',
56
+ 'Afficher ou masquer la barre latérale', { host: true }),
57
+ 'host.voiceMessage': entry('host.voiceMessage', ['mod+d'], 'app', 'Voice message', 'Message vocal', { host: true }),
58
+ 'host.modelMenu': entry('host.modelMenu', ['alt+p'], 'app', 'Model menu', 'Menu du modèle', { host: true }),
59
+ 'host.speedMenu': entry('host.speedMenu', ['alt+s'], 'app', 'Speed menu', 'Menu de vitesse', { host: true }),
60
+ 'host.toggleVoiceCall': entry('host.toggleVoiceCall', ['shift+tab'], 'app', 'Toggle voice call in panel',
61
+ 'Afficher ou masquer l’appel vocal dans le panneau', { host: true }),
62
+ 'host.commandMenu': entry('host.commandMenu', ['mod+k'], 'app', 'Command menu', 'Menu des commandes',
63
+ { host: true }),
64
+ 'host.goInbox': entry('host.goInbox', ['g i'], 'app', 'Go to Inbox', 'Aller à l’Inbox',
65
+ { host: true, managerOnly: true }),
66
+ 'host.goStarred': entry('host.goStarred', ['g s'], 'app', 'Go to Starred', 'Aller aux Favoris',
67
+ { host: true, managerOnly: true }),
68
+ 'host.goDone': entry('host.goDone', ['g d'], 'app', 'Go to Done', 'Aller à Terminé',
69
+ { host: true, managerOnly: true }),
70
+ }) satisfies Readonly<Record<string, KeymapEntry>>;
71
+
72
+ export type KeymapId = keyof typeof DEFAULT_KEYMAP;
@@ -0,0 +1,297 @@
1
+ /**
2
+ * Key specs: parsing, canonical forms and matching against a plain keyboard event.
3
+ * Pure: `matchKey` takes `{ key, code, metaKey, ctrlKey, altKey, shiftKey }`, not a DOM event.
4
+ *
5
+ * Rules (toolkit spec 2.1):
6
+ * - Letters and symbols match on `event.key`, so they follow the keyboard layout (`#` and `?` work on AZERTY).
7
+ * - Any chord with Alt matches on `event.code`, because Option on a Mac changes `event.key` (Option+P gives "π").
8
+ * - `mod` is Cmd on a Mac and Ctrl elsewhere.
9
+ */
10
+
11
+ import type { KeySpec, Platform } from './types';
12
+
13
+ export interface Chord {
14
+ /** Normalized key: a lowercase letter, a digit, a symbol, or a named key ('escape', 'arrowdown', ...). */
15
+ key: string;
16
+ mod: boolean;
17
+ ctrl: boolean;
18
+ alt: boolean;
19
+ shift: boolean;
20
+ }
21
+
22
+ /** One alternative: a single chord, or a sequence of chords ('g i'). */
23
+ export type ChordSequence = Chord[];
24
+
25
+ /** The fields of a KeyboardEvent the matcher reads. */
26
+ export interface KeyEventLike {
27
+ key: string;
28
+ code?: string;
29
+ metaKey?: boolean;
30
+ ctrlKey?: boolean;
31
+ altKey?: boolean;
32
+ shiftKey?: boolean;
33
+ }
34
+
35
+ const KEY_ALIASES: Readonly<Record<string, string>> = {
36
+ esc: 'escape',
37
+ return: 'enter',
38
+ up: 'arrowup',
39
+ down: 'arrowdown',
40
+ left: 'arrowleft',
41
+ right: 'arrowright',
42
+ del: 'delete',
43
+ spacebar: 'space',
44
+ ' ': 'space',
45
+ plus: '+',
46
+ pgup: 'pageup',
47
+ pgdn: 'pagedown',
48
+ };
49
+
50
+ const MOD_ALIASES: Readonly<Record<string, keyof Omit<Chord, 'key'>>> = {
51
+ mod: 'mod',
52
+ cmd: 'mod',
53
+ command: 'mod',
54
+ meta: 'mod',
55
+ ctrl: 'ctrl',
56
+ control: 'ctrl',
57
+ alt: 'alt',
58
+ option: 'alt',
59
+ opt: 'alt',
60
+ shift: 'shift',
61
+ };
62
+
63
+ export const NAMED_KEYS: ReadonlySet<string> = new Set([
64
+ 'escape', 'enter', 'tab', 'space', 'backspace', 'delete', 'insert',
65
+ 'arrowup', 'arrowdown', 'arrowleft', 'arrowright',
66
+ 'home', 'end', 'pageup', 'pagedown',
67
+ 'f1', 'f2', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9', 'f10', 'f11', 'f12',
68
+ ]);
69
+
70
+ /** US physical key of each symbol, for Alt chords (matched on `event.code`). */
71
+ const SYMBOL_CODES: Readonly<Record<string, { code: string; shift: boolean }>> = {
72
+ '/': { code: 'Slash', shift: false }, '?': { code: 'Slash', shift: true },
73
+ '[': { code: 'BracketLeft', shift: false }, '{': { code: 'BracketLeft', shift: true },
74
+ ']': { code: 'BracketRight', shift: false }, '}': { code: 'BracketRight', shift: true },
75
+ ',': { code: 'Comma', shift: false }, '<': { code: 'Comma', shift: true },
76
+ '.': { code: 'Period', shift: false }, '>': { code: 'Period', shift: true },
77
+ ';': { code: 'Semicolon', shift: false }, ':': { code: 'Semicolon', shift: true },
78
+ "'": { code: 'Quote', shift: false }, '"': { code: 'Quote', shift: true },
79
+ '`': { code: 'Backquote', shift: false }, '~': { code: 'Backquote', shift: true },
80
+ '-': { code: 'Minus', shift: false }, '_': { code: 'Minus', shift: true },
81
+ '=': { code: 'Equal', shift: false }, '+': { code: 'Equal', shift: true },
82
+ '\\': { code: 'Backslash', shift: false }, '|': { code: 'Backslash', shift: true },
83
+ '!': { code: 'Digit1', shift: true }, '@': { code: 'Digit2', shift: true },
84
+ '#': { code: 'Digit3', shift: true }, '$': { code: 'Digit4', shift: true },
85
+ '%': { code: 'Digit5', shift: true }, '^': { code: 'Digit6', shift: true },
86
+ '&': { code: 'Digit7', shift: true }, '*': { code: 'Digit8', shift: true },
87
+ '(': { code: 'Digit9', shift: true }, ')': { code: 'Digit0', shift: true },
88
+ };
89
+
90
+ const NAMED_CODES: Readonly<Record<string, readonly string[]>> = {
91
+ escape: ['Escape'], enter: ['Enter', 'NumpadEnter'], tab: ['Tab'], space: ['Space'],
92
+ backspace: ['Backspace'], delete: ['Delete'], insert: ['Insert'],
93
+ arrowup: ['ArrowUp'], arrowdown: ['ArrowDown'], arrowleft: ['ArrowLeft'], arrowright: ['ArrowRight'],
94
+ home: ['Home'], end: ['End'], pageup: ['PageUp'], pagedown: ['PageDown'],
95
+ };
96
+
97
+ export function isLetterKey(key: string): boolean {
98
+ return /^[a-z]$/.test(key);
99
+ }
100
+
101
+ export function isDigitKey(key: string): boolean {
102
+ return /^[0-9]$/.test(key);
103
+ }
104
+
105
+ /** A printable key that is neither a letter nor a digit ('#', '?', '[', ...). */
106
+ export function isSymbolKey(key: string): boolean {
107
+ return key.length === 1 && !isLetterKey(key) && !isDigitKey(key);
108
+ }
109
+
110
+ function normalizeKeyName(raw: string): string {
111
+ if (raw.length === 1) {
112
+ const lower = raw.toLowerCase();
113
+ return KEY_ALIASES[raw] ?? lower;
114
+ }
115
+ const lower = raw.toLowerCase();
116
+ return KEY_ALIASES[lower] ?? lower;
117
+ }
118
+
119
+ export class KeySpecError extends Error {
120
+ constructor(spec: string, reason: string) {
121
+ super(`Invalid key spec "${spec}": ${reason}`);
122
+ this.name = 'KeySpecError';
123
+ }
124
+ }
125
+
126
+ function parseChord(step: string, spec: string): Chord {
127
+ const chord: Chord = { key: '', mod: false, ctrl: false, alt: false, shift: false };
128
+ let parts: string[];
129
+ if (step === '+') parts = ['+'];
130
+ else if (step.endsWith('++')) parts = [...step.slice(0, -2).split('+'), '+'];
131
+ else parts = step.split('+');
132
+ const rawKey = parts.pop() ?? '';
133
+ if (!rawKey) throw new KeySpecError(spec, 'missing key');
134
+ for (const part of parts) {
135
+ const modifier = MOD_ALIASES[part.toLowerCase()];
136
+ if (!modifier) throw new KeySpecError(spec, `unknown modifier "${part}"`);
137
+ chord[modifier] = true;
138
+ }
139
+ const key = normalizeKeyName(rawKey);
140
+ if (key.length !== 1 && !NAMED_KEYS.has(key)) throw new KeySpecError(spec, `unknown key "${rawKey}"`);
141
+ chord.key = key;
142
+ // A symbol already says whether Shift is down ('?' is Shift+/ on a US keyboard, a plain key elsewhere).
143
+ if (isSymbolKey(key) && !chord.alt) chord.shift = false;
144
+ return chord;
145
+ }
146
+
147
+ const parseCache = new Map<string, ChordSequence>();
148
+
149
+ /** 'g i' gives two chords; 'shift+e' one. Throws KeySpecError on an unknown key or modifier. */
150
+ export function parseKeySpec(spec: KeySpec): ChordSequence {
151
+ const cached = parseCache.get(spec);
152
+ if (cached) return cached;
153
+ const trimmed = spec.trim();
154
+ if (!trimmed) throw new KeySpecError(spec, 'empty');
155
+ const steps = trimmed === '+' ? ['+'] : trimmed.split(/\s+/);
156
+ const parsed = steps.map((step) => parseChord(step, spec));
157
+ parseCache.set(spec, parsed);
158
+ return parsed;
159
+ }
160
+
161
+ export function isValidKeySpec(spec: KeySpec): boolean {
162
+ try { parseKeySpec(spec); return true; } catch { return false; }
163
+ }
164
+
165
+ export function isSequenceSpec(spec: KeySpec): boolean {
166
+ return parseKeySpec(spec).length > 1;
167
+ }
168
+
169
+ /** Alternatives as a list. */
170
+ export function keyList(keys: KeySpec | readonly KeySpec[] | null | undefined): KeySpec[] {
171
+ if (keys == null) return [];
172
+ return typeof keys === 'string' ? [keys] : [...keys];
173
+ }
174
+
175
+ function chordToString(chord: Chord, platform: Platform): string {
176
+ const mod = chord.mod || (platform === 'other' && chord.ctrl);
177
+ const ctrl = platform === 'mac' && chord.ctrl;
178
+ const parts: string[] = [];
179
+ if (mod) parts.push('mod');
180
+ if (ctrl) parts.push('ctrl');
181
+ if (chord.alt) parts.push('alt');
182
+ if (chord.shift) parts.push('shift');
183
+ parts.push(chord.key);
184
+ return parts.join('+');
185
+ }
186
+
187
+ /** One normalized string per spec, for comparing keys ('Shift+E' and 'shift+e' are the same). */
188
+ export function canonicalKey(spec: KeySpec, platform: Platform = 'mac'): string {
189
+ return parseKeySpec(spec).map((chord) => chordToString(chord, platform)).join(' ');
190
+ }
191
+
192
+ /** Codes a chord's key sits on, for Alt chords. */
193
+ function codesFor(chord: Chord): { codes: string[]; shift: boolean | null } {
194
+ const { key } = chord;
195
+ if (isLetterKey(key)) return { codes: [`Key${key.toUpperCase()}`], shift: null };
196
+ if (isDigitKey(key)) return { codes: [`Digit${key}`, `Numpad${key}`], shift: null };
197
+ const named = NAMED_CODES[key];
198
+ if (named) return { codes: [...named], shift: null };
199
+ const symbol = SYMBOL_CODES[key];
200
+ if (symbol) return { codes: [symbol.code], shift: symbol.shift };
201
+ return { codes: [], shift: null };
202
+ }
203
+
204
+ function normalizedEventKey(key: string): string {
205
+ if (key === ' ' || key === 'Spacebar') return 'space';
206
+ if (key === 'Esc') return 'escape';
207
+ if (key === 'Up' || key === 'Down' || key === 'Left' || key === 'Right') return `arrow${key.toLowerCase()}`;
208
+ if (key === 'Del') return 'delete';
209
+ return key.length === 1 ? key.toLowerCase() : key.toLowerCase();
210
+ }
211
+
212
+ /** Does this event press this chord? */
213
+ export function matchChord(event: KeyEventLike, chord: Chord, platform: Platform): boolean {
214
+ const meta = !!event.metaKey;
215
+ const ctrl = !!event.ctrlKey;
216
+ const alt = !!event.altKey;
217
+ const shift = !!event.shiftKey;
218
+ const wantMeta = platform === 'mac' ? chord.mod : false;
219
+ const wantCtrl = platform === 'mac' ? chord.ctrl : chord.mod || chord.ctrl;
220
+
221
+ if (chord.alt) {
222
+ // Physical key: Option changes event.key on a Mac.
223
+ if (!alt || meta !== wantMeta || ctrl !== wantCtrl) return false;
224
+ const { codes, shift: symbolShift } = codesFor(chord);
225
+ if (!event.code || !codes.includes(event.code)) return false;
226
+ const wantShift = symbolShift ?? chord.shift;
227
+ return shift === wantShift;
228
+ }
229
+
230
+ const key = normalizedEventKey(event.key ?? '');
231
+
232
+ if (isSymbolKey(chord.key)) {
233
+ if (key !== chord.key) return false;
234
+ // Layouts put symbols behind Shift, Option (Mac) or AltGr (Ctrl+Alt elsewhere): those do not count.
235
+ if (meta !== wantMeta) return false;
236
+ if (platform === 'other') {
237
+ if (wantCtrl) return ctrl;
238
+ return !ctrl || alt;
239
+ }
240
+ return ctrl === wantCtrl;
241
+ }
242
+
243
+ if (alt || meta !== wantMeta || ctrl !== wantCtrl) return false;
244
+
245
+ if (isLetterKey(chord.key)) {
246
+ if (shift !== chord.shift) return false;
247
+ if (key === chord.key) return true;
248
+ // Non-Latin layouts (Cyrillic, Greek...): the letter is not on event.key, so fall back to the physical key.
249
+ // Only for letters outside a-z: a Latin layout that moves keys (AZERTY's comma on KeyM) keeps event.key.
250
+ const foreignLetter = key.length === 1 && /\p{L}/u.test(key) && !/^[a-z]$/.test(key);
251
+ return foreignLetter && event.code === `Key${chord.key.toUpperCase()}`;
252
+ }
253
+
254
+ if (isDigitKey(chord.key)) {
255
+ // Shift may be needed to type a digit (AZERTY), so it does not count.
256
+ return key === chord.key;
257
+ }
258
+
259
+ return key === chord.key && shift === chord.shift;
260
+ }
261
+
262
+ /** Does this event press this single-chord spec (or the first step of a sequence when `firstStep`)? */
263
+ export function matchKey(event: KeyEventLike, spec: KeySpec, platform: Platform, firstStep = false): boolean {
264
+ let chords: ChordSequence;
265
+ try { chords = parseKeySpec(spec); } catch { return false; }
266
+ if (chords.length > 1 && !firstStep) return false;
267
+ return matchChord(event, chords[0]!, platform);
268
+ }
269
+
270
+ /** Pure modifier keys never start or finish a chord. */
271
+ export function isModifierOnlyKey(key: string): boolean {
272
+ return key === 'Shift' || key === 'Control' || key === 'Alt' || key === 'Meta' || key === 'AltGraph'
273
+ || key === 'CapsLock' || key === 'OS' || key === 'Fn' || key === 'Hyper' || key === 'Super';
274
+ }
275
+
276
+ /**
277
+ * Keys the Notis host owns. A Space may not bind them (toolkit spec 2.1, clash rule 1).
278
+ * '?' help sheet, '/' search, 'c' new chat, 'shift+c' call, the rest host chrome.
279
+ */
280
+ export const HOST_RESERVED_KEYS: readonly KeySpec[] = Object.freeze([
281
+ '/', 'c', 'shift+c', 'shift+tab', 'mod+s', 'mod+d', 'alt+p', 'alt+s', '?', 'escape', 'mod+k',
282
+ ]);
283
+
284
+ const reservedCanonical = {
285
+ mac: new Set(HOST_RESERVED_KEYS.map((spec) => canonicalKey(spec, 'mac'))),
286
+ other: new Set(HOST_RESERVED_KEYS.map((spec) => canonicalKey(spec, 'other'))),
287
+ };
288
+
289
+ /** True when the spec is host-reserved on either platform, or starts with a reserved key ('escape x'). */
290
+ export function isHostReserved(spec: KeySpec): boolean {
291
+ for (const platform of ['mac', 'other'] as const) {
292
+ const canonical = canonicalKey(spec, platform);
293
+ const first = canonical.split(' ')[0]!;
294
+ if (reservedCanonical[platform].has(canonical) || reservedCanonical[platform].has(first)) return true;
295
+ }
296
+ return false;
297
+ }
@@ -0,0 +1,46 @@
1
+ /** Shell words shared by every toolkit piece (toolkit spec 2.7). French uses tu and a narrow no-break space before ":". */
2
+
3
+ import { fillWords, type Words } from './types';
4
+
5
+ export const SHELL_WORDS = Object.freeze({
6
+ open: { en: 'Open', fr: 'Ouvrir' },
7
+ back: { en: 'Back', fr: 'Retour' },
8
+ close: { en: 'Close', fr: 'Fermer' },
9
+ previous: { en: 'Previous', fr: 'Précédent' },
10
+ next: { en: 'Next', fr: 'Suivant' },
11
+ more: { en: 'More', fr: 'Plus' },
12
+ clear: { en: 'Clear', fr: 'Effacer' },
13
+ removeNamed: { en: 'Remove {name}', fr: 'Retirer {name}' },
14
+ savingNamed: { en: 'Saving {name}', fr: 'Enregistrement de {name}' },
15
+ clearSelection: { en: 'Clear selection', fr: 'Effacer la sélection' },
16
+ undo: { en: 'Undo', fr: 'Annuler' },
17
+ select: { en: 'Select', fr: 'Sélectionner' },
18
+ selectAll: { en: 'Select all', fr: 'Tout sélectionner' },
19
+ extendSelection: { en: 'Extend selection', fr: 'Étendre la sélection' },
20
+ closeThenClear: { en: 'Close, then clear selection', fr: 'Fermer, puis effacer la sélection' },
21
+ showShortcuts: { en: 'Show shortcuts', fr: 'Afficher les raccourcis' },
22
+ search: { en: 'Search', fr: 'Rechercher' },
23
+ tryAgain: { en: 'Try again', fr: 'Réessayer' },
24
+ selectedOne: { en: '1 selected', fr: '1 sélectionné' },
25
+ selectedMany: { en: '{n} selected', fr: '{n} sélectionnés' },
26
+ sidePeek: { en: 'Side peek', fr: 'Aperçu latéral' },
27
+ centerPeek: { en: 'Center peek', fr: 'Aperçu centré' },
28
+ fullPage: { en: 'Full page', fr: 'Pleine page' },
29
+ openAs: { en: 'Open as', fr: 'Ouvrir en' },
30
+ }) satisfies Readonly<Record<string, Words>>;
31
+
32
+ export type ShellWordId = keyof typeof SHELL_WORDS;
33
+
34
+ /** "1 selected" / "{n} selected", count filled in. */
35
+ export function selectedCount(n: number): Words {
36
+ return n === 1 ? SHELL_WORDS.selectedOne : fillWords(SHELL_WORDS.selectedMany, { n });
37
+ }
38
+
39
+ /** Words of the keyboard help sheet and the engine (toolkit spec 2.1). */
40
+ export const HELP_WORDS = Object.freeze({
41
+ title: { en: 'Keyboard shortcuts', fr: 'Raccourcis clavier' },
42
+ groupSpace: { en: 'This Space', fr: 'Ce Space' },
43
+ groupNotis: { en: 'Notis', fr: 'Notis' },
44
+ close: { en: 'Close', fr: 'Fermer' },
45
+ empty: { en: 'No shortcuts here.', fr: 'Aucun raccourci ici.' },
46
+ }) satisfies Readonly<Record<string, Words>>;
@@ -0,0 +1,65 @@
1
+ /** Shared vocabulary of the toolkit. Pure: no React, no DOM. */
2
+
3
+ export type Locale = 'en' | 'fr';
4
+ export type Platform = 'mac' | 'other';
5
+
6
+ /** Every user-facing string the toolkit shows, in both languages. */
7
+ export type Words = { en: string; fr: string };
8
+
9
+ /** 'j', 'shift+j', 'mod+a', 'g i' (one key, then another), '#', '?'. */
10
+ export type KeySpec = string;
11
+
12
+ /** Keyboard scopes, lowest to highest. A higher scope is asked first. */
13
+ export type Scope = 'app' | 'route' | 'collection' | 'detail' | 'modal';
14
+
15
+ export const SCOPE_ORDER: Readonly<Record<Scope, number>> = Object.freeze({
16
+ app: 0,
17
+ route: 100,
18
+ collection: 200,
19
+ detail: 300,
20
+ modal: 400,
21
+ });
22
+
23
+ export const SCOPES: readonly Scope[] = Object.freeze(['app', 'route', 'collection', 'detail', 'modal'] as Scope[]);
24
+
25
+ export function isScope(value: unknown): value is Scope {
26
+ return typeof value === 'string' && Object.prototype.hasOwnProperty.call(SCOPE_ORDER, value);
27
+ }
28
+
29
+ export function isWords(value: unknown): value is Words {
30
+ return !!value && typeof value === 'object'
31
+ && typeof (value as Words).en === 'string' && typeof (value as Words).fr === 'string';
32
+ }
33
+
34
+ /** The string for a locale. A plain string is returned as is (Space code may pass its own words). */
35
+ export function pick(words: Words | string, locale: Locale): string {
36
+ if (typeof words === 'string') return words;
37
+ return locale === 'fr' ? words.fr : words.en;
38
+ }
39
+
40
+ /** Replaces `{name}` placeholders. Unknown names stay as written. */
41
+ export function interpolate(template: string, vars: Readonly<Record<string, string | number>>): string {
42
+ return template.replace(/\{(\w+)\}/g, (whole, name: string) =>
43
+ Object.prototype.hasOwnProperty.call(vars, name) ? String(vars[name]) : whole);
44
+ }
45
+
46
+ /** Fills the same placeholders in both languages. */
47
+ export function fillWords(words: Words, vars: Readonly<Record<string, string | number>>): Words {
48
+ return { en: interpolate(words.en, vars), fr: interpolate(words.fr, vars) };
49
+ }
50
+
51
+ /** Shorthand for a Words pair. */
52
+ export function w(en: string, fr: string): Words {
53
+ return { en, fr };
54
+ }
55
+
56
+ /** Narrow no-break space, used in French before `:` `?` `!` `;`. */
57
+ export const NNBSP = ' ';
58
+
59
+ /**
60
+ * "Label: value" (an accessible name such as "Status: Done"). French puts the narrow no-break space before the
61
+ * colon: "Statut : Terminé".
62
+ */
63
+ export function labelValue(label: string, value: string, locale: Locale): string {
64
+ return locale === 'fr' ? `${label}${NNBSP}: ${value}` : `${label}: ${value}`;
65
+ }