synabun 2.1.0 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (332) hide show
  1. package/CHANGELOG.md +139 -0
  2. package/README.md +1 -1
  3. package/docs/whatsapp.md +11 -11
  4. package/lib/client-config-repair.js +13 -9
  5. package/lib/external-tools.js +6 -5
  6. package/lib/update-snapshot.js +77 -3
  7. package/lib/virustotal.mjs +12 -1
  8. package/mcp-server/dist/index.js +1 -1
  9. package/mcp-server/dist/index.js.map +1 -1
  10. package/mcp-server/dist/services/budget-judgments.d.ts +1 -1
  11. package/mcp-server/dist/services/budget-judgments.d.ts.map +1 -1
  12. package/mcp-server/dist/services/budget-judgments.js +5 -3
  13. package/mcp-server/dist/services/budget-judgments.js.map +1 -1
  14. package/mcp-server/dist/services/identity.d.ts +2 -2
  15. package/mcp-server/dist/services/identity.js +2 -2
  16. package/mcp-server/dist/services/memory-sessions.d.ts +1 -1
  17. package/mcp-server/dist/services/memory-sessions.d.ts.map +1 -1
  18. package/mcp-server/dist/services/memory-sessions.js +28 -2
  19. package/mcp-server/dist/services/memory-sessions.js.map +1 -1
  20. package/mcp-server/dist/services/neural-interface.d.ts +2 -1
  21. package/mcp-server/dist/services/neural-interface.d.ts.map +1 -1
  22. package/mcp-server/dist/services/neural-interface.js.map +1 -1
  23. package/mcp-server/dist/services/server-instructions.d.ts.map +1 -1
  24. package/mcp-server/dist/services/server-instructions.js +2 -1
  25. package/mcp-server/dist/services/server-instructions.js.map +1 -1
  26. package/mcp-server/dist/services/tool-catalog.js +1 -1
  27. package/mcp-server/dist/services/tool-catalog.js.map +1 -1
  28. package/mcp-server/dist/services/tool-list.d.ts +1 -1
  29. package/mcp-server/dist/services/tool-list.d.ts.map +1 -1
  30. package/mcp-server/dist/services/tool-runtime.d.ts +2 -2
  31. package/mcp-server/dist/services/tool-runtime.d.ts.map +1 -1
  32. package/mcp-server/dist/services/tool-runtime.js +1 -1
  33. package/mcp-server/dist/services/tool-runtime.js.map +1 -1
  34. package/mcp-server/dist/services/typesafe-config.js +1 -1
  35. package/mcp-server/dist/services/typesafe-config.js.map +1 -1
  36. package/mcp-server/dist/tools/agents.d.ts +8 -8
  37. package/mcp-server/dist/tools/agents.d.ts.map +1 -1
  38. package/mcp-server/dist/tools/agents.js +10 -9
  39. package/mcp-server/dist/tools/agents.js.map +1 -1
  40. package/mcp-server/dist/tools/choice.d.ts +6 -0
  41. package/mcp-server/dist/tools/choice.d.ts.map +1 -1
  42. package/mcp-server/dist/tools/choice.js +18 -13
  43. package/mcp-server/dist/tools/choice.js.map +1 -1
  44. package/mcp-server/dist/tools/git-tools.js +2 -2
  45. package/mcp-server/dist/tools/git-tools.js.map +1 -1
  46. package/mcp-server/dist/tools/morelogin.d.ts.map +1 -1
  47. package/mcp-server/dist/tools/morelogin.js +9 -6
  48. package/mcp-server/dist/tools/morelogin.js.map +1 -1
  49. package/mcp-server/dist/tools/skill-tools.d.ts +8 -8
  50. package/mcp-server/dist/tools/skill-tools.d.ts.map +1 -1
  51. package/mcp-server/dist/tools/skill-tools.js +11 -8
  52. package/mcp-server/dist/tools/skill-tools.js.map +1 -1
  53. package/mcp-server/package-lock.json +2 -2
  54. package/mcp-server/package.json +1 -1
  55. package/mcp-server/src/index.ts +1 -1
  56. package/mcp-server/src/services/budget-judgments.ts +5 -3
  57. package/mcp-server/src/services/identity.ts +2 -2
  58. package/mcp-server/src/services/memory-sessions.ts +24 -3
  59. package/mcp-server/src/services/neural-interface.ts +2 -1
  60. package/mcp-server/src/services/server-instructions.ts +2 -1
  61. package/mcp-server/src/services/tool-catalog.ts +1 -1
  62. package/mcp-server/src/services/tool-list.ts +1 -1
  63. package/mcp-server/src/services/tool-runtime.ts +2 -2
  64. package/mcp-server/src/services/typesafe-config.ts +1 -1
  65. package/mcp-server/src/tools/agents.ts +10 -9
  66. package/mcp-server/src/tools/choice.ts +17 -12
  67. package/mcp-server/src/tools/git-tools.ts +2 -2
  68. package/mcp-server/src/tools/morelogin.ts +6 -4
  69. package/mcp-server/src/tools/skill-tools.ts +11 -8
  70. package/neural-interface/i18n/en.json +504 -39
  71. package/neural-interface/i18n/pt-BR.json +504 -39
  72. package/neural-interface/lib/agent-sandbox-elevation.js +6 -0
  73. package/neural-interface/lib/antigravity/acp-client-host.js +554 -0
  74. package/neural-interface/lib/antigravity/acp-connection.js +299 -0
  75. package/neural-interface/lib/antigravity/bypass.js +100 -0
  76. package/neural-interface/lib/antigravity/catalog.js +273 -0
  77. package/neural-interface/lib/antigravity/home.js +260 -0
  78. package/neural-interface/lib/antigravity/migrate-gemini.js +139 -0
  79. package/neural-interface/lib/antigravity/panel-bridge.js +1058 -0
  80. package/neural-interface/lib/antigravity/policy-hook.mjs +78 -0
  81. package/neural-interface/lib/antigravity/policy.js +74 -0
  82. package/neural-interface/lib/antigravity/tool-call.js +178 -0
  83. package/neural-interface/lib/antigravity/transcripts.js +252 -0
  84. package/neural-interface/lib/antigravity/usage.js +43 -0
  85. package/neural-interface/lib/assistant-api.js +1 -0
  86. package/neural-interface/lib/assistant-brains/antigravity.js +423 -0
  87. package/neural-interface/lib/assistant-brains/codex.js +6 -1
  88. package/neural-interface/lib/assistant-budget-planner.js +7 -3
  89. package/neural-interface/lib/assistant-budget-split.js +42 -15
  90. package/neural-interface/lib/assistant-catalog.js +76 -9
  91. package/neural-interface/lib/assistant-config.js +6 -3
  92. package/neural-interface/lib/assistant-dispatch.js +211 -50
  93. package/neural-interface/lib/assistant-envelope.js +153 -1
  94. package/neural-interface/lib/assistant-evidence.js +80 -0
  95. package/neural-interface/lib/assistant-hidden-models.js +7 -5
  96. package/neural-interface/lib/assistant-persona.js +16 -11
  97. package/neural-interface/lib/assistant-plan-permissions.js +15 -0
  98. package/neural-interface/lib/assistant-router.js +237 -35
  99. package/neural-interface/lib/assistant-runtime.js +158 -12
  100. package/neural-interface/lib/assistant-task-prompt.js +10 -8
  101. package/neural-interface/lib/assistant-tools.js +2 -2
  102. package/neural-interface/lib/assistant-usage.js +29 -0
  103. package/neural-interface/lib/backup-service.js +6 -1
  104. package/neural-interface/lib/browser-guard-cdp.js +5 -4
  105. package/neural-interface/lib/browser-network-guard.js +274 -82
  106. package/neural-interface/lib/browser-tool-policy.js +12 -7
  107. package/neural-interface/lib/claude-agent-bridge.js +7 -0
  108. package/neural-interface/lib/claude-history-cache.js +210 -0
  109. package/neural-interface/lib/claude-model-catalog.js +5 -1
  110. package/neural-interface/lib/cli-install-api.js +1 -1
  111. package/neural-interface/lib/cli-install.js +160 -31
  112. package/neural-interface/lib/cli-resolver.js +9 -2
  113. package/neural-interface/lib/cli-update-planner.js +25 -15
  114. package/neural-interface/lib/codex-rollout-reader.js +52 -0
  115. package/neural-interface/lib/codex-worker-app-server.js +25 -6
  116. package/neural-interface/lib/editor-git-api.js +3 -20
  117. package/neural-interface/lib/exec-loop-command.js +9 -2
  118. package/neural-interface/lib/file-explorer-api.js +74 -40
  119. package/neural-interface/lib/file-explorer-watch.js +9 -3
  120. package/neural-interface/lib/git-bash/install.js +52 -6
  121. package/neural-interface/lib/git-bash/manager.js +59 -17
  122. package/neural-interface/lib/git-bash/pins.js +12 -0
  123. package/neural-interface/lib/git-read.js +2 -2
  124. package/neural-interface/lib/git-status-runner.js +390 -0
  125. package/neural-interface/lib/git-status-watchdog.js +40 -0
  126. package/neural-interface/lib/greeting/budgets.js +1 -0
  127. package/neural-interface/lib/greeting/engine.js +2 -2
  128. package/neural-interface/lib/greeting/sources.js +10 -12
  129. package/neural-interface/lib/greeting/writer.js +1 -1
  130. package/neural-interface/lib/mcp-client-config.js +37 -6
  131. package/neural-interface/lib/mcp-external-servers.js +40 -7
  132. package/neural-interface/lib/mcp-installer.js +2 -1
  133. package/neural-interface/lib/memory-session-capture.js +17 -3
  134. package/neural-interface/lib/morelogin.js +147 -25
  135. package/neural-interface/lib/native-loop-providers.js +423 -23
  136. package/neural-interface/lib/native-loop-runtime.js +23 -2
  137. package/neural-interface/lib/opencode-native-boundary.js +165 -22
  138. package/neural-interface/lib/owner-authority.js +1 -1
  139. package/neural-interface/lib/path-confine.js +12 -9
  140. package/neural-interface/lib/peer-identity.js +80 -6
  141. package/neural-interface/lib/pty-host/core.js +28 -1
  142. package/neural-interface/lib/pty-host/lifetime.js +12 -0
  143. package/neural-interface/lib/pty-host/manager.js +12 -1
  144. package/neural-interface/lib/remote-policy.js +1 -1
  145. package/neural-interface/lib/request-gate.js +3 -0
  146. package/neural-interface/lib/rulesets/api.js +6 -5
  147. package/neural-interface/lib/rulesets/fs-safe.js +1 -1
  148. package/neural-interface/lib/rulesets/installer.js +89 -10
  149. package/neural-interface/lib/rulesets/render.js +1 -1
  150. package/neural-interface/lib/session-cache-builder.js +64 -1
  151. package/neural-interface/lib/skills/catalog.js +5 -1
  152. package/neural-interface/lib/skills/creation-brief.js +1 -1
  153. package/neural-interface/lib/skills/creation-sources.js +19 -1
  154. package/neural-interface/lib/skills/deploy.js +9 -4
  155. package/neural-interface/lib/skills/draft-confinement.js +3 -1
  156. package/neural-interface/lib/skills/eval-confinement.js +3 -1
  157. package/neural-interface/lib/skills/eval-trace.js +16 -0
  158. package/neural-interface/lib/skills/export.js +5 -2
  159. package/neural-interface/lib/skills/known-bridge-digests.json +58 -4
  160. package/neural-interface/lib/skills/ownership.js +1 -1
  161. package/neural-interface/lib/skills/roots.js +10 -3
  162. package/neural-interface/lib/skills/runtime-checks.js +6 -3
  163. package/neural-interface/lib/skills/service.js +15 -7
  164. package/neural-interface/lib/skills/telemetry.js +6 -0
  165. package/neural-interface/lib/synabun-version.js +1 -1
  166. package/neural-interface/lib/system-activity.js +1 -1
  167. package/neural-interface/lib/whatsapp/brain.js +7 -3
  168. package/neural-interface/lib/whatsapp/cards.js +22 -9
  169. package/neural-interface/lib/whatsapp/config.js +1 -1
  170. package/neural-interface/lib/worker-confinement.js +77 -1
  171. package/neural-interface/lib/worker-question.js +74 -0
  172. package/neural-interface/lib/worker-resume.js +13 -4
  173. package/neural-interface/lib/worker-sandbox.js +46 -16
  174. package/neural-interface/package-lock.json +18 -2
  175. package/neural-interface/package.json +2 -1
  176. package/neural-interface/public/onboarding.html +1 -1
  177. package/neural-interface/public/shared/agent-runtime-options.js +89 -11
  178. package/neural-interface/public/shared/agy/agy-bypass.js +47 -0
  179. package/neural-interface/public/shared/agy/agy-client.js +266 -0
  180. package/neural-interface/public/shared/agy/agy-commands.js +120 -0
  181. package/neural-interface/public/shared/agy/agy-copy.js +326 -0
  182. package/neural-interface/public/shared/agy/agy-diff.js +161 -0
  183. package/neural-interface/public/shared/agy/agy-events.js +469 -0
  184. package/neural-interface/public/shared/agy/agy-greeting.js +26 -0
  185. package/neural-interface/public/shared/agy/agy-options.js +232 -0
  186. package/neural-interface/public/shared/agy/agy-panel.js +2543 -0
  187. package/neural-interface/public/shared/agy/agy-render.js +392 -0
  188. package/neural-interface/public/shared/agy/agy-setup.js +149 -0
  189. package/neural-interface/public/shared/agy/agy-styles.js +604 -0
  190. package/neural-interface/public/shared/agy/agy-tabs-logic.js +181 -0
  191. package/neural-interface/public/shared/api.js +4 -4
  192. package/neural-interface/public/shared/assistant/asst-agents-dock.js +3 -4
  193. package/neural-interface/public/shared/assistant/asst-api.js +15 -3
  194. package/neural-interface/public/shared/assistant/asst-brain-picker.js +56 -9
  195. package/neural-interface/public/shared/assistant/asst-budget.js +5 -4
  196. package/neural-interface/public/shared/assistant/asst-computer.js +26 -2
  197. package/neural-interface/public/shared/assistant/asst-control.js +48 -0
  198. package/neural-interface/public/shared/assistant/asst-hosts.js +16 -7
  199. package/neural-interface/public/shared/assistant/asst-limit-dock.js +6 -1
  200. package/neural-interface/public/shared/assistant/asst-limit-wait.js +38 -5
  201. package/neural-interface/public/shared/assistant/asst-motion-gate.js +80 -0
  202. package/neural-interface/public/shared/assistant/asst-panel.js +46 -9
  203. package/neural-interface/public/shared/assistant/asst-render.js +112 -18
  204. package/neural-interface/public/shared/assistant/asst-route.js +237 -57
  205. package/neural-interface/public/shared/assistant/asst-routes-editor.js +9 -8
  206. package/neural-interface/public/shared/assistant/asst-scene-styles.js +55 -23
  207. package/neural-interface/public/shared/assistant/asst-scenes.js +279 -80
  208. package/neural-interface/public/shared/assistant/asst-sidepanel-styles.js +4 -4
  209. package/neural-interface/public/shared/assistant/asst-sidepanel.js +51 -7
  210. package/neural-interface/public/shared/assistant/asst-slash.js +8 -2
  211. package/neural-interface/public/shared/assistant/asst-state.js +76 -9
  212. package/neural-interface/public/shared/assistant/asst-style-cache.js +44 -0
  213. package/neural-interface/public/shared/assistant/asst-styles.js +58 -4
  214. package/neural-interface/public/shared/assistant/asst-tool-kinds.js +34 -7
  215. package/neural-interface/public/shared/assistant/asst-usage.js +2 -1
  216. package/neural-interface/public/shared/cdx/cdx-output.js +26 -3
  217. package/neural-interface/public/shared/cdx/cdx-panel.js +14 -2
  218. package/neural-interface/public/shared/cdx/cdx-preview.js +12 -0
  219. package/neural-interface/public/shared/cdx/cdx-protocol.js +11 -10
  220. package/neural-interface/public/shared/cdx/cdx-render.js +10 -9
  221. package/neural-interface/public/shared/cdx/cdx-requests.js +11 -1
  222. package/neural-interface/public/shared/cdx/cdx-styles.js +4 -0
  223. package/neural-interface/public/shared/cdx/cdx-tabs.js +75 -5
  224. package/neural-interface/public/shared/cli-install-controller.js +218 -0
  225. package/neural-interface/public/shared/cli-install-view.js +7 -4
  226. package/neural-interface/public/shared/cli-status.js +9 -8
  227. package/neural-interface/public/shared/constants.js +3 -2
  228. package/neural-interface/public/shared/context-menu-model.js +1 -0
  229. package/neural-interface/public/shared/cp/cp-permissions.js +12 -0
  230. package/neural-interface/public/shared/cp/cp-restore.js +162 -0
  231. package/neural-interface/public/shared/cp/cp-tool-cards.js +10 -3
  232. package/neural-interface/public/shared/desktop-bridge.js +17 -0
  233. package/neural-interface/public/shared/desktop-titlebar-view.js +12 -0
  234. package/neural-interface/public/shared/desktop-titlebar.js +13 -4
  235. package/neural-interface/public/shared/desktop-update-view.js +1 -0
  236. package/neural-interface/public/shared/explorer/fx-api.js +4 -4
  237. package/neural-interface/public/shared/explorer/fx-links.js +3 -3
  238. package/neural-interface/public/shared/explorer/fx-logic.js +1 -1
  239. package/neural-interface/public/shared/explorer/fx-main.js +48 -16
  240. package/neural-interface/public/shared/explorer/fx-tree.js +1 -0
  241. package/neural-interface/public/shared/greeting/greeting-card.js +2 -2
  242. package/neural-interface/public/shared/greeting/greeting-view.js +1 -0
  243. package/neural-interface/public/shared/html-shell.js +21 -4
  244. package/neural-interface/public/shared/loading-teardown.js +34 -0
  245. package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +6 -1
  246. package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +28 -7
  247. package/neural-interface/public/shared/ocp-v2/ocp-v2-rehydrate.js +13 -4
  248. package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +17 -1
  249. package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +6 -0
  250. package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +101 -68
  251. package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +46 -3
  252. package/neural-interface/public/shared/provider-icons.js +4 -2
  253. package/neural-interface/public/shared/settings/settings-rules.js +2 -2
  254. package/neural-interface/public/shared/settings/settings-skill-folders-pane.js +1 -1
  255. package/neural-interface/public/shared/settings/settings-skill-folders.js +2 -2
  256. package/neural-interface/public/shared/showcase-privacy.js +6 -1
  257. package/neural-interface/public/shared/sidepanel-session-status.js +3 -3
  258. package/neural-interface/public/shared/skills/ss-creation.js +6 -3
  259. package/neural-interface/public/shared/skills/ss-dialog.js +5 -2
  260. package/neural-interface/public/shared/skills/ss-lint.js +9 -1
  261. package/neural-interface/public/shared/skills/ss-profiles.js +44 -6
  262. package/neural-interface/public/shared/skills/ss-review-view.js +2 -2
  263. package/neural-interface/public/shared/skills/studio/sx-actions.js +1 -1
  264. package/neural-interface/public/shared/skills/studio/sx-create-ai-view.js +1 -1
  265. package/neural-interface/public/shared/skills/studio/sx-create-ai.js +2 -1
  266. package/neural-interface/public/shared/skills/studio/sx-dom.js +2 -1
  267. package/neural-interface/public/shared/skills/studio/sx-fixes-view.js +359 -0
  268. package/neural-interface/public/shared/skills/studio/sx-fixes.js +327 -0
  269. package/neural-interface/public/shared/skills/studio/sx-inspector.js +1 -1
  270. package/neural-interface/public/shared/skills/studio/sx-install-flow.js +9 -6
  271. package/neural-interface/public/shared/skills/studio/sx-install.js +20 -3
  272. package/neural-interface/public/shared/skills/studio/sx-library-view.js +144 -35
  273. package/neural-interface/public/shared/skills/studio/sx-library.js +6 -2
  274. package/neural-interface/public/shared/skills/studio/sx-main.js +11 -2
  275. package/neural-interface/public/shared/skills/studio/sx-review-view.js +1 -1
  276. package/neural-interface/public/shared/skills/studio/sx-subject.js +7 -6
  277. package/neural-interface/public/shared/skills/studio/sx-workspace-view.js +6 -5
  278. package/neural-interface/public/shared/state.js +1 -1
  279. package/neural-interface/public/shared/storage-retry.js +47 -0
  280. package/neural-interface/public/shared/storage.js +25 -4
  281. package/neural-interface/public/shared/streaming-markdown.js +58 -0
  282. package/neural-interface/public/shared/styles.css +362 -35
  283. package/neural-interface/public/shared/synabun-mascot.js +27 -8
  284. package/neural-interface/public/shared/synabun-ticker.js +147 -12
  285. package/neural-interface/public/shared/text-cache.js +29 -0
  286. package/neural-interface/public/shared/tool-list-picker.js +2 -2
  287. package/neural-interface/public/shared/tool-list-view.js +2 -2
  288. package/neural-interface/public/shared/topright-overflow-logic.js +54 -0
  289. package/neural-interface/public/shared/ui-antigravity-panel.js +19 -0
  290. package/neural-interface/public/shared/ui-assistant.js +2 -0
  291. package/neural-interface/public/shared/ui-automation-studio.js +20 -12
  292. package/neural-interface/public/shared/ui-claude-panel.js +374 -134
  293. package/neural-interface/public/shared/ui-cli-banner.js +23 -167
  294. package/neural-interface/public/shared/ui-file-explorer.js +48 -6
  295. package/neural-interface/public/shared/ui-file-links.js +2 -2
  296. package/neural-interface/public/shared/ui-games.js +2 -2
  297. package/neural-interface/public/shared/ui-judgments.js +1 -1
  298. package/neural-interface/public/shared/ui-keybinds.js +8 -2
  299. package/neural-interface/public/shared/ui-link.js +3 -3
  300. package/neural-interface/public/shared/ui-loading.js +5 -4
  301. package/neural-interface/public/shared/ui-menubar.js +2 -2
  302. package/neural-interface/public/shared/ui-native-loop-router.js +4 -0
  303. package/neural-interface/public/shared/ui-navbar.js +45 -0
  304. package/neural-interface/public/shared/ui-notifications.js +1 -0
  305. package/neural-interface/public/shared/ui-resume.js +9 -7
  306. package/neural-interface/public/shared/ui-schedules-studio.js +41 -10
  307. package/neural-interface/public/shared/ui-scroll-follow.js +16 -5
  308. package/neural-interface/public/shared/ui-sessions.js +22 -12
  309. package/neural-interface/public/shared/ui-settings.js +90 -65
  310. package/neural-interface/public/shared/ui-sidepanel-session-entry.js +9 -2
  311. package/neural-interface/public/shared/ui-sidepanel-sessions.js +3 -3
  312. package/neural-interface/public/shared/ui-sidepanel-tray.js +48 -7
  313. package/neural-interface/public/shared/ui-sidepanel-windows.js +3 -3
  314. package/neural-interface/public/shared/ui-terminal.js +146 -10
  315. package/neural-interface/public/shared/ui-tool-updates.js +3 -3
  316. package/neural-interface/public/shared/ui-tooltip.js +71 -46
  317. package/neural-interface/public/shared/ui-topright-overflow.js +233 -0
  318. package/neural-interface/public/shared/ui-whiteboard.js +1 -1
  319. package/neural-interface/public/shared/ui-worker-end.js +1 -1
  320. package/neural-interface/public/shared/whatsapp/wa-view.js +2 -2
  321. package/neural-interface/public/shared/worker-questions.js +34 -0
  322. package/neural-interface/public/sidepanel-session.html +1 -1
  323. package/neural-interface/server.js +731 -466
  324. package/neural-interface/templates/rulesets/README.md +1 -1
  325. package/neural-interface/templates/rulesets/legacy-hashes.json +5 -5
  326. package/neural-interface/templates/rulesets/manifest.json +1 -1
  327. package/npm-shrinkwrap.json +18 -2
  328. package/package.json +2 -1
  329. package/skills/skill-forge/SKILL.md +4 -4
  330. package/skills/synabun/SKILL.md +1 -1
  331. package/skills/synabun/modules/schedule.md +3 -3
  332. /package/neural-interface/templates/rulesets/{gemini.md → antigravity.md} +0 -0
@@ -18,11 +18,13 @@ import { html, raw, setHTML, ICONS, healthIcon, relativeTime, fullTime, rovingFo
18
18
  import { dropKept, dropDraft } from './sx-docs.js';
19
19
  import { encodeArtifactId } from '../ss-ids.js';
20
20
  import { revealHidden } from '../ss-unicode.js';
21
+ import { issuesOf } from './sx-fixes.js';
22
+ import { openFixPanel, fixAllSafe, fixNoticeHTML, unreadableFingerprint } from './sx-fixes-view.js';
21
23
 
22
24
  let _life = null; // the listeners of the drawn header (filters): they end with the next drawing
23
25
  let _lastChecked = null;
24
26
 
25
- const RT_LETTER = { claude: 'C', codex: 'X', opencode: 'O' };
27
+ const RT_LETTER = { claude: 'C', codex: 'X', opencode: 'O', antigravity: 'A' };
26
28
  const DIAG_REASONS = ['no-skill-md', 'unreadable-head', 'link', 'too-deep', 'unreadable', 'outside-project', 'not-a-folder'];
27
29
 
28
30
  /** Where a row comes from, in words: a label and a detail. */
@@ -65,11 +67,22 @@ export function healthWhy(h, t) {
65
67
  return '';
66
68
  }
67
69
 
68
- export function healthCapsule(row, t) {
70
+ /**
71
+ * `fix`: the capsule opens the row's fix panel (sx-fixes-view.js) when it holds a problem: in the Library a
72
+ * `data-fix` button, in a workspace a `data-ws="fix-issues"` one. A capsule with nothing to fix stays a label.
73
+ */
74
+ export function healthCapsule(row, t, { fix = null } = {}) {
69
75
  const worst = worstHealth(row);
70
76
  const rest = row.health.filter((h) => h !== worst && h.sev !== 'ok');
71
77
  const all = row.health.map((h) => { const why = healthWhy(h, t); return why ? `${healthWords(h, t)}: ${why}` : healthWords(h, t); }).join(' · ');
72
- return html`<span class="sx-health sx-health--${worst.sev}" title="${all}">${healthIcon(worst.code)}<span>${healthWords(worst, t)}</span>${rest.length ? html`<span class="sx-health-more">+${rest.length}</span>` : ''}</span>`;
78
+ const inner = html`${healthIcon(worst.code)}<span>${healthWords(worst, t)}</span>${rest.length ? html`<span class="sx-health-more">+${rest.length}</span>` : ''}`;
79
+ if (fix && issuesOf(row, { open: true }).length) {
80
+ const label = t('skillsStudio.fix.open', { problems: all });
81
+ return fix === 'library'
82
+ ? html`<button type="button" class="sx-health sx-health--${worst.sev} sx-health--fix" data-fix="${row.key}" tabindex="-1" title="${label}" aria-label="${label}">${inner}</button>`
83
+ : html`<button type="button" class="sx-health sx-health--${worst.sev} sx-health--fix" data-ws="fix-issues" title="${label}" aria-label="${label}">${inner}</button>`;
84
+ }
85
+ return html`<span class="sx-health sx-health--${worst.sev}" title="${all}">${inner}</span>`;
73
86
  }
74
87
 
75
88
  /**
@@ -80,12 +93,12 @@ export function healthCapsule(row, t) {
80
93
  export function statusPill(status, ctx, { button = null, id = null } = {}) {
81
94
  const { t } = ctx;
82
95
  const where = status.where.map((w) => {
83
- const runtime = t(`skillsStudio.runtime.${['claude', 'codex', 'opencode', 'agents'].includes(w.runtime) ? w.runtime : 'claude'}`);
96
+ const runtime = t(`skillsStudio.runtime.${['claude', 'codex', 'opencode', 'antigravity', 'agents'].includes(w.runtime) ? w.runtime : 'claude'}`);
84
97
  if (w.scope === 'project') return t('skillsStudio.publish.targetProject', { runtime, project: ctx.projects?.find((p) => p.path === w.projectPath)?.label || w.projectPath || '' });
85
98
  if (w.accountId && w.accountId !== 'default') return t('skillsStudio.publish.targetAccount', { runtime, account: w.accountId });
86
99
  return t('skillsStudio.publish.targetUser', { runtime });
87
100
  });
88
- const runtimes = [...new Set(status.where.map((w) => t(`skillsStudio.runtime.${['claude', 'codex', 'opencode', 'agents'].includes(w.runtime) ? w.runtime : 'claude'}`)))].join(', ');
101
+ const runtimes = [...new Set(status.where.map((w) => t(`skillsStudio.runtime.${['claude', 'codex', 'opencode', 'antigravity', 'agents'].includes(w.runtime) ? w.runtime : 'claude'}`)))].join(', ');
89
102
  const text = status.code === 'installed' ? t('skillsStudio.status.installed', { where: runtimes }) : status.code === 'outdated' ? t('skillsStudio.status.outdated', { where: runtimes }) : t(`skillsStudio.status.${status.code}`);
90
103
  const title = where.length ? where.join(', ') : t(`skillsStudio.status.help.${status.code}`);
91
104
  // Not installed is no tick: the dashed circle (healthIcon has no 'off' and would fall through to the tick).
@@ -96,7 +109,21 @@ export function statusPill(status, ctx, { button = null, id = null } = {}) {
96
109
  : html`<span class="sx-status sx-status--${status.code}"${id ? html` id="${id}"` : ''} title="${title}">${inner}</span>`;
97
110
  }
98
111
 
99
- /** The runtime capsules of a row (C X O), each named in full. */
112
+ /**
113
+ * The status and health pills of a row, once each, in the Library cell and the workspace head alike: a library skill
114
+ * shows its status, and its health only when something is wrong (an info item such as "draft" or "in review" is
115
+ * what the status already says); anything else shows its health.
116
+ */
117
+ export function statusPills(row, ctx, { status = installStatus({ skill: row.skill, row }), button = null, id = null, fix = null } = {}) {
118
+ const how = ctx.guest ? null : fix;
119
+ if (!status) return healthCapsule(row, ctx.t, { fix: how });
120
+ const sev = worstHealth(row).sev;
121
+ // An installation older than the latest version is said too: the status reads "installed" for it.
122
+ const stale = row.health.some((h) => h.code === 'stale-install');
123
+ return html`${statusPill(status, ctx, { button, id })}${sev === 'err' || sev === 'warn' || stale ? healthCapsule(row, ctx.t, { fix: how }) : ''}`;
124
+ }
125
+
126
+ /** The runtime capsules of a row (C X O A), each named in full. */
100
127
  export function runtimeCapsules(row, t) {
101
128
  return html`<span class="sx-rts">${RUNTIMES.map((rt) => {
102
129
  const st = row.runtimes[rt]?.state || 'none';
@@ -169,11 +196,10 @@ function rowHTML(row, ctx, selected, first) {
169
196
  function healthCellHTML(row, ctx) {
170
197
  const { t } = ctx;
171
198
  const status = installStatus({ skill: row.skill, row });
172
- if (!status) return healthCapsule(row, t);
173
- const worst = worstHealth(row);
199
+ if (!status) return healthCapsule(row, t, { fix: ctx.guest ? null : 'library' });
174
200
  // Installed for one assistant and not the others in effect: one press from the row adds them.
175
201
  const also = ['installed', 'outdated'].includes(status.code) && !ctx.guest ? alsoInstall(ctx, { skill: row.skill, row }) : { targets: [], words: '' };
176
- return html`<span class="sx-health-cell">${statusPill(status, ctx)}${worst.sev === 'err' || worst.sev === 'warn' ? healthCapsule(row, t) : ''}${status.code === 'approved' && !ctx.guest ? html`<button type="button" class="sx-btn sx-btn--sm sx-btn--primary" data-install="${row.key}" tabindex="-1" title="${t('skillsStudio.library.installHelp')}">${t('skillsStudio.library.install')}</button>` : ''}${also.targets.length ? html`<button type="button" class="sx-btn sx-btn--sm" data-also-install="${row.key}" tabindex="-1" title="${t('skillsStudio.library.alsoInstallHelp')}">${t('skillsStudio.library.alsoInstall', { targets: also.words })}</button>` : ''}</span>`;
202
+ return html`<span class="sx-health-cell">${statusPills(row, ctx, { status, fix: 'library' })}${status.code === 'approved' && !ctx.guest ? html`<button type="button" class="sx-btn sx-btn--sm sx-btn--primary" data-install="${row.key}" tabindex="-1" title="${t('skillsStudio.library.installHelp')}">${t('skillsStudio.library.install')}</button>` : ''}${also.targets.length ? html`<button type="button" class="sx-btn sx-btn--sm" data-also-install="${row.key}" tabindex="-1" title="${t('skillsStudio.library.alsoInstallHelp')}">${t('skillsStudio.library.alsoInstall', { targets: also.words })}</button>` : ''}</span>`;
177
203
  }
178
204
 
179
205
  /**
@@ -269,9 +295,11 @@ function summaryHTML(ctx, shown) {
269
295
  const parts = [t('skillsStudio.library.shown', { shown: shown.length, total })];
270
296
  if (counts?.views?.attention) parts.push(tp('skillsStudio.library.attentionCount', counts.views.attention));
271
297
  const diag = data?.catalog?.diagnostics || [];
298
+ // Folders the owner said to leave as they are are not said again until their number changes.
299
+ const diagSaid = diag.length && !ctx.dismissed?.().has(unreadableFingerprint(diag.length));
272
300
  // A server older than the page reads no change times: the dashes are said once, with what brings them.
273
301
  const restart = knowsUpdated(ctx) ? '' : html` · <span class="sx-note-warn" id="sx-lib-updated-restart">${t('skillsStudio.library.updatedRestart')}</span>`;
274
- return html`<p class="sx-summary" role="status">${parts.join(' · ')}${diag.length ? html` · <button type="button" class="sx-link" data-lib="diagnostics">${tp('skillsStudio.library.diagnostics', diag.length)}</button>` : ''}${data?.catalog?.truncated ? html` · <span class="sx-warn-text">${t('skillsStudio.library.truncated')}</span>` : ''}${restart}</p>`;
302
+ return html`<p class="sx-summary" role="status">${parts.join(' · ')}${diagSaid ? html` · <button type="button" class="sx-link" data-lib="diagnostics">${tp('skillsStudio.library.diagnostics', diag.length)}</button>` : ''}${data?.catalog?.truncated ? html` · <span class="sx-warn-text">${t('skillsStudio.library.truncated')}</span>` : ''}${restart}</p>`;
275
303
  }
276
304
 
277
305
  /**
@@ -394,7 +422,7 @@ export function renderLibraryPlace(host, ctx) {
394
422
  const shown = visible(ctx);
395
423
  setHTML(host, html`<section class="sx-place sx-place--library" aria-labelledby="sx-lib-title">
396
424
  ${filterHeadHTML(ctx)}
397
- <div class="sx-place-notices">${installNoticeHTML(ctx)}${migrationNoticeHTML(ctx)}${keptNoticeHTML(ctx)}${updateNoticeHTML(ctx)}</div>
425
+ <div class="sx-place-notices">${fixNoticeHTML(ctx, deleteKind)}${installNoticeHTML(ctx)}${migrationNoticeHTML(ctx)}${keptNoticeHTML(ctx)}${updateNoticeHTML(ctx)}</div>
398
426
  <div class="sx-place-scroll" id="sx-lib-scroll">
399
427
  <div id="sx-lib-summary">${summaryHTML(ctx, shown)}</div>
400
428
  <div id="sx-lib-table">${tableHTML(ctx, shown)}</div>
@@ -450,6 +478,8 @@ async function onClick(e, host, ctx) {
450
478
  if (also && host.contains(also) && !also.disabled) { onAlsoInstall(also, host, ctx); return; }
451
479
  const restore = e.target.closest?.('[data-restore]');
452
480
  if (restore && host.contains(restore) && !restore.disabled) { onRestore(restore, host, ctx); return; }
481
+ const fix = e.target.closest?.('[data-fix]');
482
+ if (fix && host.contains(fix)) { const row = ctx.rows.find((r) => r.key === fix.dataset.fix); ctx.lib.focus = fix.dataset.fix; if (row) openFixPanel(ctx, row, { deleteKind, bulkDelete }); return; }
453
483
  const open = e.target.closest?.('[data-open]');
454
484
  if (open && host.contains(open)) { ctx.lib.focus = open.dataset.open; ctx.openRow(open.dataset.open); return; }
455
485
  const btn = e.target.closest?.('[data-lib]');
@@ -458,6 +488,9 @@ async function onClick(e, host, ctx) {
458
488
  if (act === 'new') ctx.goPlace('create');
459
489
  else if (act === 'retry') ctx.reload();
460
490
  else if (act === 'install-notice-ok') { ctx.lib.installNotice = null; ctx.render(); }
491
+ else if (act === 'fix-notice-ok') { ctx.lib.fixNotice = null; ctx.render(); }
492
+ else if (act === 'fix-all') fixAllSafe(ctx, ctx.rows, deleteKind);
493
+ else if (act === 'fix-show') { Object.assign(ctx.lib, { view: 'all', health: 'attention' }); ctx.saveLibraryView(); ctx.render(); }
461
494
  else if (act === 'clear-filters') { Object.assign(ctx.lib, { view: 'all', kind: 'all', source: 'all', runtime: 'all', health: 'any', q: '' }); ctx.saveLibraryView(); ctx.render(); }
462
495
  else if (act === 'bulk-clear') { ctx.lib.selected.clear(); redrawRows(host, ctx); }
463
496
  else if (act === 'diagnostics') showDiagnostics(ctx);
@@ -515,7 +548,18 @@ async function showDiagnostics(ctx) {
515
548
  li.title = d.message || '';
516
549
  list.append(li);
517
550
  }
518
- await ctx.ask({ title: t('skillsStudio.library.diagnosticsTitle'), lines: [t('skillsStudio.library.diagnosticsHelp')], extra: list, buttons: [{ id: 'ok', label: t('skillsStudio.dialog.close'), kind: 'primary' }], cancelId: 'ok', wide: true });
551
+ const buttons = ctx.guest ? [{ id: 'ok', label: t('skillsStudio.dialog.close'), kind: 'primary' }]
552
+ : [{ id: 'dismiss', label: t('skillsStudio.fix.action.dismiss') }, { id: 'ok', label: t('skillsStudio.dialog.close') }, { id: 'rescan', label: t('skillsStudio.fix.action.rescan'), kind: 'primary' }];
553
+ const answer = await ctx.ask({ title: t('skillsStudio.library.diagnosticsTitle'), lines: [t('skillsStudio.library.diagnosticsHelp'), t('skillsStudio.fix.explain.unreadable')], extra: list, buttons, cancelId: 'ok', wide: true });
554
+ if (answer === 'dismiss') { ctx.dismissIssues([unreadableFingerprint(diag.length)]); ctx.toast(t('skillsStudio.fix.dismissed')); return; }
555
+ if (answer !== 'rescan') return;
556
+ const gen = ctx.gen();
557
+ try {
558
+ await ctx.api.rescanSkillsCatalog();
559
+ if (!ctx.alive(gen)) return;
560
+ ctx.toast(t('skillsStudio.fix.rescanned'));
561
+ } catch (err) { if (ctx.alive(gen)) ctx.toast(t('skillsStudio.fix.failed', { error: studioErrorText(ctx.t, err) }), 'error'); }
562
+ if (ctx.alive(gen)) await ctx.reload();
519
563
  }
520
564
 
521
565
  async function reopenKept(ctx) {
@@ -581,7 +625,7 @@ function bulkExport(ctx) {
581
625
  ctx.toast(ctx.tp('skillsStudio.library.bulk.exported', n));
582
626
  }
583
627
 
584
- async function bulkDelete(ctx) {
628
+ export async function bulkDelete(ctx) {
585
629
  const { t, tp, api } = ctx;
586
630
  const chosen = ctx.rows.filter((r) => ctx.lib.selected.has(r.key)).map((r) => ({ row: r, s: subjectOf(r, { caps: ctx.caps }) }))
587
631
  .map((x) => ({ ...x, kind: deleteKind(ctx, x.row, x.s) })).filter((x) => x.kind);
@@ -590,32 +634,25 @@ async function bulkDelete(ctx) {
590
634
  const inLibrary = chosen.filter((x) => x.kind === 'library');
591
635
  // Unsaved text in an open workspace of one of them goes with it only when the owner says so.
592
636
  const unsaved = chosen.filter((x) => ctx.workspaces.get(x.row.key)?.tabs.some((tab) => tab.dirty));
593
- const box = document.createElement('div');
594
- box.className = 'sx-dlg-form';
595
- const label = document.createElement('label');
596
- label.className = 'sx-field-label';
597
- label.htmlFor = 'sx-del-confirm';
598
- label.textContent = t('skillsStudio.library.bulk.deleteType', { word: t('skillsStudio.library.bulk.deleteWord') });
599
- const input = document.createElement('input');
600
- input.id = 'sx-del-confirm';
601
- input.className = 'sx-input';
602
- input.autocomplete = 'off';
603
- const err = document.createElement('p');
604
- err.className = 'ss-dlg-error';
605
- err.setAttribute('role', 'alert');
606
- box.append(label, input, err);
637
+ const word = t('skillsStudio.library.bulk.deleteWord');
638
+ const { box, input, err } = deleteConfirmBox(ctx, chosen, { onDisk, inLibrary, unsaved, word });
607
639
  const answer = await ctx.ask({
608
640
  title: tp('skillsStudio.library.bulk.deleteTitle', chosen.length),
609
- // Two kinds, said apart: what is removed from where it is, and what only leaves the library.
610
- lines: [
611
- ...(onDisk.length ? [t('skillsStudio.library.bulk.deleteExplain'), ...onDisk.slice(0, 8).map((x) => `• ${x.row.label} (${x.row.path || ''})`)] : []),
612
- ...(inLibrary.length ? [t('skillsStudio.library.bulk.deleteLibraryExplain'), ...inLibrary.slice(0, 8).map((x) => `• ${x.row.label}`)] : []),
613
- ...(unsaved.length ? [tp('skillsStudio.library.bulk.deleteUnsaved', unsaved.length)] : []),
614
- ],
615
641
  extra: box,
616
- buttons: [{ id: 'cancel', label: t('skillsStudio.dialog.cancel') }, { id: 'delete', label: t('skillsStudio.library.bulk.deleteConfirm'), kind: 'danger' }],
642
+ className: 'ss-dlg--confirm',
643
+ buttons: [{ id: 'cancel', label: t('skillsStudio.dialog.cancel') }, { id: 'delete', label: t('skillsStudio.library.bulk.deleteConfirm'), kind: 'danger', disabled: true }],
617
644
  focus: 'cancel',
618
- accept: (id) => { if (id === 'delete' && input.value.trim().toLowerCase() !== t('skillsStudio.library.bulk.deleteWord').toLowerCase()) { err.textContent = t('skillsStudio.library.bulk.deleteTypeError'); return false; } return true; },
645
+ // Delete stays off until the word is typed; Enter in the field answers once it is.
646
+ onOpen: (ui) => {
647
+ input.addEventListener('input', () => { ui.setDisabled('delete', !deleteWordMatches(input.value, word)); if (deleteWordMatches(input.value, word)) err.textContent = ''; });
648
+ input.addEventListener('keydown', (e) => {
649
+ if (e.key !== 'Enter' || e.isComposing) return;
650
+ e.preventDefault();
651
+ if (deleteWordMatches(input.value, word)) ui.done('delete'); else err.textContent = t('skillsStudio.library.bulk.deleteTypeError');
652
+ });
653
+ input.focus({ preventScroll: true });
654
+ },
655
+ accept: (id) => { if (id === 'delete' && !deleteWordMatches(input.value, word)) { err.textContent = t('skillsStudio.library.bulk.deleteTypeError'); return false; } return true; },
619
656
  });
620
657
  if (answer !== 'delete') return;
621
658
  const gen = ctx.gen();
@@ -635,5 +672,77 @@ async function bulkDelete(ctx) {
635
672
  await ctx.reload();
636
673
  }
637
674
 
675
+ /** Whether what was typed is the confirmation word (case and surrounding spaces aside). */
676
+ export function deleteWordMatches(value, word) {
677
+ return String(value ?? '').trim().toLocaleLowerCase() === String(word ?? '').trim().toLocaleLowerCase() && !!String(word ?? '').trim();
678
+ }
679
+
680
+ /**
681
+ * The body of the delete question: one tidy row per item (its name, its kind, where it is, shortened, with the full
682
+ * path on hover, and what happens to it), what that means, and the field the word is typed in.
683
+ */
684
+ export function deleteConfirmBox(ctx, chosen, { onDisk = [], inLibrary = [], unsaved = [], word }) {
685
+ const { t, tp } = ctx;
686
+ const el = (tag, cls, text) => { const n = document.createElement(tag); if (cls) n.className = cls; if (text !== undefined) n.textContent = text; return n; };
687
+ const box = el('div', 'sx-del');
688
+ const list = el('ul', 'sx-del-list');
689
+ list.setAttribute('aria-label', t('skillsStudio.library.bulk.deleteListLabel'));
690
+ const SHOWN = 6;
691
+ for (const x of chosen.slice(0, SHOWN)) {
692
+ const li = el('li', 'sx-del-item');
693
+ const icon = el('span', 'sx-del-icon');
694
+ icon.setAttribute('aria-hidden', 'true');
695
+ setHTML(icon, x.row.kind === 'skill' ? ICONS.folder : ICONS.file);
696
+ const main = el('span', 'sx-del-main');
697
+ main.append(el('span', 'sx-del-name', x.row.label));
698
+ const where = x.kind === 'disk' ? shortPath(x.row.path || '') : t('skillsStudio.library.bulk.deleteInLibrary');
699
+ const path = el('span', 'sx-del-path', where);
700
+ if (x.kind === 'disk' && x.row.path) path.title = x.row.path;
701
+ main.append(path);
702
+ const kind = el('span', 'sx-del-kind', kindLabel(x.row, t));
703
+ const effect = el('span', `sx-del-effect sx-del-effect--${x.kind}`, x.kind === 'disk' ? t('skillsStudio.library.bulk.deleteEffectDisk') : t('skillsStudio.library.bulk.deleteEffectLibrary'));
704
+ li.append(icon, main, kind, effect);
705
+ list.append(li);
706
+ }
707
+ if (chosen.length > SHOWN) list.append(el('li', 'sx-del-more', t('skillsStudio.library.bulk.deleteMore', { count: chosen.length - SHOWN })));
708
+ box.append(list);
709
+ // Two kinds, said apart: what is removed from where it is (for good), and what only leaves the library.
710
+ if (onDisk.length) {
711
+ const note = el('p', 'sx-del-note sx-del-note--danger');
712
+ setHTML(note, html`${ICONS.warn}<span>${t('skillsStudio.library.bulk.deleteExplain')}</span>`);
713
+ box.append(note);
714
+ }
715
+ if (inLibrary.length) {
716
+ const note = el('p', 'sx-del-note');
717
+ setHTML(note, html`${ICONS.info}<span>${t('skillsStudio.library.bulk.deleteLibraryExplain')}</span>`);
718
+ box.append(note);
719
+ }
720
+ if (unsaved.length) {
721
+ const note = el('p', 'sx-del-note sx-del-note--danger');
722
+ setHTML(note, html`${ICONS.draft}<span>${tp('skillsStudio.library.bulk.deleteUnsaved', unsaved.length)}</span>`);
723
+ box.append(note);
724
+ }
725
+ const field = el('div', 'sx-del-field');
726
+ const label = el('label', 'sx-del-label');
727
+ label.htmlFor = 'sx-del-confirm';
728
+ // "Type {word} to confirm", with the word set apart as what is to be typed.
729
+ const sentence = t('skillsStudio.library.bulk.deleteType', { word: '\u0000' });
730
+ const [before, after = ''] = sentence.split('\u0000');
731
+ label.append(before, el('code', 'sx-del-word', word), after);
732
+ const input = el('input', 'sx-del-input');
733
+ input.id = 'sx-del-confirm';
734
+ input.type = 'text';
735
+ input.autocomplete = 'off';
736
+ input.spellcheck = false;
737
+ input.setAttribute('autocapitalize', 'off');
738
+ input.setAttribute('aria-describedby', 'sx-del-error');
739
+ const err = el('p', 'ss-dlg-error');
740
+ err.id = 'sx-del-error';
741
+ err.setAttribute('role', 'alert');
742
+ field.append(label, input, err);
743
+ box.append(field);
744
+ return { box, input, err };
745
+ }
746
+
638
747
  /** The P0 id of a row's entry (exported for the workspace's Export of an agent or a command). */
639
748
  export const v1ExportId = (entry) => (entry?.path ? encodeArtifactId(`${entry.kind}:${entry.path}`) : null);
@@ -18,7 +18,7 @@
18
18
  // Health is a list of codes with a severity (err, warn, info, off, ok), never a colour alone: the view writes the
19
19
  // words (i18n `skillsStudio.health.*`). No DOM and no imports.
20
20
 
21
- export const RUNTIMES = Object.freeze(['claude', 'codex', 'opencode']);
21
+ export const RUNTIMES = Object.freeze(['claude', 'codex', 'opencode', 'antigravity']);
22
22
  export const SORTS = Object.freeze(['attention', 'name', 'updated', 'kind', 'source']);
23
23
  export const KINDS = Object.freeze(['skill', 'agent', 'command']);
24
24
  export const SOURCES = Object.freeze(['mine', 'project', 'synabun', 'plugin', 'account', 'system', 'shared', 'library']);
@@ -322,6 +322,10 @@ function finishRow(row) {
322
322
  row.label = revealHidden(row.name || row.folderName || '');
323
323
  row.shownDescription = revealHidden(row.description || '');
324
324
  row.runtimes = runtimeStates(row);
325
+ // A runtime that lists two copies of this very row (SynaBun installed one library skill into ~/.claude/skills and
326
+ // ~/.config/opencode/skills, both read by OpenCode) has no other skill to confuse it with: no "duplicate".
327
+ const own = new Set(row.entries.map((e) => e.id));
328
+ row.collisions = row.collisions.filter((c) => !(Array.isArray(c.with) && c.with.length && c.with.every((id) => own.has(id))));
325
329
  row.health = healthOf(row);
326
330
  row.attention = row.health.reduce((n, h) => n + (ATTENTION[h.code] || 0) * (h.code === 'invalid' || h.code === 'warnings' ? Math.max(1, Math.min(9, num(h.count))) : 1), 0);
327
331
  row.needsAttention = row.attention > 0;
@@ -338,7 +342,7 @@ export function healthMatches(row, health) {
338
342
  case 'invalid': return codes.has('invalid') || codes.has('not-text');
339
343
  case 'warnings': return codes.has('warnings');
340
344
  case 'update': return codes.has('update');
341
- case 'duplicates': return codes.has('shadowed') || codes.has('twice') || codes.has('same-description');
345
+ case 'duplicates': return codes.has('shadowed') || codes.has('twice') || codes.has('same-description') || codes.has('duplicate-record');
342
346
  case 'drafts': return codes.has('draft');
343
347
  case 'readonly': return codes.has('readonly');
344
348
  case 'ok': return codes.has('ok');
@@ -25,6 +25,7 @@ import { askInPanel, dialogOpen, closeDialog } from '../ss-dialog.js';
25
25
  import { closeDropdowns } from '../../cc-dropdown.js';
26
26
  import * as api from '../../api.js';
27
27
  import { readWholeCatalog, buildRows, countRows, projectsOf, keepSelection } from './sx-library.js';
28
+ import { applyFixState, readDismissed, addDismissed, removeDismissed, DISMISSED_KEY } from './sx-fixes.js';
28
29
  import { initialRect, parseOpen } from './sx-geometry.js';
29
30
  import { frameRect } from '../../app-frame.js';
30
31
  import { updateCards, reviewCount } from './sx-review.js';
@@ -134,6 +135,9 @@ function deriveRows() {
134
135
  // Review cards and the rail's count all leave a kept update out.
135
136
  const kept = readJson(KEPT_UPDATES_KEY, {});
136
137
  S.rows = buildRows({ entries: d.catalog.items, skills: d.skills, status: d.status, reviews: d.reviews.filter((r) => ['pending', 'changes_requested', 'approved', 'partial', 'publish_failed'].includes(r.state)), upstream: d.upstream, kept });
138
+ // The problems only the fix system finds (a second library record, an older installation), and the ones the owner
139
+ // dismissed taken out (sx-fixes.js), before anything counts them.
140
+ applyFixState(S.rows, readDismissed(storage.getItem(DISMISSED_KEY)));
137
141
  S.counts = countRows(S.rows);
138
142
  S.counts.views.archived = Array.isArray(d.archived) ? d.archived.length : 0;
139
143
  S.projects = projectsOf(d.catalog.roots, S.rows);
@@ -174,7 +178,7 @@ export async function openStudio(detail = {}) {
174
178
  S.opener = document.activeElement instanceof HTMLElement ? document.activeElement : null;
175
179
  S.docked = storage.getItem(DOCK_KEY) === 'true';
176
180
  S.dim = storage.getItem(DIM_KEY) === 'true' && !S.docked;
177
- S.lib = { ...S.lib, ...readJson(LIB_KEY, {}), selected: new Set(), focus: null, installNotice: null };
181
+ S.lib = { ...S.lib, ...readJson(LIB_KEY, {}), selected: new Set(), focus: null, installNotice: null, fixNotice: null };
178
182
  S.backdrop = document.createElement('div');
179
183
  S.backdrop.className = `studio-backdrop sx-backdrop${S.dim ? '' : ' backdrop-hidden'}`;
180
184
  document.body.appendChild(S.backdrop);
@@ -538,7 +542,7 @@ function runtimeFootHTML() {
538
542
  if (own.some((r) => r.status === 'ok')) return 'ready';
539
543
  return own.length ? 'empty' : 'unknown';
540
544
  };
541
- const rows = ['claude', 'codex', 'opencode'].map((rt) => {
545
+ const rows = ['claude', 'codex', 'opencode', 'antigravity'].map((rt) => {
542
546
  const st = state(rt);
543
547
  const name = t(`skillsStudio.runtime.${rt}`);
544
548
  const label = st === 'bridge' ? t('skillsStudio.rail.runtimeFix', { runtime: name }) : `${name}: ${t(`skillsStudio.rail.runtimeState.${st}`)}`;
@@ -673,6 +677,11 @@ function ctx() {
673
677
  keepUpdate: (name, revisionId) => { const kept = readJson(KEPT_UPDATES_KEY, {}); kept[name] = revisionId; storage.setItem(KEPT_UPDATES_KEY, JSON.stringify(kept)); deriveRows(); },
674
678
  saveLibraryView: () => storage.setItem(LIB_KEY, JSON.stringify({ view: S.lib.view, kind: S.lib.kind, source: S.lib.source, runtime: S.lib.runtime, health: S.lib.health, sort: S.lib.sort })),
675
679
  keptDrafts: () => keptDrafts(),
680
+ // Problems the owner has seen and leaves as they are (sx-fixes.js fingerprints): kept, and said again when they change.
681
+ dismissed: () => readDismissed(storage.getItem(DISMISSED_KEY)),
682
+ dismissIssues: (fingerprints) => { storage.setItem(DISMISSED_KEY, addDismissed(storage.getItem(DISMISSED_KEY), fingerprints)); deriveRows(); render({ keepFocus: true }); },
683
+ restoreIssues: (fingerprints) => { storage.setItem(DISMISSED_KEY, removeDismissed(storage.getItem(DISMISSED_KEY), fingerprints)); deriveRows(); render({ keepFocus: true }); },
684
+ openSkillFolders,
676
685
  workspaces: S.workspaces,
677
686
  closeWorkspace: (ws) => { disposeWorkspace(ws); S.workspaces.delete(ws.key); if (S.ws === ws) { S.ws = null; S.place = 'library'; render(); } },
678
687
  key: idempotencyKey,
@@ -51,7 +51,7 @@ function destinationHTML(ctx, card, dest) {
51
51
  // One tile per runtime, as Publish shows them: where this card's install goes (Codex and OpenCode are first-class).
52
52
  if (dest.targets.length) {
53
53
  const picked = (rt) => [...dest.keys].some((k) => String(k).startsWith(`${rt}:`));
54
- lines.push(html`<li class="sx-dest-tiles" role="group" aria-label="${t('skillsStudio.review.destTilesLabel')}">${['claude', 'codex', 'opencode'].map((rt) => html`<span class="sx-dest-tile${picked(rt) ? ' is-on' : ''}" title="${picked(rt) ? t('skillsStudio.review.destTileOn', { runtime: t(`skillsStudio.runtime.${rt}`) }) : t('skillsStudio.review.destTileOff', { runtime: t(`skillsStudio.runtime.${rt}`) })}">${picked(rt) ? ICONS.ok : ''}<span>${t(`skillsStudio.runtime.${rt}`)}</span></span>`)}</li>`);
54
+ lines.push(html`<li class="sx-dest-tiles" role="group" aria-label="${t('skillsStudio.review.destTilesLabel')}">${['claude', 'codex', 'opencode', 'antigravity'].map((rt) => html`<span class="sx-dest-tile${picked(rt) ? ' is-on' : ''}" title="${picked(rt) ? t('skillsStudio.review.destTileOn', { runtime: t(`skillsStudio.runtime.${rt}`) }) : t('skillsStudio.review.destTileOff', { runtime: t(`skillsStudio.runtime.${rt}`) })}">${picked(rt) ? ICONS.ok : ''}<span>${t(`skillsStudio.runtime.${rt}`)}</span></span>`)}</li>`);
55
55
  }
56
56
  return html`${lines}`;
57
57
  }
@@ -142,7 +142,7 @@ export function sameAsk(a, b) {
142
142
  return x.length > 0 && x.length === y.length && x.every((k, i) => k === y[i]);
143
143
  }
144
144
 
145
- const RUNTIME_OF_FAMILY = { claude: 'claude', codex: 'codex', opencode: 'opencode', shared: 'agents' };
145
+ const RUNTIME_OF_FAMILY = { claude: 'claude', codex: 'codex', opencode: 'opencode', antigravity: 'antigravity', shared: 'agents' };
146
146
 
147
147
  /**
148
148
  * The targets a skill can be installed into: each runtime's own skills folder for each account, the shared
@@ -175,12 +175,13 @@ export function installTargets({ roots = [], config = {}, projects = [] } = {})
175
175
  for (const runtime of ['claude', 'codex', 'opencode']) {
176
176
  const target = { runtime, scope: 'project', projectPath: p.path };
177
177
  const sub = runtime === 'claude' ? '.claude/skills' : runtime === 'codex' ? '.agents/skills' : '.opencode/skills';
178
- // Who reads a project's folders (lib/skills/roots.js): .claude/skills and .agents/skills are read by OpenCode too.
179
- const readers = runtime === 'opencode' ? ['opencode'] : [runtime, 'opencode'];
178
+ // Who reads a project's folders (lib/skills/roots.js): .claude/skills and .agents/skills are read by OpenCode too,
179
+ // .agents/skills by Antigravity as well (it has no project folder of its own).
180
+ const readers = runtime === 'opencode' ? ['opencode'] : runtime === 'codex' ? ['codex', 'opencode', 'antigravity'] : [runtime, 'opencode'];
180
181
  push({ key: targetKey(target), target, runtime, scope: 'project', accountId: 'default', projectPath: p.path, projectLabel: p.label || p.path, path: `${p.path.replace(/[\\/]+$/, '')}/${sub}`, status: null, policy: null, shared: false, off: false, runtimes: readers });
181
182
  }
182
183
  }
183
- const order = { claude: 0, codex: 1, opencode: 2, agents: 3 };
184
+ const order = { claude: 0, codex: 1, opencode: 2, antigravity: 3, agents: 4 };
184
185
  return out.sort((a, b) => (a.scope === b.scope ? 0 : a.scope === 'user' ? -1 : 1) || (a.projectLabel || '').localeCompare(b.projectLabel || '') || (order[a.runtime] - order[b.runtime]) || a.accountId.localeCompare(b.accountId));
185
186
  }
186
187
 
@@ -221,11 +222,11 @@ export function defaultTargets(targets, { skill = null, row = null, assistants =
221
222
  const there = targets.filter((t) => !t.off && ['installed', 'outdated', 'present'].includes(targetState(t, { skill, row }).state));
222
223
  if (there.length) return new Set(there.map((t) => t.key));
223
224
  const own = (runtime) => targets.find((t) => t.runtime === runtime && t.scope === 'user' && t.accountId === 'default' && !t.off) || null;
224
- const wanted = Array.isArray(assistants) && assistants.length ? assistants.filter((r) => ['claude', 'codex', 'opencode'].includes(r)) : ['claude'];
225
+ const wanted = Array.isArray(assistants) && assistants.length ? assistants.filter((r) => ['claude', 'codex', 'opencode', 'antigravity'].includes(r)) : ['claude'];
225
226
  // Every assistant gets its own folder: a terminal OpenCode reads Claude Code's folder, but SynaBun's own OpenCode
226
227
  // serves (the panel, an Assistant brain) start with OPENCODE_DISABLE_CLAUDE_CODE=1 and never see it (checked with
227
228
  // opencode 2.1.4), so an OpenCode among the assistants is ticked for a real copy.
228
- const picked = ['claude', 'codex', 'opencode'].filter((runtime) => wanted.includes(runtime)).map(own).filter(Boolean);
229
+ const picked = ['claude', 'codex', 'opencode', 'antigravity'].filter((runtime) => wanted.includes(runtime)).map(own).filter(Boolean);
229
230
  if (!picked.length) { const claude = own('claude'); return new Set(claude ? [claude.key] : []); }
230
231
  return new Set(picked.map((t) => t.key));
231
232
  }
@@ -13,7 +13,8 @@
13
13
  import { studioErrorText } from './sx-errors.js';
14
14
  import { html, setHTML, ICONS } from './sx-dom.js';
15
15
  import { loadWorkspace, dirtyTabs, saveVersion } from './sx-docs.js';
16
- import { healthCapsule, sourceLabel, statusPill } from './sx-library-view.js';
16
+ import { sourceLabel, statusPills, deleteKind, bulkDelete } from './sx-library-view.js';
17
+ import { openFixPanel } from './sx-fixes-view.js';
17
18
  import { installStatus } from './sx-install.js';
18
19
  import { reviewTargets, installFlow, flowWords, installBusy, acquireInstall, alsoInstall, alsoInstallNow } from './sx-install-flow.js';
19
20
  import { changedFiles } from './sx-diff.js';
@@ -117,7 +118,7 @@ function bannersHTML(ctx, ws) {
117
118
  const also = ['installed', 'outdated'].includes(status?.code) ? alsoInstall(ctx, { skill: ws.skill || row.skill, row }) : { targets: [] };
118
119
  if (also.targets.length) {
119
120
  const busy = installBusy((ws.skill || row.skill)?.skillId);
120
- const where = [...new Set(status.where.map((w) => t(`skillsStudio.runtime.${['claude', 'codex', 'opencode', 'agents'].includes(w.runtime) ? w.runtime : 'claude'}`)))].join(' · ');
121
+ const where = [...new Set(status.where.map((w) => t(`skillsStudio.runtime.${['claude', 'codex', 'opencode', 'antigravity', 'agents'].includes(w.runtime) ? w.runtime : 'claude'}`)))].join(' · ');
121
122
  out.push(banner('info', ICONS.publish, t('skillsStudio.ws.banner.alsoInstall', { where, targets: also.words }),
122
123
  html`<button type="button" class="sx-btn sx-btn--primary" data-ws="also-install" id="sx-ws-also-install"${busy ? ' disabled' : ''} aria-disabled="${busy ? 'true' : 'false'}">${ICONS.publish}<span>${busy ? t('skillsStudio.review.installing') : t('skillsStudio.ws.alsoInstallFor', { targets: also.words })}</span></button>`));
123
124
  }
@@ -163,7 +164,6 @@ function headHTML(ctx, ws) {
163
164
  // A library skill shows one status (it opens Publish) and its health only when something is wrong; the rest shows
164
165
  // where it comes from and its health.
165
166
  const status = installStatus({ skill: ws.skill || row.skill, row });
166
- const worst = row.health[0] || { sev: 'ok' };
167
167
  return html`<div class="sx-ws-head">
168
168
  <nav class="sx-crumb" aria-label="${t('skillsStudio.ws.breadcrumb')}">
169
169
  <button type="button" class="sx-link" data-ws="back">${ICONS.back}<span>${t('skillsStudio.rail.library')}</span></button>
@@ -172,8 +172,8 @@ function headHTML(ctx, ws) {
172
172
  <div class="sx-ws-title">
173
173
  <h3 class="sx-ws-name" id="sx-ws-name" tabindex="-1">${ws.label}</h3>
174
174
  <span class="sx-tag">${kind}</span>
175
- ${status ? statusPill(status, ctx, { button: s.canPublish ? 'go-publish' : null, id: 'sx-ws-status' }) : html`<span class="sx-tag" title="${src.detail}">${src.label}${src.detail ? html` · ${src.detail}` : ''}</span>`}
176
- ${!status || worst.sev === 'err' || worst.sev === 'warn' ? healthCapsule(row, t) : ''}
175
+ ${status ? '' : html`<span class="sx-tag" title="${src.detail}">${src.label}${src.detail ? html` · ${src.detail}` : ''}</span>`}
176
+ ${statusPills(row, ctx, { status, button: s.canPublish ? 'go-publish' : null, id: 'sx-ws-status', fix: 'workspace' })}
177
177
  <span class="sx-spacer"></span>
178
178
  ${s.editable ? html`<button type="button" class="sx-btn" data-ws="save" id="sx-save"${dirty ? '' : ' disabled'} aria-keyshortcuts="${ctx.mac ? 'Meta+S' : 'Control+S'}">${t('skillsStudio.ws.save')} <kbd>${ctx.mac ? '⌘S' : 'Ctrl+S'}</kbd></button>` : ''}
179
179
  <button type="button" class="sx-icon-btn" data-ws="more" aria-haspopup="menu" aria-expanded="false" aria-label="${t('skillsStudio.ws.more')}" title="${t('skillsStudio.ws.more')}">${ICONS.more}</button>
@@ -292,6 +292,7 @@ async function onAction(act, btn, ctx, ws) {
292
292
  case 'retry': ws.error = null; ws.loaded = false; ctx.render(); return;
293
293
  case 'save': saveActive(ctx, ws); return;
294
294
  case 'go-publish': switchTab(ctx, ws, 'publish'); return;
295
+ case 'fix-issues': if (ws.row) openFixPanel(ctx, ws.row, { deleteKind, bulkDelete }); return;
295
296
  case 'more': openMore(btn, ctx, ws); return;
296
297
  case 'customize': customizeCopy(ctx, ws); return;
297
298
  case 'adopt': adoptNow(ctx, ws); return;
@@ -46,7 +46,7 @@ export const state = {
46
46
  linkedSessionIds: new Set(), // session IDs currently in an active link
47
47
 
48
48
  // ── Side panel routing ──
49
- lastActivePanel: 'claude', // 'claude' | 'codex' | 'opencode' | 'assistant' — used by whiteboard "Send to Panel"
49
+ lastActivePanel: 'claude', // 'claude' | 'codex' | 'opencode' | 'antigravity' | 'assistant' — used by whiteboard "Send to Panel"
50
50
  };
51
51
 
52
52
 
@@ -0,0 +1,47 @@
1
+ // SynaBun — when storage.js tries a refused flush again. DOM-free (unit-tested under node --test).
2
+ //
3
+ // A failure that may pass by itself (network, 5xx, 408, 429) waits base·2^(attempt-1), capped, with full jitter in
4
+ // the upper half. A 401 / 403 is the server saying this client is not allowed: nothing is retried on a timer until
5
+ // something changes (a new write, the window regaining focus, the network coming back). Unsent values stay queued.
6
+
7
+ export const RETRY_BASE_MS = 1000;
8
+ export const RETRY_CAP_MS = 60_000;
9
+
10
+ /** Whether an HTTP status (or null for a network error) is a refusal that a timer will not fix. */
11
+ export function isAuthRefusal(status) {
12
+ return status === 401 || status === 403;
13
+ }
14
+
15
+ /** The wait before retry number `attempt` (1-based): between half and all of min(cap, base·2^(attempt-1)). */
16
+ export function retryDelay(attempt, { base = RETRY_BASE_MS, cap = RETRY_CAP_MS, random = Math.random } = {}) {
17
+ const n = Math.max(1, Math.floor(Number(attempt) || 1));
18
+ const ceiling = Math.min(cap, base * 2 ** Math.min(n - 1, 30));
19
+ const r = Math.min(1, Math.max(0, Number(random()) || 0));
20
+ return Math.round(ceiling / 2 + r * (ceiling / 2));
21
+ }
22
+
23
+ /**
24
+ * The retry state of one flusher. `fail(status)` → { blocked, delay } (delay null when blocked);
25
+ * `ok()` resets; `poke()` (a write or a reconnect) lifts a block and gives the wait the next try still owes.
26
+ */
27
+ export function createRetryState({ base = RETRY_BASE_MS, cap = RETRY_CAP_MS, random = Math.random, now = () => Date.now() } = {}) {
28
+ let attempt = 0;
29
+ let blocked = false;
30
+ let notBefore = 0;
31
+ return {
32
+ get attempt() { return attempt; },
33
+ get blocked() { return blocked; },
34
+ fail(status) {
35
+ attempt++;
36
+ const delay = retryDelay(attempt, { base, cap, random });
37
+ notBefore = now() + delay;
38
+ blocked = isAuthRefusal(status);
39
+ return { blocked, delay: blocked ? null : delay };
40
+ },
41
+ ok() { attempt = 0; blocked = false; notBefore = 0; },
42
+ /** How long a flush asked for now must still wait (0 when none is owed). Lifts an auth block. */
43
+ poke() { blocked = false; return Math.max(0, notBefore - now()); },
44
+ /** How long a flush must still wait, without lifting a block (Infinity while blocked). */
45
+ wait() { return blocked ? Infinity : Math.max(0, notBefore - now()); },
46
+ };
47
+ }
@@ -1,4 +1,5 @@
1
1
  import { hostedSidepanelId, sidepanelHost } from './ui-sidepanel-runtime.js';
2
+ import { createRetryState } from './storage-retry.js';
2
3
  // ═══════════════════════════════════════════
3
4
  // STORAGE — Server-synced persistent state
4
5
  // ═══════════════════════════════════════════
@@ -19,6 +20,8 @@ let _dirty = {};
19
20
  let _flushTimer = null;
20
21
  let _hydrated = false;
21
22
  const DEBOUNCE_MS = 500;
23
+ // A refused flush is tried again with backoff (storage-retry.js); a 401 / 403 waits for a write or a reconnect.
24
+ const _retry = createRetryState();
22
25
  // Keys that migrated to the per-entry blob store (see blob-store.js and the
23
26
  // server's FAT_KEY_ROUTES). Excluded from hydration so they can never re-enter
24
27
  // the whole-map PATCH cycle.
@@ -129,10 +132,20 @@ export const storage = (hostedSidepanelId && sidepanelHost()?.storageFor(hostedS
129
132
 
130
133
  // ── Debounced flush to server ────────────────────────
131
134
 
132
- function _scheduleDebouncedFlush() {
135
+ /** `fromWrite`: a new write or a reconnect, which lifts a 401 / 403 block (the backoff still applies). */
136
+ function _scheduleDebouncedFlush(fromWrite = true) {
133
137
  if (SHOWCASE_MODE) { _dirty = {}; return; }
138
+ const owed = fromWrite ? _retry.poke() : _retry.wait();
139
+ if (!Number.isFinite(owed)) return; // blocked: values stay in _dirty until something changes
134
140
  if (_flushTimer) clearTimeout(_flushTimer);
135
- _flushTimer = setTimeout(_flushToServer, DEBOUNCE_MS);
141
+ _flushTimer = setTimeout(_flushToServer, Math.max(DEBOUNCE_MS, owed));
142
+ }
143
+
144
+ if (!SHOWCASE_MODE) {
145
+ const _reconnected = () => { if (Object.keys(_dirty).length) _scheduleDebouncedFlush(true); };
146
+ window.addEventListener('online', _reconnected);
147
+ window.addEventListener('focus', _reconnected);
148
+ window.addEventListener('synabun:auth-changed', _reconnected);
136
149
  }
137
150
 
138
151
  async function _flushToServer() {
@@ -150,20 +163,28 @@ async function _flushToServer() {
150
163
  _dirty = {};
151
164
  _flushTimer = null;
152
165
  if (Object.keys(payload).length === 0) return;
166
+ let status = null;
153
167
  try {
154
168
  const res = await fetch('/api/ui-state', {
155
169
  method: 'PATCH',
156
170
  headers: { 'Content-Type': 'application/json' },
157
171
  body: JSON.stringify(payload),
158
172
  });
173
+ status = res.status;
159
174
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
175
+ _retry.ok();
160
176
  } catch (err) {
161
- console.warn('[storage] Flush failed:', err.message, '— will retry');
162
177
  // Re-queue failed keys (don't overwrite newer dirty values)
163
178
  for (const [k, v] of Object.entries(payload)) {
164
179
  if (!(k in _dirty)) _dirty[k] = v;
165
180
  }
166
- _scheduleDebouncedFlush();
181
+ const { blocked, delay } = _retry.fail(status);
182
+ if (blocked) {
183
+ if (_retry.attempt === 1 || _retry.attempt % 20 === 0) console.warn('[storage] Flush refused:', err.message, '— kept; will try again on the next change or reconnect');
184
+ } else {
185
+ if (_retry.attempt <= 3 || _retry.attempt % 20 === 0) console.warn('[storage] Flush failed:', err.message, `— will retry in ${Math.round(delay / 1000)} s`);
186
+ _scheduleDebouncedFlush(false);
187
+ }
167
188
  }
168
189
  }
169
190