@agent-native/core 0.80.7 → 0.80.9

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 (302) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +18 -0
  3. package/corpus/core/docs/content/locales/ar-SA/template-design.mdx +6 -0
  4. package/corpus/core/docs/content/locales/de-DE/template-design.mdx +8 -0
  5. package/corpus/core/docs/content/locales/es-ES/template-design.mdx +10 -2
  6. package/corpus/core/docs/content/locales/fr-FR/template-design.mdx +10 -2
  7. package/corpus/core/docs/content/locales/hi-IN/template-design.mdx +7 -0
  8. package/corpus/core/docs/content/locales/ja-JP/template-design.mdx +7 -0
  9. package/corpus/core/docs/content/locales/ko-KR/template-design.mdx +7 -0
  10. package/corpus/core/docs/content/locales/pt-BR/template-design.mdx +9 -2
  11. package/corpus/core/docs/content/locales/zh-CN/template-design.mdx +6 -0
  12. package/corpus/core/docs/content/locales/zh-TW/template-design.mdx +6 -0
  13. package/corpus/core/docs/content/template-design.mdx +9 -2
  14. package/corpus/core/package.json +1 -1
  15. package/corpus/core/src/cli/mcp.ts +10 -0
  16. package/corpus/core/src/cli/skills.ts +58 -12
  17. package/corpus/core/src/client/AssistantChat.tsx +70 -27
  18. package/corpus/core/src/client/analytics.ts +3 -1
  19. package/corpus/core/src/client/error-format.ts +25 -0
  20. package/corpus/core/src/client/extensions/ExtensionViewer.tsx +13 -11
  21. package/corpus/core/src/client/guided-questions.tsx +32 -4
  22. package/corpus/core/src/client/onboarding/OnboardingPanel.tsx +33 -1
  23. package/corpus/core/src/client/resources/BuiltinCapabilityDetail.tsx +8 -0
  24. package/corpus/core/src/client/resources/use-builtin-capabilities.ts +4 -2
  25. package/corpus/core/src/client/session-replay.ts +34 -0
  26. package/corpus/core/src/client/sharing/ShareButton.tsx +106 -21
  27. package/corpus/core/src/coding-tools/index.ts +1 -1
  28. package/corpus/core/src/mcp/index.ts +2 -0
  29. package/corpus/core/src/mcp/screen-memory-stdio.ts +290 -0
  30. package/corpus/core/src/mcp-client/builtin-capabilities.ts +13 -1
  31. package/corpus/core/src/mcp-client/index.ts +9 -0
  32. package/corpus/core/src/mcp-client/screen-memory-local.ts +461 -0
  33. package/corpus/core/src/onboarding/types.ts +7 -0
  34. package/corpus/core/src/server/agent-chat-plugin.ts +25 -0
  35. package/corpus/core/src/server/agents-bundle.ts +35 -7
  36. package/corpus/core/src/styles/agent-native.css +5 -0
  37. package/corpus/core/src/templates/default/app/global.css +35 -35
  38. package/corpus/templates/analytics/AGENTS.md +6 -0
  39. package/corpus/templates/analytics/actions/github-repo-files.ts +9 -0
  40. package/corpus/templates/analytics/app/components/layout/Layout.tsx +4 -4
  41. package/corpus/templates/analytics/app/global.css +144 -40
  42. package/corpus/templates/analytics/app/pages/adhoc/explorer/index.tsx +1 -1
  43. package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/ChartCard.tsx +1 -1
  44. package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/index.tsx +1 -1
  45. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +4 -4
  46. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +9 -9
  47. package/corpus/templates/analytics/changelog/2026-06-29-agents-can-search-and-read-connected-github-repositories-whe.md +6 -0
  48. package/corpus/templates/analytics/changelog/2026-06-29-sessions-and-explorer-layouts-adapt-when-the-agent-sidebar-is.md +6 -0
  49. package/corpus/templates/assets/app/components/layout/Layout.tsx +3 -1
  50. package/corpus/templates/assets/app/components/library/LibraryPresetGrid.tsx +1 -1
  51. package/corpus/templates/assets/app/global.css +99 -35
  52. package/corpus/templates/assets/app/routes/asset.$id.tsx +2 -2
  53. package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +4 -4
  54. package/corpus/templates/assets/app/routes/library.tsx +4 -4
  55. package/corpus/templates/assets/changelog/2026-06-29-asset-library-and-detail-layouts-adapt-when-the-agent-sidebar.md +6 -0
  56. package/corpus/templates/brain/app/components/layout/Layout.tsx +1 -1
  57. package/corpus/templates/brain/app/global.css +63 -35
  58. package/corpus/templates/brain/app/routes/search.tsx +1 -1
  59. package/corpus/templates/brain/app/routes/settings.tsx +1 -1
  60. package/corpus/templates/brain/app/routes/sources.tsx +2 -2
  61. package/corpus/templates/brain/changelog/2026-06-29-search-settings-and-source-layouts-adapt-when-the-agent-sidebar.md +6 -0
  62. package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +4 -4
  63. package/corpus/templates/calendar/app/components/calendar/FindTimePanel.tsx +3 -2
  64. package/corpus/templates/calendar/app/components/layout/AppLayout.tsx +4 -2
  65. package/corpus/templates/calendar/app/global.css +69 -35
  66. package/corpus/templates/calendar/changelog/2026-06-29-event-and-find-time-layouts-adapt-when-the-agent-sidebar-is.md +6 -0
  67. package/corpus/templates/chat/app/components/layout/Layout.tsx +1 -1
  68. package/corpus/templates/chat/app/global.css +35 -35
  69. package/corpus/templates/chat/changelog/2026-06-29-chat-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
  70. package/corpus/templates/clips/AGENTS.md +13 -0
  71. package/corpus/templates/clips/actions/get-screen-memory-status.ts +12 -0
  72. package/corpus/templates/clips/actions/query-screen-memory-context.ts +29 -0
  73. package/corpus/templates/clips/actions/view-screen.ts +2 -2
  74. package/corpus/templates/clips/app/components/library/library-layout.tsx +1 -1
  75. package/corpus/templates/clips/app/global.css +47 -35
  76. package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +3 -3
  77. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +45 -18
  78. package/corpus/templates/clips/changelog/2026-06-29-meeting-detail-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
  79. package/corpus/templates/clips/changelog/2026-06-29-screen-memory-keeps-a-local-rolling-screen-buffer-for-agent-context.md +5 -0
  80. package/corpus/templates/clips/changelog/2026-06-29-video-chat-is-easier-to-find-on-recording-pages-and-s3-setup.md +6 -0
  81. package/corpus/templates/clips/chrome-extension/src/styles.css +39 -39
  82. package/corpus/templates/clips/desktop/src/app.tsx +290 -1
  83. package/corpus/templates/clips/desktop/src/shared/config.ts +83 -0
  84. package/corpus/templates/clips/desktop/src/styles.css +67 -52
  85. package/corpus/templates/clips/desktop/src-tauri/src/config.rs +50 -0
  86. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +17 -0
  87. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +13 -13
  88. package/corpus/templates/clips/desktop/src-tauri/src/screen_memory.rs +990 -0
  89. package/corpus/templates/clips/server/plugins/onboarding.ts +2 -0
  90. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +69 -16
  91. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +3 -3
  92. package/corpus/templates/content/app/components/layout/Layout.tsx +1 -1
  93. package/corpus/templates/content/app/global.css +29 -15
  94. package/corpus/templates/content/changelog/2026-06-29-database-gallery-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
  95. package/corpus/templates/content/changelog/2026-06-29-read-only-pages-no-longer-try-to-autosave-or-open-editor-onl.md +6 -0
  96. package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +25 -20
  97. package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +8 -0
  98. package/corpus/templates/design/.agents/skills/export-handoff/SKILL.md +8 -0
  99. package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +13 -0
  100. package/corpus/templates/design/AGENTS.md +24 -14
  101. package/corpus/templates/design/CODE_NATIVE_DESIGN_STUDIO_IMPLEMENTATION_PLAN.md +1054 -0
  102. package/corpus/templates/design/actions/apply-visual-edit.ts +24 -4
  103. package/corpus/templates/design/actions/edit-design.ts +6 -3
  104. package/corpus/templates/design/actions/export-coding-handoff.ts +12 -4
  105. package/corpus/templates/design/actions/generate-design.ts +6 -21
  106. package/corpus/templates/design/actions/generate-screens.ts +48 -5
  107. package/corpus/templates/design/actions/get-design.ts +1 -0
  108. package/corpus/templates/design/actions/present-design-variants.ts +355 -30
  109. package/corpus/templates/design/actions/update-design.ts +47 -10
  110. package/corpus/templates/design/actions/update-file.ts +33 -8
  111. package/corpus/templates/design/actions/view-screen.ts +89 -31
  112. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +389 -71
  113. package/corpus/templates/design/app/components/design/DesignEditorSkeleton.tsx +1 -1
  114. package/corpus/templates/design/app/components/design/EditPanel.tsx +596 -95
  115. package/corpus/templates/design/app/components/design/LayersPanel.tsx +428 -221
  116. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +1197 -228
  117. package/corpus/templates/design/app/components/design/QuestionFlow.tsx +94 -217
  118. package/corpus/templates/design/app/components/design/TweaksPanel.tsx +28 -14
  119. package/corpus/templates/design/app/components/design/index.ts +0 -1
  120. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +228 -170
  121. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +140 -13
  122. package/corpus/templates/design/app/components/design/inspector/ExportSettingsPanel.tsx +39 -8
  123. package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +4 -2
  124. package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +75 -5
  125. package/corpus/templates/design/app/components/design/inspector/ScrubInput.tsx +37 -12
  126. package/corpus/templates/design/app/components/design/inspector/index.ts +1 -0
  127. package/corpus/templates/design/app/components/design/inspector/scrub-input-utils.ts +17 -4
  128. package/corpus/templates/design/app/components/design/types.ts +9 -0
  129. package/corpus/templates/design/app/components/layout/Layout.tsx +23 -5
  130. package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +448 -259
  131. package/corpus/templates/design/app/global.css +77 -49
  132. package/corpus/templates/design/app/hooks/use-question-flow.ts +2 -2
  133. package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +8 -1
  134. package/corpus/templates/design/app/i18n/zh-TW.ts +4 -0
  135. package/corpus/templates/design/app/i18n-data.ts +55 -3
  136. package/corpus/templates/design/app/pages/DesignEditor.tsx +2783 -658
  137. package/corpus/templates/design/app/root.tsx +1 -1
  138. package/corpus/templates/design/changelog/2026-06-29-agent-edits-to-a-design-are-no-longer-occasionally-dropped-o.md +6 -0
  139. package/corpus/templates/design/changelog/2026-06-29-box-shadow-and-gradient-colors-keep-named-css-colors-like-re.md +6 -0
  140. package/corpus/templates/design/changelog/2026-06-29-containers-no-longer-block-selecting-nested-elements-and-nor.md +6 -0
  141. package/corpus/templates/design/changelog/2026-06-29-cut-cmd-ctrl-x-now-removes-the-selected-element-to-the-clipb.md +6 -0
  142. package/corpus/templates/design/changelog/2026-06-29-deleting-with-multiple-layers-selected-in-the-layers-panel-n.md +6 -0
  143. package/corpus/templates/design/changelog/2026-06-29-design-agents-now-generate-and-refine-variants-from-stronger.md +6 -0
  144. package/corpus/templates/design/changelog/2026-06-29-design-inspector-controls-now-match-figmas-softer-dark-chrome.md +6 -0
  145. package/corpus/templates/design/changelog/2026-06-29-design-questions-now-appear-as-a-cleaner-canvas-intake-with-.md +6 -0
  146. package/corpus/templates/design/changelog/2026-06-29-design-variant-directions-now-appear-as-board-screens-with-chat-buttons.md +5 -0
  147. package/corpus/templates/design/changelog/2026-06-29-duplicating-a-screen-now-gives-the-copy-its-own-layer-ids-so.md +6 -0
  148. package/corpus/templates/design/changelog/2026-06-29-element-selection-no-longer-snaps-back-to-the-previous-layer.md +6 -0
  149. package/corpus/templates/design/changelog/2026-06-29-frame-resize-measurements-now-stay-beside-the-cursor-while-d.md +6 -0
  150. package/corpus/templates/design/changelog/2026-06-29-hiding-and-re-showing-a-gradient-fill-now-preserves-each-sto.md +6 -0
  151. package/corpus/templates/design/changelog/2026-06-29-inspector-number-fields-have-cleaner-tooltips-and-draggable-.md +6 -0
  152. package/corpus/templates/design/changelog/2026-06-29-layer-locking-layer-nesting-generated-question-intake-and-im.md +6 -0
  153. package/corpus/templates/design/changelog/2026-06-29-layer-names-in-the-sidebar-can-scroll-horizontally-instead-o.md +6 -0
  154. package/corpus/templates/design/changelog/2026-06-29-layer-nesting-in-the-sidebar-now-uses-tighter-spacing-so-nam.md +6 -0
  155. package/corpus/templates/design/changelog/2026-06-29-layers-no-longer-repeat-document-wrapper-rows-when-a-screen-.md +6 -0
  156. package/corpus/templates/design/changelog/2026-06-29-overview-previews-now-keep-element-hover-and-selection-tied-.md +6 -0
  157. package/corpus/templates/design/changelog/2026-06-29-picking-a-design-direction-now-submits-to-the-agent-right-aw.md +6 -0
  158. package/corpus/templates/design/changelog/2026-06-29-pressing-escape-now-cancels-a-stuck-screen-drag-in-the-overv.md +6 -0
  159. package/corpus/templates/design/changelog/2026-06-29-screen-overview-is-easier-to-find-and-named-screens-open-dir.md +6 -0
  160. package/corpus/templates/design/changelog/2026-06-29-screen-overview-only-highlights-screens-when-the-frame-or-ti.md +6 -0
  161. package/corpus/templates/design/changelog/2026-06-29-screen-overview-previews-now-resize-to-match-the-selected-de.md +6 -0
  162. package/corpus/templates/design/changelog/2026-06-29-screen-overview-selection-now-uses-a-pointer-cursor-instead-.md +6 -0
  163. package/corpus/templates/design/changelog/2026-06-29-screen-overview-selection-outlines-now-stay-locked-to-the-se.md +6 -0
  164. package/corpus/templates/design/changelog/2026-06-29-screen-overview-zoom-now-reports-real-screen-scale-separatel.md +6 -0
  165. package/corpus/templates/design/changelog/2026-06-29-screen-overview-zooming-feels-smoother-and-selection-outline.md +6 -0
  166. package/corpus/templates/design/changelog/2026-06-29-screen-overview-zooming-now-stays-on-the-overview-canvas-ins.md +6 -0
  167. package/corpus/templates/design/changelog/2026-06-29-selecting-a-nested-layer-from-all-screens-now-keeps-you-in-a.md +6 -0
  168. package/corpus/templates/design/changelog/2026-06-29-share-now-groups-link-sharing-exports-and-agent-handoff.md +6 -0
  169. package/corpus/templates/design/changelog/2026-06-29-style-changes-that-set-several-properties-at-once-fixed-size.md +6 -0
  170. package/corpus/templates/design/changelog/2026-06-29-the-design-editor-loading-skeleton-now-uses-a-softer-charcoa.md +6 -0
  171. package/corpus/templates/design/changelog/2026-06-29-the-device-preview-menu-now-sits-next-to-the-preview-button-.md +6 -0
  172. package/corpus/templates/design/changelog/2026-06-29-the-editor-no-longer-crashes-when-a-screen-ends-up-with-dupl.md +6 -0
  173. package/corpus/templates/design/changelog/2026-06-29-the-inspector-stays-read-only-while-an-empty-design-is-still.md +6 -0
  174. package/corpus/templates/design/changelog/2026-06-29-the-missing-design-view-now-matches-the-rest-of-the-editor-c.md +6 -0
  175. package/corpus/templates/design/changelog/2026-06-29-the-pen-tool-icon-and-tool-option-menus-better-match-the-edi.md +6 -0
  176. package/corpus/templates/design/changelog/2026-06-29-typography-controls-now-keep-the-font-name-visible-and-move-.md +6 -0
  177. package/corpus/templates/design/e2e/README.md +4 -3
  178. package/corpus/templates/design/e2e/global-setup.ts +24 -23
  179. package/corpus/templates/design/e2e/helpers.ts +122 -11
  180. package/corpus/templates/design/playwright.config.ts +19 -4
  181. package/corpus/templates/design/server/lib/coding-handoff.ts +126 -2
  182. package/corpus/templates/design/server/plugins/core-routes.ts +1 -2
  183. package/corpus/templates/design/server/routes/api/design-handoff/[id].zip.get.ts +86 -0
  184. package/corpus/templates/design/shared/code-layer.ts +125 -22
  185. package/corpus/templates/design/shared/color-utils.ts +159 -0
  186. package/corpus/templates/design/shared/generation-session.ts +42 -0
  187. package/corpus/templates/dispatch/app/global.css +79 -35
  188. package/corpus/templates/dispatch/app/routes/integrations.tsx +8 -7
  189. package/corpus/templates/dispatch/changelog/2026-06-29-integration-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
  190. package/corpus/templates/forms/app/components/layout/Layout.tsx +6 -2
  191. package/corpus/templates/forms/app/global.css +64 -35
  192. package/corpus/templates/forms/app/pages/FormsListPage.tsx +2 -2
  193. package/corpus/templates/forms/app/pages/ResponseInsightsPage.tsx +3 -3
  194. package/corpus/templates/forms/changelog/2026-06-29-form-list-and-insight-layouts-adapt-when-the-agent-sidebar.md +6 -0
  195. package/corpus/templates/macros/app/components/layout/AppLayout.tsx +3 -1
  196. package/corpus/templates/macros/app/global.css +25 -9
  197. package/corpus/templates/macros/changelog/2026-06-29-macro-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
  198. package/corpus/templates/mail/app/components/layout/AppLayout.tsx +6 -2
  199. package/corpus/templates/mail/app/global.css +42 -36
  200. package/corpus/templates/mail/app/pages/InboxPage.tsx +1 -1
  201. package/corpus/templates/mail/changelog/2026-06-29-the-contact-panel-now-adapts-to-the-available-mail-pane-width.md +6 -0
  202. package/corpus/templates/plan/app/components/layout/Layout.tsx +3 -1
  203. package/corpus/templates/plan/app/components/plan/DocumentArea.tsx +6 -7
  204. package/corpus/templates/plan/app/global.css +74 -42
  205. package/corpus/templates/plan/changelog/2026-06-29-plan-document-blocks-adapt-to-the-available-document-width.md +6 -0
  206. package/corpus/templates/plan/changelog/2026-06-29-plan-titles-and-summaries-can-be-edited-inline-without-focus.md +6 -0
  207. package/corpus/templates/slides/app/components/layout/Layout.tsx +3 -1
  208. package/corpus/templates/slides/app/global.css +49 -33
  209. package/corpus/templates/slides/changelog/2026-06-29-slide-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
  210. package/corpus/templates/videos/app/components/layout/Layout.tsx +3 -1
  211. package/corpus/templates/videos/app/global.css +35 -35
  212. package/corpus/templates/videos/changelog/2026-06-29-video-layouts-adapt-when-the-agent-sidebar-is-open.md +6 -0
  213. package/dist/cli/mcp.d.ts.map +1 -1
  214. package/dist/cli/mcp.js +10 -0
  215. package/dist/cli/mcp.js.map +1 -1
  216. package/dist/cli/skills.d.ts.map +1 -1
  217. package/dist/cli/skills.js +58 -12
  218. package/dist/cli/skills.js.map +1 -1
  219. package/dist/client/AssistantChat.d.ts.map +1 -1
  220. package/dist/client/AssistantChat.js +54 -24
  221. package/dist/client/AssistantChat.js.map +1 -1
  222. package/dist/client/analytics.d.ts.map +1 -1
  223. package/dist/client/analytics.js +2 -1
  224. package/dist/client/analytics.js.map +1 -1
  225. package/dist/client/error-format.d.ts.map +1 -1
  226. package/dist/client/error-format.js +17 -0
  227. package/dist/client/error-format.js.map +1 -1
  228. package/dist/client/extensions/ExtensionViewer.d.ts.map +1 -1
  229. package/dist/client/extensions/ExtensionViewer.js +3 -4
  230. package/dist/client/extensions/ExtensionViewer.js.map +1 -1
  231. package/dist/client/guided-questions.d.ts +4 -0
  232. package/dist/client/guided-questions.d.ts.map +1 -1
  233. package/dist/client/guided-questions.js +16 -7
  234. package/dist/client/guided-questions.js.map +1 -1
  235. package/dist/client/onboarding/OnboardingPanel.js +26 -1
  236. package/dist/client/onboarding/OnboardingPanel.js.map +1 -1
  237. package/dist/client/resources/BuiltinCapabilityDetail.d.ts.map +1 -1
  238. package/dist/client/resources/BuiltinCapabilityDetail.js +1 -1
  239. package/dist/client/resources/BuiltinCapabilityDetail.js.map +1 -1
  240. package/dist/client/resources/use-builtin-capabilities.d.ts +1 -1
  241. package/dist/client/resources/use-builtin-capabilities.d.ts.map +1 -1
  242. package/dist/client/resources/use-builtin-capabilities.js +2 -1
  243. package/dist/client/resources/use-builtin-capabilities.js.map +1 -1
  244. package/dist/client/session-replay.d.ts.map +1 -1
  245. package/dist/client/session-replay.js +31 -0
  246. package/dist/client/session-replay.js.map +1 -1
  247. package/dist/client/sharing/ShareButton.d.ts +14 -0
  248. package/dist/client/sharing/ShareButton.d.ts.map +1 -1
  249. package/dist/client/sharing/ShareButton.js +36 -7
  250. package/dist/client/sharing/ShareButton.js.map +1 -1
  251. package/dist/coding-tools/index.js +1 -1
  252. package/dist/coding-tools/index.js.map +1 -1
  253. package/dist/collab/routes.d.ts +1 -1
  254. package/dist/mcp/index.d.ts +2 -0
  255. package/dist/mcp/index.d.ts.map +1 -1
  256. package/dist/mcp/index.js +1 -0
  257. package/dist/mcp/index.js.map +1 -1
  258. package/dist/mcp/screen-memory-stdio.d.ts +6 -0
  259. package/dist/mcp/screen-memory-stdio.d.ts.map +1 -0
  260. package/dist/mcp/screen-memory-stdio.js +213 -0
  261. package/dist/mcp/screen-memory-stdio.js.map +1 -0
  262. package/dist/mcp-client/builtin-capabilities.d.ts +1 -1
  263. package/dist/mcp-client/builtin-capabilities.d.ts.map +1 -1
  264. package/dist/mcp-client/builtin-capabilities.js +10 -0
  265. package/dist/mcp-client/builtin-capabilities.js.map +1 -1
  266. package/dist/mcp-client/index.d.ts +1 -0
  267. package/dist/mcp-client/index.d.ts.map +1 -1
  268. package/dist/mcp-client/index.js +1 -0
  269. package/dist/mcp-client/index.js.map +1 -1
  270. package/dist/mcp-client/screen-memory-local.d.ts +61 -0
  271. package/dist/mcp-client/screen-memory-local.d.ts.map +1 -0
  272. package/dist/mcp-client/screen-memory-local.js +336 -0
  273. package/dist/mcp-client/screen-memory-local.js.map +1 -0
  274. package/dist/onboarding/types.d.ts +7 -0
  275. package/dist/onboarding/types.d.ts.map +1 -1
  276. package/dist/onboarding/types.js.map +1 -1
  277. package/dist/resources/handlers.d.ts +2 -2
  278. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  279. package/dist/server/agent-chat-plugin.js +26 -0
  280. package/dist/server/agent-chat-plugin.js.map +1 -1
  281. package/dist/server/agents-bundle.d.ts.map +1 -1
  282. package/dist/server/agents-bundle.js +22 -7
  283. package/dist/server/agents-bundle.js.map +1 -1
  284. package/dist/server/transcribe-voice.d.ts +1 -1
  285. package/dist/styles/agent-native.css +5 -0
  286. package/dist/templates/default/app/global.css +35 -35
  287. package/docs/content/locales/ar-SA/template-design.mdx +6 -0
  288. package/docs/content/locales/de-DE/template-design.mdx +8 -0
  289. package/docs/content/locales/es-ES/template-design.mdx +10 -2
  290. package/docs/content/locales/fr-FR/template-design.mdx +10 -2
  291. package/docs/content/locales/hi-IN/template-design.mdx +7 -0
  292. package/docs/content/locales/ja-JP/template-design.mdx +7 -0
  293. package/docs/content/locales/ko-KR/template-design.mdx +7 -0
  294. package/docs/content/locales/pt-BR/template-design.mdx +9 -2
  295. package/docs/content/locales/zh-CN/template-design.mdx +6 -0
  296. package/docs/content/locales/zh-TW/template-design.mdx +6 -0
  297. package/docs/content/template-design.mdx +9 -2
  298. package/package.json +1 -1
  299. package/src/templates/default/app/global.css +35 -35
  300. package/corpus/templates/design/app/components/design/VariantGrid.tsx +0 -189
  301. package/corpus/templates/design/app/components/design/VariantHandoffCard.tsx +0 -115
  302. package/corpus/templates/design/app/hooks/use-variant-flow.ts +0 -258
@@ -0,0 +1,990 @@
1
+ use crate::config::{FeatureConfig, ScreenMemoryConfig};
2
+ use crate::native_screen::{
3
+ self, NativeFullscreenBackend, DISK_SPACE_BLOCK_BYTES, MP4_RECORDING_MIME_TYPE,
4
+ QUICKTIME_RECORDING_MIME_TYPE,
5
+ };
6
+ use chrono::{DateTime, Duration as ChronoDuration, Utc};
7
+ use serde::{Deserialize, Serialize};
8
+ use std::fs::{File, OpenOptions};
9
+ use std::io::{BufRead, BufReader, Write};
10
+ use std::path::{Path, PathBuf};
11
+ use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
12
+ use std::sync::{Arc, Mutex};
13
+ use std::time::{Duration, Instant};
14
+ use tauri::{AppHandle, Emitter, Manager};
15
+
16
+ const SCREEN_MEMORY_DIR: &str = "screen-memory";
17
+ const SCREEN_MEMORY_EVENT: &str = "clips:screen-memory-changed";
18
+ const SCREEN_MEMORY_EVENTS_JSONL: &str = "events.jsonl";
19
+ const MIN_SEGMENT_SECONDS: u64 = 15;
20
+ const MAX_SEGMENT_SECONDS: u64 = 30 * 60;
21
+ const MIN_RETENTION_HOURS: u32 = 1;
22
+ const MAX_RETENTION_HOURS: u32 = 24 * 30;
23
+ const MIN_MAX_BYTES: u64 = 100 * 1024 * 1024;
24
+ const MAX_MAX_BYTES: u64 = 1024 * 1024 * 1024 * 1024;
25
+ const ROTATOR_TICK: Duration = Duration::from_millis(500);
26
+
27
+ static SEGMENT_COUNTER: AtomicU64 = AtomicU64::new(0);
28
+
29
+ #[derive(Default)]
30
+ pub struct ScreenMemoryState {
31
+ inner: Mutex<ScreenMemoryRuntime>,
32
+ }
33
+
34
+ #[derive(Default)]
35
+ struct ScreenMemoryRuntime {
36
+ active: Option<ActiveScreenMemorySegment>,
37
+ worker_stop: Option<Arc<AtomicBool>>,
38
+ last_error: Option<String>,
39
+ }
40
+
41
+ struct ActiveScreenMemorySegment {
42
+ id: String,
43
+ path: PathBuf,
44
+ mime_type: &'static str,
45
+ backend: NativeFullscreenBackend,
46
+ started_at: Instant,
47
+ started_at_iso: String,
48
+ width: Option<u32>,
49
+ height: Option<u32>,
50
+ }
51
+
52
+ #[derive(Debug, Clone, Serialize, Deserialize)]
53
+ #[serde(rename_all = "camelCase")]
54
+ pub struct ScreenMemorySegmentMetadata {
55
+ pub id: String,
56
+ pub path: PathBuf,
57
+ pub file_name: String,
58
+ pub mime_type: String,
59
+ pub started_at: String,
60
+ pub ended_at: String,
61
+ pub duration_ms: u128,
62
+ pub width: Option<u32>,
63
+ pub height: Option<u32>,
64
+ pub bytes: u64,
65
+ pub corrupt: bool,
66
+ pub error: Option<String>,
67
+ }
68
+
69
+ #[derive(Debug, Clone, Serialize, Deserialize)]
70
+ #[serde(rename_all = "camelCase")]
71
+ pub struct ScreenMemoryEvent {
72
+ pub captured_at: String,
73
+ pub app_name: Option<String>,
74
+ pub window_title: Option<String>,
75
+ pub bundle_id: Option<String>,
76
+ pub source: String,
77
+ }
78
+
79
+ #[derive(Debug, Clone, Serialize)]
80
+ #[serde(rename_all = "camelCase")]
81
+ pub struct ScreenMemoryQueryResult {
82
+ pub query: Option<String>,
83
+ pub minutes: u64,
84
+ pub events: Vec<ScreenMemoryEvent>,
85
+ pub segments: Vec<ScreenMemorySegmentMetadata>,
86
+ }
87
+
88
+ #[derive(Debug, Clone, Serialize)]
89
+ #[serde(rename_all = "kebab-case")]
90
+ pub enum ScreenMemoryRuntimeState {
91
+ Disabled,
92
+ Idle,
93
+ Recording,
94
+ Paused,
95
+ }
96
+
97
+ #[derive(Debug, Clone, Serialize)]
98
+ #[serde(rename_all = "camelCase")]
99
+ pub struct ScreenMemoryActiveSegment {
100
+ pub id: String,
101
+ pub path: PathBuf,
102
+ pub mime_type: String,
103
+ pub started_at: String,
104
+ pub duration_ms: u128,
105
+ pub width: Option<u32>,
106
+ pub height: Option<u32>,
107
+ }
108
+
109
+ #[derive(Debug, Clone, Serialize)]
110
+ #[serde(rename_all = "camelCase")]
111
+ pub struct ScreenMemoryStatus {
112
+ pub available: bool,
113
+ pub state: ScreenMemoryRuntimeState,
114
+ pub config: ScreenMemoryConfig,
115
+ pub storage_dir: PathBuf,
116
+ pub active_segment: Option<ScreenMemoryActiveSegment>,
117
+ pub recent_segments: Vec<ScreenMemorySegmentMetadata>,
118
+ pub last_error: Option<String>,
119
+ }
120
+
121
+ #[derive(Debug, Clone, Serialize)]
122
+ #[serde(rename_all = "camelCase")]
123
+ pub struct ScreenMemoryDeleteResult {
124
+ pub deleted_segments: usize,
125
+ pub deleted_bytes: u64,
126
+ }
127
+
128
+ #[derive(Debug, Clone, Serialize)]
129
+ #[serde(rename_all = "camelCase")]
130
+ pub struct ScreenMemoryExportFile {
131
+ pub path: String,
132
+ pub file_name: String,
133
+ pub bytes: u64,
134
+ pub mime_type: String,
135
+ }
136
+
137
+ #[derive(Debug, Clone, Serialize)]
138
+ #[serde(rename_all = "camelCase")]
139
+ pub struct ScreenMemoryExportResult {
140
+ pub folder_path: String,
141
+ pub files: Vec<ScreenMemoryExportFile>,
142
+ }
143
+
144
+ /// Keep the local recorder aligned with the persisted feature config. This is
145
+ /// called from `set_feature_config`, so any future UI or agent command that
146
+ /// writes the config uses the same local-only backend.
147
+ pub fn sync_from_config(app: &AppHandle, feature_config: &FeatureConfig) {
148
+ let config = normalize_screen_memory_config(feature_config.screen_memory.clone());
149
+ if !config.enabled || config.paused {
150
+ if let Err(err) = stop_active_segment(app) {
151
+ record_error(app, err);
152
+ }
153
+ return;
154
+ }
155
+
156
+ if let Err(err) = ensure_running(app, &config) {
157
+ record_error(app, err);
158
+ }
159
+ }
160
+
161
+ #[tauri::command]
162
+ pub async fn screen_memory_status(app: AppHandle) -> Result<ScreenMemoryStatus, String> {
163
+ build_status(&app)
164
+ }
165
+
166
+ #[tauri::command]
167
+ pub async fn screen_memory_configure(
168
+ app: AppHandle,
169
+ config: ScreenMemoryConfig,
170
+ ) -> Result<ScreenMemoryStatus, String> {
171
+ let mut feature_config = crate::config::feature_config(&app);
172
+ feature_config.screen_memory = normalize_screen_memory_config(config);
173
+ crate::config::set_feature_config(app.clone(), feature_config).await?;
174
+ build_status(&app)
175
+ }
176
+
177
+ #[tauri::command]
178
+ pub async fn screen_memory_start(app: AppHandle) -> Result<ScreenMemoryStatus, String> {
179
+ let mut feature_config = crate::config::feature_config(&app);
180
+ feature_config.screen_memory.enabled = true;
181
+ feature_config.screen_memory.paused = false;
182
+ feature_config.screen_memory = normalize_screen_memory_config(feature_config.screen_memory);
183
+ crate::config::set_feature_config(app.clone(), feature_config).await?;
184
+ build_status(&app)
185
+ }
186
+
187
+ #[tauri::command]
188
+ pub async fn screen_memory_pause(app: AppHandle) -> Result<ScreenMemoryStatus, String> {
189
+ let mut feature_config = crate::config::feature_config(&app);
190
+ feature_config.screen_memory.enabled = true;
191
+ feature_config.screen_memory.paused = true;
192
+ feature_config.screen_memory = normalize_screen_memory_config(feature_config.screen_memory);
193
+ crate::config::set_feature_config(app.clone(), feature_config).await?;
194
+ build_status(&app)
195
+ }
196
+
197
+ #[tauri::command]
198
+ pub async fn screen_memory_stop(app: AppHandle) -> Result<ScreenMemoryStatus, String> {
199
+ let mut feature_config = crate::config::feature_config(&app);
200
+ feature_config.screen_memory.enabled = false;
201
+ feature_config.screen_memory.paused = false;
202
+ feature_config.screen_memory = normalize_screen_memory_config(feature_config.screen_memory);
203
+ crate::config::set_feature_config(app.clone(), feature_config).await?;
204
+ build_status(&app)
205
+ }
206
+
207
+ #[tauri::command]
208
+ pub async fn screen_memory_recent_segments(
209
+ app: AppHandle,
210
+ limit: Option<usize>,
211
+ ) -> Result<Vec<ScreenMemorySegmentMetadata>, String> {
212
+ recent_segments(&app, limit)
213
+ }
214
+
215
+ #[tauri::command]
216
+ pub async fn screen_memory_query(
217
+ app: AppHandle,
218
+ query: Option<String>,
219
+ minutes: Option<u64>,
220
+ limit: Option<usize>,
221
+ ) -> Result<ScreenMemoryQueryResult, String> {
222
+ query_screen_memory(&app, query, minutes.unwrap_or(30), limit.unwrap_or(40))
223
+ }
224
+
225
+ #[tauri::command]
226
+ pub async fn screen_memory_delete_all(app: AppHandle) -> Result<ScreenMemoryStatus, String> {
227
+ let _ = screen_memory_delete(app.clone(), None).await?;
228
+ build_status(&app)
229
+ }
230
+
231
+ #[tauri::command]
232
+ pub async fn screen_memory_export_recent(
233
+ app: AppHandle,
234
+ minutes: Option<u64>,
235
+ ) -> Result<ScreenMemoryExportResult, String> {
236
+ export_recent(&app, minutes.unwrap_or(5))
237
+ }
238
+
239
+ #[tauri::command]
240
+ pub async fn screen_memory_open_folder(app: AppHandle) -> Result<(), String> {
241
+ let dir = screen_memory_dir(&app)?;
242
+ crate::clips::open_local_recording_folder(dir.to_string_lossy().to_string())
243
+ }
244
+
245
+ #[tauri::command]
246
+ pub async fn screen_memory_delete(
247
+ app: AppHandle,
248
+ segment_id: Option<String>,
249
+ ) -> Result<ScreenMemoryDeleteResult, String> {
250
+ if let Some(segment_id) = segment_id
251
+ .as_deref()
252
+ .map(str::trim)
253
+ .filter(|value| !value.is_empty())
254
+ {
255
+ stop_active_segment_if_matches(&app, segment_id)?;
256
+ let result = delete_segment(&app, segment_id)?;
257
+ let _ = app.emit(SCREEN_MEMORY_EVENT, ());
258
+ return Ok(result);
259
+ }
260
+
261
+ let mut feature_config = crate::config::feature_config(&app);
262
+ feature_config.screen_memory.enabled = false;
263
+ feature_config.screen_memory.paused = false;
264
+ crate::config::set_feature_config(app.clone(), feature_config).await?;
265
+
266
+ let result = delete_all_segments(&app)?;
267
+ let _ = app.emit(SCREEN_MEMORY_EVENT, ());
268
+ Ok(result)
269
+ }
270
+
271
+ fn normalize_screen_memory_config(mut config: ScreenMemoryConfig) -> ScreenMemoryConfig {
272
+ config.retention_hours = config
273
+ .retention_hours
274
+ .clamp(MIN_RETENTION_HOURS, MAX_RETENTION_HOURS);
275
+ config.max_bytes = config.max_bytes.clamp(MIN_MAX_BYTES, MAX_MAX_BYTES);
276
+ config.segment_seconds = config
277
+ .segment_seconds
278
+ .clamp(MIN_SEGMENT_SECONDS, MAX_SEGMENT_SECONDS);
279
+ config.sample_interval_seconds = config
280
+ .sample_interval_seconds
281
+ .clamp(1, config.segment_seconds);
282
+ config
283
+ }
284
+
285
+ fn screen_memory_available() -> bool {
286
+ #[cfg(target_os = "macos")]
287
+ {
288
+ std::path::Path::new("/usr/sbin/screencapture").exists()
289
+ }
290
+ #[cfg(not(target_os = "macos"))]
291
+ {
292
+ false
293
+ }
294
+ }
295
+
296
+ fn screen_memory_dir(app: &AppHandle) -> Result<PathBuf, String> {
297
+ let dir = app
298
+ .path()
299
+ .app_data_dir()
300
+ .map_err(|e| format!("app data directory unavailable: {e}"))?
301
+ .join(SCREEN_MEMORY_DIR);
302
+ std::fs::create_dir_all(&dir)
303
+ .map_err(|e| format!("screen memory directory unavailable: {e}"))?;
304
+ Ok(dir)
305
+ }
306
+
307
+ fn screen_memory_events_path(app: &AppHandle) -> Result<PathBuf, String> {
308
+ Ok(screen_memory_dir(app)?.join(SCREEN_MEMORY_EVENTS_JSONL))
309
+ }
310
+
311
+ fn segment_metadata_path(app: &AppHandle, segment_id: &str) -> Result<PathBuf, String> {
312
+ Ok(screen_memory_dir(app)?.join(format!("{}.json", sanitize_segment_id(segment_id))))
313
+ }
314
+
315
+ fn segment_media_path(
316
+ app: &AppHandle,
317
+ segment_id: &str,
318
+ extension: &str,
319
+ ) -> Result<PathBuf, String> {
320
+ Ok(screen_memory_dir(app)?.join(format!(
321
+ "{}.{}",
322
+ sanitize_segment_id(segment_id),
323
+ extension.trim_start_matches('.')
324
+ )))
325
+ }
326
+
327
+ fn sanitize_segment_id(value: &str) -> String {
328
+ let safe: String = value
329
+ .chars()
330
+ .filter(|c| c.is_ascii_alphanumeric() || *c == '-' || *c == '_')
331
+ .collect();
332
+ if safe.is_empty() {
333
+ "segment".to_string()
334
+ } else {
335
+ safe
336
+ }
337
+ }
338
+
339
+ fn next_segment_id() -> String {
340
+ let counter = SEGMENT_COUNTER.fetch_add(1, Ordering::Relaxed);
341
+ format!("segment-{}-{counter}", Utc::now().timestamp_millis())
342
+ }
343
+
344
+ fn build_status(app: &AppHandle) -> Result<ScreenMemoryStatus, String> {
345
+ let config = normalize_screen_memory_config(crate::config::feature_config(app).screen_memory);
346
+ let storage_dir = screen_memory_dir(app)?;
347
+ let (active_segment, last_error) = {
348
+ let state = app.state::<ScreenMemoryState>();
349
+ let guard = state.inner.lock().map_err(|e| e.to_string())?;
350
+ (
351
+ guard
352
+ .active
353
+ .as_ref()
354
+ .map(|active| ScreenMemoryActiveSegment {
355
+ id: active.id.clone(),
356
+ path: active.path.clone(),
357
+ mime_type: active.mime_type.to_string(),
358
+ started_at: active.started_at_iso.clone(),
359
+ duration_ms: active.started_at.elapsed().as_millis(),
360
+ width: active.width,
361
+ height: active.height,
362
+ }),
363
+ guard.last_error.clone(),
364
+ )
365
+ };
366
+ let state = if !config.enabled {
367
+ ScreenMemoryRuntimeState::Disabled
368
+ } else if active_segment.is_some() {
369
+ ScreenMemoryRuntimeState::Recording
370
+ } else if config.paused {
371
+ ScreenMemoryRuntimeState::Paused
372
+ } else {
373
+ ScreenMemoryRuntimeState::Idle
374
+ };
375
+
376
+ Ok(ScreenMemoryStatus {
377
+ available: screen_memory_available(),
378
+ state,
379
+ config,
380
+ storage_dir,
381
+ active_segment,
382
+ recent_segments: recent_segments(app, Some(5))?,
383
+ last_error,
384
+ })
385
+ }
386
+
387
+ fn ensure_running(app: &AppHandle, config: &ScreenMemoryConfig) -> Result<(), String> {
388
+ if !screen_memory_available() {
389
+ return Err("Screen Memory capture is currently macOS-only.".to_string());
390
+ }
391
+
392
+ {
393
+ let state = app.state::<ScreenMemoryState>();
394
+ let guard = state.inner.lock().map_err(|e| e.to_string())?;
395
+ if guard.active.is_some() {
396
+ return Ok(());
397
+ }
398
+ }
399
+
400
+ let active = start_new_segment(app)?;
401
+ let stop = Arc::new(AtomicBool::new(false));
402
+ {
403
+ let state = app.state::<ScreenMemoryState>();
404
+ let mut guard = state.inner.lock().map_err(|e| e.to_string())?;
405
+ if guard.active.is_some() {
406
+ drop(guard);
407
+ discard_active_segment(active);
408
+ return Ok(());
409
+ }
410
+ guard.active = Some(active);
411
+ guard.worker_stop = Some(Arc::clone(&stop));
412
+ guard.last_error = None;
413
+ }
414
+
415
+ spawn_rotator(app.clone(), stop);
416
+ prune_segments(app, config)?;
417
+ let _ = app.emit(SCREEN_MEMORY_EVENT, ());
418
+ Ok(())
419
+ }
420
+
421
+ fn spawn_rotator(app: AppHandle, stop: Arc<AtomicBool>) {
422
+ std::thread::spawn(move || loop {
423
+ let config =
424
+ normalize_screen_memory_config(crate::config::feature_config(&app).screen_memory);
425
+ if wait_for_rotation(&app, &stop, &config) {
426
+ return;
427
+ }
428
+ let config =
429
+ normalize_screen_memory_config(crate::config::feature_config(&app).screen_memory);
430
+ if !config.enabled || config.paused {
431
+ return;
432
+ }
433
+ if let Err(err) = rotate_segment(&app, &config) {
434
+ record_error(&app, err);
435
+ }
436
+ });
437
+ }
438
+
439
+ fn wait_for_rotation(app: &AppHandle, stop: &AtomicBool, config: &ScreenMemoryConfig) -> bool {
440
+ let deadline = Instant::now() + Duration::from_secs(config.segment_seconds);
441
+ let sample_interval = Duration::from_secs(config.sample_interval_seconds.max(1));
442
+ let mut next_sample = Instant::now();
443
+ loop {
444
+ if stop.load(Ordering::Relaxed) {
445
+ return true;
446
+ }
447
+ if Instant::now() >= deadline {
448
+ return false;
449
+ }
450
+ if Instant::now() >= next_sample {
451
+ append_event(app, sample_active_window());
452
+ next_sample = Instant::now() + sample_interval;
453
+ }
454
+ let remaining = deadline.saturating_duration_since(Instant::now());
455
+ let until_sample = next_sample.saturating_duration_since(Instant::now());
456
+ std::thread::sleep(remaining.min(until_sample).min(ROTATOR_TICK));
457
+ }
458
+ }
459
+
460
+ fn sample_active_window() -> ScreenMemoryEvent {
461
+ #[cfg(target_os = "macos")]
462
+ {
463
+ let context = crate::accessibility::macos::active_window_context_impl();
464
+ ScreenMemoryEvent {
465
+ captured_at: now_iso(),
466
+ app_name: context.app_name,
467
+ window_title: context.window_title,
468
+ bundle_id: context.bundle_id,
469
+ source: context.source,
470
+ }
471
+ }
472
+ #[cfg(not(target_os = "macos"))]
473
+ {
474
+ ScreenMemoryEvent {
475
+ captured_at: now_iso(),
476
+ app_name: None,
477
+ window_title: None,
478
+ bundle_id: None,
479
+ source: "unsupported".to_string(),
480
+ }
481
+ }
482
+ }
483
+
484
+ fn append_event(app: &AppHandle, event: ScreenMemoryEvent) {
485
+ if let Err(err) = append_event_inner(app, &event) {
486
+ eprintln!("[clips-tray] Screen Memory event append failed: {err}");
487
+ }
488
+ }
489
+
490
+ fn append_event_inner(app: &AppHandle, event: &ScreenMemoryEvent) -> Result<(), String> {
491
+ let path = screen_memory_events_path(app)?;
492
+ let mut file = OpenOptions::new()
493
+ .create(true)
494
+ .append(true)
495
+ .open(&path)
496
+ .map_err(|e| format!("screen memory events open failed: {e}"))?;
497
+ serde_json::to_writer(&mut file, event)
498
+ .map_err(|e| format!("screen memory event encode failed: {e}"))?;
499
+ file.write_all(b"\n")
500
+ .map_err(|e| format!("screen memory event write failed: {e}"))
501
+ }
502
+
503
+ fn rotate_segment(app: &AppHandle, config: &ScreenMemoryConfig) -> Result<(), String> {
504
+ let active = {
505
+ let state = app.state::<ScreenMemoryState>();
506
+ let mut guard = state.inner.lock().map_err(|e| e.to_string())?;
507
+ guard.active.take()
508
+ };
509
+
510
+ if let Some(active) = active {
511
+ match finalize_active_segment(active) {
512
+ Ok(segment) => {
513
+ write_segment_metadata(app, &segment)?;
514
+ prune_segments(app, config)?;
515
+ }
516
+ Err(err) => {
517
+ record_error(app, err);
518
+ }
519
+ }
520
+ }
521
+
522
+ let mut next = Some(start_new_segment(app)?);
523
+ let installed = {
524
+ let state = app.state::<ScreenMemoryState>();
525
+ let mut guard = state.inner.lock().map_err(|e| e.to_string())?;
526
+ let still_running = guard
527
+ .worker_stop
528
+ .as_ref()
529
+ .map(|stop| !stop.load(Ordering::Relaxed))
530
+ .unwrap_or(false);
531
+ if still_running && guard.active.is_none() {
532
+ guard.active = next.take();
533
+ guard.last_error = None;
534
+ true
535
+ } else {
536
+ false
537
+ }
538
+ };
539
+ if !installed {
540
+ if let Some(next) = next {
541
+ discard_active_segment(next);
542
+ }
543
+ }
544
+
545
+ let _ = app.emit(SCREEN_MEMORY_EVENT, ());
546
+ Ok(())
547
+ }
548
+
549
+ fn stop_active_segment(app: &AppHandle) -> Result<Option<ScreenMemorySegmentMetadata>, String> {
550
+ let active = {
551
+ let state = app.state::<ScreenMemoryState>();
552
+ let mut guard = state.inner.lock().map_err(|e| e.to_string())?;
553
+ if let Some(stop) = guard.worker_stop.take() {
554
+ stop.store(true, Ordering::Relaxed);
555
+ }
556
+ guard.active.take()
557
+ };
558
+
559
+ let Some(active) = active else {
560
+ return Ok(None);
561
+ };
562
+
563
+ let segment = finalize_active_segment(active)?;
564
+ write_segment_metadata(app, &segment)?;
565
+ let config = normalize_screen_memory_config(crate::config::feature_config(app).screen_memory);
566
+ prune_segments(app, &config)?;
567
+ let _ = app.emit(SCREEN_MEMORY_EVENT, ());
568
+ Ok(Some(segment))
569
+ }
570
+
571
+ fn stop_active_segment_if_matches(app: &AppHandle, segment_id: &str) -> Result<(), String> {
572
+ let active_matches = {
573
+ let state = app.state::<ScreenMemoryState>();
574
+ let guard = state.inner.lock().map_err(|e| e.to_string())?;
575
+ guard
576
+ .active
577
+ .as_ref()
578
+ .map(|active| active.id == sanitize_segment_id(segment_id))
579
+ .unwrap_or(false)
580
+ };
581
+ if active_matches {
582
+ stop_active_segment(app)?;
583
+ }
584
+ Ok(())
585
+ }
586
+
587
+ fn start_new_segment(app: &AppHandle) -> Result<ActiveScreenMemorySegment, String> {
588
+ #[cfg(not(target_os = "macos"))]
589
+ {
590
+ let _ = app;
591
+ Err("Screen Memory capture is currently macOS-only.".to_string())
592
+ }
593
+
594
+ #[cfg(target_os = "macos")]
595
+ {
596
+ let dir = screen_memory_dir(app)?;
597
+ if let Some(free) = native_screen::free_disk_bytes(&dir) {
598
+ if free < DISK_SPACE_BLOCK_BYTES {
599
+ return Err(format!(
600
+ "Not enough disk space to start Screen Memory. Free up at least {} and try again (currently {} free).",
601
+ native_screen::format_mb(DISK_SPACE_BLOCK_BYTES),
602
+ native_screen::format_mb(free)
603
+ ));
604
+ }
605
+ }
606
+
607
+ let id = next_segment_id();
608
+ let target_display_id = native_screen::tray_display_id(app);
609
+ let mp4_path = segment_media_path(app, &id, "mp4")?;
610
+ let _ = std::fs::remove_file(&mp4_path);
611
+ let (fallback_width, fallback_height) = native_screen::primary_monitor_size(app);
612
+
613
+ match native_screen::start_screencapturekit_backend_at(
614
+ &mp4_path,
615
+ false,
616
+ false,
617
+ None,
618
+ None,
619
+ target_display_id,
620
+ None,
621
+ false,
622
+ ) {
623
+ Ok((backend, width, height)) => {
624
+ return Ok(ActiveScreenMemorySegment {
625
+ id,
626
+ path: mp4_path,
627
+ mime_type: MP4_RECORDING_MIME_TYPE,
628
+ backend,
629
+ started_at: Instant::now(),
630
+ started_at_iso: now_iso(),
631
+ width: width.or(fallback_width),
632
+ height: height.or(fallback_height),
633
+ });
634
+ }
635
+ Err(sck_err) => {
636
+ eprintln!(
637
+ "[clips-tray] Screen Memory ScreenCaptureKit unavailable; falling back to screencapture: {sck_err}"
638
+ );
639
+ let _ = std::fs::remove_file(&mp4_path);
640
+ }
641
+ }
642
+
643
+ let mov_path = segment_media_path(app, &id, "mov")?;
644
+ let _ = std::fs::remove_file(&mov_path);
645
+ let (backend, width, height) = native_screen::start_screencapture_backend_at(
646
+ app,
647
+ &mov_path,
648
+ false,
649
+ target_display_id,
650
+ None,
651
+ )?;
652
+ Ok(ActiveScreenMemorySegment {
653
+ id,
654
+ path: mov_path,
655
+ mime_type: QUICKTIME_RECORDING_MIME_TYPE,
656
+ backend,
657
+ started_at: Instant::now(),
658
+ started_at_iso: now_iso(),
659
+ width: width.or(fallback_width),
660
+ height: height.or(fallback_height),
661
+ })
662
+ }
663
+ }
664
+
665
+ fn discard_active_segment(mut active: ActiveScreenMemorySegment) {
666
+ let _ = native_screen::stop_native_recording(&mut active.backend, false);
667
+ let _ = std::fs::remove_file(active.path);
668
+ }
669
+
670
+ fn finalize_active_segment(
671
+ mut active: ActiveScreenMemorySegment,
672
+ ) -> Result<ScreenMemorySegmentMetadata, String> {
673
+ let stop_error = native_screen::stop_native_recording(&mut active.backend, true).err();
674
+ let ended_at = now_iso();
675
+ let duration_ms = active.started_at.elapsed().as_millis();
676
+ let bytes = std::fs::metadata(&active.path)
677
+ .map_err(|e| {
678
+ let suffix = stop_error
679
+ .as_ref()
680
+ .map(|err| format!(" after stop error: {err}"))
681
+ .unwrap_or_default();
682
+ format!("Screen Memory segment missing{suffix}: {e}")
683
+ })?
684
+ .len();
685
+ if bytes == 0 {
686
+ let _ = std::fs::remove_file(&active.path);
687
+ return Err("Screen Memory segment produced an empty file.".to_string());
688
+ }
689
+
690
+ let corrupt = if active.mime_type == MP4_RECORDING_MIME_TYPE
691
+ || active.mime_type == QUICKTIME_RECORDING_MIME_TYPE
692
+ {
693
+ native_screen::mp4_has_moov(&active.path) == Some(false)
694
+ } else {
695
+ false
696
+ };
697
+ let error =
698
+ if corrupt {
699
+ Some(stop_error.unwrap_or_else(|| {
700
+ "Screen Memory segment is missing playback metadata.".to_string()
701
+ }))
702
+ } else {
703
+ stop_error
704
+ };
705
+
706
+ Ok(ScreenMemorySegmentMetadata {
707
+ id: active.id,
708
+ file_name: active
709
+ .path
710
+ .file_name()
711
+ .map(|value| value.to_string_lossy().to_string())
712
+ .unwrap_or_default(),
713
+ path: active.path,
714
+ mime_type: active.mime_type.to_string(),
715
+ started_at: active.started_at_iso,
716
+ ended_at,
717
+ duration_ms,
718
+ width: active.width,
719
+ height: active.height,
720
+ bytes,
721
+ corrupt,
722
+ error,
723
+ })
724
+ }
725
+
726
+ fn write_segment_metadata(
727
+ app: &AppHandle,
728
+ segment: &ScreenMemorySegmentMetadata,
729
+ ) -> Result<(), String> {
730
+ let path = segment_metadata_path(app, &segment.id)?;
731
+ let data = serde_json::to_vec_pretty(segment)
732
+ .map_err(|e| format!("screen memory metadata encode failed: {e}"))?;
733
+ std::fs::write(path, data).map_err(|e| format!("screen memory metadata write failed: {e}"))
734
+ }
735
+
736
+ fn read_segment_metadata_path(path: &Path) -> Result<ScreenMemorySegmentMetadata, String> {
737
+ let data =
738
+ std::fs::read(path).map_err(|e| format!("screen memory metadata read failed: {e}"))?;
739
+ serde_json::from_slice(&data).map_err(|e| format!("screen memory metadata decode failed: {e}"))
740
+ }
741
+
742
+ fn recent_segments(
743
+ app: &AppHandle,
744
+ limit: Option<usize>,
745
+ ) -> Result<Vec<ScreenMemorySegmentMetadata>, String> {
746
+ let dir = screen_memory_dir(app)?;
747
+ let mut segments = Vec::new();
748
+ for entry in std::fs::read_dir(&dir)
749
+ .map_err(|e| format!("screen memory directory read failed: {e}"))?
750
+ .flatten()
751
+ {
752
+ let path = entry.path();
753
+ if path.extension().and_then(|value| value.to_str()) != Some("json") {
754
+ continue;
755
+ }
756
+ let Ok(segment) = read_segment_metadata_path(&path) else {
757
+ continue;
758
+ };
759
+ if segment.path.exists() {
760
+ segments.push(segment);
761
+ } else {
762
+ let _ = std::fs::remove_file(path);
763
+ }
764
+ }
765
+ segments.sort_by(|a, b| b.ended_at.cmp(&a.ended_at));
766
+ if let Some(limit) = limit {
767
+ segments.truncate(limit);
768
+ }
769
+ Ok(segments)
770
+ }
771
+
772
+ fn query_screen_memory(
773
+ app: &AppHandle,
774
+ query: Option<String>,
775
+ minutes: u64,
776
+ limit: usize,
777
+ ) -> Result<ScreenMemoryQueryResult, String> {
778
+ let minutes = minutes.clamp(1, 24 * 30 * 60);
779
+ let limit = limit.clamp(1, 500);
780
+ let cutoff = Utc::now() - ChronoDuration::minutes(minutes as i64);
781
+ let query = query
782
+ .map(|value| value.trim().to_string())
783
+ .filter(|value| !value.is_empty());
784
+ let query_lc = query.as_ref().map(|value| value.to_lowercase());
785
+ let events = recent_events(app, cutoff, query_lc.as_deref(), limit)?;
786
+ let mut segments = recent_segments(app, None)?
787
+ .into_iter()
788
+ .filter(|segment| {
789
+ DateTime::parse_from_rfc3339(&segment.ended_at)
790
+ .map(|value| value.with_timezone(&Utc) >= cutoff)
791
+ .unwrap_or(true)
792
+ })
793
+ .filter(|segment| {
794
+ let Some(query) = query_lc.as_ref() else {
795
+ return true;
796
+ };
797
+ segment.id.to_lowercase().contains(query)
798
+ || segment.file_name.to_lowercase().contains(query)
799
+ || segment
800
+ .path
801
+ .to_string_lossy()
802
+ .to_lowercase()
803
+ .contains(query)
804
+ })
805
+ .collect::<Vec<_>>();
806
+ segments.truncate(limit);
807
+ Ok(ScreenMemoryQueryResult {
808
+ query,
809
+ minutes,
810
+ events,
811
+ segments,
812
+ })
813
+ }
814
+
815
+ fn recent_events(
816
+ app: &AppHandle,
817
+ cutoff: DateTime<Utc>,
818
+ query: Option<&str>,
819
+ limit: usize,
820
+ ) -> Result<Vec<ScreenMemoryEvent>, String> {
821
+ let path = screen_memory_events_path(app)?;
822
+ if !path.exists() {
823
+ return Ok(Vec::new());
824
+ }
825
+ let file = File::open(&path).map_err(|e| format!("screen memory events open failed: {e}"))?;
826
+ let reader = BufReader::new(file);
827
+ let mut events = Vec::new();
828
+ for line in reader.lines().map_while(Result::ok) {
829
+ let Ok(event) = serde_json::from_str::<ScreenMemoryEvent>(&line) else {
830
+ continue;
831
+ };
832
+ let captured_at = DateTime::parse_from_rfc3339(&event.captured_at)
833
+ .map(|value| value.with_timezone(&Utc))
834
+ .ok();
835
+ if captured_at.map(|value| value < cutoff).unwrap_or(false) {
836
+ continue;
837
+ }
838
+ if let Some(query) = query {
839
+ let haystack = format!(
840
+ "{} {} {}",
841
+ event.app_name.as_deref().unwrap_or(""),
842
+ event.window_title.as_deref().unwrap_or(""),
843
+ event.bundle_id.as_deref().unwrap_or("")
844
+ )
845
+ .to_lowercase();
846
+ if !haystack.contains(query) {
847
+ continue;
848
+ }
849
+ }
850
+ events.push(event);
851
+ }
852
+ events.sort_by(|a, b| b.captured_at.cmp(&a.captured_at));
853
+ events.truncate(limit);
854
+ Ok(events)
855
+ }
856
+
857
+ fn export_recent(app: &AppHandle, minutes: u64) -> Result<ScreenMemoryExportResult, String> {
858
+ let minutes = minutes.clamp(1, 24 * 30 * 60);
859
+ let cutoff = Utc::now() - ChronoDuration::minutes(minutes as i64);
860
+ let segments = recent_segments(app, None)?
861
+ .into_iter()
862
+ .filter(|segment| {
863
+ DateTime::parse_from_rfc3339(&segment.ended_at)
864
+ .map(|value| value.with_timezone(&Utc) >= cutoff)
865
+ .unwrap_or(true)
866
+ })
867
+ .collect::<Vec<_>>();
868
+ if segments.is_empty() {
869
+ return Err("No Screen Memory segments are available for that range.".to_string());
870
+ }
871
+
872
+ let folder = app
873
+ .path()
874
+ .video_dir()
875
+ .map_err(|e| format!("videos directory unavailable: {e}"))?
876
+ .join("Clips")
877
+ .join("Screen Memory")
878
+ .join(Utc::now().format("%Y-%m-%d-%H%M%S").to_string());
879
+ std::fs::create_dir_all(&folder)
880
+ .map_err(|e| format!("screen memory export folder unavailable: {e}"))?;
881
+
882
+ let mut files = Vec::new();
883
+ for (index, segment) in segments.iter().rev().enumerate() {
884
+ let extension = segment
885
+ .path
886
+ .extension()
887
+ .and_then(|value| value.to_str())
888
+ .unwrap_or("mp4");
889
+ let file_name = format!("segment-{:02}.{extension}", index + 1);
890
+ let destination = folder.join(&file_name);
891
+ let _ = std::fs::remove_file(&destination);
892
+ std::fs::copy(&segment.path, &destination)
893
+ .map_err(|e| format!("screen memory export copy failed: {e}"))?;
894
+ let bytes = std::fs::metadata(&destination)
895
+ .map_err(|e| format!("screen memory export metadata failed: {e}"))?
896
+ .len();
897
+ files.push(ScreenMemoryExportFile {
898
+ path: destination.to_string_lossy().to_string(),
899
+ file_name,
900
+ bytes,
901
+ mime_type: segment.mime_type.clone(),
902
+ });
903
+ }
904
+
905
+ Ok(ScreenMemoryExportResult {
906
+ folder_path: folder.to_string_lossy().to_string(),
907
+ files,
908
+ })
909
+ }
910
+
911
+ fn prune_segments(app: &AppHandle, config: &ScreenMemoryConfig) -> Result<(), String> {
912
+ let segments = recent_segments(app, None)?;
913
+ let cutoff = Utc::now() - ChronoDuration::hours(config.retention_hours as i64);
914
+ let mut kept_bytes = 0_u64;
915
+ for segment in segments {
916
+ let ended_at = DateTime::parse_from_rfc3339(&segment.ended_at)
917
+ .map(|value| value.with_timezone(&Utc))
918
+ .ok();
919
+ let expired = ended_at.map(|value| value < cutoff).unwrap_or(false);
920
+ kept_bytes = kept_bytes.saturating_add(segment.bytes);
921
+ if expired || kept_bytes > config.max_bytes {
922
+ let _ = delete_segment(app, &segment.id);
923
+ }
924
+ }
925
+ Ok(())
926
+ }
927
+
928
+ fn delete_segment(app: &AppHandle, segment_id: &str) -> Result<ScreenMemoryDeleteResult, String> {
929
+ let metadata_path = segment_metadata_path(app, segment_id)?;
930
+ let segment = read_segment_metadata_path(&metadata_path).ok();
931
+ let mut deleted_segments = 0_usize;
932
+ let mut deleted_bytes = 0_u64;
933
+ if let Some(segment) = segment {
934
+ deleted_bytes = segment.bytes;
935
+ remove_file_if_exists(&segment.path)?;
936
+ deleted_segments = 1;
937
+ }
938
+ remove_file_if_exists(&metadata_path)?;
939
+ Ok(ScreenMemoryDeleteResult {
940
+ deleted_segments,
941
+ deleted_bytes,
942
+ })
943
+ }
944
+
945
+ fn delete_all_segments(app: &AppHandle) -> Result<ScreenMemoryDeleteResult, String> {
946
+ let dir = screen_memory_dir(app)?;
947
+ let mut deleted_segments = 0_usize;
948
+ let mut deleted_bytes = 0_u64;
949
+ for segment in recent_segments(app, None)? {
950
+ deleted_bytes = deleted_bytes.saturating_add(segment.bytes);
951
+ remove_file_if_exists(&segment.path)?;
952
+ remove_file_if_exists(&segment_metadata_path(app, &segment.id)?)?;
953
+ deleted_segments += 1;
954
+ }
955
+ for entry in std::fs::read_dir(&dir)
956
+ .map_err(|e| format!("screen memory directory read failed: {e}"))?
957
+ .flatten()
958
+ {
959
+ let path = entry.path();
960
+ if path.is_file() {
961
+ remove_file_if_exists(&path)?;
962
+ }
963
+ }
964
+ Ok(ScreenMemoryDeleteResult {
965
+ deleted_segments,
966
+ deleted_bytes,
967
+ })
968
+ }
969
+
970
+ fn remove_file_if_exists(path: &Path) -> Result<(), String> {
971
+ match std::fs::remove_file(path) {
972
+ Ok(()) => Ok(()),
973
+ Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
974
+ Err(err) => Err(format!("remove {} failed: {err}", path.display())),
975
+ }
976
+ }
977
+
978
+ fn record_error(app: &AppHandle, error: String) {
979
+ eprintln!("[clips-tray] Screen Memory error: {error}");
980
+ if let Some(state) = app.try_state::<ScreenMemoryState>() {
981
+ if let Ok(mut guard) = state.inner.lock() {
982
+ guard.last_error = Some(error);
983
+ }
984
+ }
985
+ let _ = app.emit(SCREEN_MEMORY_EVENT, ());
986
+ }
987
+
988
+ fn now_iso() -> String {
989
+ Utc::now().to_rfc3339()
990
+ }