synabun 2.0.10 → 2.2.0

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 (656) hide show
  1. package/CHANGELOG.md +207 -0
  2. package/README.md +25 -2
  3. package/THIRD-PARTY-LICENSES.md +22 -0
  4. package/bin/synabun.js +71 -24
  5. package/docs/whatsapp.md +13 -13
  6. package/hooks/claude-code/pre-websearch.mjs +12 -1
  7. package/hooks/claude-code/shared.mjs +5 -0
  8. package/hooks/claude-code/stop.mjs +1 -1
  9. package/lib/claude-hooks.js +172 -18
  10. package/lib/client-config-repair.js +30 -17
  11. package/lib/desktop-app.js +7 -1
  12. package/lib/external-tools.js +7 -6
  13. package/lib/first-launch-protection.js +57 -12
  14. package/lib/mcp-dist.js +176 -0
  15. package/lib/release-npm-ci.mjs +138 -0
  16. package/lib/release-publish.mjs +91 -0
  17. package/lib/release-scan.mjs +50 -0
  18. package/lib/server-crash-record.js +123 -0
  19. package/lib/server-exit.js +318 -0
  20. package/lib/server-supervisor.js +508 -0
  21. package/lib/start-launcher.js +37 -4
  22. package/lib/tools-cli.js +45 -0
  23. package/lib/update-snapshot.js +302 -0
  24. package/lib/virustotal.mjs +203 -0
  25. package/lib/windows-crash-event.js +269 -0
  26. package/mcp-server/dist/http.d.ts +20 -1
  27. package/mcp-server/dist/http.d.ts.map +1 -1
  28. package/mcp-server/dist/http.js +59 -12
  29. package/mcp-server/dist/http.js.map +1 -1
  30. package/mcp-server/dist/index.d.ts +33 -27
  31. package/mcp-server/dist/index.d.ts.map +1 -1
  32. package/mcp-server/dist/index.js +128 -143
  33. package/mcp-server/dist/index.js.map +1 -1
  34. package/mcp-server/dist/preload.js +1 -1
  35. package/mcp-server/dist/preload.js.map +1 -1
  36. package/mcp-server/dist/services/budget-judgments.d.ts +1 -1
  37. package/mcp-server/dist/services/budget-judgments.d.ts.map +1 -1
  38. package/mcp-server/dist/services/budget-judgments.js +5 -3
  39. package/mcp-server/dist/services/budget-judgments.js.map +1 -1
  40. package/mcp-server/dist/services/codex-config-heal.d.ts +6 -2
  41. package/mcp-server/dist/services/codex-config-heal.d.ts.map +1 -1
  42. package/mcp-server/dist/services/codex-config-heal.js +81 -28
  43. package/mcp-server/dist/services/codex-config-heal.js.map +1 -1
  44. package/mcp-server/dist/services/identity.d.ts +2 -2
  45. package/mcp-server/dist/services/identity.js +2 -2
  46. package/mcp-server/dist/services/memory-judge-backfill.d.ts +6 -0
  47. package/mcp-server/dist/services/memory-judge-backfill.d.ts.map +1 -1
  48. package/mcp-server/dist/services/memory-judge-backfill.js +7 -1
  49. package/mcp-server/dist/services/memory-judge-backfill.js.map +1 -1
  50. package/mcp-server/dist/services/memory-sessions.d.ts +1 -1
  51. package/mcp-server/dist/services/memory-sessions.d.ts.map +1 -1
  52. package/mcp-server/dist/services/memory-sessions.js +28 -2
  53. package/mcp-server/dist/services/memory-sessions.js.map +1 -1
  54. package/mcp-server/dist/services/neural-interface.d.ts +18 -11
  55. package/mcp-server/dist/services/neural-interface.d.ts.map +1 -1
  56. package/mcp-server/dist/services/neural-interface.js +67 -11
  57. package/mcp-server/dist/services/neural-interface.js.map +1 -1
  58. package/mcp-server/dist/services/server-instructions.d.ts +7 -2
  59. package/mcp-server/dist/services/server-instructions.d.ts.map +1 -1
  60. package/mcp-server/dist/services/server-instructions.js +22 -63
  61. package/mcp-server/dist/services/server-instructions.js.map +1 -1
  62. package/mcp-server/dist/services/tool-catalog.d.ts +120 -0
  63. package/mcp-server/dist/services/tool-catalog.d.ts.map +1 -0
  64. package/mcp-server/dist/services/tool-catalog.js +339 -0
  65. package/mcp-server/dist/services/tool-catalog.js.map +1 -0
  66. package/mcp-server/dist/services/tool-list-refresh-barrier.d.ts +12 -0
  67. package/mcp-server/dist/services/tool-list-refresh-barrier.d.ts.map +1 -0
  68. package/mcp-server/dist/services/tool-list-refresh-barrier.js +40 -0
  69. package/mcp-server/dist/services/tool-list-refresh-barrier.js.map +1 -0
  70. package/mcp-server/dist/services/tool-list.d.ts +108 -0
  71. package/mcp-server/dist/services/tool-list.d.ts.map +1 -0
  72. package/mcp-server/dist/services/tool-list.js +334 -0
  73. package/mcp-server/dist/services/tool-list.js.map +1 -0
  74. package/mcp-server/dist/services/tool-runtime.d.ts +161 -0
  75. package/mcp-server/dist/services/tool-runtime.d.ts.map +1 -0
  76. package/mcp-server/dist/services/tool-runtime.js +400 -0
  77. package/mcp-server/dist/services/tool-runtime.js.map +1 -0
  78. package/mcp-server/dist/services/typesafe-config.js +1 -1
  79. package/mcp-server/dist/services/typesafe-config.js.map +1 -1
  80. package/mcp-server/dist/services/whiteboard-geometry.d.ts +26 -2
  81. package/mcp-server/dist/services/whiteboard-geometry.d.ts.map +1 -1
  82. package/mcp-server/dist/services/whiteboard-geometry.js +91 -8
  83. package/mcp-server/dist/services/whiteboard-geometry.js.map +1 -1
  84. package/mcp-server/dist/tools/agents.d.ts +14 -14
  85. package/mcp-server/dist/tools/agents.d.ts.map +1 -1
  86. package/mcp-server/dist/tools/agents.js +29 -19
  87. package/mcp-server/dist/tools/agents.js.map +1 -1
  88. package/mcp-server/dist/tools/browser-batch.d.ts +3 -3
  89. package/mcp-server/dist/tools/choice.d.ts +6 -0
  90. package/mcp-server/dist/tools/choice.d.ts.map +1 -1
  91. package/mcp-server/dist/tools/choice.js +18 -13
  92. package/mcp-server/dist/tools/choice.js.map +1 -1
  93. package/mcp-server/dist/tools/git-tools.js +2 -2
  94. package/mcp-server/dist/tools/git-tools.js.map +1 -1
  95. package/mcp-server/dist/tools/image-tools.d.ts +1 -1
  96. package/mcp-server/dist/tools/image-tools.js +1 -1
  97. package/mcp-server/dist/tools/morelogin.d.ts.map +1 -1
  98. package/mcp-server/dist/tools/morelogin.js +9 -6
  99. package/mcp-server/dist/tools/morelogin.js.map +1 -1
  100. package/mcp-server/dist/tools/skill-tools.d.ts +505 -0
  101. package/mcp-server/dist/tools/skill-tools.d.ts.map +1 -0
  102. package/mcp-server/dist/tools/skill-tools.js +642 -0
  103. package/mcp-server/dist/tools/skill-tools.js.map +1 -0
  104. package/mcp-server/dist/tools/skill.d.ts +7 -0
  105. package/mcp-server/dist/tools/skill.d.ts.map +1 -0
  106. package/mcp-server/dist/tools/skill.js +11 -0
  107. package/mcp-server/dist/tools/skill.js.map +1 -0
  108. package/mcp-server/dist/tools/tool-groups.d.ts +68 -0
  109. package/mcp-server/dist/tools/tool-groups.d.ts.map +1 -0
  110. package/mcp-server/dist/tools/tool-groups.js +147 -0
  111. package/mcp-server/dist/tools/tool-groups.js.map +1 -0
  112. package/mcp-server/dist/tools/whiteboard-format.d.ts.map +1 -1
  113. package/mcp-server/dist/tools/whiteboard-format.js +7 -4
  114. package/mcp-server/dist/tools/whiteboard-format.js.map +1 -1
  115. package/mcp-server/dist/tools/whiteboard-tools.d.ts +64 -13
  116. package/mcp-server/dist/tools/whiteboard-tools.d.ts.map +1 -1
  117. package/mcp-server/dist/tools/whiteboard-tools.js +30 -5
  118. package/mcp-server/dist/tools/whiteboard-tools.js.map +1 -1
  119. package/mcp-server/package-lock.json +2 -2
  120. package/mcp-server/package.json +1 -1
  121. package/mcp-server/src/http.ts +75 -12
  122. package/mcp-server/src/index.ts +147 -154
  123. package/mcp-server/src/preload.ts +12 -12
  124. package/mcp-server/src/services/budget-judgments.ts +5 -3
  125. package/mcp-server/src/services/codex-config-heal.ts +80 -29
  126. package/mcp-server/src/services/identity.ts +2 -2
  127. package/mcp-server/src/services/memory-judge-backfill.ts +7 -1
  128. package/mcp-server/src/services/memory-sessions.ts +24 -3
  129. package/mcp-server/src/services/neural-interface.ts +81 -21
  130. package/mcp-server/src/services/server-instructions.ts +29 -64
  131. package/mcp-server/src/services/tool-catalog.ts +403 -0
  132. package/mcp-server/src/services/tool-list-refresh-barrier.ts +33 -0
  133. package/mcp-server/src/services/tool-list.ts +410 -0
  134. package/mcp-server/src/services/tool-runtime.ts +457 -0
  135. package/mcp-server/src/services/typesafe-config.ts +1 -1
  136. package/mcp-server/src/services/whiteboard-geometry.ts +94 -9
  137. package/mcp-server/src/tools/agents.ts +29 -20
  138. package/mcp-server/src/tools/choice.ts +17 -12
  139. package/mcp-server/src/tools/git-tools.ts +2 -2
  140. package/mcp-server/src/tools/image-tools.ts +1 -1
  141. package/mcp-server/src/tools/morelogin.ts +6 -4
  142. package/mcp-server/src/tools/skill-tools.ts +592 -0
  143. package/mcp-server/src/tools/skill.ts +13 -0
  144. package/mcp-server/src/tools/tool-groups.ts +164 -0
  145. package/mcp-server/src/tools/whiteboard-format.ts +9 -3
  146. package/mcp-server/src/tools/whiteboard-tools.ts +39 -5
  147. package/neural-interface/i18n/en.json +3953 -228
  148. package/neural-interface/i18n/pt-BR.json +3901 -179
  149. package/neural-interface/lib/agent-sandbox-elevation.js +6 -0
  150. package/neural-interface/lib/antigravity/acp-client-host.js +554 -0
  151. package/neural-interface/lib/antigravity/acp-connection.js +299 -0
  152. package/neural-interface/lib/antigravity/bypass.js +100 -0
  153. package/neural-interface/lib/antigravity/catalog.js +273 -0
  154. package/neural-interface/lib/antigravity/home.js +260 -0
  155. package/neural-interface/lib/antigravity/migrate-gemini.js +139 -0
  156. package/neural-interface/lib/antigravity/panel-bridge.js +1058 -0
  157. package/neural-interface/lib/antigravity/policy-hook.mjs +78 -0
  158. package/neural-interface/lib/antigravity/policy.js +74 -0
  159. package/neural-interface/lib/antigravity/tool-call.js +178 -0
  160. package/neural-interface/lib/antigravity/transcripts.js +252 -0
  161. package/neural-interface/lib/antigravity/usage.js +43 -0
  162. package/neural-interface/lib/assistant-api.js +128 -28
  163. package/neural-interface/lib/assistant-brains/antigravity.js +423 -0
  164. package/neural-interface/lib/assistant-brains/claude.js +4 -3
  165. package/neural-interface/lib/assistant-brains/codex-browser-policy-hook.mjs +12 -2
  166. package/neural-interface/lib/assistant-brains/codex-route-gate-hook.mjs +32 -9
  167. package/neural-interface/lib/assistant-brains/codex.js +9 -5
  168. package/neural-interface/lib/assistant-brains/opencode.js +120 -54
  169. package/neural-interface/lib/assistant-brains/opencode2-browser-policy/index.js +28 -13
  170. package/neural-interface/lib/assistant-brains/opencode2-route-gate/index.js +59 -19
  171. package/neural-interface/lib/assistant-budget-planner.js +7 -3
  172. package/neural-interface/lib/assistant-budget-split.js +42 -15
  173. package/neural-interface/lib/assistant-catalog.js +81 -15
  174. package/neural-interface/lib/assistant-config.js +6 -3
  175. package/neural-interface/lib/assistant-dispatch.js +1073 -200
  176. package/neural-interface/lib/assistant-envelope.js +156 -2
  177. package/neural-interface/lib/assistant-evidence.js +80 -0
  178. package/neural-interface/lib/assistant-hidden-models.js +7 -5
  179. package/neural-interface/lib/assistant-persona.js +93 -19
  180. package/neural-interface/lib/assistant-plan-permissions.js +25 -9
  181. package/neural-interface/lib/assistant-route-gate-codex.js +3 -0
  182. package/neural-interface/lib/assistant-route-gate.js +6 -4
  183. package/neural-interface/lib/assistant-router.js +245 -38
  184. package/neural-interface/lib/assistant-runtime.js +214 -30
  185. package/neural-interface/lib/assistant-task-prompt.js +87 -21
  186. package/neural-interface/lib/assistant-tools.js +51 -0
  187. package/neural-interface/lib/assistant-usage.js +29 -0
  188. package/neural-interface/lib/backup-service.js +626 -4
  189. package/neural-interface/lib/backup-zip-reader.js +10 -5
  190. package/neural-interface/lib/backup-zip-writer.js +257 -0
  191. package/neural-interface/lib/browser-execution.js +10 -2
  192. package/neural-interface/lib/browser-guard-cdp.js +84 -0
  193. package/neural-interface/lib/browser-network-guard.js +476 -0
  194. package/neural-interface/lib/browser-policy-roots.js +64 -0
  195. package/neural-interface/lib/browser-tool-policy.js +198 -38
  196. package/neural-interface/lib/claude-agent-bridge.js +11 -2
  197. package/neural-interface/lib/claude-history-cache.js +210 -0
  198. package/neural-interface/lib/claude-hooks-status.js +103 -0
  199. package/neural-interface/lib/claude-model-catalog.js +5 -1
  200. package/neural-interface/lib/claude-temporary.js +5 -11
  201. package/neural-interface/lib/cli-install-api.js +1 -1
  202. package/neural-interface/lib/cli-install.js +163 -154
  203. package/neural-interface/lib/cli-resolver.js +13 -3
  204. package/neural-interface/lib/cli-update-planner.js +52 -41
  205. package/neural-interface/lib/cloudflared/tunnel.js +42 -2
  206. package/neural-interface/lib/codex-mcp-approvals.js +11 -0
  207. package/neural-interface/lib/codex-rollout-reader.js +52 -0
  208. package/neural-interface/lib/codex-worker-app-server.js +191 -0
  209. package/neural-interface/lib/confined-fs.js +30 -3
  210. package/neural-interface/lib/crash-notice-api.js +98 -0
  211. package/neural-interface/lib/editor-completion-opencode.js +68 -68
  212. package/neural-interface/lib/editor-git-api.js +3 -20
  213. package/neural-interface/lib/exec-loop-command.js +13 -10
  214. package/neural-interface/lib/file-explorer-api.js +74 -40
  215. package/neural-interface/lib/file-explorer-watch.js +9 -3
  216. package/neural-interface/lib/git-bash/install.js +52 -6
  217. package/neural-interface/lib/git-bash/manager.js +59 -17
  218. package/neural-interface/lib/git-bash/pins.js +12 -0
  219. package/neural-interface/lib/git-read.js +2 -2
  220. package/neural-interface/lib/git-status-runner.js +390 -0
  221. package/neural-interface/lib/git-status-watchdog.js +40 -0
  222. package/neural-interface/lib/greeting/budgets.js +1 -0
  223. package/neural-interface/lib/greeting/engine.js +2 -2
  224. package/neural-interface/lib/greeting/sources.js +10 -12
  225. package/neural-interface/lib/greeting/writer.js +1 -1
  226. package/neural-interface/lib/http-guards.js +8 -2
  227. package/neural-interface/lib/install-details.js +4 -1
  228. package/neural-interface/lib/mcp-agent-servers.js +67 -0
  229. package/neural-interface/lib/mcp-client-config.js +35 -33
  230. package/neural-interface/lib/mcp-external-servers.js +332 -0
  231. package/neural-interface/lib/mcp-installer.js +2 -1
  232. package/neural-interface/lib/{mcp-profile-refresh-coordinator.js → mcp-tools-refresh-coordinator.js} +4 -4
  233. package/neural-interface/lib/memory-session-capture.js +17 -3
  234. package/neural-interface/lib/morelogin.js +177 -25
  235. package/neural-interface/lib/native-loop-providers.js +965 -66
  236. package/neural-interface/lib/native-loop-runtime.js +290 -42
  237. package/neural-interface/lib/opencode-config.js +557 -0
  238. package/neural-interface/lib/opencode-db-schema.js +17 -34
  239. package/neural-interface/lib/opencode-history.js +48 -218
  240. package/neural-interface/lib/opencode-mcp-ready.js +108 -0
  241. package/neural-interface/lib/opencode-native-boundary.js +53 -0
  242. package/neural-interface/lib/opencode-protocol.js +249 -112
  243. package/neural-interface/lib/opencode-runtime-config.js +32 -22
  244. package/neural-interface/lib/opencode-serve-records.js +39 -21
  245. package/neural-interface/lib/opencode-v2-client.js +48 -153
  246. package/neural-interface/lib/opencode-v2-ws-requests.js +36 -210
  247. package/neural-interface/lib/opencode-worker-data.js +27 -0
  248. package/neural-interface/lib/opencode2-client.js +73 -37
  249. package/neural-interface/lib/opencode2-proxy.js +23 -63
  250. package/neural-interface/lib/opencode2-translate.js +51 -2
  251. package/neural-interface/lib/owner-authority.js +249 -0
  252. package/neural-interface/lib/page-session.js +275 -0
  253. package/neural-interface/lib/path-confine.js +12 -9
  254. package/neural-interface/lib/peer-identity.js +793 -0
  255. package/neural-interface/lib/pty-host/core.js +28 -1
  256. package/neural-interface/lib/pty-host/lifetime.js +12 -0
  257. package/neural-interface/lib/pty-host/manager.js +12 -1
  258. package/neural-interface/lib/remote-policy.js +1 -1
  259. package/neural-interface/lib/request-gate.js +20 -2
  260. package/neural-interface/lib/rulesets/api.js +7 -6
  261. package/neural-interface/lib/rulesets/fs-safe.js +1 -1
  262. package/neural-interface/lib/rulesets/installer.js +180 -36
  263. package/neural-interface/lib/rulesets/json-edit.js +1 -1
  264. package/neural-interface/lib/rulesets/render.js +1 -1
  265. package/neural-interface/lib/session-cache-builder.js +81 -31
  266. package/neural-interface/lib/session-title-generator.js +58 -36
  267. package/neural-interface/lib/skills/api-v2.js +495 -0
  268. package/neural-interface/lib/skills/bundle.js +418 -0
  269. package/neural-interface/lib/skills/catalog.js +298 -0
  270. package/neural-interface/lib/skills/creation-api.js +191 -0
  271. package/neural-interface/lib/skills/creation-brief.js +247 -0
  272. package/neural-interface/lib/skills/creation-sources.js +285 -0
  273. package/neural-interface/lib/skills/creation.js +843 -0
  274. package/neural-interface/lib/skills/deploy.js +569 -0
  275. package/neural-interface/lib/skills/digest.js +119 -0
  276. package/neural-interface/lib/skills/discover.js +111 -0
  277. package/neural-interface/lib/skills/draft-confinement.js +216 -0
  278. package/neural-interface/lib/skills/eval-confinement.js +399 -0
  279. package/neural-interface/lib/skills/eval-sandbox.js +280 -0
  280. package/neural-interface/lib/skills/eval-trace.js +135 -0
  281. package/neural-interface/lib/skills/evals-api.js +212 -0
  282. package/neural-interface/lib/skills/evals.js +1573 -0
  283. package/neural-interface/lib/skills/export.js +203 -0
  284. package/neural-interface/lib/skills/frontmatter.js +64 -0
  285. package/neural-interface/lib/skills/health.js +75 -0
  286. package/neural-interface/lib/skills/json-edit.js +165 -0
  287. package/neural-interface/lib/skills/known-bridge-digests.json +441 -0
  288. package/neural-interface/lib/skills/known-bridges.js +96 -0
  289. package/neural-interface/lib/skills/library.js +823 -0
  290. package/neural-interface/lib/skills/local-store.js +21 -0
  291. package/neural-interface/lib/skills/migration.js +433 -0
  292. package/neural-interface/lib/skills/operations.js +45 -0
  293. package/neural-interface/lib/skills/ownership.js +427 -0
  294. package/neural-interface/lib/skills/packages.js +88 -0
  295. package/neural-interface/lib/skills/reviews.js +212 -0
  296. package/neural-interface/lib/skills/revision.js +80 -0
  297. package/neural-interface/lib/skills/roots.js +269 -0
  298. package/neural-interface/lib/skills/runtime-checks.js +358 -0
  299. package/neural-interface/lib/skills/scan-assets.js +252 -0
  300. package/neural-interface/lib/skills/scan-context.js +26 -0
  301. package/neural-interface/lib/skills/scan-shell.js +324 -0
  302. package/neural-interface/lib/skills/scan.js +364 -0
  303. package/neural-interface/lib/skills/scanners.js +54 -0
  304. package/neural-interface/lib/skills/service.js +530 -0
  305. package/neural-interface/lib/skills/telemetry.js +119 -0
  306. package/neural-interface/lib/sqlite-integrity-worker.js +22 -0
  307. package/neural-interface/lib/synabun-version.js +1 -1
  308. package/neural-interface/lib/system-activity.js +1 -1
  309. package/neural-interface/lib/tool-lists.js +124 -0
  310. package/neural-interface/lib/whatsapp/brain.js +7 -3
  311. package/neural-interface/lib/whatsapp/bridge.js +6 -3
  312. package/neural-interface/lib/whatsapp/cards.js +22 -9
  313. package/neural-interface/lib/whatsapp/config.js +1 -1
  314. package/neural-interface/lib/worker-confinement.js +1268 -0
  315. package/neural-interface/lib/worker-guard-setting.js +56 -0
  316. package/neural-interface/lib/worker-mcp-channel.js +175 -0
  317. package/neural-interface/lib/worker-question.js +74 -0
  318. package/neural-interface/lib/worker-relay-ports.js +227 -0
  319. package/neural-interface/lib/worker-resume.js +238 -0
  320. package/neural-interface/lib/worker-sandbox.js +329 -0
  321. package/neural-interface/package-lock.json +34 -12
  322. package/neural-interface/package.json +3 -2
  323. package/neural-interface/public/index.html +3 -1
  324. package/neural-interface/public/index2d.html +19 -11
  325. package/neural-interface/public/onboarding.html +3 -3
  326. package/neural-interface/public/shared/account-menu.js +8 -5
  327. package/neural-interface/public/shared/agent-runtime-options.js +125 -31
  328. package/neural-interface/public/shared/agy/agy-bypass.js +47 -0
  329. package/neural-interface/public/shared/agy/agy-client.js +266 -0
  330. package/neural-interface/public/shared/agy/agy-commands.js +120 -0
  331. package/neural-interface/public/shared/agy/agy-copy.js +326 -0
  332. package/neural-interface/public/shared/agy/agy-diff.js +161 -0
  333. package/neural-interface/public/shared/agy/agy-events.js +469 -0
  334. package/neural-interface/public/shared/agy/agy-greeting.js +26 -0
  335. package/neural-interface/public/shared/agy/agy-options.js +232 -0
  336. package/neural-interface/public/shared/agy/agy-panel.js +2543 -0
  337. package/neural-interface/public/shared/agy/agy-render.js +392 -0
  338. package/neural-interface/public/shared/agy/agy-setup.js +149 -0
  339. package/neural-interface/public/shared/agy/agy-styles.js +604 -0
  340. package/neural-interface/public/shared/agy/agy-tabs-logic.js +181 -0
  341. package/neural-interface/public/shared/api.js +1527 -1336
  342. package/neural-interface/public/shared/app-frame.js +192 -0
  343. package/neural-interface/public/shared/assistant/asst-agents-dock.js +36 -6
  344. package/neural-interface/public/shared/assistant/asst-api.js +1 -1
  345. package/neural-interface/public/shared/assistant/asst-ask-styles.js +270 -0
  346. package/neural-interface/public/shared/assistant/asst-ask-tail.js +118 -0
  347. package/neural-interface/public/shared/assistant/asst-ask.js +397 -0
  348. package/neural-interface/public/shared/assistant/asst-brain-picker.js +105 -44
  349. package/neural-interface/public/shared/assistant/asst-budget.js +5 -4
  350. package/neural-interface/public/shared/assistant/asst-control.js +205 -26
  351. package/neural-interface/public/shared/assistant/asst-followups-card.js +383 -0
  352. package/neural-interface/public/shared/assistant/asst-followups.js +417 -0
  353. package/neural-interface/public/shared/assistant/asst-hosts.js +16 -7
  354. package/neural-interface/public/shared/assistant/asst-limit-dock.js +45 -0
  355. package/neural-interface/public/shared/assistant/asst-limit-wait.js +327 -0
  356. package/neural-interface/public/shared/assistant/asst-limit.js +168 -0
  357. package/neural-interface/public/shared/assistant/asst-mascot.js +49 -12
  358. package/neural-interface/public/shared/assistant/asst-menu.js +7 -6
  359. package/neural-interface/public/shared/assistant/asst-panel.js +151 -19
  360. package/neural-interface/public/shared/assistant/asst-rack-styles.js +66 -49
  361. package/neural-interface/public/shared/assistant/asst-render.js +445 -82
  362. package/neural-interface/public/shared/assistant/asst-route.js +304 -53
  363. package/neural-interface/public/shared/assistant/asst-routes-editor.js +9 -8
  364. package/neural-interface/public/shared/assistant/asst-scenes.js +56 -1
  365. package/neural-interface/public/shared/assistant/asst-sidepanel.js +117 -10
  366. package/neural-interface/public/shared/assistant/asst-slash.js +17 -9
  367. package/neural-interface/public/shared/assistant/asst-state.js +87 -24
  368. package/neural-interface/public/shared/assistant/asst-stop.js +127 -0
  369. package/neural-interface/public/shared/assistant/asst-styles.js +93 -21
  370. package/neural-interface/public/shared/assistant/asst-tool-kinds.js +190 -23
  371. package/neural-interface/public/shared/assistant/asst-usage.js +2 -1
  372. package/neural-interface/public/shared/automation-context.js +5 -2
  373. package/neural-interface/public/shared/cc-dropdown.js +89 -19
  374. package/neural-interface/public/shared/cdx/cdx-context-menu.js +10 -9
  375. package/neural-interface/public/shared/cdx/cdx-copy.js +16 -0
  376. package/neural-interface/public/shared/cdx/cdx-output-model.js +22 -1
  377. package/neural-interface/public/shared/cdx/cdx-output.js +34 -10
  378. package/neural-interface/public/shared/cdx/cdx-panel.js +59 -151
  379. package/neural-interface/public/shared/cdx/cdx-preview.js +12 -0
  380. package/neural-interface/public/shared/cdx/cdx-protocol.js +39 -11
  381. package/neural-interface/public/shared/cdx/cdx-render.js +57 -38
  382. package/neural-interface/public/shared/cdx/cdx-requests.js +14 -4
  383. package/neural-interface/public/shared/cdx/cdx-skill-hints.js +54 -0
  384. package/neural-interface/public/shared/cdx/cdx-styles.js +15 -3
  385. package/neural-interface/public/shared/cdx/cdx-tabs.js +127 -44
  386. package/neural-interface/public/shared/cdx/cdx-ws.js +0 -1
  387. package/neural-interface/public/shared/cli-install-controller.js +218 -0
  388. package/neural-interface/public/shared/cli-install-view.js +11 -12
  389. package/neural-interface/public/shared/cli-status.js +11 -10
  390. package/neural-interface/public/shared/constants.js +5 -4
  391. package/neural-interface/public/shared/context-menu-areas.js +34 -0
  392. package/neural-interface/public/shared/context-menu-logic.js +243 -0
  393. package/neural-interface/public/shared/context-menu-model.js +367 -0
  394. package/neural-interface/public/shared/context-menu.js +372 -0
  395. package/neural-interface/public/shared/cp/cp-context-menu.js +10 -9
  396. package/neural-interface/public/shared/cp/cp-restore.js +162 -0
  397. package/neural-interface/public/shared/cp/cp-tool-cards.js +10 -3
  398. package/neural-interface/public/shared/crash-notice-view.js +168 -0
  399. package/neural-interface/public/shared/desktop-bridge.js +17 -0
  400. package/neural-interface/public/shared/desktop-titlebar-view.js +12 -0
  401. package/neural-interface/public/shared/desktop-titlebar.js +13 -4
  402. package/neural-interface/public/shared/desktop-update-view.js +21 -0
  403. package/neural-interface/public/shared/editor/editor-panel-logic.js +2 -2
  404. package/neural-interface/public/shared/explorer/fx-api.js +4 -4
  405. package/neural-interface/public/shared/explorer/fx-links.js +3 -3
  406. package/neural-interface/public/shared/explorer/fx-logic.js +1 -1
  407. package/neural-interface/public/shared/explorer/fx-main.js +48 -16
  408. package/neural-interface/public/shared/explorer/fx-tree.js +1 -0
  409. package/neural-interface/public/shared/greeting/greeting-card.js +2 -2
  410. package/neural-interface/public/shared/greeting/greeting-view.js +1 -0
  411. package/neural-interface/public/shared/html-shell.js +30 -15
  412. package/neural-interface/public/shared/i18n.js +2 -0
  413. package/neural-interface/public/shared/ocp-v2/ocp-v2-approvals.js +7 -7
  414. package/neural-interface/public/shared/ocp-v2/ocp-v2-binding.js +4 -4
  415. package/neural-interface/public/shared/ocp-v2/ocp-v2-changes-logic.js +3 -101
  416. package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +2 -0
  417. package/neural-interface/public/shared/ocp-v2/ocp-v2-compact-button.js +1 -3
  418. package/neural-interface/public/shared/ocp-v2/ocp-v2-composer-logic.js +2 -6
  419. package/neural-interface/public/shared/ocp-v2/ocp-v2-context-menu.js +11 -11
  420. package/neural-interface/public/shared/ocp-v2/ocp-v2-context-model.js +0 -3
  421. package/neural-interface/public/shared/ocp-v2/ocp-v2-events.js +8 -4
  422. package/neural-interface/public/shared/ocp-v2/ocp-v2-manager.js +7 -7
  423. package/neural-interface/public/shared/ocp-v2/ocp-v2-outcome-notifications.js +16 -0
  424. package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +74 -259
  425. package/neural-interface/public/shared/ocp-v2/ocp-v2-plan.js +1 -2
  426. package/neural-interface/public/shared/ocp-v2/ocp-v2-projectbar.js +95 -202
  427. package/neural-interface/public/shared/ocp-v2/ocp-v2-rehydrate.js +13 -4
  428. package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +13 -22
  429. package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +4 -4
  430. package/neural-interface/public/shared/ocp-v2/ocp-v2-session-actions.js +8 -61
  431. package/neural-interface/public/shared/ocp-v2/ocp-v2-sessions-logic.js +2 -91
  432. package/neural-interface/public/shared/ocp-v2/ocp-v2-slash-hints.js +16 -15
  433. package/neural-interface/public/shared/ocp-v2/ocp-v2-state.js +8 -21
  434. package/neural-interface/public/shared/ocp-v2/ocp-v2-status-logic.js +1 -99
  435. package/neural-interface/public/shared/ocp-v2/ocp-v2-status.js +5 -35
  436. package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +17 -21
  437. package/neural-interface/public/shared/ocp-v2/{ocp-v2-profile-inheritance.js → ocp-v2-tools-inheritance.js} +16 -16
  438. package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +60 -50
  439. package/neural-interface/public/shared/page-session.js +96 -0
  440. package/neural-interface/public/shared/provider-icons.js +4 -2
  441. package/neural-interface/public/shared/registry.js +5 -3
  442. package/neural-interface/public/shared/retired-2d.js +3 -0
  443. package/neural-interface/public/shared/schedules-view.js +195 -61
  444. package/neural-interface/public/shared/settings/settings-claude-hooks.js +135 -0
  445. package/neural-interface/public/shared/settings/settings-data.js +3 -1
  446. package/neural-interface/public/shared/settings/settings-ia.js +6 -6
  447. package/neural-interface/public/shared/settings/settings-opencode.js +89 -0
  448. package/neural-interface/public/shared/settings/settings-rules.js +2 -2
  449. package/neural-interface/public/shared/settings/settings-skill-folders-pane.js +265 -0
  450. package/neural-interface/public/shared/settings/settings-skill-folders.js +130 -0
  451. package/neural-interface/public/shared/settings/settings-worker-guard.js +72 -0
  452. package/neural-interface/public/shared/showcase-privacy.js +6 -1
  453. package/neural-interface/public/shared/sidepanel-session-status.js +3 -3
  454. package/neural-interface/public/shared/skills/ss-args.js +96 -0
  455. package/neural-interface/public/shared/skills/ss-creation.js +222 -0
  456. package/neural-interface/public/shared/skills/ss-dialog.js +133 -0
  457. package/neural-interface/public/shared/skills/ss-doc.js +173 -0
  458. package/neural-interface/public/shared/skills/ss-eval-adapters.js +269 -0
  459. package/neural-interface/public/shared/skills/ss-eval-grade.js +359 -0
  460. package/neural-interface/public/shared/skills/ss-eval-optimise.js +153 -0
  461. package/neural-interface/public/shared/skills/ss-eval-plan.js +315 -0
  462. package/neural-interface/public/shared/skills/ss-evals.js +417 -0
  463. package/neural-interface/public/shared/skills/ss-form.js +159 -0
  464. package/neural-interface/public/shared/skills/ss-frontmatter.js +376 -0
  465. package/neural-interface/public/shared/skills/ss-ids.js +32 -0
  466. package/neural-interface/public/shared/skills/ss-lint.js +149 -0
  467. package/neural-interface/public/shared/skills/ss-presets.js +117 -0
  468. package/neural-interface/public/shared/skills/ss-profiles.js +513 -0
  469. package/neural-interface/public/shared/skills/ss-resources.js +165 -0
  470. package/neural-interface/public/shared/skills/ss-review-view.js +190 -0
  471. package/neural-interface/public/shared/skills/ss-tabs.js +79 -0
  472. package/neural-interface/public/shared/skills/ss-unicode.js +135 -0
  473. package/neural-interface/public/shared/skills/studio/sx-actions.js +222 -0
  474. package/neural-interface/public/shared/skills/studio/sx-cm.js +101 -0
  475. package/neural-interface/public/shared/skills/studio/sx-create-ai-view.js +749 -0
  476. package/neural-interface/public/shared/skills/studio/sx-create-ai.js +237 -0
  477. package/neural-interface/public/shared/skills/studio/sx-create-view.js +254 -0
  478. package/neural-interface/public/shared/skills/studio/sx-create.js +126 -0
  479. package/neural-interface/public/shared/skills/studio/sx-diff-view.js +43 -0
  480. package/neural-interface/public/shared/skills/studio/sx-diff.js +133 -0
  481. package/neural-interface/public/shared/skills/studio/sx-docs.js +480 -0
  482. package/neural-interface/public/shared/skills/studio/sx-dom.js +230 -0
  483. package/neural-interface/public/shared/skills/studio/sx-edit-view.js +848 -0
  484. package/neural-interface/public/shared/skills/studio/sx-editor-model.js +193 -0
  485. package/neural-interface/public/shared/skills/studio/sx-errors.js +22 -0
  486. package/neural-interface/public/shared/skills/studio/sx-fixes-view.js +359 -0
  487. package/neural-interface/public/shared/skills/studio/sx-fixes.js +327 -0
  488. package/neural-interface/public/shared/skills/studio/sx-geometry.js +82 -0
  489. package/neural-interface/public/shared/skills/studio/sx-import.js +262 -0
  490. package/neural-interface/public/shared/skills/studio/sx-inspector.js +308 -0
  491. package/neural-interface/public/shared/skills/studio/sx-install-flow.js +282 -0
  492. package/neural-interface/public/shared/skills/studio/sx-install.js +313 -0
  493. package/neural-interface/public/shared/skills/studio/sx-library-view.js +748 -0
  494. package/neural-interface/public/shared/skills/studio/sx-library.js +479 -0
  495. package/neural-interface/public/shared/skills/studio/sx-main.js +690 -0
  496. package/neural-interface/public/shared/skills/studio/sx-operations-view.js +98 -0
  497. package/neural-interface/public/shared/skills/studio/sx-operations.js +11 -0
  498. package/neural-interface/public/shared/skills/studio/sx-plan.js +158 -0
  499. package/neural-interface/public/shared/skills/studio/sx-publish-view.js +621 -0
  500. package/neural-interface/public/shared/skills/studio/sx-review-view.js +369 -0
  501. package/neural-interface/public/shared/skills/studio/sx-review.js +148 -0
  502. package/neural-interface/public/shared/skills/studio/sx-scan.js +35 -0
  503. package/neural-interface/public/shared/skills/studio/sx-snippets.js +96 -0
  504. package/neural-interface/public/shared/skills/studio/sx-subject.js +249 -0
  505. package/neural-interface/public/shared/skills/studio/sx-test-view.js +994 -0
  506. package/neural-interface/public/shared/skills/studio/sx-test.js +328 -0
  507. package/neural-interface/public/shared/skills/studio/sx-versions-view.js +241 -0
  508. package/neural-interface/public/shared/skills/studio/sx-workspace-view.js +426 -0
  509. package/neural-interface/public/shared/state.js +1 -1
  510. package/neural-interface/public/shared/streaming-markdown.js +58 -0
  511. package/neural-interface/public/shared/styleguide/sg-panel.js +4 -2
  512. package/neural-interface/public/shared/styles.css +1822 -1756
  513. package/neural-interface/public/shared/synabun-mascot.js +1115 -9
  514. package/neural-interface/public/shared/text-cache.js +29 -0
  515. package/neural-interface/public/shared/tool-list-picker.js +55 -0
  516. package/neural-interface/public/shared/tool-list-storage.js +24 -0
  517. package/neural-interface/public/shared/tool-list-view.js +112 -0
  518. package/neural-interface/public/shared/topright-overflow-logic.js +54 -0
  519. package/neural-interface/public/shared/tour/tour-actor.js +268 -0
  520. package/neural-interface/public/shared/tour/tour-beats.js +336 -0
  521. package/neural-interface/public/shared/tour/tour-engine.js +884 -0
  522. package/neural-interface/public/shared/tour/tour-ink.js +390 -0
  523. package/neural-interface/public/shared/tour/tour-stage.js +440 -0
  524. package/neural-interface/public/shared/tour/tour-state.js +40 -0
  525. package/neural-interface/public/shared/tour/tour-tips.js +99 -0
  526. package/neural-interface/public/shared/tour/tour.css +144 -0
  527. package/neural-interface/public/shared/ui-antigravity-panel.js +19 -0
  528. package/neural-interface/public/shared/ui-assistant.js +2 -0
  529. package/neural-interface/public/shared/ui-automation-studio.js +111 -140
  530. package/neural-interface/public/shared/ui-claude-panel.js +376 -82
  531. package/neural-interface/public/shared/ui-cli-banner.js +23 -167
  532. package/neural-interface/public/shared/ui-command-runner.js +6 -3
  533. package/neural-interface/public/shared/ui-context-menu.js +295 -0
  534. package/neural-interface/public/shared/ui-cost-widget.js +57 -59
  535. package/neural-interface/public/shared/ui-crash-notice.js +187 -0
  536. package/neural-interface/public/shared/ui-detail.js +15 -3
  537. package/neural-interface/public/shared/ui-file-explorer.js +48 -6
  538. package/neural-interface/public/shared/ui-file-links.js +2 -2
  539. package/neural-interface/public/shared/ui-games.js +2 -2
  540. package/neural-interface/public/shared/ui-image-gallery.js +50 -47
  541. package/neural-interface/public/shared/ui-judgments.js +1 -1
  542. package/neural-interface/public/shared/ui-keybinds.js +33 -2
  543. package/neural-interface/public/shared/ui-link.js +3 -3
  544. package/neural-interface/public/shared/ui-menubar.js +10 -20
  545. package/neural-interface/public/shared/ui-native-loop-router-state.js +5 -0
  546. package/neural-interface/public/shared/ui-native-loop-router.js +11 -1
  547. package/neural-interface/public/shared/ui-navbar.js +49 -24
  548. package/neural-interface/public/shared/ui-notifications.js +1 -0
  549. package/neural-interface/public/shared/ui-panels.js +96 -65
  550. package/neural-interface/public/shared/ui-resume.js +9 -7
  551. package/neural-interface/public/shared/ui-schedules-studio.js +546 -431
  552. package/neural-interface/public/shared/ui-scroll-follow.js +16 -5
  553. package/neural-interface/public/shared/ui-sessions.js +27 -15
  554. package/neural-interface/public/shared/ui-settings.js +683 -526
  555. package/neural-interface/public/shared/ui-sidepanel-session-entry.js +9 -2
  556. package/neural-interface/public/shared/ui-sidepanel-sessions.js +4 -4
  557. package/neural-interface/public/shared/ui-sidepanel-state.js +23 -12
  558. package/neural-interface/public/shared/ui-sidepanel-tray.js +48 -7
  559. package/neural-interface/public/shared/ui-sidepanel-windows.js +18 -13
  560. package/neural-interface/public/shared/ui-skills.js +33 -2830
  561. package/neural-interface/public/shared/ui-styleguide.js +5 -2
  562. package/neural-interface/public/shared/ui-sync.js +15 -5
  563. package/neural-interface/public/shared/ui-terminal.js +270 -190
  564. package/neural-interface/public/shared/ui-tool-updates.js +6 -4
  565. package/neural-interface/public/shared/ui-tooltip.js +81 -51
  566. package/neural-interface/public/shared/ui-topright-overflow.js +233 -0
  567. package/neural-interface/public/shared/ui-trash.js +6 -4
  568. package/neural-interface/public/shared/ui-tutorial-layout.js +3 -117
  569. package/neural-interface/public/shared/ui-tutorial-preview.js +103 -0
  570. package/neural-interface/public/shared/ui-tutorial-steps.js +3 -65
  571. package/neural-interface/public/shared/ui-tutorial.css +47 -100
  572. package/neural-interface/public/shared/ui-tutorial.js +285 -509
  573. package/neural-interface/public/shared/ui-update.js +38 -2
  574. package/neural-interface/public/shared/ui-whiteboard.js +312 -59
  575. package/neural-interface/public/shared/ui-worker-end.js +5 -1
  576. package/neural-interface/public/shared/ui-workspaces.js +3 -2
  577. package/neural-interface/public/shared/view-variant.js +66 -0
  578. package/neural-interface/public/shared/whatsapp/wa-view.js +3 -3
  579. package/neural-interface/public/shared/whiteboard-list.js +172 -0
  580. package/neural-interface/public/shared/worker-end.js +10 -3
  581. package/neural-interface/public/shared/worker-questions.js +34 -0
  582. package/neural-interface/public/shared/worker-restart.js +12 -0
  583. package/neural-interface/public/sidepanel-session.html +2 -2
  584. package/neural-interface/public/variant/2d/background.js +2 -1
  585. package/neural-interface/public/variant/2d/context-menu.js +8 -2
  586. package/neural-interface/public/variant/2d/graph.js +42 -20
  587. package/neural-interface/public/variant/2d/hulls.js +14 -9
  588. package/neural-interface/public/variant/2d/lasso.js +11 -7
  589. package/neural-interface/public/variant/2d/main.js +32 -1
  590. package/neural-interface/public/variant/2d/minimap.js +11 -13
  591. package/neural-interface/public/variant/3d/main.js +69 -6
  592. package/neural-interface/public/variant/3d/map-controls.js +24 -9
  593. package/neural-interface/public/vendor/caveat/OFL.txt +93 -0
  594. package/neural-interface/public/vendor/caveat/caveat-latin-400-normal.woff2 +0 -0
  595. package/neural-interface/public/vendor/caveat/caveat-latin-700-normal.woff2 +0 -0
  596. package/neural-interface/public/vendor/caveat/caveat.css +17 -0
  597. package/neural-interface/public/vendor/caveat/manifest.json +12 -0
  598. package/neural-interface/public/vendor/yaml/LICENSE +13 -0
  599. package/neural-interface/public/vendor/yaml/manifest.json +4 -0
  600. package/neural-interface/public/vendor/yaml/yaml-2.9.1.min.js +137 -0
  601. package/neural-interface/scripts/build-vendor.mjs +32 -0
  602. package/neural-interface/server.js +4553 -2855
  603. package/neural-interface/templates/rulesets/README.md +1 -1
  604. package/neural-interface/templates/rulesets/core.md +1 -1
  605. package/neural-interface/templates/rulesets/legacy-hashes.json +23 -5
  606. package/neural-interface/templates/rulesets/manifest.json +8 -8
  607. package/npm-shrinkwrap.json +34 -12
  608. package/package.json +17 -3
  609. package/postinstall.js +9 -4
  610. package/preuninstall.js +1 -1
  611. package/setup.js +97 -322
  612. package/skills/skill-forge/SKILL.md +177 -0
  613. package/skills/skill-forge/evals/evals.json +54 -0
  614. package/skills/skill-forge/references/package.md +105 -0
  615. package/skills/skill-forge/references/review-and-recovery.md +63 -0
  616. package/skills/skill-forge/references/runtime-profiles.md +20 -0
  617. package/skills/skill-forge/references/sources-and-capabilities.md +51 -0
  618. package/skills/synabun/SKILL.md +10 -7
  619. package/skills/synabun/modules/audit.md +1 -5
  620. package/skills/synabun/modules/changelog.md +1 -5
  621. package/skills/synabun/modules/idea.md +1 -1
  622. package/skills/synabun/modules/memorize.md +6 -9
  623. package/skills/synabun/modules/schedule.md +6 -6
  624. package/mcp-server/dist/services/profiles.d.ts +0 -120
  625. package/mcp-server/dist/services/profiles.d.ts.map +0 -1
  626. package/mcp-server/dist/services/profiles.js +0 -387
  627. package/mcp-server/dist/services/profiles.js.map +0 -1
  628. package/mcp-server/dist/tools/leonardo-browser-tools.d.ts +0 -97
  629. package/mcp-server/dist/tools/leonardo-browser-tools.d.ts.map +0 -1
  630. package/mcp-server/dist/tools/leonardo-browser-tools.js +0 -228
  631. package/mcp-server/dist/tools/leonardo-browser-tools.js.map +0 -1
  632. package/mcp-server/dist/tools/leonardo.d.ts +0 -13
  633. package/mcp-server/dist/tools/leonardo.d.ts.map +0 -1
  634. package/mcp-server/dist/tools/leonardo.js +0 -21
  635. package/mcp-server/dist/tools/leonardo.js.map +0 -1
  636. package/mcp-server/dist/tools/profile.d.ts +0 -17
  637. package/mcp-server/dist/tools/profile.d.ts.map +0 -1
  638. package/mcp-server/dist/tools/profile.js +0 -114
  639. package/mcp-server/dist/tools/profile.js.map +0 -1
  640. package/mcp-server/src/services/profiles.ts +0 -428
  641. package/mcp-server/src/tools/leonardo-browser-tools.ts +0 -291
  642. package/mcp-server/src/tools/leonardo.ts +0 -30
  643. package/mcp-server/src/tools/profile.ts +0 -122
  644. package/neural-interface/lib/assistant-brains/opencode-browser-policy.js +0 -47
  645. package/neural-interface/lib/assistant-brains/opencode-route-gate.js +0 -62
  646. package/neural-interface/lib/mcp-profile-servers.js +0 -120
  647. package/neural-interface/public/shared/ui-tutorial-draw.js +0 -311
  648. package/skills/leonardo/SKILL.md +0 -168
  649. package/skills/leonardo/icon.svg +0 -1
  650. package/skills/leonardo/modules/browser-guide.md +0 -378
  651. package/skills/leonardo/modules/image-prompter.md +0 -283
  652. package/skills/leonardo/modules/model-advisor.md +0 -100
  653. package/skills/leonardo/modules/prompt-library.md +0 -110
  654. package/skills/leonardo/modules/style-guide.md +0 -186
  655. package/skills/leonardo/modules/video-prompter.md +0 -317
  656. /package/neural-interface/templates/rulesets/{gemini.md → antigravity.md} +0 -0
@@ -1,3 +1,5 @@
1
+ import { migrateStoredTools } from './tool-list-storage.js';
2
+ import { configureToolPickerTranslation, toolPickerHTML, readToolPicker, updateToolPicker, taskToolHost as strictToolHost, toolWord } from './tool-list-picker.js';
1
3
  // SynaBun Neural Interface — the Schedules window.
2
4
  // A rail of views (Up next, All, Needs attention, Paused, Timers, Activity) and of groups, one
3
5
  // list of schedules with their details on demand, and the editors in place of the list. What a
@@ -5,10 +7,13 @@
5
7
 
6
8
  import { emit, on } from './state.js';
7
9
  import { storage } from './storage.js';
10
+ import { frameRect, clampBox } from './app-frame.js';
11
+ import { t, tp, getLocale } from './i18n.js';
8
12
  import { KEYS } from './constants.js';
9
13
  import { isGuest, hasPermission, showGuestToast } from './ui-sync.js';
10
14
  import { registerLaunchPrefetch } from './launch-prefetch.js';
11
15
  import { sendToPanel } from './ui-claude-panel.js';
16
+ import { openAntigravityWithPrompt } from './ui-antigravity-panel.js';
12
17
  import { getProviderMeta } from './provider-icons.js';
13
18
  import {
14
19
  getNativeLoopRouterClaimToken,
@@ -17,14 +22,17 @@ import {
17
22
  import {
18
23
  CLI_PROFILES,
19
24
  ensureModelsForProfile,
20
- fetchMcpProfilePresets,
25
+ fetchToolCatalog,
21
26
  fetchCodexAccounts,
22
27
  getCachedCodexAccounts,
23
- getCachedMcpProfilePresets,
28
+ getCachedToolCatalog,
24
29
  getEffortLevelsForProfile,
25
30
  getModelsForProfile,
26
31
  formatModelOptionLabel,
27
32
  isDynamicModelProfile,
33
+ migrateLaunchRecords,
34
+ migrateProviderId,
35
+ migrateProviderModel,
28
36
  modelSelectorValue,
29
37
  modelMatchesSelector,
30
38
  normalizeEffortForProfile,
@@ -64,6 +72,7 @@ import {
64
72
  describeCron,
65
73
  formatAgo,
66
74
  formatCountdown,
75
+ fullDateTime,
67
76
  groupIdOf,
68
77
  groupSummary,
69
78
  inScope,
@@ -73,8 +82,10 @@ import {
73
82
  matchesQuery,
74
83
  oneLine,
75
84
  scopeCounts,
85
+ setSchedulesViewTranslator,
76
86
  shortWhen,
77
87
  sortSchedules,
88
+ weekdayName,
78
89
  whenLabel,
79
90
  } from './schedules-view.js';
80
91
  import {
@@ -93,6 +104,7 @@ const LAYOUT_KEY = KEYS.PANEL_PREFIX + 'schedules-window';
93
104
  const SCOPE_KEY = 'sched-view-scope';
94
105
  const SORT_KEY = 'sched-view-sort';
95
106
  const COLLAPSED_KEY = 'sched-collapsed-groups';
107
+ const REOPEN_KEY = 'synabun-schedules-reopen'; // sessionStorage, for one reload
96
108
  const DEFAULT_GROUP_COLOR = '#4fc3f7';
97
109
 
98
110
  let _panel = null;
@@ -135,16 +147,16 @@ let _promptBackdrop = null;
135
147
  let _promptKeyHandler = null;
136
148
  const _modelLoadRequests = new Set();
137
149
 
138
- let _launchProfile = storage.getItem('as-launch-profile') || 'claude-code';
150
+ let _launchProfile = migrateProviderId(storage.getItem('as-launch-profile') || 'claude-code');
139
151
  const _storedLaunchSelection = normalizeAutomationSelection(
140
- _launchProfile, storage.getItem('as-launch-model'), storage.getItem('as-launch-context-mode'),
152
+ _launchProfile, migrateProviderModel(_launchProfile, storage.getItem('as-launch-model')), storage.getItem('as-launch-context-mode'),
141
153
  );
142
154
  let _launchModel = _storedLaunchSelection.model;
143
155
  let _launchContextMode = _storedLaunchSelection.contextMode || 'default';
144
156
  storage.setItem('as-launch-model', _launchModel || '');
145
157
  storage.setItem('as-launch-context-mode', _launchContextMode);
146
158
  let _launchEffort = storage.getItem('as-launch-effort') || 'off';
147
- let _launchMcpProfile = storage.getItem('as-launch-mcp-profile') || 'full';
159
+ let _launchTools = (() => { try { return JSON.parse(storage.getItem('as-launch-tools') || 'null'); } catch { return null; } })();
148
160
  let _launchAccount = storage.getItem('as-launch-account') || 'default'; // Codex ChatGPT account (CODEX_HOME)
149
161
  let _codexAccountsRequested = false; // lazy-fetch guard for account selects
150
162
 
@@ -183,41 +195,60 @@ const ICONS = {
183
195
  };
184
196
  const RESULT_ICON = { ok: ICONS.ok, failed: ICONS.failed, started: ICONS.started, stopped: ICONS.stopped, interrupted: ICONS.alert, never: ICONS.never, other: ICONS.never };
185
197
 
198
+ // Words: every string the window shows is a `schedules.*` key of i18n/<locale>.json; the ones
199
+ // schedules-view.js decides read through the translator set here. What the user named (schedules,
200
+ // groups, automations) and what a provider named (models, accounts, CLIs) show as they are.
201
+ setSchedulesViewTranslator((key, params) => t(`schedules.view.${key}`, params), getLocale);
202
+ // The same strings, escaped for markup.
203
+ const te = (key, params) => esc(t(key, params));
204
+ const tpe = (key, count, params) => esc(tp(key, count, params));
205
+
186
206
  const VIEWS = {
187
- upcoming: { label: 'Up next', icon: ICONS.next },
188
- all: { label: 'All schedules', icon: ICONS.list },
189
- attention: { label: 'Needs attention', icon: ICONS.alert },
190
- paused: { label: 'Paused', icon: ICONS.pause },
191
- timers: { label: 'Timers', icon: ICONS.timer },
192
- activity: { label: 'Activity', icon: ICONS.activity },
207
+ upcoming: { icon: ICONS.next },
208
+ all: { icon: ICONS.list },
209
+ attention: { icon: ICONS.alert },
210
+ paused: { icon: ICONS.pause },
211
+ timers: { icon: ICONS.timer },
212
+ activity: { icon: ICONS.activity },
193
213
  };
214
+ const viewLabel = (scope) => t(`schedules.views.${scope}`);
194
215
 
216
+ // A preset is a phrase (`key`) or a time of day, which reads the same in every language.
195
217
  const CRON_PRESET_GROUPS = [
196
- { group: 'Frequency', presets: [
197
- { label: 'Every 15 min', cron: '*/15 * * * *' },
198
- { label: 'Every 30 min', cron: '*/30 * * * *' },
199
- { label: 'Every hour', cron: '0 * * * *' },
200
- { label: 'Every 2 hours', cron: '0 */2 * * *' },
201
- { label: 'Every 6 hours', cron: '0 */6 * * *' },
218
+ { group: 'frequency', presets: [
219
+ { key: 'every15min', cron: '*/15 * * * *' },
220
+ { key: 'every30min', cron: '*/30 * * * *' },
221
+ { key: 'everyHour', cron: '0 * * * *' },
222
+ { key: 'every2hours', cron: '0 */2 * * *' },
223
+ { key: 'every6hours', cron: '0 */6 * * *' },
202
224
  ] },
203
- { group: 'Daily', presets: [
204
- { label: '09:00', cron: '0 9 * * *' },
205
- { label: '12:00', cron: '0 12 * * *' },
206
- { label: '18:00', cron: '0 18 * * *' },
207
- { label: 'Twice a day', cron: '0 10,18 * * *' },
208
- { label: 'Three times a day', cron: '0 9,14,19 * * *' },
225
+ { group: 'daily', presets: [
226
+ { time: '09:00', cron: '0 9 * * *' },
227
+ { time: '12:00', cron: '0 12 * * *' },
228
+ { time: '18:00', cron: '0 18 * * *' },
229
+ { key: 'twiceADay', cron: '0 10,18 * * *' },
230
+ { key: 'threeTimesADay', cron: '0 9,14,19 * * *' },
209
231
  ] },
210
- { group: 'Weekly', presets: [
211
- { label: 'Weekdays 09:00', cron: '0 9 * * 1-5' },
212
- { label: 'Weekdays twice', cron: '0 10,18 * * 1-5' },
213
- { label: 'Weekends 10:00', cron: '0 10 * * 0,6' },
214
- { label: 'Mon, Wed, Fri', cron: '0 9 * * 1,3,5' },
215
- { label: 'Tue, Thu', cron: '0 9 * * 2,4' },
232
+ { group: 'weekly', presets: [
233
+ { key: 'weekdays0900', cron: '0 9 * * 1-5' },
234
+ { key: 'weekdaysTwice', cron: '0 10,18 * * 1-5' },
235
+ { key: 'weekends1000', cron: '0 10 * * 0,6' },
236
+ { key: 'monWedFri', cron: '0 9 * * 1,3,5' },
237
+ { key: 'tueThu', cron: '0 9 * * 2,4' },
216
238
  ] },
217
239
  ];
218
-
219
- const DAY_NAMES = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
220
- const DAY_LONG = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
240
+ const presetLabel = (preset) => (preset.key ? t(`schedules.presets.${preset.key}`) : preset.time);
241
+
242
+ // The words of the launch popups that agent-runtime-options.js and automation-context.js write in
243
+ // English for every window: the ones the Schedules window shows read in the page's language. A
244
+ // word not listed (a level or a description a provider named) shows as it is.
245
+ const LAUNCH_WORDS = {
246
+ Off: 'off', Default: 'default', Low: 'low', Med: 'medium', High: 'high', XHigh: 'xhigh', Max: 'max', Ultra: 'ultra',
247
+ 'No thinking': 'noThinking', Minimal: 'minimal', Balanced: 'balanced', Deep: 'deep', 'Very deep': 'veryDeep',
248
+ Maximum: 'maximum', 'Config default': 'configDefault', 'Model default': 'modelDefault', Light: 'light',
249
+ 'Maximum, with delegation': 'maximumDelegated',
250
+ };
251
+ const launchWord = (text) => (Object.prototype.hasOwnProperty.call(LAUNCH_WORDS, text) ? t(`schedules.launch.word.${LAUNCH_WORDS[text]}`) : text);
221
252
 
222
253
  function readJson(key, fallback) {
223
254
  try { const value = JSON.parse(storage.getItem(key) || 'null'); return value ?? fallback; } catch { return fallback; }
@@ -238,7 +269,6 @@ const domId = (value) => String(value ?? '').replace(/[^\w-]/g, '_');
238
269
  // A choice of a popup (cc-dropdown.js). The one shown is the last marked `selected`, else the
239
270
  // first, as in a <select>: a saved value a list lacks is added as a choice of its own, marked.
240
271
  const opt = (value, label, selected = false) => ({ value: value ?? '', label: String(label ?? ''), selected: !!selected });
241
- const plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
242
272
  const cssColor = (value) => (value && CSS.supports('color', String(value)) ? String(value) : 'var(--stg-off)');
243
273
 
244
274
  function showToast(msg) {
@@ -281,14 +311,14 @@ const listScope = () => LIST_SCOPES.includes(_scope) || _scope === 'ungrouped' |
281
311
 
282
312
  function formatTimerCountdown(firesAtISO) {
283
313
  const diff = new Date(firesAtISO) - Date.now();
284
- if (diff <= 0) return 'firing';
314
+ if (diff <= 0) return t('schedules.timers.firing');
285
315
  const mins = Math.ceil(diff / 60_000);
286
316
  if (mins >= 60) {
287
317
  const h = Math.floor(mins / 60);
288
318
  const m = mins % 60;
289
- return m ? `${h}h ${m}m` : `${h}h`;
319
+ return m ? t('schedules.timers.countdownHours', { h, m }) : t('schedules.timers.countdownHour', { h });
290
320
  }
291
- return `${mins}m`;
321
+ return t('schedules.timers.countdownMinutes', { m: mins });
292
322
  }
293
323
 
294
324
  // ── Opening and closing ──
@@ -300,11 +330,28 @@ export function initSchedulesStudio() {
300
330
  on('schedules:open', (opts) => openPanel(opts || {}));
301
331
  on('schedules:toggle', () => (_panel ? closePanel() : openPanel({})));
302
332
  setupSync();
333
+ // A language change reloads the app (i18n.js): an open window opens again, in the new language,
334
+ // on the view it showed (Settings does the same for itself).
335
+ window.addEventListener('synabun:locale-change', () => {
336
+ if (!_panel) return;
337
+ try { sessionStorage.setItem(REOPEN_KEY, _scope); } catch { /* private mode: the reload leaves it closed */ }
338
+ });
339
+ const reopen = takeReopen();
340
+ if (reopen) setTimeout(() => openPanel({ scope: reopen }), 0);
341
+ }
342
+
343
+ /** The view to open the window on after a reload a language change asked for; read once. */
344
+ function takeReopen() {
345
+ try {
346
+ const scope = sessionStorage.getItem(REOPEN_KEY);
347
+ if (scope) sessionStorage.removeItem(REOPEN_KEY);
348
+ return scope && (VIEW_SCOPES.includes(scope) || scope === 'ungrouped' || scope.startsWith('group:')) ? scope : null;
349
+ } catch { return null; }
303
350
  }
304
351
 
305
352
  async function openPanel(opts = {}) {
306
353
  if (isGuest() && !hasPermission('automations')) {
307
- showGuestToast('Schedules are disabled by the host');
354
+ showGuestToast(t('schedules.window.guestDisabled'));
308
355
  return;
309
356
  }
310
357
  if (_panel) {
@@ -378,7 +425,6 @@ function applyOpenOptions(opts = {}) {
378
425
  }
379
426
 
380
427
  function placePanel() {
381
- const navH = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--navbar-height')) || 48;
382
428
  const saved = readJson(LAYOUT_KEY, null);
383
429
  if (saved && saved.w > 0 && saved.h > 0) {
384
430
  _panel.style.width = saved.w + 'px';
@@ -386,10 +432,13 @@ function placePanel() {
386
432
  }
387
433
  const width = _panel.offsetWidth;
388
434
  const height = _panel.offsetHeight;
389
- const left = saved && Number.isFinite(saved.x) ? saved.x : (window.innerWidth - width) / 2;
390
- const top = saved && Number.isFinite(saved.y) ? saved.y : navH + Math.max(8, (window.innerHeight - navH - height) / 2);
391
- _panel.style.left = Math.round(Math.max(8, Math.min(left, window.innerWidth - width - 8))) + 'px';
392
- _panel.style.top = Math.round(Math.max(navH + 8, Math.min(top, window.innerHeight - height - 8))) + 'px';
435
+ // Inside the app frame (8px from the window and the bar, as before, and clear of an open explorer)
436
+ const f = frameRect();
437
+ const left = saved && Number.isFinite(saved.x) ? saved.x : f.left + (f.width - width) / 2;
438
+ const top = saved && Number.isFinite(saved.y) ? saved.y : f.top + Math.max(0, (f.height - height) / 2);
439
+ const at = clampBox(left, top, width, height, f);
440
+ _panel.style.left = Math.round(at.x) + 'px';
441
+ _panel.style.top = Math.round(at.y) + 'px';
393
442
  }
394
443
 
395
444
  function saveLayout() {
@@ -443,21 +492,21 @@ function buildPanelHTML() {
443
492
  <header class="sched-header drag-handle" data-drag="${PANEL_ID}">
444
493
  <div class="sched-header-left">
445
494
  <span class="sched-header-icon">${ICONS.clock}</span>
446
- <h3 id="sched-title">Schedules</h3>
495
+ <h3 id="sched-title">${te('schedules.window.title')}</h3>
447
496
  <span class="sched-count" id="sched-total-count"></span>
448
497
  </div>
449
- <button type="button" class="backdrop-toggle-btn" id="sched-backdrop-toggle" aria-pressed="false" aria-label="Hide the backdrop" data-tooltip="Hide the backdrop">${ICONS.eye}</button>
450
- <button type="button" class="sched-close" id="sched-close" aria-label="Close Schedules" data-tooltip="Close (Esc)">${ICONS.close}</button>
498
+ <button type="button" class="backdrop-toggle-btn" id="sched-backdrop-toggle" aria-pressed="false" aria-label="${te('schedules.window.hideBackdrop')}" data-tooltip="${te('schedules.window.hideBackdrop')}">${ICONS.eye}</button>
499
+ <button type="button" class="sched-close" id="sched-close" aria-label="${te('schedules.window.close')}" data-tooltip="${te('schedules.window.closeTip')}">${ICONS.close}</button>
451
500
  </header>
452
501
  <div class="sched-body">
453
- <nav class="sched-rail" id="sched-rail" aria-label="Schedule views and groups"></nav>
502
+ <nav class="sched-rail" id="sched-rail" aria-label="${te('schedules.window.railLabel')}"></nav>
454
503
  <section class="sched-main" aria-labelledby="sched-view-title">
455
504
  <!-- A window too narrow for the rail: its views and groups as a popup, and New group. Outside
456
505
  the frame below, which each view and editor replaces, so it is there in every one. -->
457
506
  <div class="sched-narrow-nav" id="sched-narrow-nav">
458
- <span class="sched-sr" id="sched-scope-label">View</span>
507
+ <span class="sched-sr" id="sched-scope-label">${te('schedules.window.viewLabel')}</span>
459
508
  ${dropdownHTML('sched-scope-select', [], { classes: 'sched-scope-select', labelledBy: 'sched-scope-label', inputAttrs: 'data-change="scope"' })}
460
- <button type="button" class="sched-btn" data-action="group-new" id="sched-narrow-new-group">${ICONS.plus}<span>New group</span></button>
509
+ <button type="button" class="sched-btn" data-action="group-new" id="sched-narrow-new-group">${ICONS.plus}<span>${te('schedules.rail.newGroup')}</span></button>
461
510
  </div>
462
511
  <div class="sched-frame" id="sched-main"></div>
463
512
  </section>
@@ -476,7 +525,7 @@ function wirePanel() {
476
525
  if (!btn) return;
477
526
  btn.classList.toggle('active', hidden);
478
527
  btn.setAttribute('aria-pressed', String(hidden));
479
- const label = hidden ? 'Show the backdrop' : 'Hide the backdrop';
528
+ const label = t(hidden ? 'schedules.window.showBackdrop' : 'schedules.window.hideBackdrop');
480
529
  btn.setAttribute('aria-label', label);
481
530
  btn.dataset.tooltip = label;
482
531
  }, { signal });
@@ -492,6 +541,14 @@ function wirePanel() {
492
541
  _panel.addEventListener('keydown', handlePanelKeydown, { signal });
493
542
  // The popups (cc-dropdown.js): one set of listeners for every one the window draws.
494
543
  wireDropdowns(_panel, { signal });
544
+ _panel.addEventListener('change', event => {
545
+ if (!['sched-ge-browser', 'sched-ed-browser'].includes(event.target.id)) return;
546
+ const group = event.target.id === 'sched-ge-browser';
547
+ const inherited = group ? false : getGroup(_scheduleEditor?.groupId)?.usesBrowser ?? getTemplate(_scheduleEditor?.templateId)?.usesBrowser;
548
+ const usesBrowser = event.target.value === '' ? !!inherited : event.target.value === 'true';
549
+ const input = $(group ? 'sched-ge-mcp' : 'sched-ed-mcp');
550
+ updateToolPicker(input, readToolPicker(input, { requested: true }), { inheritLabel: toolWord(group ? 'noOverride' : 'templateDefault'), usesBrowser });
551
+ }, { signal });
495
552
  // Capture: the menubar's own Escape (it closes an open menu) has run by the time a bubbling
496
553
  // listener hears the key, so a menu that was open is seen here first.
497
554
  window.addEventListener('keydown', handleEscape, { capture: true, signal });
@@ -510,7 +567,7 @@ function withDeadline(request) {
510
567
  let timer;
511
568
  const late = new Promise((_, reject) => {
512
569
  timer = setTimeout(() => {
513
- const err = new Error(`no answer within ${LOAD_DEADLINE / 1000} seconds`);
570
+ const err = new Error(t('schedules.load.noAnswer', { seconds: LOAD_DEADLINE / 1000 }));
514
571
  err.timeout = true;
515
572
  reject(err);
516
573
  }, LOAD_DEADLINE);
@@ -518,9 +575,11 @@ function withDeadline(request) {
518
575
  return Promise.race([request, late]).finally(() => clearTimeout(timer));
519
576
  }
520
577
 
521
- const failureReason = (result) => String(result.reason?.message || 'an unexpected answer').replace(/\.$/, '');
578
+ // The reason is the server's or the network's own words ("HTTP 500"): it shows as it came.
579
+ const failureReason = (result) => String(result.reason?.message || t('schedules.load.unexpected')).replace(/\.$/, '');
522
580
 
523
581
  async function loadData() {
582
+ _launchTools = await migrateStoredTools(storage, 'as-launch-tools', 'as-launch-mcp-profile');
524
583
  const seq = ++_loadSeq;
525
584
  loadTimers();
526
585
  const [schedules, templates, groups] = await Promise.allSettled([
@@ -528,21 +587,25 @@ async function loadData() {
528
587
  ]);
529
588
  if (seq !== _loadSeq) return; // a newer load is under way: it finishes for both
530
589
  const failed = [];
590
+ // A record still saved for the Gemini CLI reads as Antigravity's (migrateLaunchRecords).
531
591
  if (schedules.status === 'fulfilled' && Array.isArray(schedules.value)) {
532
- _schedules = schedules.value;
592
+ _schedules = migrateLaunchRecords(schedules.value);
533
593
  _loaded.schedules = true;
534
594
  } else failed.push(['schedules', failureReason(schedules)]);
535
595
  if (templates.status === 'fulfilled' && Array.isArray(templates.value)) {
536
- _templates = templates.value;
596
+ _templates = migrateLaunchRecords(templates.value);
537
597
  _loaded.templates = true;
538
598
  } else failed.push(['automations', failureReason(templates)]);
539
599
  if (groups.status === 'fulfilled' && Array.isArray(groups.value)) {
540
- _groups = groups.value;
600
+ _groups = migrateLaunchRecords(groups.value);
541
601
  _loaded.groups = true;
542
602
  if (groupIdOf(_scope) && !getGroup(groupIdOf(_scope))) setScope('all', { keepEditors: true });
543
603
  } else failed.push(['groups', failureReason(groups)]);
544
604
  _loadError = failed.length
545
- ? `The ${listJoin(failed.map(([what]) => what))} did not load: ${[...new Set(failed.map(([, why]) => why))].join('; ')}.`
605
+ ? t('schedules.load.failed', {
606
+ lists: listJoin(failed.map(([what]) => t(`schedules.load.list.${what}`))),
607
+ reasons: [...new Set(failed.map(([, why]) => why))].join('; '),
608
+ })
546
609
  : null;
547
610
  loadFinished();
548
611
  }
@@ -575,10 +638,10 @@ async function loadTimers() {
575
638
  const failed = [];
576
639
  if (timers.status === 'fulfilled' && timers.value && typeof timers.value === 'object') _scheduleTimerData = timers.value;
577
640
  else failed.push(failureReason(timers));
578
- if (quick.status === 'fulfilled' && Array.isArray(quick.value)) _quickTimers = quick.value;
641
+ if (quick.status === 'fulfilled' && Array.isArray(quick.value)) _quickTimers = migrateLaunchRecords(quick.value);
579
642
  else failed.push(failureReason(quick));
580
643
  _timersLoaded = true;
581
- _timersError = failed.length ? `The timers did not load: ${[...new Set(failed)].join('; ')}.` : null;
644
+ _timersError = failed.length ? t('schedules.load.timersFailed', { reasons: [...new Set(failed)].join('; ') }) : null;
582
645
  if (!_panel) return;
583
646
  renderRail();
584
647
  renderActiveTimersInto();
@@ -647,22 +710,27 @@ function renderRail() {
647
710
  const timers = _quickTimers.length + Object.keys(_scheduleTimerData).length;
648
711
  const ids = groupIds();
649
712
  const view = (scope, count, { alert = false } = {}) => {
650
- const { label, icon } = VIEWS[scope];
713
+ const { icon } = VIEWS[scope];
651
714
  const current = _scope === scope;
652
715
  const countHTML = count == null || !countsKnown() ? '' : `<span class="sched-nav-count${alert && count ? ' sched-nav-count--alert' : ''}">${count}</span>`;
653
716
  return `<button type="button" class="sched-nav-item" data-action="set-scope" data-scope="${scope}"${current ? ' aria-current="page"' : ''}>
654
- <span class="sched-nav-icon">${icon}</span><span class="sched-nav-label">${label}</span>${countHTML}</button>`;
717
+ <span class="sched-nav-icon">${icon}</span><span class="sched-nav-label">${esc(viewLabel(scope))}</span>${countHTML}</button>`;
655
718
  };
656
719
  const groupRows = orderedGroups().map(group => {
657
720
  const members = _schedules.filter(s => s.groupId === group.id);
658
721
  const summary = groupSummary(members, now);
659
722
  const scope = `group:${group.id}`;
660
- const name = `${group.name}: ${plural(summary.total, 'schedule')}${summary.attention ? `, ${summary.attention} need attention` : ''}${group.mutualExclusion ? ', one at a time' : ''}`;
723
+ const facts = [
724
+ tp('schedules.count.schedules', summary.total),
725
+ summary.attention ? tp('schedules.count.attention', summary.attention) : '',
726
+ group.mutualExclusion ? t('schedules.rail.oneAtATimeFact') : '',
727
+ ].filter(Boolean).join(', ');
728
+ const name = t('schedules.rail.groupLabel', { name: group.name, facts });
661
729
  return `<button type="button" class="sched-nav-item" data-action="set-scope" data-scope="${esc(scope)}" data-drop-group="${esc(group.id)}" aria-label="${esc(name)}"${_scope === scope ? ' aria-current="page"' : ''}>
662
730
  <span class="sched-nav-icon"><span class="sched-dot" style="--dot:${esc(cssColor(group.color))}"></span></span>
663
731
  <span class="sched-nav-label">${esc(group.name)}</span>
664
- ${group.mutualExclusion ? `<span class="sched-nav-flag" data-tooltip="One at a time">${ICONS.lock}</span>` : ''}
665
- ${summary.attention ? `<span class="sched-nav-flag sched-nav-flag--alert" data-tooltip="${esc(plural(summary.attention, 'schedule'))} need attention">${ICONS.alert}${summary.attention}</span>` : ''}
732
+ ${group.mutualExclusion ? `<span class="sched-nav-flag" data-tooltip="${te('schedules.oneAtATime')}">${ICONS.lock}</span>` : ''}
733
+ ${summary.attention ? `<span class="sched-nav-flag sched-nav-flag--alert" data-tooltip="${tpe('schedules.rail.attentionTip', summary.attention)}">${ICONS.alert}${summary.attention}</span>` : ''}
666
734
  <span class="sched-nav-count">${summary.total}</span>
667
735
  </button>`;
668
736
  }).join('');
@@ -682,11 +750,11 @@ function renderRail() {
682
750
  <div class="sched-rail-sep" role="presentation"></div>
683
751
  ${view('timers', timers || null)}
684
752
  ${view('activity', null)}
685
- <div class="sched-rail-head"><span id="sched-rail-groups">Groups</span><button type="button" class="sched-icon-btn" data-action="group-new" aria-label="New group" data-tooltip="New group"${editorsWait() ? ' disabled' : ''}>${ICONS.plus}</button></div>
753
+ <div class="sched-rail-head"><span id="sched-rail-groups">${te('schedules.rail.groups')}</span><button type="button" class="sched-icon-btn" data-action="group-new" aria-label="${te('schedules.rail.newGroup')}" data-tooltip="${te('schedules.rail.newGroup')}"${editorsWait() ? ' disabled' : ''}>${ICONS.plus}</button></div>
686
754
  <div role="group" aria-labelledby="sched-rail-groups">
687
- ${groupRows || (countsKnown() ? '<div class="sched-rail-empty">No groups yet.</div>' : '')}
688
- <button type="button" class="sched-nav-item" data-action="set-scope" data-scope="ungrouped" data-drop-group="" aria-label="Ungrouped${countsKnown() ? `: ${esc(plural(ungrouped, 'schedule'))}` : ''}"${_scope === 'ungrouped' ? ' aria-current="page"' : ''}>
689
- <span class="sched-nav-icon">${ICONS.folder}</span><span class="sched-nav-label">Ungrouped</span><span class="sched-nav-count">${countsKnown() ? ungrouped : ''}</span>
755
+ ${groupRows || (countsKnown() ? `<div class="sched-rail-empty">${te('schedules.rail.noGroups')}</div>` : '')}
756
+ <button type="button" class="sched-nav-item" data-action="set-scope" data-scope="ungrouped" data-drop-group="" aria-label="${countsKnown() ? te('schedules.rail.groupLabel', { name: viewLabel('ungrouped'), facts: tp('schedules.count.schedules', ungrouped) }) : esc(viewLabel('ungrouped'))}"${_scope === 'ungrouped' ? ' aria-current="page"' : ''}>
757
+ <span class="sched-nav-icon">${ICONS.folder}</span><span class="sched-nav-label">${esc(viewLabel('ungrouped'))}</span><span class="sched-nav-count">${countsKnown() ? ungrouped : ''}</span>
690
758
  </button>
691
759
  </div>
692
760
  `;
@@ -737,24 +805,24 @@ function buildListFrame(frame) {
737
805
  <div class="sched-toolbar">
738
806
  <div class="sched-search">
739
807
  ${ICONS.search}
740
- <label class="sched-sr" for="sched-search">${activity ? 'Search runs' : 'Search schedules'}</label>
808
+ <label class="sched-sr" for="sched-search">${activity ? te('schedules.list.searchRuns') : te('schedules.list.searchSchedules')}</label>
741
809
  <input id="sched-search" type="search" autocomplete="off" spellcheck="false"
742
- placeholder="${activity ? 'Search runs by schedule or automation…' : 'Search by name, automation, group or time…'}" value="${esc(_query)}">
743
- <button type="button" class="sched-icon-btn" data-action="clear-search" aria-label="Clear search"${_query ? '' : ' hidden'}>${ICONS.close}</button>
810
+ placeholder="${activity ? te('schedules.list.searchRunsPlaceholder') : te('schedules.list.searchPlaceholder')}" value="${esc(_query)}">
811
+ <button type="button" class="sched-icon-btn" data-action="clear-search" aria-label="${te('schedules.list.clearSearch')}"${_query ? '' : ' hidden'}>${ICONS.close}</button>
744
812
  <kbd aria-hidden="true">/</kbd>
745
813
  </div>
746
- ${activity ? '' : `<span class="sched-sort" id="sched-sort-wrap"><span class="sched-sr" id="sched-sort-label">Order</span>
814
+ ${activity ? '' : `<span class="sched-sort" id="sched-sort-wrap"><span class="sched-sr" id="sched-sort-label">${te('schedules.list.order')}</span>
747
815
  ${dropdownHTML('sched-sort', [
748
- opt('next', 'Next run first', _sort === 'next'),
749
- opt('name', 'Name', _sort === 'name'),
750
- opt('last', 'Last run first', _sort === 'last'),
816
+ opt('next', t('schedules.list.sortNext'), _sort === 'next'),
817
+ opt('name', t('schedules.list.sortName'), _sort === 'name'),
818
+ opt('last', t('schedules.list.sortLast'), _sort === 'last'),
751
819
  ], { labelledBy: 'sched-sort-label', inputAttrs: 'data-change="sort"' })}</span>`}
752
820
  </div>
753
821
  <div class="sched-load-note" id="sched-load-note" role="status"></div>
754
822
  <div class="sched-list" id="sched-list" tabindex="-1"></div>
755
823
  <footer class="sched-footer">
756
824
  <span id="sched-status" role="status" aria-live="polite" aria-atomic="true"></span>
757
- ${activity ? '' : `<span class="sched-footer-hint">${ICONS.move}Drag a schedule onto a group in the sidebar to move it</span>`}
825
+ ${activity ? '' : `<span class="sched-footer-hint">${ICONS.move}${te('schedules.list.dragHint')}</span>`}
758
826
  </footer>
759
827
  `;
760
828
  if (caret) {
@@ -768,9 +836,9 @@ function headActions({ group = null } = {}) {
768
836
  // An editor needs the automations and groups: nothing opens one before they have loaded.
769
837
  const wait = editorsWait() ? ' disabled' : '';
770
838
  return `<div class="sched-view-actions">
771
- ${group ? `<button type="button" class="sched-btn" data-action="group-edit" data-id="${esc(group.id)}"${wait}>${ICONS.edit}<span>Edit group</span></button>` : ''}
772
- <button type="button" class="sched-btn sched-btn--text" data-action="schedule-prompt" data-tooltip="Describe the schedule to Claude Code in a side panel">${ICONS.sparkle}<span>Prompt a schedule</span></button>
773
- <button type="button" class="sched-btn sched-btn--primary" data-action="schedule-new"${wait}>${ICONS.plus}<span>New schedule</span></button>
839
+ ${group ? `<button type="button" class="sched-btn" data-action="group-edit" data-id="${esc(group.id)}"${wait}>${ICONS.edit}<span>${te('schedules.head.editGroup')}</span></button>` : ''}
840
+ <button type="button" class="sched-btn sched-btn--text" data-action="schedule-prompt" data-tooltip="${te('schedules.head.promptTip')}">${ICONS.sparkle}<span>${te('schedules.head.prompt')}</span></button>
841
+ <button type="button" class="sched-btn sched-btn--primary" data-action="schedule-new"${wait}>${ICONS.plus}<span>${te('schedules.head.newSchedule')}</span></button>
774
842
  </div>`;
775
843
  }
776
844
 
@@ -778,52 +846,61 @@ function groupOverridesText(group) {
778
846
  const parts = [];
779
847
  if (group.profile) parts.push(CLI_PROFILES.find(c => c.id === group.profile)?.label || group.profile);
780
848
  if (group.model) parts.push(group.model);
781
- if (group.contextMode && group.contextMode !== 'default') parts.push(`context ${group.contextMode}`);
782
- if (group.effort) parts.push(`thinking ${group.effort}`);
783
- if (group.mcpProfile) parts.push(`MCP ${group.mcpProfile}`);
784
- if (group.usesBrowser === true) parts.push('browser on');
785
- else if (group.usesBrowser === false) parts.push('browser off');
849
+ if (group.contextMode && group.contextMode !== 'default') parts.push(t('schedules.overrides.context', { value: group.contextMode }));
850
+ if (group.effort) parts.push(t('schedules.overrides.thinking', { value: group.effort }));
851
+ if (group.tools) parts.push(t('schedules.overrides.mcp', { value: group.tools.join(', ') }));
852
+ if (group.usesBrowser === true) parts.push(t('schedules.overrides.browserOn'));
853
+ else if (group.usesBrowser === false) parts.push(t('schedules.overrides.browserOff'));
786
854
  return parts.join(' · ');
787
855
  }
788
856
 
857
+ /** "4 schedules · 3 active · 1 paused · 1 needs attention": the facts of a set of schedules. */
858
+ function countsLine({ total, active, paused, attention }) {
859
+ return [
860
+ tp('schedules.count.schedules', total),
861
+ tp('schedules.count.active', active),
862
+ tp('schedules.count.paused', paused),
863
+ attention ? tp('schedules.count.attention', attention) : '',
864
+ ].filter(Boolean).join(' · ');
865
+ }
866
+
789
867
  function renderViewHead() {
790
868
  const head = $('sched-view-head');
791
869
  if (!head) return;
792
870
  const now = Date.now();
793
871
  const counts = scopeCounts(_schedules, now);
794
- let title = VIEWS[_scope]?.label || 'Schedules';
872
+ let title = VIEWS[_scope] ? viewLabel(_scope) : t('schedules.window.title');
795
873
  let dot = '';
796
874
  let sub = '';
797
875
  let meta = '';
798
876
  let group = null;
799
877
  if (_scope === 'all') {
800
- sub = `${plural(counts.all, 'schedule')} · ${counts.active} active · ${counts.paused} paused${counts.attention ? ` · ${counts.attention} need attention` : ''}`;
878
+ sub = countsLine({ total: counts.all, active: counts.active, paused: counts.paused, attention: counts.attention });
801
879
  } else if (_scope === 'upcoming') {
802
880
  // One row per schedule, at its next run (a schedule that runs three times today is listed once).
803
- sub = counts.upcoming
804
- ? `${counts.upcoming === 1 ? '1 schedule runs' : `${counts.upcoming} schedules run`} in the next 24 hours, each at its next run, soonest first.`
805
- : 'Nothing runs in the next 24 hours.';
881
+ sub = counts.upcoming ? tp('schedules.head.upcoming', counts.upcoming) : t('schedules.head.upcomingNone');
806
882
  } else if (_scope === 'attention') {
807
- sub = 'Active schedules whose last run failed or that are late. Open one to see why.';
883
+ sub = t('schedules.head.attention');
808
884
  } else if (_scope === 'paused') {
809
- sub = 'Paused schedules keep their settings. Turn one on to put it back on its timetable.';
885
+ sub = t('schedules.head.paused');
810
886
  } else if (_scope === 'activity') {
811
- sub = 'The latest run of each schedule, newest first. Open one to see its schedule.';
887
+ sub = t('schedules.head.activity');
812
888
  } else if (_scope === 'ungrouped') {
813
- title = 'Ungrouped';
814
- sub = 'Schedules in no group. Drag one onto a group in the sidebar to file it.';
889
+ title = viewLabel('ungrouped');
890
+ sub = t('schedules.head.ungrouped');
815
891
  } else if (groupIdOf(_scope)) {
816
892
  group = getGroup(groupIdOf(_scope));
817
893
  if (group) {
818
894
  title = group.name;
819
895
  dot = `<span class="sched-dot" style="--dot:${esc(cssColor(group.color))}" aria-hidden="true"></span>`;
820
896
  const summary = groupSummary(_schedules.filter(s => s.groupId === group.id), now);
821
- sub = `${plural(summary.total, 'schedule')} · ${summary.active} active · ${summary.paused} paused${summary.attention ? ` · ${summary.attention} need attention` : ''}${group.mutualExclusion ? ' · One at a time' : ''}`;
897
+ sub = countsLine(summary);
898
+ if (group.mutualExclusion) sub = `${sub} · ${t('schedules.oneAtATime')}`;
822
899
  const overrides = groupOverridesText(group);
823
- if (overrides) meta = `<p class="sched-view-meta sched-group-overrides">Group launch settings: ${esc(overrides)}</p>`;
900
+ if (overrides) meta = `<p class="sched-view-meta sched-group-overrides">${te('schedules.head.groupLaunch', { settings: overrides })}</p>`;
824
901
  }
825
902
  }
826
- if (_loading) sub = 'Loading schedules…';
903
+ if (_loading) sub = t('schedules.list.loading');
827
904
  else if (dataIncomplete()) sub = '';
828
905
  // A live update changes counts, not the head's controls: keep them (and the focus on them).
829
906
  const key = `${_scope}|${title}|${dot}|${meta}|${editorsWait()}`;
@@ -849,11 +926,11 @@ function renderNarrowNav() {
849
926
  if (scope) {
850
927
  const counts = scopeCounts(_schedules, Date.now());
851
928
  const timers = _quickTimers.length + Object.keys(_scheduleTimerData).length;
852
- const n = (value) => (!countsKnown() || !value && value !== 0 ? '' : ` (${value})`);
929
+ const n = (label, value) => (!countsKnown() || !value && value !== 0 ? label : t('schedules.rail.withCount', { label, count: value }));
853
930
  const views = [['upcoming', counts.upcoming], ['all', counts.all], ['attention', counts.attention], ['paused', counts.paused], ['timers', timers || null], ['activity', null]]
854
- .map(([id, count]) => opt(id, `${VIEWS[id].label}${n(count)}`));
855
- const groups = orderedGroups().map(g => opt(`group:${g.id}`, `${g.name}${n(_schedules.filter(s => s.groupId === g.id).length)}`));
856
- const entries = [{ group: 'Views', options: views }, { group: 'Groups', options: [...groups, opt('ungrouped', 'Ungrouped')] }];
931
+ .map(([id, count]) => opt(id, n(viewLabel(id), count)));
932
+ const groups = orderedGroups().map(g => opt(`group:${g.id}`, n(g.name, _schedules.filter(s => s.groupId === g.id).length)));
933
+ const entries = [{ group: t('schedules.rail.views'), options: views }, { group: t('schedules.rail.groups'), options: [...groups, opt('ungrouped', viewLabel('ungrouped'))] }];
857
934
  // Redrawn only when it changes (an open popup stays open, its highlighted choice too).
858
935
  const key = JSON.stringify(entries);
859
936
  if (scope._key !== key) { setDropdownItems(scope, entries); scope._key = key; }
@@ -879,7 +956,7 @@ function renderNote() {
879
956
  if (!note) return;
880
957
  const listView = _frame === 'list' || _frame === 'activity';
881
958
  const failed = _loading || !_loadError || (listView && dataIncomplete()) ? ''
882
- : dataIncomplete() ? _loadError : `${_loadError} Showing what loaded before.`;
959
+ : dataIncomplete() ? _loadError : t('schedules.note.showingBefore', { error: _loadError });
883
960
  const report = !failed && _frame === 'list' ? cliReportNote() : null;
884
961
  const kind = failed ? 'load' : report?.kind || '';
885
962
  const text = failed || report?.text || '';
@@ -889,8 +966,8 @@ function renderNote() {
889
966
  note.classList.toggle('sched-load-note--ok', kind === 'ok');
890
967
  note.classList.toggle('sched-load-note--busy', kind === 'busy');
891
968
  const icon = kind === 'ok' ? ICONS.ok : kind === 'busy' ? ICONS.clock : ICONS.alert;
892
- const action = kind === 'load' ? '<button type="button" class="sched-btn" data-action="retry-load">Try again</button>'
893
- : kind === 'busy' ? '' : '<button type="button" class="sched-btn" data-action="dismiss-report">Dismiss</button>';
969
+ const action = kind === 'load' ? `<button type="button" class="sched-btn" data-action="retry-load">${te('schedules.note.tryAgain')}</button>`
970
+ : kind === 'busy' ? '' : `<button type="button" class="sched-btn" data-action="dismiss-report">${te('schedules.note.dismiss')}</button>`;
894
971
  note.innerHTML = text ? `${icon}<span>${esc(text)}</span>${action}` : '';
895
972
  }
896
973
 
@@ -898,23 +975,23 @@ function renderNote() {
898
975
  function cliReportNote() {
899
976
  const r = _cliReport;
900
977
  if (!r || _scope !== `group:${r.groupId}`) return null;
901
- if (r.pending) return { kind: 'busy', text: "Clearing the old CLI's settings from the group's schedules…" };
978
+ if (r.pending) return { kind: 'busy', text: t('schedules.cliReport.clearing') };
902
979
  const cli = r.profile ? CLI_PROFILES.find(p => p.id === r.profile)?.label || r.profile : null;
903
980
  const kept = r.changed.length + r.failed.length;
904
- const its = (list) => (list.length === 1 ? 'its' : 'their');
905
- const parts = [cli ? `The group now runs ${cli}.` : 'The group no longer sets a CLI.'];
906
- if (r.reset.length) parts.push(`Cleared the old CLI's settings on ${listJoin(r.reset)}.`);
907
- else if (!kept) parts.push('None of its schedules had settings of the old CLI left.');
908
- if (r.changed.length) parts.push(`${listJoin(r.changed)} changed meanwhile and kept ${its(r.changed)} settings.`);
909
- if (r.failed.length) parts.push(`${listJoin(r.failed)} could not be checked or updated and kept ${its(r.failed)} settings.`);
910
- if (kept) parts.push(`Open ${kept === 1 ? 'it' : 'them'} to choose ${kept === 1 ? 'its' : 'their'} settings.`);
981
+ // Each sentence is whole in every language; the report is the sentences that apply, in order.
982
+ const parts = [cli ? t('schedules.cliReport.nowRuns', { cli }) : t('schedules.cliReport.noCli')];
983
+ if (r.reset.length) parts.push(t('schedules.cliReport.cleared', { names: listJoin(r.reset) }));
984
+ else if (!kept) parts.push(t('schedules.cliReport.noneLeft'));
985
+ if (r.changed.length) parts.push(tp('schedules.cliReport.changed', r.changed.length, { names: listJoin(r.changed) }));
986
+ if (r.failed.length) parts.push(tp('schedules.cliReport.failed', r.failed.length, { names: listJoin(r.failed) }));
987
+ if (kept) parts.push(tp('schedules.cliReport.open', kept));
911
988
  return { kind: kept ? 'warn' : 'ok', text: parts.join(' ') };
912
989
  }
913
990
 
914
991
  function loadFailedHTML() {
915
- const waiting = _pendingAction ? ` The ${_pendingAction === 'new-group' ? 'new group' : 'new schedule'} opens once they load.` : '';
916
- return emptyState(ICONS.alert, 'Schedules could not be loaded', `${_loadError || 'Not everything loaded.'}${waiting}`,
917
- '<button type="button" class="sched-btn" data-action="retry-load">Try again</button>');
992
+ const waiting = _pendingAction ? t(_pendingAction === 'new-group' ? 'schedules.loadFailed.waitingGroup' : 'schedules.loadFailed.waitingSchedule') : '';
993
+ return emptyState(ICONS.alert, t('schedules.loadFailed.title'), [_loadError || t('schedules.loadFailed.notEverything'), waiting].filter(Boolean).join(' '),
994
+ `<button type="button" class="sched-btn" data-action="retry-load">${te('schedules.note.tryAgain')}</button>`);
918
995
  }
919
996
 
920
997
  /** Keep the place: scroll position and the focused control survive a re-render of the list. */
@@ -970,29 +1047,30 @@ function renderList() {
970
1047
  renderNote();
971
1048
 
972
1049
  if (_loading) {
973
- list.innerHTML = emptyState(ICONS.clock, 'Loading schedules…', '');
974
- setStatus('Loading…');
1050
+ list.innerHTML = emptyState(ICONS.clock, t('schedules.list.loading'), '');
1051
+ setStatus(t('schedules.list.statusLoading'));
975
1052
  return;
976
1053
  }
977
1054
  if (dataIncomplete()) {
978
1055
  list.innerHTML = loadFailedHTML();
979
- setStatus('Not loaded');
1056
+ setStatus(t('schedules.list.statusNotLoaded'));
980
1057
  return;
981
1058
  }
982
1059
 
983
1060
  const now = Date.now();
984
1061
  const shown = visibleSchedules();
985
1062
  const scopeTotal = _schedules.filter(s => inScope(s, _scope, { now, groupIds: groupIds() })).length;
1063
+ const query = _query.trim();
986
1064
  if (!_schedules.length) {
987
- list.innerHTML = emptyState(ICONS.clock, 'No schedules yet', 'A schedule launches one of your automations on a timetable.',
988
- `<button type="button" class="sched-btn sched-btn--primary" data-action="schedule-new">${ICONS.plus}<span>New schedule</span></button>
989
- <button type="button" class="sched-btn" data-action="schedule-prompt">${ICONS.sparkle}<span>Prompt a schedule</span></button>`);
990
- } else if (!shown.length && _query.trim()) {
1065
+ list.innerHTML = emptyState(ICONS.clock, t('schedules.empty.none'), t('schedules.empty.noneText'),
1066
+ `<button type="button" class="sched-btn sched-btn--primary" data-action="schedule-new">${ICONS.plus}<span>${te('schedules.head.newSchedule')}</span></button>
1067
+ <button type="button" class="sched-btn" data-action="schedule-prompt">${ICONS.sparkle}<span>${te('schedules.head.prompt')}</span></button>`);
1068
+ } else if (!shown.length && query) {
991
1069
  const elsewhere = _scope !== 'all' && _schedules.some(s => matchesQuery(s, _query, scheduleExtras(s)));
992
- list.innerHTML = emptyState(ICONS.search, `No schedules match “${_query.trim()}”`,
993
- elsewhere ? 'Some do in other views.' : 'Try another word, or clear the search.',
994
- `<button type="button" class="sched-btn" data-action="clear-search">Clear search</button>
995
- ${elsewhere ? '<button type="button" class="sched-btn" data-action="search-all">Search all schedules</button>' : ''}`);
1070
+ list.innerHTML = emptyState(ICONS.search, t('schedules.empty.noMatch', { query }),
1071
+ elsewhere ? t('schedules.empty.elsewhere') : t('schedules.empty.tryAnother'),
1072
+ `<button type="button" class="sched-btn" data-action="clear-search">${te('schedules.list.clearSearch')}</button>
1073
+ ${elsewhere ? `<button type="button" class="sched-btn" data-action="search-all">${te('schedules.empty.searchAll')}</button>` : ''}`);
996
1074
  } else if (!shown.length) {
997
1075
  list.innerHTML = scopeEmptyState();
998
1076
  } else if (_scope === 'all') {
@@ -1001,9 +1079,9 @@ function renderList() {
1001
1079
  list.innerHTML = renderUpcomingList(shown, now);
1002
1080
  } else {
1003
1081
  const rows = sortSchedules(shown, _sort).map(s => rowHTML(s, { now, showGroup: !groupIdOf(_scope) })).join('');
1004
- list.innerHTML = `<div class="sched-box"><ul class="sched-rows" aria-label="${esc(VIEWS[_scope]?.label || 'Schedules')}">${rows}</ul></div>`;
1082
+ list.innerHTML = `<div class="sched-box"><ul class="sched-rows" aria-label="${esc(VIEWS[_scope] ? viewLabel(_scope) : t('schedules.window.title'))}">${rows}</ul></div>`;
1005
1083
  }
1006
- setStatus(_query.trim() ? `${plural(shown.length, 'schedule')} match “${_query.trim()}”` : `Showing ${shown.length} of ${plural(scopeTotal, 'schedule')}`);
1084
+ setStatus(query ? tp('schedules.list.statusMatch', shown.length, { query }) : tp('schedules.list.statusShowing', scopeTotal, { shown: shown.length }));
1007
1085
  restoreListState(list, saved);
1008
1086
  }
1009
1087
 
@@ -1017,12 +1095,12 @@ function emptyState(icon, title, text, actions = '') {
1017
1095
  }
1018
1096
 
1019
1097
  function scopeEmptyState() {
1020
- if (_scope === 'attention') return emptyState(ICONS.ok, 'Nothing needs attention', 'The last run of every active schedule worked, and none is late.');
1021
- if (_scope === 'paused') return emptyState(ICONS.pause, 'No paused schedules', 'Every schedule is on its timetable.');
1022
- if (_scope === 'upcoming') return emptyState(ICONS.next, 'Nothing runs in the next 24 hours', 'Active schedules show here once their next run is less than a day away.');
1023
- if (_scope === 'ungrouped') return emptyState(ICONS.folder, 'Every schedule is in a group', '');
1024
- return emptyState(ICONS.folder, 'No schedules in this group yet', 'Drag one onto the group in the sidebar, or create one here.',
1025
- `<button type="button" class="sched-btn sched-btn--primary" data-action="schedule-new">${ICONS.plus}<span>New schedule</span></button>`);
1098
+ if (_scope === 'attention') return emptyState(ICONS.ok, t('schedules.empty.attention'), t('schedules.empty.attentionText'));
1099
+ if (_scope === 'paused') return emptyState(ICONS.pause, t('schedules.empty.paused'), t('schedules.empty.pausedText'));
1100
+ if (_scope === 'upcoming') return emptyState(ICONS.next, t('schedules.empty.upcoming'), t('schedules.empty.upcomingText'));
1101
+ if (_scope === 'ungrouped') return emptyState(ICONS.folder, t('schedules.empty.ungrouped'), '');
1102
+ return emptyState(ICONS.folder, t('schedules.empty.group'), t('schedules.empty.groupText'),
1103
+ `<button type="button" class="sched-btn sched-btn--primary" data-action="schedule-new">${ICONS.plus}<span>${te('schedules.head.newSchedule')}</span></button>`);
1026
1104
  }
1027
1105
 
1028
1106
  /** All schedules: one box per group, in the groups' order, then the ungrouped ones. */
@@ -1037,21 +1115,21 @@ function renderGroupedList(shown, now) {
1037
1115
  const all = group ? _schedules.filter(s => s.groupId === group.id) : items;
1038
1116
  const summary = groupSummary(all, now);
1039
1117
  const meta = [
1040
- summary.attention ? `<span class="is-alert">${ICONS.alert} ${summary.attention} need attention</span>` : '',
1041
- summary.paused ? `<span>${summary.paused} paused</span>` : '',
1042
- group?.mutualExclusion ? `<span>${ICONS.lock} One at a time</span>` : '',
1118
+ summary.attention ? `<span class="is-alert">${ICONS.alert} ${tpe('schedules.count.attention', summary.attention)}</span>` : '',
1119
+ summary.paused ? `<span>${tpe('schedules.count.paused', summary.paused)}</span>` : '',
1120
+ group?.mutualExclusion ? `<span>${ICONS.lock} ${te('schedules.oneAtATime')}</span>` : '',
1043
1121
  ].filter(Boolean).join('<span aria-hidden="true">·</span>');
1044
- const name = group ? group.name : 'Ungrouped';
1122
+ const name = group ? group.name : viewLabel('ungrouped');
1045
1123
  return `<section class="sched-box${collapsed ? ' is-collapsed' : ''}" data-group-section="${esc(key)}">
1046
1124
  <div class="sched-group-head" data-drop-group="${group ? esc(group.id) : ''}">
1047
1125
  <button type="button" class="sched-group-toggle" data-action="group-collapse" data-key="${esc(key)}" aria-expanded="${!collapsed}" aria-controls="${listId}">
1048
1126
  <span class="sched-group-chevron">${ICONS.chevron}</span>
1049
1127
  ${group ? `<span class="sched-dot" style="--dot:${esc(cssColor(group.color))}" aria-hidden="true"></span>` : `<span class="sched-nav-icon">${ICONS.folder}</span>`}
1050
1128
  <span class="sched-group-name">${esc(name)}</span>
1051
- <span class="sched-group-count">${items.length === all.length ? items.length : `${items.length} of ${all.length}`}</span>
1129
+ <span class="sched-group-count">${items.length === all.length ? items.length : te('schedules.list.shownOf', { shown: items.length, total: all.length })}</span>
1052
1130
  </button>
1053
1131
  ${meta ? `<span class="sched-group-meta">${meta}</span>` : ''}
1054
- ${group ? `<button type="button" class="sched-icon-btn" data-action="group-edit" data-id="${esc(group.id)}" aria-label="Edit group ${esc(name)}" data-tooltip="Edit group">${ICONS.edit}</button>` : ''}
1132
+ ${group ? `<button type="button" class="sched-icon-btn" data-action="group-edit" data-id="${esc(group.id)}" aria-label="${te('schedules.list.editGroupNamed', { name })}" data-tooltip="${te('schedules.head.editGroup')}">${ICONS.edit}</button>` : ''}
1055
1133
  </div>
1056
1134
  <ul class="sched-rows" id="${listId}" aria-label="${esc(name)}"${collapsed ? ' hidden' : ''}>
1057
1135
  ${sortSchedules(items, _sort).map(s => rowHTML(s, { now, showGroup: false })).join('')}
@@ -1065,19 +1143,19 @@ function renderUpcomingList(shown, now) {
1065
1143
  const ordered = sortSchedules(shown, 'next');
1066
1144
  const days = new Map();
1067
1145
  for (const s of ordered) {
1068
- const label = dayLabel(s.nextRun, now) || 'Later';
1146
+ const label = dayLabel(s.nextRun, now) || t('schedules.list.later');
1069
1147
  if (!days.has(label)) days.set(label, []);
1070
1148
  days.get(label).push(s);
1071
1149
  }
1072
1150
  return [...days.entries()].map(([label, items], i) => `
1073
1151
  <section class="sched-section" aria-labelledby="sched-day-${i}">
1074
- <h3 class="sched-section-title" id="sched-day-${i}">${esc(label)}<span>${plural(items.length, 'schedule')}</span></h3>
1152
+ <h3 class="sched-section-title" id="sched-day-${i}">${esc(label)}<span>${tpe('schedules.count.schedules', items.length)}</span></h3>
1075
1153
  <div class="sched-box"><ul class="sched-rows">${items.map(s => rowHTML(s, { now, showGroup: true })).join('')}</ul></div>
1076
1154
  </section>`).join('');
1077
1155
  }
1078
1156
 
1079
1157
  function nextRunHTML(s, now) {
1080
- if (!s.enabled) return '<span class="sched-next" data-state="paused"><span class="sched-next-rel">Paused</span></span>';
1158
+ if (!s.enabled) return `<span class="sched-next" data-state="paused"><span class="sched-next-rel">${te('schedules.row.paused')}</span></span>`;
1081
1159
  const countdown = formatCountdown(s.nextRun, now);
1082
1160
  const when = s.nextRun ? whenLabel(s.nextRun, now) : '';
1083
1161
  return `<span class="sched-next" data-state="${countdown.state}" data-countdown="${esc(s.nextRun || '')}"${when ? ` title="${esc(when)}"` : ''}>
@@ -1086,7 +1164,7 @@ function nextRunHTML(s, now) {
1086
1164
 
1087
1165
  function lastRunHTML(s, now, run = lastRunState(s), { ago: withAgo = true } = {}) {
1088
1166
  const ago = withAgo && s.lastRun ? formatAgo(s.lastRun, now) : '';
1089
- const title = s.lastRun ? new Date(s.lastRun).toLocaleString() : '';
1167
+ const title = s.lastRun ? fullDateTime(s.lastRun) : '';
1090
1168
  return `<span class="sched-last" data-kind="${run.kind}"${title ? ` title="${esc(title)}"` : ''}>
1091
1169
  <span class="sched-last-word">${RESULT_ICON[run.kind] || ICONS.never}<span>${esc(run.label)}</span></span>${ago ? `<span class="sched-last-ago">${esc(ago)}</span>` : ''}</span>`;
1092
1170
  }
@@ -1105,7 +1183,7 @@ function rowHTML(s, { now = Date.now(), showGroup = false } = {}) {
1105
1183
  <div class="sched-row-line" data-action="schedule-expand" data-id="${esc(s.id)}" draggable="true">
1106
1184
  <span class="sched-switch-cell">
1107
1185
  <button type="button" class="sched-switch" role="switch" aria-checked="${!!s.enabled}" data-action="schedule-toggle" data-id="${esc(s.id)}"
1108
- aria-label="Run ${esc(s.name)} on schedule" data-tooltip="${s.enabled ? 'Pause' : 'Resume'}"></button>
1186
+ aria-label="${te('schedules.row.switchLabel', { name: s.name })}" data-tooltip="${s.enabled ? te('schedules.row.pause') : te('schedules.row.resume')}"></button>
1109
1187
  </span>
1110
1188
  <button type="button" class="sched-row-toggle" data-action="schedule-expand" data-id="${esc(s.id)}" aria-expanded="${open}"${open ? ` aria-controls="${detailId}"` : ''}>
1111
1189
  <span class="sched-row-name">${esc(s.name)}</span>
@@ -1123,55 +1201,53 @@ function rowHTML(s, { now = Date.now(), showGroup = false } = {}) {
1123
1201
  function detailHTML(s, now, run) {
1124
1202
  const template = getTemplate(s.templateId);
1125
1203
  const group = getGroup(s.groupId);
1126
- const presets = getCachedMcpProfilePresets();
1127
- const mcpLabels = Object.fromEntries(Object.entries(presets).map(([id, p]) => [id, p.label || id]));
1128
- const facts = launchFacts(s, { group, template, profiles: CLI_PROFILES, mcpLabels });
1129
- const sourceNote = (source) => (source === 'group' ? ' <span class="sched-source">(group)</span>'
1130
- : source === 'automation' ? ' <span class="sched-source">(automation)</span>'
1131
- : source === 'default' ? ' <span class="sched-source">(default)</span>' : '');
1204
+ const toolCatalog = getCachedToolCatalog();
1205
+ const facts = launchFacts(s, { group, template, profiles: CLI_PROFILES, toolCatalog, translate: t });
1206
+ const sourceNote = (source) => (['group', 'automation', 'default'].includes(source)
1207
+ ? ` <span class="sched-source">${te(`schedules.detail.source.${source}`)}</span>` : '');
1132
1208
  const next = s.enabled
1133
- ? (s.nextRun ? `${esc(whenLabel(s.nextRun, now))} · <span data-countdown="${esc(s.nextRun)}">${esc(formatCountdown(s.nextRun, now).text)}</span>` : 'Not scheduled')
1134
- : 'Paused';
1135
- const last = run.kind === 'never' ? 'Not run yet'
1209
+ ? (s.nextRun ? `${esc(whenLabel(s.nextRun, now))} · <span data-countdown="${esc(s.nextRun)}">${esc(formatCountdown(s.nextRun, now).text)}</span>` : esc(formatCountdown(null, now).text))
1210
+ : te('schedules.row.paused');
1211
+ const last = run.kind === 'never' ? esc(run.label)
1136
1212
  : `<span class="${run.kind === 'failed' ? 'is-failed' : ''}">${esc(run.label)}</span>${s.lastRun ? ` · ${esc(whenLabel(s.lastRun, now))}` : ''}`;
1137
1213
  // Any entry counts: with day themes, the server runs a schedule only on the days that have one.
1138
- const themes = [1, 2, 3, 4, 5, 6, 0].filter(d => s.dayThemes?.[String(d)]).map(d => DAY_NAMES[d]);
1214
+ const themes = [1, 2, 3, 4, 5, 6, 0].filter(d => s.dayThemes?.[String(d)]).map(d => weekdayName(d));
1139
1215
  const groupOptions = [
1140
- opt('', 'Ungrouped', !s.groupId),
1216
+ opt('', viewLabel('ungrouped'), !s.groupId),
1141
1217
  ...orderedGroups().map(g => opt(g.id, g.name, s.groupId === g.id)),
1142
1218
  ];
1143
1219
  const fact = (label, value, cls = '') => `<div class="sched-fact"><dt>${esc(label)}</dt><dd${cls ? ` class="${cls}"` : ''}>${value}</dd></div>`;
1144
1220
  return `
1145
1221
  <div class="sched-detail-grid">
1146
1222
  <section class="sched-facts-block">
1147
- <h4 class="sched-facts-title">Runs</h4>
1223
+ <h4 class="sched-facts-title">${te('schedules.detail.runs')}</h4>
1148
1224
  <dl class="sched-facts">
1149
- ${fact('Automation', template ? esc(template.name) : '<span class="is-failed">Missing automation</span>')}
1150
- ${fact('Timetable', `${esc(describeCron(s.cron))} <span class="sched-source">(${esc(s.cron || '')})</span>`)}
1151
- ${fact('Next run', next)}
1152
- ${fact('Last run', last)}
1153
- ${run.reason ? fact(run.kind === 'failed' ? 'Error' : 'Detail', esc(oneLine(run.reason))) : ''}
1154
- ${fact('Runs so far', esc(String(s.runCount || 0)))}
1155
- ${fact('Time zone', esc(s.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone))}
1156
- ${themes.length ? fact('Day themes', `${esc(themes.join(', '))} <span class="sched-source">(runs on these days only)</span>`) : ''}
1225
+ ${fact(t('schedules.detail.automation'), template ? esc(template.name) : `<span class="is-failed">${te('schedules.detail.missingAutomation')}</span>`)}
1226
+ ${fact(t('schedules.detail.timetable'), `${esc(describeCron(s.cron))} <span class="sched-source">(${esc(s.cron || '')})</span>`)}
1227
+ ${fact(t('schedules.detail.nextRun'), next)}
1228
+ ${fact(t('schedules.detail.lastRun'), last)}
1229
+ ${run.reason ? fact(t(run.kind === 'failed' ? 'schedules.detail.error' : 'schedules.detail.detail'), esc(oneLine(run.reason))) : ''}
1230
+ ${fact(t('schedules.detail.runCount'), esc(String(s.runCount || 0)))}
1231
+ ${fact(t('schedules.detail.timeZone'), esc(s.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone))}
1232
+ ${themes.length ? fact(t('schedules.detail.dayThemes'), `${esc(themes.join(', '))} <span class="sched-source">${te('schedules.detail.themedDaysOnly')}</span>`) : ''}
1157
1233
  </dl>
1158
1234
  </section>
1159
1235
  <section class="sched-facts-block">
1160
- <h4 class="sched-facts-title">Launch settings</h4>
1236
+ <h4 class="sched-facts-title">${te('schedules.detail.launchSettings')}</h4>
1161
1237
  <dl class="sched-facts">
1162
1238
  ${facts.map(f => fact(f.label, `${esc(f.value)}${sourceNote(f.source)}`, f.mono ? 'sched-mono' : '')).join('')}
1163
1239
  </dl>
1164
1240
  </section>
1165
1241
  </div>
1166
1242
  <div class="sched-detail-bar">
1167
- <div class="sched-detail-group"><span aria-hidden="true">Group</span>
1168
- ${dropdownHTML(`sched-move-${domId(s.id)}`, groupOptions, { label: `Group of ${s.name}`, inputAttrs: `data-change="move-group" data-id="${esc(s.id)}"` })}
1243
+ <div class="sched-detail-group"><span aria-hidden="true">${te('schedules.detail.group')}</span>
1244
+ ${dropdownHTML(`sched-move-${domId(s.id)}`, groupOptions, { label: t('schedules.detail.groupOf', { name: s.name }), inputAttrs: `data-change="move-group" data-id="${esc(s.id)}"` })}
1169
1245
  </div>
1170
1246
  <span class="sched-detail-spacer"></span>
1171
- <button type="button" class="sched-btn" data-action="schedule-test" data-id="${esc(s.id)}" data-tooltip="Start one run now; the timetable is unchanged">${ICONS.bolt}<span>Run now</span></button>
1172
- <button type="button" class="sched-btn" data-action="schedule-edit-prompt" data-id="${esc(s.id)}" data-template-id="${esc(s.templateId || '')}" data-tooltip="Open its automation in Automation Studio">${ICONS.script}<span>Edit prompt</span></button>
1173
- <button type="button" class="sched-btn" data-action="schedule-edit" data-id="${esc(s.id)}">${ICONS.edit}<span>Edit schedule</span></button>
1174
- <button type="button" class="sched-btn sched-btn--danger" data-action="schedule-delete" data-id="${esc(s.id)}">${ICONS.trash}<span>Delete…</span></button>
1247
+ <button type="button" class="sched-btn" data-action="schedule-test" data-id="${esc(s.id)}" data-tooltip="${te('schedules.detail.runNowTip')}">${ICONS.bolt}<span>${te('schedules.detail.runNow')}</span></button>
1248
+ <button type="button" class="sched-btn" data-action="schedule-edit-prompt" data-id="${esc(s.id)}" data-template-id="${esc(s.templateId || '')}" data-tooltip="${te('schedules.detail.editPromptTip')}">${ICONS.script}<span>${te('schedules.detail.editPrompt')}</span></button>
1249
+ <button type="button" class="sched-btn" data-action="schedule-edit" data-id="${esc(s.id)}">${ICONS.edit}<span>${te('schedules.detail.editSchedule')}</span></button>
1250
+ <button type="button" class="sched-btn sched-btn--danger" data-action="schedule-delete" data-id="${esc(s.id)}">${ICONS.trash}<span>${te('schedules.detail.delete')}</span></button>
1175
1251
  </div>`;
1176
1252
  }
1177
1253
 
@@ -1183,36 +1259,37 @@ function renderActivityList() {
1183
1259
  if (clear) clear.hidden = !_query;
1184
1260
  renderNote();
1185
1261
  if (_loading || dataIncomplete()) {
1186
- list.innerHTML = _loading ? emptyState(ICONS.clock, 'Loading schedules…', '') : loadFailedHTML();
1262
+ list.innerHTML = _loading ? emptyState(ICONS.clock, t('schedules.list.loading'), '') : loadFailedHTML();
1187
1263
  return;
1188
1264
  }
1189
1265
  const now = Date.now();
1190
1266
  const ran = _schedules.filter(s => s.lastRun);
1191
1267
  const shown = sortSchedules(ran.filter(s => matchesQuery(s, _query, scheduleExtras(s))), 'last');
1268
+ const query = _query.trim();
1192
1269
  if (!shown.length) {
1193
- list.innerHTML = _query.trim()
1194
- ? emptyState(ICONS.search, `No runs match “${_query.trim()}”`, 'Try another word, or clear the search.', '<button type="button" class="sched-btn" data-action="clear-search">Clear search</button>')
1195
- : emptyState(ICONS.activity, 'No runs yet', 'Each schedule\'s latest run shows here once it has run.');
1196
- setStatus(_query.trim() ? 'No matches' : 'No runs yet');
1270
+ list.innerHTML = query
1271
+ ? emptyState(ICONS.search, t('schedules.empty.noRunMatch', { query }), t('schedules.empty.tryAnother'), `<button type="button" class="sched-btn" data-action="clear-search">${te('schedules.list.clearSearch')}</button>`)
1272
+ : emptyState(ICONS.activity, t('schedules.empty.noRuns'), t('schedules.empty.noRunsText'));
1273
+ setStatus(query ? t('schedules.list.statusNoMatches') : t('schedules.empty.noRuns'));
1197
1274
  restoreListState(list, saved);
1198
1275
  return;
1199
1276
  }
1200
- list.innerHTML = `<div class="sched-box"><ul class="sched-rows sched-rows--activity" aria-label="Latest runs">${shown.map(s => {
1277
+ list.innerHTML = `<div class="sched-box"><ul class="sched-rows sched-rows--activity" aria-label="${te('schedules.activity.listLabel')}">${shown.map(s => {
1201
1278
  const run = lastRunState(s);
1202
1279
  const template = getTemplate(s.templateId);
1203
- const sub = run.kind === 'failed' && run.reason ? oneLine(run.reason) : template?.name || 'Missing automation';
1204
- const next = s.enabled ? formatCountdown(s.nextRun, now).text : 'Paused';
1280
+ const sub = run.kind === 'failed' && run.reason ? oneLine(run.reason) : template?.name || t('schedules.detail.missingAutomation');
1281
+ const next = s.enabled ? formatCountdown(s.nextRun, now).text : t('schedules.row.paused');
1205
1282
  return `<li class="sched-row" data-schedule-id="${esc(s.id)}">
1206
- <button type="button" class="sched-activity-btn" data-action="reveal-schedule" data-id="${esc(s.id)}" aria-label="${esc(`${s.name}: ${run.label} ${formatAgo(s.lastRun, now)}. Show it in All schedules`)}">
1283
+ <button type="button" class="sched-activity-btn" data-action="reveal-schedule" data-id="${esc(s.id)}" aria-label="${te('schedules.activity.rowLabel', { name: s.name, result: run.label, ago: formatAgo(s.lastRun, now) })}">
1207
1284
  ${lastRunHTML(s, now, run, { ago: false })}
1208
1285
  <span class="sched-activity-main"><span class="sched-row-name">${esc(s.name)}</span><span class="sched-row-sub"><span>${esc(sub)}</span></span></span>
1209
1286
  <span class="sched-activity-when"><span>${esc(formatAgo(s.lastRun, now))}</span><span>${esc(whenLabel(s.lastRun, now))}</span></span>
1210
- <span class="sched-activity-when sched-activity-next"><span>${s.enabled ? 'Next' : ''}</span><span>${esc(next)}</span></span>
1287
+ <span class="sched-activity-when sched-activity-next"><span>${s.enabled ? te('schedules.activity.next') : ''}</span><span>${esc(next)}</span></span>
1211
1288
  <span class="sched-row-chevron" aria-hidden="true">${ICONS.chevron}</span>
1212
1289
  </button>
1213
1290
  </li>`;
1214
1291
  }).join('')}</ul></div>`;
1215
- setStatus(`${plural(shown.length, 'run')}${_query.trim() ? ` match “${_query.trim()}”` : ''}`);
1292
+ setStatus(query ? tp('schedules.activity.statusMatch', shown.length, { query }) : tp('schedules.count.runs', shown.length));
1216
1293
  restoreListState(list, saved);
1217
1294
  }
1218
1295
 
@@ -1247,20 +1324,19 @@ function renderTimersView() {
1247
1324
  const customMinutes = $('sched-qt-custom-min')?.value || '';
1248
1325
  const selectedSchedule = $('sched-existing-timer-schedule')?.value || '';
1249
1326
  const scheduleMinutes = $('sched-existing-timer-min')?.value || '';
1250
- const templateOptions = [opt('', 'Choose an automation…'), ..._templates.map(t => opt(t.id, t.name, selectedTemplate === t.id))];
1251
- const scheduleOptions = [opt('', 'Choose a schedule…'), ...sortSchedules(_schedules, 'name').map(s => opt(s.id, s.name, selectedSchedule === s.id))];
1327
+ const templateOptions = [opt('', t('schedules.form.chooseAutomation')), ..._templates.map(tpl => opt(tpl.id, tpl.name, selectedTemplate === tpl.id))];
1328
+ const scheduleOptions = [opt('', t('schedules.timers.chooseSchedule')), ...sortSchedules(_schedules, 'name').map(s => opt(s.id, s.name, selectedSchedule === s.id))];
1252
1329
  const profileOptions = CLI_PROFILES.map(p => opt(p.id, p.label, p.id === _launchProfile));
1253
1330
  const modelOptions = renderModelOptions(_launchProfile, _launchModel, { accountId: _launchAccount });
1254
1331
  const contextOptions = renderContextOptions(_launchProfile, _launchModel, _launchContextMode, { accountId: _launchAccount });
1255
1332
  const effortOptions = renderEffortOptions(_launchProfile, _launchEffort);
1256
- const mcpOptions = renderMcpOptions(_launchMcpProfile);
1257
1333
  const accountOptions = renderAccountOptions(_launchAccount, false);
1258
1334
  const showEffort = profileSupportsEffort(_launchProfile);
1259
- const showMcp = _launchProfile !== 'claude-code';
1335
+ const showMcp = strictToolHost(_launchProfile);
1260
1336
  const showAccount = _launchProfile === 'codex';
1261
1337
  const usesBrowser = _qtUsesBrowser !== null ? _qtUsesBrowser : !!getTemplate(selectedTemplate)?.usesBrowser;
1262
1338
  const qtPresetButtons = [5, 15, 30, 60, 120, 240]
1263
- .map(min => `<button type="button" class="sched-chip" data-action="qt-select" data-minutes="${min}" aria-pressed="${_selectedQtMinutes === min}">${min >= 60 ? `${min / 60} h` : `${min} min`}</button>`)
1339
+ .map(min => `<button type="button" class="sched-chip" data-action="qt-select" data-minutes="${min}" aria-pressed="${_selectedQtMinutes === min}">${min >= 60 ? te('schedules.timers.presetHours', { n: min / 60 }) : te('schedules.timers.presetMinutes', { n: min })}</button>`)
1264
1340
  .join('');
1265
1341
 
1266
1342
  const modelLoadKey = `${_launchProfile}:${_launchAccount}`;
@@ -1269,52 +1345,52 @@ function renderTimersView() {
1269
1345
  ensureModelsForProfile(_launchProfile, false, _launchAccount).then(() => { if (_panel && _frame === 'timers') renderTimersView(); });
1270
1346
  }
1271
1347
  if (showAccount) ensureCodexAccountsLoaded(() => { if (_panel && _frame === 'timers') renderTimersView(); });
1272
- if (showMcp && !Object.keys(getCachedMcpProfilePresets()).length) {
1273
- fetchMcpProfilePresets().then(() => { if (_panel && _frame === 'timers') renderTimersView(); });
1348
+ if (showMcp && !getCachedToolCatalog()) {
1349
+ fetchToolCatalog().then(catalog => { if (catalog && _panel && _frame === 'timers') renderTimersView(); });
1274
1350
  }
1275
1351
 
1276
1352
  main.innerHTML = `
1277
1353
  <div class="sched-view-head">
1278
1354
  <div class="sched-view-titles">
1279
- <h2 id="sched-view-title">Timers</h2>
1280
- <p class="sched-view-sub">Run an automation, or a saved schedule, once after a delay.</p>
1355
+ <h2 id="sched-view-title">${esc(viewLabel('timers'))}</h2>
1356
+ <p class="sched-view-sub">${te('schedules.timers.sub')}</p>
1281
1357
  </div>
1282
1358
  </div>
1283
1359
  <div class="sched-load-note" id="sched-load-note" role="status"></div>
1284
1360
  <div class="sched-editor-body">
1285
1361
  <div class="sched-form">
1286
1362
  <section class="sched-form-section" aria-labelledby="sched-timers-active-title">
1287
- <h3 class="sched-form-title" id="sched-timers-active-title">Waiting to run</h3>
1363
+ <h3 class="sched-form-title" id="sched-timers-active-title">${te('schedules.timers.waiting')}</h3>
1288
1364
  <div id="sched-active-timers"></div>
1289
1365
  </section>
1290
1366
  <section class="sched-form-section" aria-labelledby="sched-qt-title">
1291
- <h3 class="sched-form-title" id="sched-qt-title">Run an automation once</h3>
1367
+ <h3 class="sched-form-title" id="sched-qt-title">${te('schedules.timers.runOnce')}</h3>
1292
1368
  <div class="sched-form-group">
1293
- ${dropdownRow('sched-qt-template', 'Automation', templateOptions)}
1294
- ${dropdownRow('sched-qt-profile', 'CLI', profileOptions)}
1295
- ${dropdownRow('sched-qt-model', 'Model', modelOptions)}
1296
- ${dropdownRow('sched-qt-context', 'Context', contextOptions)}
1297
- ${dropdownRow('sched-qt-effort', 'Thinking', effortOptions, { hidden: !showEffort })}
1298
- ${dropdownRow('sched-qt-mcp', 'MCP tools', mcpOptions, { hidden: !showMcp })}
1299
- ${dropdownRow('sched-qt-account', 'OpenAI account', accountOptions, { hidden: !showAccount, rowId: 'sched-qt-account-field' })}
1300
- ${formRow('sched-qt-browser', 'Uses the browser', toggleHTML('sched-qt-browser', usesBrowser), { toggle: true })}
1301
- ${formRow('sched-qt-custom-min', 'Start in', `<span class="sched-chip-list" role="group" aria-label="Delay">${qtPresetButtons}</span>
1302
- <input class="sched-input" id="sched-qt-custom-min" type="number" min="1" max="1440" value="${esc(customMinutes)}" placeholder="min" aria-label="Minutes from now">`, { top: true })}
1369
+ ${dropdownRow('sched-qt-template', t('schedules.form.automation'), templateOptions)}
1370
+ ${dropdownRow('sched-qt-profile', t('schedules.form.cli'), profileOptions)}
1371
+ ${dropdownRow('sched-qt-model', t('schedules.form.model'), modelOptions)}
1372
+ ${dropdownRow('sched-qt-context', t('schedules.form.context'), contextOptions)}
1373
+ ${dropdownRow('sched-qt-effort', t('schedules.form.thinking'), effortOptions, { hidden: !showEffort })}
1374
+ ${toolRow('sched-qt-mcp', _launchTools, { provider: _launchProfile, usesBrowser })}
1375
+ ${dropdownRow('sched-qt-account', t('schedules.form.account'), accountOptions, { hidden: !showAccount, rowId: 'sched-qt-account-field' })}
1376
+ ${formRow('sched-qt-browser', t('schedules.timers.usesBrowser'), toggleHTML('sched-qt-browser', usesBrowser), { toggle: true })}
1377
+ ${formRow('sched-qt-custom-min', t('schedules.timers.startIn'), `<span class="sched-chip-list" role="group" aria-label="${te('schedules.timers.delay')}">${qtPresetButtons}</span>
1378
+ <input class="sched-input" id="sched-qt-custom-min" type="number" min="1" max="1440" value="${esc(customMinutes)}" placeholder="${te('schedules.timers.minutesPlaceholder')}" aria-label="${te('schedules.timers.minutesFromNow')}">`, { top: true })}
1303
1379
  </div>
1304
1380
  <div class="sched-form-actions">
1305
- <button type="button" class="sched-btn" data-action="qt-now">${ICONS.bolt}<span>Run now</span></button>
1306
- <button type="button" class="sched-btn sched-btn--primary" data-action="qt-go">${ICONS.timer}<span>Start timer</span></button>
1381
+ <button type="button" class="sched-btn" data-action="qt-now">${ICONS.bolt}<span>${te('schedules.detail.runNow')}</span></button>
1382
+ <button type="button" class="sched-btn sched-btn--primary" data-action="qt-go">${ICONS.timer}<span>${te('schedules.timers.start')}</span></button>
1307
1383
  </div>
1308
1384
  </section>
1309
1385
  <section class="sched-form-section" aria-labelledby="sched-st-title">
1310
- <h3 class="sched-form-title" id="sched-st-title">Fire a saved schedule once</h3>
1311
- <p class="sched-form-intro">Runs it with its own settings; its timetable is unchanged.</p>
1386
+ <h3 class="sched-form-title" id="sched-st-title">${te('schedules.timers.fireSaved')}</h3>
1387
+ <p class="sched-form-intro">${te('schedules.timers.fireSavedIntro')}</p>
1312
1388
  <div class="sched-form-group">
1313
- ${dropdownRow('sched-existing-timer-schedule', 'Schedule', scheduleOptions)}
1314
- ${formRow('sched-existing-timer-min', 'Minutes from now', `<input class="sched-input" id="sched-existing-timer-min" type="number" min="1" max="1440" value="${esc(scheduleMinutes)}" placeholder="15">`)}
1389
+ ${dropdownRow('sched-existing-timer-schedule', t('schedules.timers.schedule'), scheduleOptions)}
1390
+ ${formRow('sched-existing-timer-min', t('schedules.timers.minutesFromNow'), `<input class="sched-input" id="sched-existing-timer-min" type="number" min="1" max="1440" value="${esc(scheduleMinutes)}" placeholder="15">`)}
1315
1391
  </div>
1316
1392
  <div class="sched-form-actions">
1317
- <button type="button" class="sched-btn sched-btn--primary" data-action="schedule-timer-create">Set timer</button>
1393
+ <button type="button" class="sched-btn sched-btn--primary" data-action="schedule-timer-create">${te('schedules.timers.set')}</button>
1318
1394
  </div>
1319
1395
  </section>
1320
1396
  </div>
@@ -1353,8 +1429,9 @@ function renderTimersView() {
1353
1429
  storage.setItem('as-launch-effort', _launchEffort);
1354
1430
  });
1355
1431
  $('sched-qt-mcp')?.addEventListener('change', (e) => {
1356
- _launchMcpProfile = e.target.value || 'full';
1357
- storage.setItem('as-launch-mcp-profile', _launchMcpProfile);
1432
+ _launchTools = readToolPicker(e.target);
1433
+ storage.setItem('as-launch-tools', JSON.stringify(_launchTools));
1434
+ updateToolPicker(e.target, _launchTools, { inheritLabel: toolWord('templateDefault'), usesBrowser });
1358
1435
  });
1359
1436
  $('sched-qt-account')?.addEventListener('change', (e) => {
1360
1437
  _launchAccount = e.target.value || 'default';
@@ -1371,13 +1448,19 @@ function renderTimersView() {
1371
1448
  if (contextPopup) setDropdownItems(contextPopup, renderContextOptions('codex', _launchModel, _launchContextMode, { accountId: _launchAccount }));
1372
1449
  });
1373
1450
  });
1374
- $('sched-qt-browser')?.addEventListener('change', (e) => { _qtUsesBrowser = e.target.checked; });
1451
+ $('sched-qt-browser')?.addEventListener('change', (e) => {
1452
+ _qtUsesBrowser = e.target.checked;
1453
+ const input = $('sched-qt-mcp');
1454
+ updateToolPicker(input, readToolPicker(input, { requested: true }), { inheritLabel: toolWord('templateDefault'), usesBrowser: _qtUsesBrowser });
1455
+ });
1375
1456
  $('sched-qt-template')?.addEventListener('change', (e) => {
1376
1457
  const template = getTemplate(e.target.value);
1377
1458
  const browser = $('sched-qt-browser');
1378
1459
  if (browser && template) {
1379
1460
  browser.checked = !!template.usesBrowser;
1380
1461
  _qtUsesBrowser = null;
1462
+ const input = $('sched-qt-mcp');
1463
+ updateToolPicker(input, readToolPicker(input, { requested: true }), { inheritLabel: toolWord('templateDefault'), usesBrowser: browser.checked });
1381
1464
  }
1382
1465
  });
1383
1466
  }
@@ -1389,31 +1472,38 @@ function renderModelOptions(profile, selected, { showService = false, accountId
1389
1472
  if (!models.length) {
1390
1473
  // List not loaded yet — preserve any stored selection so saving the form cannot
1391
1474
  // blank it (a blank popup makes save send model:null, which DELETES the override).
1392
- if (selected) return [opt('', 'Template/default'), opt(selected, selected, true)];
1393
- return [opt('', 'Default', true)];
1475
+ if (selected) return [opt('', t('schedules.launch.templateOrDefault')), opt(selected, selected, true)];
1476
+ return [opt('', t('schedules.launch.default'), true)];
1394
1477
  }
1395
1478
  const known = !!selected && models.some(m => modelMatchesSelector(m, selected));
1396
1479
  const current = selected || '';
1397
1480
  const opts = [
1398
- opt('', 'Template/default', !current),
1481
+ opt('', t('schedules.launch.templateOrDefault'), !current),
1399
1482
  ...models.map(m => opt(modelSelectorValue(m), formatModelOptionLabel(m, {
1400
1483
  includeService: showService && profile === 'opencode',
1401
1484
  }), modelMatchesSelector(m, current))),
1402
1485
  ];
1403
1486
  // Preserve a stored model that isn't in the (still-loading or curated) list so the
1404
1487
  // dropdown never silently blanks it and a save can't wipe the persisted override.
1405
- if (selected && !known) opts.push(opt(selected, `${selected} (saved)`, true));
1488
+ if (selected && !known) opts.push(opt(selected, t('schedules.launch.saved', { value: selected }), true));
1406
1489
  return opts;
1407
1490
  }
1408
1491
 
1492
+ /** A context choice of automation-context.js in the page's language (its size stays as written). */
1493
+ function contextChoiceLabel(choice) {
1494
+ if (choice.id === 'default') return choice.size ? t('schedules.launch.contextDefaultSize', { size: choice.size }) : t('schedules.launch.contextProviderDefault');
1495
+ if (choice.id === 'extended' && choice.size) return t('schedules.launch.contextExtendedSize', { size: choice.size });
1496
+ return choice.label;
1497
+ }
1498
+
1409
1499
  function renderContextOptions(profile, model, selected, { accountId = 'default', inherit = false } = {}) {
1410
1500
  const models = getModelsForProfile(profile, accountId);
1411
1501
  const choices = automationContextOptions(profile, model, models);
1412
1502
  const current = selected || (inherit ? '' : 'default');
1413
- const options = inherit ? [opt('', 'No override', !current)] : [];
1414
- for (const choice of choices) options.push(opt(choice.id, choice.label, current === choice.id));
1503
+ const options = inherit ? [opt('', t('schedules.launch.noOverride'), !current)] : [];
1504
+ for (const choice of choices) options.push(opt(choice.id, contextChoiceLabel(choice), current === choice.id));
1415
1505
  if (current && !choices.some((choice) => choice.id === current)) {
1416
- options.push(opt(current, `${current} (unavailable — choose another)`, true));
1506
+ options.push(opt(current, t('schedules.launch.unavailable', { value: current }), true));
1417
1507
  }
1418
1508
  return options;
1419
1509
  }
@@ -1434,33 +1524,22 @@ function modelChoiceAvailable(profile, model, accountId = 'default') {
1434
1524
  function renderEffortOptions(profile, selected, { inheritLabel = null } = {}) {
1435
1525
  const levels = getEffortLevelsForProfile(profile);
1436
1526
  const value = selected || '';
1437
- if (!inheritLabel && !levels.length) return [opt('off', 'Default', true)];
1527
+ if (!inheritLabel && !levels.length) return [opt('off', t('schedules.launch.default'), true)];
1438
1528
  const known = !!value && levels.some(e => e.id === value);
1439
1529
  const opts = inheritLabel ? [opt('', inheritLabel, !value)] : [];
1440
- for (const e of levels) opts.push(opt(e.id, `${e.label}${e.desc ? ' - ' + e.desc : ''}`, e.id === value));
1530
+ for (const e of levels) {
1531
+ opts.push(opt(e.id, e.desc ? t('schedules.launch.effortOption', { level: launchWord(e.label), desc: launchWord(e.desc) }) : launchWord(e.label), e.id === value));
1532
+ }
1441
1533
  // A saved level this CLI does not list (set for another CLI, or by hand) stays selected,
1442
1534
  // so saving the form cannot drop it (the model, MCP and account popups do the same).
1443
- if (value && !known) opts.push(opt(value, `${value} (saved)`, true));
1535
+ if (value && !known) opts.push(opt(value, t('schedules.launch.saved', { value }), true));
1444
1536
  return opts;
1445
1537
  }
1446
1538
 
1447
- function renderMcpOptions(selected) {
1448
- const presets = getCachedMcpProfilePresets();
1449
- const entries = Object.entries(presets);
1450
- if (!entries.length) {
1451
- if (!selected) return [opt('', 'Loading...')];
1452
- return [opt(selected, `${selected} (loading...)`, true)];
1453
- }
1454
- const known = !!selected && !!presets[selected];
1455
- const options = entries.map(([id, p]) => {
1456
- const label = p.label || id;
1457
- const tools = p.tools != null ? ` - ${p.tools} tools` : '';
1458
- return opt(id, `${label}${tools}`, known && id === selected);
1459
- });
1460
- // A saved tool list that is not a preset ("browser,browser_twitter") stays selected, so saving
1461
- // the form cannot quietly replace it with another preset (the model popup does the same).
1462
- if (selected && !known) options.push(opt(selected, `${selected} (saved)`, true));
1463
- return options;
1539
+ function toolRow(id, tools, { provider, inheritLabel = toolWord('templateDefault'), usesBrowser } = {}) {
1540
+ return formRow(id, toolWord('label'), strictToolHost(provider)
1541
+ ? toolPickerHTML(id, tools, { labelledBy: `${id}-label`, provider, inheritLabel, usesBrowser })
1542
+ : `<span>${esc(toolWord('focus'))}</span>`, { labelId: `${id}-label` });
1464
1543
  }
1465
1544
 
1466
1545
  // The choices of the Codex account popup. `allowInherit` adds a
@@ -1474,12 +1553,14 @@ function renderAccountOptions(selected, allowInherit = false) {
1474
1553
  const keep = allowInherit && !!selected && !known;
1475
1554
  const current = known || keep ? selected : (allowInherit ? '' : 'default');
1476
1555
  const opts = accounts.map(a => {
1477
- const label = a.label || a.email || (a.isDefault ? 'Default' : a.id);
1478
- const plan = a.planType ? ` (${a.planType})` : '';
1479
- return opt(a.id, `${label}${plan}`, a.id === current);
1556
+ // The default account is "Default" in SynaBun's own list (agent-runtime-options.js): that word is
1557
+ // the page's; an account's own label or address shows as it is.
1558
+ const ownDefault = a.isDefault && (a.label === 'Default' || (!a.label && !a.email));
1559
+ const label = ownDefault ? t('schedules.launch.default') : a.label || a.email || a.id;
1560
+ return opt(a.id, a.planType ? t('schedules.launch.accountPlan', { label, plan: a.planType }) : label, a.id === current);
1480
1561
  });
1481
- if (allowInherit) opts.unshift(opt('', 'Template/group default', !current));
1482
- if (keep) opts.push(opt(selected, `${selected} (saved)`, true));
1562
+ if (allowInherit) opts.unshift(opt('', t('schedules.launch.templateOrGroupDefault'), !current));
1563
+ if (keep) opts.push(opt(selected, t('schedules.launch.saved', { value: selected }), true));
1483
1564
  return opts;
1484
1565
  }
1485
1566
 
@@ -1495,12 +1576,12 @@ function renderActiveTimersInto() {
1495
1576
  if (!box) return;
1496
1577
  const scheduleTimers = Object.entries(_scheduleTimerData).map(([scheduleId, timer]) => {
1497
1578
  const schedule = _schedules.find(s => s.id === scheduleId);
1498
- const name = schedule?.name || 'Schedule';
1579
+ const name = schedule?.name || t('schedules.timers.schedule');
1499
1580
  return `
1500
1581
  <div class="sched-timer-row" data-timer-id="${esc(scheduleId)}" data-timer-kind="schedule">
1501
- <span class="sched-timer-name">${esc(name)}<span class="sched-timer-meta">Saved schedule, once</span></span>
1502
- <strong class="sched-timer-count" data-fires-at="${esc(timer.firesAt)}">${formatTimerCountdown(timer.firesAt)}</strong>
1503
- <button type="button" class="sched-btn" data-action="schedule-timer-cancel" data-id="${esc(scheduleId)}" aria-label="Cancel the timer for ${esc(name)}">Cancel</button>
1582
+ <span class="sched-timer-name">${esc(name)}<span class="sched-timer-meta">${te('schedules.timers.savedOnce')}</span></span>
1583
+ <strong class="sched-timer-count" data-fires-at="${esc(timer.firesAt)}">${esc(formatTimerCountdown(timer.firesAt))}</strong>
1584
+ <button type="button" class="sched-btn" data-action="schedule-timer-cancel" data-id="${esc(scheduleId)}" aria-label="${te('schedules.timers.cancelFor', { name })}">${te('schedules.timers.cancel')}</button>
1504
1585
  </div>`;
1505
1586
  }).join('');
1506
1587
 
@@ -1508,23 +1589,23 @@ function renderActiveTimersInto() {
1508
1589
  const meta = [
1509
1590
  timer.profile ? CLI_PROFILES.find(p => p.id === timer.profile)?.label || timer.profile : '',
1510
1591
  timer.model,
1511
- timer.contextMode && timer.contextMode !== 'default' ? `context ${timer.contextMode}` : '',
1512
- timer.effort ? `thinking ${timer.effort}` : '',
1513
- timer.mcpProfile ? `MCP ${timer.mcpProfile}` : '',
1592
+ timer.contextMode && timer.contextMode !== 'default' ? t('schedules.overrides.context', { value: timer.contextMode }) : '',
1593
+ timer.effort ? t('schedules.overrides.thinking', { value: timer.effort }) : '',
1594
+ timer.tools ? t('schedules.overrides.mcp', { value: timer.tools.join(', ') }) : '',
1514
1595
  ].filter(Boolean).join(' · ');
1515
1596
  return `
1516
1597
  <div class="sched-timer-row" data-timer-id="${esc(timer.id)}" data-timer-kind="quick">
1517
- <span class="sched-timer-name">${esc(timer.templateName)}<span class="sched-timer-meta">${esc(meta || 'Automation, once')}</span></span>
1518
- <strong class="sched-timer-count" data-fires-at="${esc(timer.firesAt)}">${formatTimerCountdown(timer.firesAt)}</strong>
1519
- <button type="button" class="sched-btn" data-action="qt-cancel" data-id="${esc(timer.id)}" aria-label="Cancel the timer for ${esc(timer.templateName)}">Cancel</button>
1598
+ <span class="sched-timer-name">${esc(timer.templateName)}<span class="sched-timer-meta">${esc(meta || t('schedules.timers.automationOnce'))}</span></span>
1599
+ <strong class="sched-timer-count" data-fires-at="${esc(timer.firesAt)}">${esc(formatTimerCountdown(timer.firesAt))}</strong>
1600
+ <button type="button" class="sched-btn" data-action="qt-cancel" data-id="${esc(timer.id)}" aria-label="${te('schedules.timers.cancelFor', { name: timer.templateName })}">${te('schedules.timers.cancel')}</button>
1520
1601
  </div>`;
1521
1602
  }).join('');
1522
1603
 
1523
1604
  // The timers load on their own: until they answer (or when they could not), say so here.
1524
1605
  const note = _timersError
1525
- ? `<div class="sched-load-note">${ICONS.alert}<span>${esc(_timersError)}</span><button type="button" class="sched-btn" data-action="retry-timers">Try again</button></div>`
1606
+ ? `<div class="sched-load-note">${ICONS.alert}<span>${esc(_timersError)}</span><button type="button" class="sched-btn" data-action="retry-timers">${te('schedules.note.tryAgain')}</button></div>`
1526
1607
  : '';
1527
- const empty = _timersError ? '' : `<p class="sched-form-intro">${_timersLoaded ? 'No timers are waiting.' : 'Loading timers…'}</p>`;
1608
+ const empty = _timersError ? '' : `<p class="sched-form-intro">${_timersLoaded ? te('schedules.timers.none') : te('schedules.timers.loading')}</p>`;
1528
1609
  box.innerHTML = note + (scheduleTimers || quickTimers ? `<div class="sched-form-group">${quickTimers}${scheduleTimers}</div>` : empty);
1529
1610
  }
1530
1611
 
@@ -1584,7 +1665,7 @@ function redrawEditor(main, html) {
1584
1665
 
1585
1666
  function editorHead({ back, title, sub, titleId }) {
1586
1667
  return `<header class="sched-editor-head">
1587
- <button type="button" class="sched-icon-btn" data-action="${back}" aria-label="Back to the list" data-tooltip="Back (Esc)">${ICONS.back}</button>
1668
+ <button type="button" class="sched-icon-btn" data-action="${back}" aria-label="${te('schedules.editor.back')}" data-tooltip="${te('schedules.editor.backTip')}">${ICONS.back}</button>
1588
1669
  <div class="sched-editor-titles"><h2 id="${titleId}">${esc(title)}</h2>${sub ? `<p>${esc(sub)}</p>` : ''}</div>
1589
1670
  </header>`;
1590
1671
  }
@@ -1598,8 +1679,8 @@ function openGroupEditor(id) {
1598
1679
  // `_original` is the group as it was when the editor opened: a save sends only what the user
1599
1680
  // changed from it, so a change made elsewhere meanwhile (a sync update) is never undone.
1600
1681
  _groupEditorDraft = group
1601
- ? { name: group.name, color: group.color, mutualExclusion: !!group.mutualExclusion, profile: group.profile || null, model: group.model || null, contextMode: group.contextMode || null, effort: group.effort || null, mcpProfile: group.mcpProfile || null, codexAccountId: group.codexAccountId || null, usesBrowser: group.usesBrowser, _original: JSON.parse(JSON.stringify(group)) }
1602
- : { name: '', color: DEFAULT_GROUP_COLOR, mutualExclusion: false, profile: null, model: null, contextMode: null, effort: null, mcpProfile: null, codexAccountId: null, usesBrowser: undefined };
1682
+ ? { name: group.name, color: group.color, mutualExclusion: !!group.mutualExclusion, profile: group.profile || null, model: group.model || null, contextMode: group.contextMode || null, effort: group.effort || null, tools: group.tools || null, codexAccountId: group.codexAccountId || null, usesBrowser: group.usesBrowser, _original: JSON.parse(JSON.stringify(group)) }
1683
+ : { name: '', color: DEFAULT_GROUP_COLOR, mutualExclusion: false, profile: null, model: null, contextMode: null, effort: null, tools: null, codexAccountId: null, usesBrowser: undefined };
1603
1684
  ensureModelsForProfile(_groupEditorDraft.profile || 'claude-code', false, _groupEditorDraft.codexAccountId || 'default').then(() => {
1604
1685
  if (_panel && _groupEditorDraft) {
1605
1686
  collectGroupEditorDraft();
@@ -1619,7 +1700,7 @@ function collectGroupEditorDraft() {
1619
1700
  model: $('sched-ge-model')?.value || null,
1620
1701
  contextMode: $('sched-ge-context')?.value || null,
1621
1702
  effort: $('sched-ge-effort')?.value || null,
1622
- mcpProfile: $('sched-ge-mcp')?.value || null,
1703
+ tools: readEditorTools('sched-ge-mcp', _groupEditorDraft?.tools),
1623
1704
  codexAccountId: $('sched-ge-account')?.value || null,
1624
1705
  usesBrowser: browserRaw === '' ? undefined : browserRaw === 'true',
1625
1706
  });
@@ -1635,20 +1716,19 @@ function renderGroupEditor(group) {
1635
1716
  const profileForModels = d.profile || 'claude-code';
1636
1717
  const accountId = d.codexAccountId || 'default';
1637
1718
  const profileOptions = [
1638
- opt('', 'No override', !d.profile),
1719
+ opt('', t('schedules.launch.noOverride'), !d.profile),
1639
1720
  ...CLI_PROFILES.map(p => opt(p.id, p.label, d.profile === p.id)),
1640
1721
  ];
1641
1722
  const modelOptions = renderModelOptions(profileForModels, d.model || '', { showService: true, accountId });
1642
1723
  const contextOptions = renderContextOptions(profileForModels, d.model, d.contextMode, { accountId, inherit: true });
1643
- const effortOptions = renderEffortOptions(profileForModels, d.effort || '', { inheritLabel: 'No override' });
1644
- const mcpOptions = renderMcpOptions(d.mcpProfile || '');
1724
+ const effortOptions = renderEffortOptions(profileForModels, d.effort || '', { inheritLabel: t('schedules.launch.noOverride') });
1645
1725
  const accountOptions = renderAccountOptions(d.codexAccountId || '', true);
1646
1726
  const showEffort = profileSupportsEffort(profileForModels);
1647
- const showMcp = profileForModels !== 'claude-code';
1727
+ const showMcp = strictToolHost(profileForModels);
1648
1728
  const showAccount = profileForModels === 'codex';
1649
1729
  const rerender = () => { if (_panel && _groupEditorDraft) { collectGroupEditorDraft(); renderGroupEditor(_groupEditorId ? getGroup(_groupEditorId) : null); } };
1650
1730
  if (showAccount) ensureCodexAccountsLoaded(rerender);
1651
- if (showMcp && !Object.keys(getCachedMcpProfilePresets()).length) fetchMcpProfilePresets().then(rerender);
1731
+ if (showMcp && !getCachedToolCatalog()) fetchToolCatalog().then(catalog => { if (catalog) rerender(); });
1652
1732
  const browserValue = d.usesBrowser === true ? 'true' : d.usesBrowser === false ? 'false' : '';
1653
1733
  const ordered = orderedGroups();
1654
1734
  const position = group ? ordered.findIndex(g => g.id === group.id) : -1;
@@ -1656,55 +1736,55 @@ function renderGroupEditor(group) {
1656
1736
 
1657
1737
  redrawEditor(main, `
1658
1738
  <div class="sched-editor">
1659
- ${editorHead({ back: 'group-editor-cancel', title: creating ? 'New group' : 'Edit group', sub: creating ? 'Groups keep related schedules together, and can share launch settings.' : (group || d._original)?.name, titleId: 'sched-view-title' })}
1739
+ ${editorHead({ back: 'group-editor-cancel', title: t(creating ? 'schedules.groupEditor.newTitle' : 'schedules.head.editGroup'), sub: creating ? t('schedules.groupEditor.newSub') : (group || d._original)?.name, titleId: 'sched-view-title' })}
1660
1740
  <div class="sched-editor-body">
1661
1741
  <div class="sched-form">
1662
1742
  <section class="sched-form-section" aria-labelledby="sched-ge-sec-group">
1663
- <h3 class="sched-form-title" id="sched-ge-sec-group">Group</h3>
1743
+ <h3 class="sched-form-title" id="sched-ge-sec-group">${te('schedules.form.group')}</h3>
1664
1744
  <div class="sched-form-group">
1665
- ${formRow('sched-ge-name', 'Name', `<input class="sched-input" id="sched-ge-name" type="text" value="${esc(d.name || '')}" placeholder="Twitter cadence" autocomplete="off">`)}
1666
- ${formRow('sched-ge-color', 'Colour', `<input class="sched-input" id="sched-ge-color" type="color" value="${esc(/^#[0-9a-f]{6}$/i.test(d.color || '') ? d.color : DEFAULT_GROUP_COLOR)}">`)}
1667
- ${formRow('sched-ge-mutex', 'One at a time', toggleHTML('sched-ge-mutex', !!d.mutualExclusion, 'sched-ge-mutex-help'),
1668
- { help: 'Schedules in every group set to one at a time take turns: while one runs, the others wait.', toggle: true })}
1745
+ ${formRow('sched-ge-name', t('schedules.form.name'), `<input class="sched-input" id="sched-ge-name" type="text" value="${esc(d.name || '')}" placeholder="${te('schedules.groupEditor.namePlaceholder')}" autocomplete="off">`)}
1746
+ ${formRow('sched-ge-color', t('schedules.groupEditor.colour'), `<input class="sched-input" id="sched-ge-color" type="color" value="${esc(/^#[0-9a-f]{6}$/i.test(d.color || '') ? d.color : DEFAULT_GROUP_COLOR)}">`)}
1747
+ ${formRow('sched-ge-mutex', t('schedules.oneAtATime'), toggleHTML('sched-ge-mutex', !!d.mutualExclusion, 'sched-ge-mutex-help'),
1748
+ { help: te('schedules.groupEditor.oneAtATimeHelp'), toggle: true })}
1669
1749
  </div>
1670
1750
  </section>
1671
1751
  <section class="sched-form-section" aria-labelledby="sched-ge-sec-launch">
1672
- <h3 class="sched-form-title" id="sched-ge-sec-launch">Launch overrides</h3>
1673
- <p class="sched-form-intro">Apply to every schedule in this group. A schedule's own setting wins.</p>
1752
+ <h3 class="sched-form-title" id="sched-ge-sec-launch">${te('schedules.form.launchOverrides')}</h3>
1753
+ <p class="sched-form-intro">${te('schedules.groupEditor.launchIntro')}</p>
1674
1754
  <div class="sched-form-group">
1675
- ${dropdownRow('sched-ge-profile', 'CLI', profileOptions)}
1676
- ${dropdownRow('sched-ge-model', 'Model', modelOptions)}
1677
- ${dropdownRow('sched-ge-context', 'Context', contextOptions)}
1678
- ${dropdownRow('sched-ge-effort', 'Thinking', effortOptions, { hidden: !showEffort })}
1679
- ${dropdownRow('sched-ge-mcp', 'MCP tools', [opt('', 'No override', !d.mcpProfile), ...mcpOptions], { hidden: !showMcp })}
1680
- ${dropdownRow('sched-ge-account', 'OpenAI account', accountOptions, { hidden: !showAccount, rowId: 'sched-ge-account-field' })}
1681
- ${dropdownRow('sched-ge-browser', 'Browser', [
1682
- opt('', 'No override', browserValue === ''),
1683
- opt('true', 'Always on', browserValue === 'true'),
1684
- opt('false', 'Always off', browserValue === 'false'),
1755
+ ${dropdownRow('sched-ge-profile', t('schedules.form.cli'), profileOptions)}
1756
+ ${dropdownRow('sched-ge-model', t('schedules.form.model'), modelOptions)}
1757
+ ${dropdownRow('sched-ge-context', t('schedules.form.context'), contextOptions)}
1758
+ ${dropdownRow('sched-ge-effort', t('schedules.form.thinking'), effortOptions, { hidden: !showEffort })}
1759
+ ${toolRow('sched-ge-mcp', d.tools, { provider: profileForModels, inheritLabel: toolWord('noOverride'), usesBrowser: d.usesBrowser })}
1760
+ ${dropdownRow('sched-ge-account', t('schedules.form.account'), accountOptions, { hidden: !showAccount, rowId: 'sched-ge-account-field' })}
1761
+ ${dropdownRow('sched-ge-browser', t('schedules.form.browser'), [
1762
+ opt('', t('schedules.launch.noOverride'), browserValue === ''),
1763
+ opt('true', t('schedules.launch.alwaysOn'), browserValue === 'true'),
1764
+ opt('false', t('schedules.launch.alwaysOff'), browserValue === 'false'),
1685
1765
  ])}
1686
1766
  </div>
1687
1767
  </section>
1688
1768
  ${!group ? '' : `
1689
1769
  <section class="sched-form-section" aria-labelledby="sched-ge-sec-order">
1690
- <h3 class="sched-form-title" id="sched-ge-sec-order">Order</h3>
1770
+ <h3 class="sched-form-title" id="sched-ge-sec-order">${te('schedules.groupEditor.order')}</h3>
1691
1771
  <div class="sched-form-group">
1692
1772
  <div class="sched-form-row">
1693
- <span class="sched-form-label">Position ${position + 1} of ${ordered.length}</span>
1773
+ <span class="sched-form-label">${te('schedules.groupEditor.position', { position: position + 1, count: ordered.length })}</span>
1694
1774
  <div class="sched-form-control">
1695
- <button type="button" class="sched-btn" data-action="group-up" data-id="${esc(group.id)}"${position <= 0 ? ' disabled' : ''}>${ICONS.up}<span>Move up</span></button>
1696
- <button type="button" class="sched-btn" data-action="group-down" data-id="${esc(group.id)}"${position >= ordered.length - 1 ? ' disabled' : ''}>${ICONS.down}<span>Move down</span></button>
1775
+ <button type="button" class="sched-btn" data-action="group-up" data-id="${esc(group.id)}"${position <= 0 ? ' disabled' : ''}>${ICONS.up}<span>${te('schedules.groupEditor.moveUp')}</span></button>
1776
+ <button type="button" class="sched-btn" data-action="group-down" data-id="${esc(group.id)}"${position >= ordered.length - 1 ? ' disabled' : ''}>${ICONS.down}<span>${te('schedules.groupEditor.moveDown')}</span></button>
1697
1777
  </div>
1698
1778
  </div>
1699
1779
  </div>
1700
1780
  </section>
1701
1781
  <section class="sched-form-section" aria-labelledby="sched-ge-sec-delete">
1702
- <h3 class="sched-form-title" id="sched-ge-sec-delete">Delete</h3>
1782
+ <h3 class="sched-form-title" id="sched-ge-sec-delete">${te('schedules.groupEditor.delete')}</h3>
1703
1783
  <div class="sched-form-group">
1704
1784
  <div class="sched-form-row">
1705
- <span class="sched-form-label">${members ? `Its ${plural(members, 'schedule')} move to Ungrouped and keep running.` : 'The group is empty.'}</span>
1785
+ <span class="sched-form-label">${members ? tpe('schedules.groupEditor.membersStay', members) : te('schedules.groupEditor.empty')}</span>
1706
1786
  <div class="sched-form-control">
1707
- <button type="button" class="sched-btn sched-btn--danger" data-action="group-delete" data-id="${esc(group.id)}">${ICONS.trash}<span>Delete group…</span></button>
1787
+ <button type="button" class="sched-btn sched-btn--danger" data-action="group-delete" data-id="${esc(group.id)}">${ICONS.trash}<span>${te('schedules.groupEditor.deleteGroup')}</span></button>
1708
1788
  </div>
1709
1789
  </div>
1710
1790
  </div>
@@ -1712,8 +1792,8 @@ function renderGroupEditor(group) {
1712
1792
  </div>
1713
1793
  </div>
1714
1794
  <footer class="sched-editor-actions">
1715
- <button type="button" class="sched-btn" data-action="group-editor-cancel">Cancel</button>
1716
- <button type="button" class="sched-btn sched-btn--primary" data-action="group-editor-save" data-id="${esc(_groupEditorId || '')}">${creating ? 'Create group' : 'Save changes'}</button>
1795
+ <button type="button" class="sched-btn" data-action="group-editor-cancel">${te('schedules.editor.cancel')}</button>
1796
+ <button type="button" class="sched-btn sched-btn--primary" data-action="group-editor-save" data-id="${esc(_groupEditorId || '')}">${creating ? te('schedules.groupEditor.create') : te('schedules.editor.save')}</button>
1717
1797
  </footer>
1718
1798
  </div>`);
1719
1799
  wireGroupEditor();
@@ -1727,7 +1807,7 @@ function wireGroupEditor() {
1727
1807
  _groupEditorDraft.model = null;
1728
1808
  _groupEditorDraft.contextMode = _groupEditorDraft.profile ? 'default' : null;
1729
1809
  _groupEditorDraft.effort = null;
1730
- _groupEditorDraft.mcpProfile = null;
1810
+ _groupEditorDraft.tools = null;
1731
1811
  _groupEditorDraft.codexAccountId = null;
1732
1812
  const rerender = () => { if (_panel && _groupEditorDraft) renderGroupEditor(_groupEditorId ? getGroup(_groupEditorId) : null); };
1733
1813
  ensureModelsForProfile(_groupEditorDraft.profile || 'claude-code', false, 'default').then(() => { collectGroupEditorDraft(); rerender(); });
@@ -1765,18 +1845,18 @@ function renderScheduleEditor() {
1765
1845
  // A saved automation or group the lists do not show (deleted elsewhere, or not in what loaded)
1766
1846
  // stays selected: a popup without it would show the first choice, and a save would move it.
1767
1847
  const templateOptions = [
1768
- opt('', 'Choose an automation…'),
1769
- ..._templates.map(t => opt(t.id, `${t.name} (${t.category || 'custom'})`, s.templateId === t.id)),
1770
- ...(s.templateId && !getTemplate(s.templateId) ? [opt(s.templateId, `${s.templateId} (saved)`, true)] : []),
1848
+ opt('', t('schedules.form.chooseAutomation')),
1849
+ ..._templates.map(tpl => opt(tpl.id, t('schedules.scheduleEditor.automationOption', { name: tpl.name, category: tpl.category || t('schedules.scheduleEditor.customCategory') }), s.templateId === tpl.id)),
1850
+ ...(s.templateId && !getTemplate(s.templateId) ? [opt(s.templateId, t('schedules.launch.saved', { value: s.templateId }), true)] : []),
1771
1851
  ];
1772
1852
  const groupOptions = [
1773
- opt('', 'Ungrouped', !s.groupId),
1853
+ opt('', viewLabel('ungrouped'), !s.groupId),
1774
1854
  ...orderedGroups().map(g => opt(g.id, g.name, s.groupId === g.id)),
1775
- ...(s.groupId && !getGroup(s.groupId) ? [opt(s.groupId, `${s.groupId} (saved)`, true)] : []),
1855
+ ...(s.groupId && !getGroup(s.groupId) ? [opt(s.groupId, t('schedules.launch.saved', { value: s.groupId }), true)] : []),
1776
1856
  ];
1777
1857
  const tz = s.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone;
1778
1858
  const profileOptions = [
1779
- opt('', 'Template default', !s.profile),
1859
+ opt('', t('schedules.launch.templateDefault'), !s.profile),
1780
1860
  ...CLI_PROFILES.map(p => opt(p.id, p.label, s.profile === p.id)),
1781
1861
  ];
1782
1862
  const template = getTemplate(s.templateId);
@@ -1786,20 +1866,22 @@ function renderScheduleEditor() {
1786
1866
  const effectiveModel = s.model || group?.model || template?.model || null;
1787
1867
  const modelOptions = renderModelOptions(profileForModels, s.model || '', { showService: true, accountId });
1788
1868
  const contextOptions = renderContextOptions(profileForModels, effectiveModel, s.contextMode, { accountId, inherit: true });
1789
- const effortOptions = renderEffortOptions(profileForModels, s.effort || '', { inheritLabel: 'Template/default' });
1790
- const mcpOptions = renderMcpOptions(s.mcpProfile || '');
1869
+ const effortOptions = renderEffortOptions(profileForModels, s.effort || '', { inheritLabel: t('schedules.launch.templateOrDefault') });
1791
1870
  const accountOptions = renderAccountOptions(s.codexAccountId || '', true);
1792
1871
  const showEffort = profileSupportsEffort(profileForModels);
1793
- const showMcp = profileForModels !== 'claude-code';
1872
+ const showMcp = strictToolHost(profileForModels);
1794
1873
  const showAccount = profileForModels === 'codex';
1795
1874
  if (showAccount) ensureCodexAccountsLoaded(() => { if (_panel && _scheduleEditor) { collectScheduleEditorDraft(); renderScheduleEditor(); } });
1796
1875
  const browserValue = s.usesBrowser === true ? 'true' : s.usesBrowser === false ? 'false' : '';
1797
1876
  const cron = s.cron || '0 9 * * *';
1798
- const presetsHTML = CRON_PRESET_GROUPS.map(group => `
1799
- <div class="sched-preset-group" role="group" aria-label="${esc(group.group)} presets">
1800
- <span>${esc(group.group)}</span>
1801
- <div class="sched-chip-list">${group.presets.map(p => `<button type="button" class="sched-chip" data-action="cron-preset" data-cron="${esc(p.cron)}" aria-pressed="${p.cron === cron}" data-tooltip="${esc(describeCron(p.cron))}">${esc(p.label)}</button>`).join('')}</div>
1802
- </div>`).join('');
1877
+ const presetsHTML = CRON_PRESET_GROUPS.map(group => {
1878
+ const name = t(`schedules.presets.group.${group.group}`);
1879
+ return `
1880
+ <div class="sched-preset-group" role="group" aria-label="${te('schedules.scheduleEditor.presetGroup', { group: name })}">
1881
+ <span>${esc(name)}</span>
1882
+ <div class="sched-chip-list">${group.presets.map(p => `<button type="button" class="sched-chip" data-action="cron-preset" data-cron="${esc(p.cron)}" aria-pressed="${p.cron === cron}" data-tooltip="${esc(describeCron(p.cron))}">${esc(presetLabel(p))}</button>`).join('')}</div>
1883
+ </div>`;
1884
+ }).join('');
1803
1885
  const dayOrder = [1, 2, 3, 4, 5, 6, 0];
1804
1886
  const hasThemes = dayOrder.some(idx => s.dayThemes?.[String(idx)]);
1805
1887
  // A textarea, not an input: a theme can span lines, and an input would drop its line breaks.
@@ -1807,13 +1889,14 @@ function renderScheduleEditor() {
1807
1889
  // whole entry away.
1808
1890
  const dayThemes = dayOrder.map(idx => {
1809
1891
  const entry = s.dayThemes?.[String(idx)];
1810
- const help = entry?.label ? `Theme: ${esc(entry.label)}` : '';
1892
+ const day = weekdayName(idx, { long: true });
1893
+ const help = entry?.label ? te('schedules.scheduleEditor.themeLabel', { label: entry.label }) : '';
1811
1894
  // A day without an entry keeps the button's room, so the fields line up.
1812
1895
  const remove = entry
1813
- ? `<button type="button" class="sched-icon-btn sched-icon-btn--danger sched-day-remove" data-action="day-theme-remove" data-day="${idx}" aria-label="Remove the ${DAY_LONG[idx]} theme" data-tooltip="Remove this day's theme">${ICONS.trash}</button>`
1896
+ ? `<button type="button" class="sched-icon-btn sched-icon-btn--danger sched-day-remove" data-action="day-theme-remove" data-day="${idx}" aria-label="${te('schedules.scheduleEditor.removeTheme', { day })}" data-tooltip="${te('schedules.scheduleEditor.removeThemeTip')}">${ICONS.trash}</button>`
1814
1897
  : '<span class="sched-icon-btn sched-day-remove sched-day-remove--room" aria-hidden="true"></span>';
1815
- return formRow(`sched-ed-day-${idx}`, DAY_LONG[idx],
1816
- `<textarea class="sched-input sched-textarea" id="sched-ed-day-${idx}" data-day="${idx}" rows="2" spellcheck="false" placeholder="Extra context for ${DAY_LONG[idx]} runs (optional)"${help ? ` aria-describedby="sched-ed-day-${idx}-help"` : ''}>${esc(entry?.contextOverride || '')}</textarea>${remove}`,
1898
+ return formRow(`sched-ed-day-${idx}`, day,
1899
+ `<textarea class="sched-input sched-textarea" id="sched-ed-day-${idx}" data-day="${idx}" rows="2" spellcheck="false" placeholder="${te('schedules.scheduleEditor.themePlaceholder', { day })}"${help ? ` aria-describedby="sched-ed-day-${idx}-help"` : ''}>${esc(entry?.contextOverride || '')}</textarea>${remove}`,
1817
1900
  { help });
1818
1901
  }).join('');
1819
1902
 
@@ -1824,72 +1907,72 @@ function renderScheduleEditor() {
1824
1907
  if (_panel && _scheduleEditor) { collectScheduleEditorDraft(); renderScheduleEditor(); }
1825
1908
  });
1826
1909
  }
1827
- if (showMcp && !Object.keys(getCachedMcpProfilePresets()).length) {
1828
- fetchMcpProfilePresets().then(() => { if (_panel && _scheduleEditor) { collectScheduleEditorDraft(); renderScheduleEditor(); } });
1910
+ if (showMcp && !getCachedToolCatalog()) {
1911
+ fetchToolCatalog().then(catalog => { if (catalog && _panel && _scheduleEditor) { collectScheduleEditorDraft(); renderScheduleEditor(); } });
1829
1912
  }
1830
1913
 
1831
1914
  redrawEditor(main, `
1832
1915
  <div class="sched-editor">
1833
- ${editorHead({ back: 'schedule-editor-close', title: isEdit ? 'Edit schedule' : 'New schedule', sub: isEdit ? s.name : 'Launches one of your automations on a timetable.', titleId: 'sched-view-title' })}
1916
+ ${editorHead({ back: 'schedule-editor-close', title: t(isEdit ? 'schedules.detail.editSchedule' : 'schedules.head.newSchedule'), sub: isEdit ? s.name : t('schedules.scheduleEditor.newSub'), titleId: 'sched-view-title' })}
1834
1917
  <div class="sched-editor-body">
1835
1918
  <div class="sched-form">
1836
1919
  <section class="sched-form-section" aria-labelledby="sched-ed-sec-basics">
1837
- <h3 class="sched-form-title" id="sched-ed-sec-basics">Schedule</h3>
1920
+ <h3 class="sched-form-title" id="sched-ed-sec-basics">${te('schedules.timers.schedule')}</h3>
1838
1921
  <div class="sched-form-group">
1839
- ${formRow('sched-ed-name', 'Name', `<input class="sched-input" id="sched-ed-name" type="text" value="${esc(s.name || '')}" placeholder="Morning post" autocomplete="off">`)}
1840
- ${dropdownRow('sched-ed-template', 'Automation', templateOptions)}
1841
- ${dropdownRow('sched-ed-group', 'Group', groupOptions)}
1842
- ${formRow('sched-ed-enabled', 'Active', toggleHTML('sched-ed-enabled', s.enabled !== false, 'sched-ed-enabled-help'),
1843
- { help: 'Turn it off to pause the schedule without deleting it.', toggle: true })}
1922
+ ${formRow('sched-ed-name', t('schedules.form.name'), `<input class="sched-input" id="sched-ed-name" type="text" value="${esc(s.name || '')}" placeholder="${te('schedules.scheduleEditor.namePlaceholder')}" autocomplete="off">`)}
1923
+ ${dropdownRow('sched-ed-template', t('schedules.form.automation'), templateOptions)}
1924
+ ${dropdownRow('sched-ed-group', t('schedules.form.group'), groupOptions)}
1925
+ ${formRow('sched-ed-enabled', t('schedules.scheduleEditor.active'), toggleHTML('sched-ed-enabled', s.enabled !== false, 'sched-ed-enabled-help'),
1926
+ { help: te('schedules.scheduleEditor.activeHelp'), toggle: true })}
1844
1927
  </div>
1845
1928
  </section>
1846
1929
 
1847
1930
  <section class="sched-form-section" aria-labelledby="sched-ed-sec-when">
1848
- <h3 class="sched-form-title" id="sched-ed-sec-when">When it runs</h3>
1931
+ <h3 class="sched-form-title" id="sched-ed-sec-when">${te('schedules.scheduleEditor.when')}</h3>
1849
1932
  <div class="sched-form-group">
1850
1933
  <div class="sched-form-row sched-form-row--top">
1851
- <label class="sched-form-label" for="sched-ed-cron">Cron expression</label>
1934
+ <label class="sched-form-label" for="sched-ed-cron">${te('schedules.scheduleEditor.cron')}</label>
1852
1935
  <div class="sched-form-stack">
1853
1936
  <div class="sched-form-control"><input class="sched-input sched-mono" id="sched-ed-cron" type="text" value="${esc(cron)}" placeholder="0 9 * * *" spellcheck="false" autocomplete="off" aria-describedby="sched-ed-cron-desc"></div>
1854
1937
  <p class="sched-help" id="sched-ed-cron-desc" aria-live="polite">${esc(describeCron(cron))}</p>
1855
1938
  </div>
1856
1939
  </div>
1857
1940
  <div class="sched-form-row sched-form-row--top">
1858
- <span class="sched-form-label">Presets</span>
1941
+ <span class="sched-form-label">${te('schedules.scheduleEditor.presets')}</span>
1859
1942
  <div class="sched-presets">${presetsHTML}</div>
1860
1943
  </div>
1861
- ${formRow('sched-ed-tz', 'Time zone', `<input class="sched-input" id="sched-ed-tz" type="text" value="${esc(tz)}" spellcheck="false" autocomplete="off">`)}
1944
+ ${formRow('sched-ed-tz', t('schedules.detail.timeZone'), `<input class="sched-input" id="sched-ed-tz" type="text" value="${esc(tz)}" spellcheck="false" autocomplete="off">`)}
1862
1945
  </div>
1863
1946
  </section>
1864
1947
 
1865
1948
  <section class="sched-form-section" aria-labelledby="sched-ed-sec-launch">
1866
- <h3 class="sched-form-title" id="sched-ed-sec-launch">Launch overrides</h3>
1867
- <p class="sched-form-intro">Leave a setting on its default to use the group's, then the automation's.</p>
1949
+ <h3 class="sched-form-title" id="sched-ed-sec-launch">${te('schedules.form.launchOverrides')}</h3>
1950
+ <p class="sched-form-intro">${te('schedules.scheduleEditor.launchIntro')}</p>
1868
1951
  <div class="sched-form-group">
1869
- ${dropdownRow('sched-ed-profile', 'CLI', profileOptions)}
1870
- ${dropdownRow('sched-ed-model', 'Model', modelOptions)}
1871
- ${dropdownRow('sched-ed-context', 'Context', contextOptions)}
1872
- ${dropdownRow('sched-ed-effort', 'Thinking', effortOptions, { hidden: !showEffort })}
1873
- ${dropdownRow('sched-ed-mcp', 'MCP tools', [opt('', 'Template/default', !s.mcpProfile), ...mcpOptions], { hidden: !showMcp })}
1874
- ${dropdownRow('sched-ed-account', 'OpenAI account', accountOptions, { hidden: !showAccount, rowId: 'sched-ed-account-field' })}
1875
- ${dropdownRow('sched-ed-browser', 'Browser', [
1876
- opt('', 'Template default', browserValue === ''),
1877
- opt('true', 'Always on', browserValue === 'true'),
1878
- opt('false', 'Always off', browserValue === 'false'),
1952
+ ${dropdownRow('sched-ed-profile', t('schedules.form.cli'), profileOptions)}
1953
+ ${dropdownRow('sched-ed-model', t('schedules.form.model'), modelOptions)}
1954
+ ${dropdownRow('sched-ed-context', t('schedules.form.context'), contextOptions)}
1955
+ ${dropdownRow('sched-ed-effort', t('schedules.form.thinking'), effortOptions, { hidden: !showEffort })}
1956
+ ${toolRow('sched-ed-mcp', s.tools, { provider: profileForModels, usesBrowser: s.usesBrowser ?? group?.usesBrowser ?? template?.usesBrowser })}
1957
+ ${dropdownRow('sched-ed-account', t('schedules.form.account'), accountOptions, { hidden: !showAccount, rowId: 'sched-ed-account-field' })}
1958
+ ${dropdownRow('sched-ed-browser', t('schedules.form.browser'), [
1959
+ opt('', t('schedules.launch.templateDefault'), browserValue === ''),
1960
+ opt('true', t('schedules.launch.alwaysOn'), browserValue === 'true'),
1961
+ opt('false', t('schedules.launch.alwaysOff'), browserValue === 'false'),
1879
1962
  ])}
1880
1963
  </div>
1881
1964
  </section>
1882
1965
 
1883
1966
  <details class="sched-form-section"${hasThemes ? ' open' : ''}>
1884
- <summary class="sched-form-title">Day themes <small>optional</small></summary>
1885
- <p class="sched-form-intro">Extra context added to the run on that weekday, such as a topic for Mondays. Once any day has a theme, the schedule runs only on the days that have one.</p>
1967
+ <summary class="sched-form-title">${te('schedules.detail.dayThemes')} <small>${te('schedules.scheduleEditor.optional')}</small></summary>
1968
+ <p class="sched-form-intro">${te('schedules.scheduleEditor.dayThemesIntro')}</p>
1886
1969
  <div class="sched-form-group" id="sched-ed-daythemes">${dayThemes}</div>
1887
1970
  </details>
1888
1971
  </div>
1889
1972
  </div>
1890
1973
  <footer class="sched-editor-actions">
1891
- <button type="button" class="sched-btn" data-action="schedule-editor-close">Cancel</button>
1892
- <button type="button" class="sched-btn sched-btn--primary" data-action="schedule-editor-save">${isEdit ? 'Save changes' : 'Create schedule'}</button>
1974
+ <button type="button" class="sched-btn" data-action="schedule-editor-close">${te('schedules.editor.cancel')}</button>
1975
+ <button type="button" class="sched-btn sched-btn--primary" data-action="schedule-editor-save">${isEdit ? te('schedules.editor.save') : te('schedules.scheduleEditor.create')}</button>
1893
1976
  </footer>
1894
1977
  </div>
1895
1978
  `);
@@ -1901,7 +1984,7 @@ function renderScheduleEditor() {
1901
1984
  _scheduleEditor.model = null;
1902
1985
  _scheduleEditor.contextMode = null;
1903
1986
  _scheduleEditor.effort = null;
1904
- _scheduleEditor.mcpProfile = null;
1987
+ _scheduleEditor.tools = null;
1905
1988
  _scheduleEditor.codexAccountId = null;
1906
1989
  renderScheduleEditor();
1907
1990
  });
@@ -1916,7 +1999,7 @@ function renderScheduleEditor() {
1916
1999
  _scheduleEditor.model = null;
1917
2000
  _scheduleEditor.contextMode = _scheduleEditor.profile ? 'default' : null;
1918
2001
  _scheduleEditor.effort = null;
1919
- _scheduleEditor.mcpProfile = null;
2002
+ _scheduleEditor.tools = null;
1920
2003
  _scheduleEditor.codexAccountId = null;
1921
2004
  renderScheduleEditor();
1922
2005
  });
@@ -1949,7 +2032,7 @@ function updateCronDesc() {
1949
2032
  const valid = value.split(/\s+/).length === 5;
1950
2033
  if (!valid && value) input.setAttribute('aria-invalid', 'true');
1951
2034
  else input.removeAttribute('aria-invalid');
1952
- if (desc) desc.textContent = valid ? describeCron(value) : 'A cron expression has five fields: minute, hour, day of month, month, day of week.';
2035
+ if (desc) desc.textContent = valid ? describeCron(value) : t('schedules.scheduleEditor.cronHelp');
1953
2036
  _panel?.querySelectorAll('[data-action="cron-preset"]').forEach(btn => btn.setAttribute('aria-pressed', String(btn.dataset.cron === value)));
1954
2037
  }
1955
2038
 
@@ -1958,12 +2041,18 @@ function updateCronDesc() {
1958
2041
  // group pushes every changed launch field onto its schedules). So an edit sends a field only when
1959
2042
  // the form's value differs from the saved one: a popup that cannot show a saved value yet, or a
1960
2043
  // save of an untouched form, never clears anything.
1961
- const LAUNCH_FIELDS = ['profile', 'model', 'contextMode', 'effort', 'mcpProfile', 'codexAccountId', 'usesBrowser'];
2044
+ const LAUNCH_FIELDS = ['profile', 'model', 'contextMode', 'effort', 'tools', 'codexAccountId', 'usesBrowser'];
1962
2045
  // What a CLI's choice decides: a model, context, thinking level, tool list or account of one CLI
1963
2046
  // means nothing to another, so a change of CLI resets them (the editors' CLI popups do the same).
1964
- const CLI_DEPENDENT = ['model', 'contextMode', 'effort', 'mcpProfile', 'codexAccountId'];
2047
+ const CLI_DEPENDENT = ['model', 'contextMode', 'effort', 'tools', 'codexAccountId'];
2048
+ // A missing picker is not a request to clear tools (focus hosts and catalog loading).
2049
+ function readEditorTools(id, saved) {
2050
+ const input = $(id);
2051
+ return input ? readToolPicker(input) : saved ?? null;
2052
+ }
2053
+
1965
2054
  const blankToNull = (value) => (value === undefined || value === null || value === '' ? null : value);
1966
- const sameValue = (a, b) => blankToNull(a) === blankToNull(b);
2055
+ const sameValue = (a, b) => stableJson(blankToNull(a)) === stableJson(blankToNull(b));
1967
2056
 
1968
2057
  function stableJson(value) {
1969
2058
  if (Array.isArray(value)) return `[${value.map(stableJson).join(',')}]`;
@@ -2006,7 +2095,7 @@ function collectScheduleEditorDraft() {
2006
2095
  model: $('sched-ed-model')?.value || null,
2007
2096
  contextMode: $('sched-ed-context')?.value || null,
2008
2097
  effort: $('sched-ed-effort')?.value || null,
2009
- mcpProfile: $('sched-ed-mcp')?.value || null,
2098
+ tools: readEditorTools('sched-ed-mcp', _scheduleEditor?.tools),
2010
2099
  codexAccountId: $('sched-ed-account')?.value || null,
2011
2100
  usesBrowser: browserRaw === '' ? undefined : browserRaw === 'true',
2012
2101
  enabled: $('sched-ed-enabled')?.checked ?? _scheduleEditor.enabled,
@@ -2022,7 +2111,7 @@ function markInvalid(id, message) {
2022
2111
  }
2023
2112
 
2024
2113
  async function saveScheduleFromEditor() {
2025
- if (editorsWait()) { showToast('The schedules have not loaded yet: save once they have'); return; }
2114
+ if (editorsWait()) { showToast(t('schedules.toast.notLoaded')); return; }
2026
2115
  const original = _scheduleEditor?._original || null;
2027
2116
  const name = $('sched-ed-name')?.value?.trim();
2028
2117
  const templateId = $('sched-ed-template')?.value;
@@ -2037,15 +2126,15 @@ async function saveScheduleFromEditor() {
2037
2126
  contextMode: $('sched-ed-context')?.value || null,
2038
2127
  // 'off' is a choice of its own (no thinking), never the same as "no override".
2039
2128
  effort: $('sched-ed-effort')?.value || null,
2040
- mcpProfile: $('sched-ed-mcp')?.value || null,
2129
+ tools: readEditorTools('sched-ed-mcp', _scheduleEditor?.tools),
2041
2130
  // Read whatever the CLI: a saved account is kept while its row is hidden, never cleared by it.
2042
2131
  codexAccountId: $('sched-ed-account')?.value || null,
2043
2132
  usesBrowser: browserRaw === '' ? null : browserRaw === 'true',
2044
2133
  };
2045
2134
 
2046
- if (!name) { markInvalid('sched-ed-name', 'Give the schedule a name'); return; }
2047
- if (!templateId) { markInvalid('sched-ed-template', 'Choose the automation it launches'); return; }
2048
- if (!cron || cron.split(/\s+/).length !== 5) { markInvalid('sched-ed-cron', 'Enter a cron expression with five fields'); return; }
2135
+ if (!name) { markInvalid('sched-ed-name', t('schedules.toast.nameRequired')); return; }
2136
+ if (!templateId) { markInvalid('sched-ed-template', t('schedules.toast.automationRequired')); return; }
2137
+ if (!cron || cron.split(/\s+/).length !== 5) { markInvalid('sched-ed-cron', t('schedules.toast.cronRequired')); return; }
2049
2138
 
2050
2139
  const template = getTemplate(templateId);
2051
2140
  const group = groupId ? getGroup(groupId) : null;
@@ -2057,10 +2146,10 @@ async function saveScheduleFromEditor() {
2057
2146
  const effectiveModel = launch.model || group?.model || template?.model || null;
2058
2147
  const effectiveAccount = launch.codexAccountId || group?.codexAccountId || template?.codexAccountId || 'default';
2059
2148
  if (!modelChoiceAvailable(effectiveProfile, effectiveModel, effectiveAccount)) {
2060
- showToast('This model is unavailable for the selected account'); return;
2149
+ showToast(t('schedules.toast.modelUnavailable')); return;
2061
2150
  }
2062
2151
  if (!contextChoiceAvailable(effectiveProfile, effectiveModel, launch.contextMode, effectiveAccount)) {
2063
- showToast('This context window is unavailable for the selected model and account'); return;
2152
+ showToast(t('schedules.toast.contextUnavailable')); return;
2064
2153
  }
2065
2154
  }
2066
2155
 
@@ -2081,7 +2170,7 @@ async function saveScheduleFromEditor() {
2081
2170
  _scheduleEditor = null;
2082
2171
  render({ frame: true });
2083
2172
  focusRow(original.id);
2084
- showToast('No changes to save');
2173
+ showToast(t('schedules.toast.noChanges'));
2085
2174
  return;
2086
2175
  }
2087
2176
  } else {
@@ -2099,11 +2188,11 @@ async function saveScheduleFromEditor() {
2099
2188
  saved = await updateSchedule(draft.id, payload);
2100
2189
  const idx = _schedules.findIndex(s => s.id === saved.id);
2101
2190
  if (idx >= 0) _schedules[idx] = saved;
2102
- showToast('Schedule updated');
2191
+ showToast(t('schedules.toast.updated'));
2103
2192
  } else {
2104
2193
  saved = await createSchedule(payload);
2105
2194
  if (!_schedules.some(s => s.id === saved.id)) _schedules.push(saved);
2106
- showToast('Schedule created');
2195
+ showToast(t('schedules.toast.created'));
2107
2196
  }
2108
2197
  if (_panel !== panel || _scheduleEditor !== draft) { refresh(); return; }
2109
2198
  _scheduleEditor = null;
@@ -2115,7 +2204,7 @@ async function saveScheduleFromEditor() {
2115
2204
  row?.querySelector('.sched-row-toggle')?.focus({ preventScroll: true });
2116
2205
  }
2117
2206
  } catch (err) {
2118
- showToast('Save failed: ' + err.message);
2207
+ showToast(t('schedules.toast.saveFailed', { error: err.message }));
2119
2208
  }
2120
2209
  }
2121
2210
 
@@ -2193,7 +2282,7 @@ async function handleClick(e) {
2193
2282
  }
2194
2283
  case 'schedule-edit-prompt': {
2195
2284
  const templateId = el.dataset.templateId;
2196
- if (!templateId) { showToast('No automation linked to this schedule'); break; }
2285
+ if (!templateId) { showToast(t('schedules.toast.noAutomation')); break; }
2197
2286
  closePanel();
2198
2287
  emit('automations:open', { templateId });
2199
2288
  break;
@@ -2263,7 +2352,7 @@ async function handleClick(e) {
2263
2352
  _scheduleEditor.dayThemes = themes;
2264
2353
  renderScheduleEditor();
2265
2354
  $(`sched-ed-day-${day}`)?.focus();
2266
- showToast(`${DAY_LONG[Number(day)] || 'Day'} theme removed`);
2355
+ showToast(t('schedules.toast.themeRemoved', { day: weekdayName(day, { long: true }) }));
2267
2356
  break;
2268
2357
  }
2269
2358
  case 'group-editor-save':
@@ -2486,10 +2575,10 @@ async function moveScheduleToGroup(id, groupId) {
2486
2575
  if (!schedule || (schedule.groupId || null) === (groupId || null)) return;
2487
2576
  try {
2488
2577
  replaceSchedule(await updateSchedule(id, { groupId }));
2489
- showToast(groupId ? `Moved to ${getGroup(groupId)?.name || 'group'}` : 'Moved to Ungrouped');
2578
+ showToast(t('schedules.toast.movedTo', { group: groupId ? getGroup(groupId)?.name || t('schedules.toast.aGroup') : viewLabel('ungrouped') }));
2490
2579
  refresh();
2491
2580
  } catch (err) {
2492
- showToast('Move failed: ' + err.message);
2581
+ showToast(t('schedules.toast.moveFailed', { error: err.message }));
2493
2582
  refresh();
2494
2583
  }
2495
2584
  }
@@ -2501,11 +2590,11 @@ async function toggleSchedule(id, el = null) {
2501
2590
  try {
2502
2591
  replaceSchedule(await updateSchedule(id, { enabled: !schedule.enabled }));
2503
2592
  const updated = _schedules.find(s => s.id === id);
2504
- showToast(updated?.enabled ? `${schedule.name} is back on its timetable` : `${schedule.name} is paused`);
2593
+ showToast(t(updated?.enabled ? 'schedules.toast.resumed' : 'schedules.toast.paused', { name: schedule.name }));
2505
2594
  refresh();
2506
2595
  } catch (err) {
2507
2596
  el?.removeAttribute('aria-busy');
2508
- showToast('Toggle failed: ' + err.message);
2597
+ showToast(t('schedules.toast.toggleFailed', { error: err.message }));
2509
2598
  }
2510
2599
  }
2511
2600
 
@@ -2516,9 +2605,9 @@ async function fireSchedule(id, el = null) {
2516
2605
  sidepanelWindowId: getNativeLoopRouterWindowId(),
2517
2606
  sidepanelClaimToken: getNativeLoopRouterClaimToken(),
2518
2607
  });
2519
- showToast('Launching schedule…');
2608
+ showToast(t('schedules.toast.launching'));
2520
2609
  } catch (err) {
2521
- showToast('Run failed: ' + err.message);
2610
+ showToast(t('schedules.toast.runFailed', { error: err.message }));
2522
2611
  } finally {
2523
2612
  if (el?.isConnected) el.disabled = false;
2524
2613
  }
@@ -2533,18 +2622,18 @@ function openConfirm({ kind, id, opener }) {
2533
2622
  if (kind === 'schedule') {
2534
2623
  const schedule = _schedules.find(s => s.id === id);
2535
2624
  if (!schedule) return;
2536
- title = 'Delete this schedule?';
2625
+ title = t('schedules.confirm.scheduleTitle');
2537
2626
  name = schedule.name;
2538
- text = 'It stops running and cannot be restored. The automation it launches stays in Automation Studio.';
2539
- accept = 'Delete schedule';
2627
+ text = t('schedules.confirm.scheduleText');
2628
+ accept = t('schedules.confirm.scheduleAccept');
2540
2629
  } else {
2541
2630
  const group = getGroup(id);
2542
2631
  if (!group) return;
2543
2632
  const count = _schedules.filter(s => s.groupId === id).length;
2544
- title = 'Delete this group?';
2633
+ title = t('schedules.confirm.groupTitle');
2545
2634
  name = group.name;
2546
- text = count ? `Its ${plural(count, 'schedule')} move to Ungrouped and keep running.` : 'The group is empty.';
2547
- accept = 'Delete group';
2635
+ text = count ? tp('schedules.groupEditor.membersStay', count) : t('schedules.groupEditor.empty');
2636
+ accept = t('schedules.confirm.groupAccept');
2548
2637
  }
2549
2638
  _confirm = { kind, id, opener };
2550
2639
  layer.innerHTML = `<div class="sched-dialog" role="alertdialog" aria-modal="true" aria-labelledby="sched-dialog-title" aria-describedby="sched-dialog-text">
@@ -2554,7 +2643,7 @@ function openConfirm({ kind, id, opener }) {
2554
2643
  <p class="sched-help" id="sched-dialog-text">${esc(text)}</p>
2555
2644
  </div>
2556
2645
  <div class="sched-dialog-actions">
2557
- <button type="button" class="sched-btn" data-action="confirm-cancel">Cancel</button>
2646
+ <button type="button" class="sched-btn" data-action="confirm-cancel">${te('schedules.editor.cancel')}</button>
2558
2647
  <button type="button" class="sched-btn sched-btn--danger" data-action="confirm-accept">${ICONS.trash}<span>${esc(accept)}</span></button>
2559
2648
  </div>
2560
2649
  </div>`;
@@ -2581,14 +2670,14 @@ async function acceptConfirm(button) {
2581
2670
  await deleteSchedule(id);
2582
2671
  _schedules = _schedules.filter(s => s.id !== id);
2583
2672
  _expanded.delete(id);
2584
- showToast('Schedule deleted');
2673
+ showToast(t('schedules.toast.deleted'));
2585
2674
  } else {
2586
2675
  await deleteScheduleGroup(id);
2587
2676
  _groups = _groups.filter(g => g.id !== id);
2588
2677
  for (const schedule of _schedules) if (schedule.groupId === id) schedule.groupId = null;
2589
2678
  if (_scope === `group:${id}`) setScope('ungrouped', { keepEditors: true });
2590
2679
  if (_groupEditorId === id) { _groupEditorId = null; _groupEditorDraft = null; }
2591
- showToast('Group deleted');
2680
+ showToast(t('schedules.toast.groupDeleted'));
2592
2681
  }
2593
2682
  _confirm.opener = null;
2594
2683
  closeConfirm();
@@ -2596,17 +2685,17 @@ async function acceptConfirm(button) {
2596
2685
  $('sched-search')?.focus();
2597
2686
  } catch (err) {
2598
2687
  button.disabled = false;
2599
- showToast(`${kind === 'schedule' ? 'Delete' : 'Group delete'} failed: ${err.message}`);
2688
+ showToast(t(kind === 'schedule' ? 'schedules.toast.deleteFailed' : 'schedules.toast.groupDeleteFailed', { error: err.message }));
2600
2689
  }
2601
2690
  }
2602
2691
 
2603
2692
  // ── Groups ──
2604
2693
 
2605
2694
  async function saveGroupFromEditor(id) {
2606
- if (editorsWait()) { showToast('The schedules have not loaded yet: save once they have'); return; }
2695
+ if (editorsWait()) { showToast(t('schedules.toast.notLoaded')); return; }
2607
2696
  collectGroupEditorDraft();
2608
2697
  const name = $('sched-ge-name')?.value?.trim();
2609
- if (!name) { markInvalid('sched-ge-name', 'Group name is required'); return; }
2698
+ if (!name) { markInvalid('sched-ge-name', t('schedules.toast.groupNameRequired')); return; }
2610
2699
  const color = $('sched-ge-color')?.value || null;
2611
2700
  const mutualExclusion = !!$('sched-ge-mutex')?.checked;
2612
2701
  const browserRaw = $('sched-ge-browser')?.value || '';
@@ -2615,7 +2704,7 @@ async function saveGroupFromEditor(id) {
2615
2704
  model: $('sched-ge-model')?.value || null,
2616
2705
  contextMode: $('sched-ge-context')?.value || null,
2617
2706
  effort: $('sched-ge-effort')?.value || null, // 'off' stays explicit
2618
- mcpProfile: $('sched-ge-mcp')?.value || null,
2707
+ tools: readEditorTools('sched-ge-mcp', _groupEditorDraft?.tools),
2619
2708
  codexAccountId: $('sched-ge-account')?.value || null, // read whatever the CLI: hidden is not cleared
2620
2709
  usesBrowser: browserRaw === '' ? null : browserRaw === 'true',
2621
2710
  };
@@ -2624,10 +2713,10 @@ async function saveGroupFromEditor(id) {
2624
2713
  if (!existing || ['profile', 'model', 'contextMode', 'codexAccountId'].some(k => !sameValue(launch[k], existing[k]))) {
2625
2714
  const effectiveProfile = launch.profile || 'claude-code';
2626
2715
  if (!modelChoiceAvailable(effectiveProfile, launch.model, launch.codexAccountId || 'default')) {
2627
- showToast('This model is unavailable for the selected account'); return;
2716
+ showToast(t('schedules.toast.modelUnavailable')); return;
2628
2717
  }
2629
2718
  if (!contextChoiceAvailable(effectiveProfile, launch.model, launch.contextMode, launch.codexAccountId || 'default')) {
2630
- showToast('This context window is unavailable for the selected model and account'); return;
2719
+ showToast(t('schedules.toast.contextUnavailable')); return;
2631
2720
  }
2632
2721
  }
2633
2722
 
@@ -2643,7 +2732,7 @@ async function saveGroupFromEditor(id) {
2643
2732
  for (const key of LAUNCH_FIELDS) if (launch[key] !== null) payload[key] = launch[key];
2644
2733
  const group = await createScheduleGroup(payload);
2645
2734
  if (!_groups.some(g => g.id === group.id)) _groups.push(group);
2646
- showToast('Group created');
2735
+ showToast(t('schedules.toast.groupCreated'));
2647
2736
  if (!stillHere()) { refresh(); return; }
2648
2737
  _groupEditorId = null;
2649
2738
  _groupEditorDraft = null;
@@ -2652,7 +2741,7 @@ async function saveGroupFromEditor(id) {
2652
2741
  render({ frame: true });
2653
2742
  _panel?.querySelector(`.sched-nav-item[data-scope="group:${CSS.escape(group.id)}"]`)?.focus();
2654
2743
  } catch (err) {
2655
- showToast('Group failed: ' + err.message);
2744
+ showToast(t('schedules.toast.groupFailed', { error: err.message }));
2656
2745
  }
2657
2746
  return;
2658
2747
  }
@@ -2674,7 +2763,7 @@ async function saveGroupFromEditor(id) {
2674
2763
  _groupEditorDraft = null;
2675
2764
  render({ frame: true });
2676
2765
  _panel?.querySelector(`.sched-nav-item[data-scope="group:${CSS.escape(id)}"]`)?.focus();
2677
- showToast('No changes to save');
2766
+ showToast(t('schedules.toast.noChanges'));
2678
2767
  return;
2679
2768
  }
2680
2769
  // What each schedule of the group holds as the user saves: a reset afterwards writes only over
@@ -2692,7 +2781,7 @@ async function saveGroupFromEditor(id) {
2692
2781
  _panel?.querySelector(`.sched-nav-item[data-scope="group:${CSS.escape(id)}"]`)?.focus();
2693
2782
  }
2694
2783
  if (!cliChanged) {
2695
- showToast('Group updated');
2784
+ showToast(t('schedules.toast.groupUpdated'));
2696
2785
  } else {
2697
2786
  _cliReport = { groupId: id, profile: launch.profile, pending: true };
2698
2787
  renderNote();
@@ -2700,15 +2789,13 @@ async function saveGroupFromEditor(id) {
2700
2789
  _cliReport = { groupId: id, profile: launch.profile, ...result };
2701
2790
  renderNote();
2702
2791
  const kept = result.changed.length + result.failed.length;
2703
- showToast(kept
2704
- ? `Group updated. ${plural(kept, 'schedule')} kept the old CLI's settings: the group's page names ${kept === 1 ? 'it' : 'them'}`
2705
- : 'Group updated');
2792
+ showToast(kept ? tp('schedules.toast.groupUpdatedKept', kept) : t('schedules.toast.groupUpdated'));
2706
2793
  }
2707
2794
  // The schedules show what the save pushed onto them. From here on nothing is navigated:
2708
2795
  // the user may have opened something else meanwhile, and it stays as it is.
2709
2796
  await loadData();
2710
2797
  } catch (err) {
2711
- showToast('Group update failed: ' + err.message);
2798
+ showToast(t('schedules.toast.groupUpdateFailed', { error: err.message }));
2712
2799
  }
2713
2800
  }
2714
2801
 
@@ -2718,7 +2805,7 @@ const RESET_KEYS = ['profile', ...CLI_DEPENDENT];
2718
2805
  function captureMembers(groupId, launch) {
2719
2806
  return _schedules.filter(s => s.groupId === groupId)
2720
2807
  .map(s => ({ id: s.id, name: s.name || s.id, values: Object.fromEntries(RESET_KEYS.map(k => [k, blankToNull(s[k])])) }))
2721
- .filter(m => RESET_KEYS.some(k => m.values[k] !== null && m.values[k] !== blankToNull(launch[k])));
2808
+ .filter(m => RESET_KEYS.some(k => m.values[k] !== null && !sameValue(m.values[k], launch[k])));
2722
2809
  }
2723
2810
 
2724
2811
  // server.js pushes a group field onto the group's schedules only when the group's own value
@@ -2746,11 +2833,11 @@ async function resetSchedulesForCli(groupId, launch, members) {
2746
2833
  const moved = !fresh || fresh.groupId !== groupId;
2747
2834
  const changed = moved || RESET_KEYS.some(key => {
2748
2835
  const now = blankToNull(fresh[key]);
2749
- return now !== member.values[key] && now !== target(key);
2836
+ return !sameValue(now, member.values[key]) && !sameValue(now, target(key));
2750
2837
  });
2751
2838
  if (changed) { report.changed.push(name); continue; }
2752
2839
  const reset = {};
2753
- for (const key of RESET_KEYS) if (blankToNull(fresh[key]) !== null && blankToNull(fresh[key]) !== target(key)) reset[key] = launch[key];
2840
+ for (const key of RESET_KEYS) if (blankToNull(fresh[key]) !== null && !sameValue(fresh[key], target(key))) reset[key] = launch[key];
2754
2841
  if (!Object.keys(reset).length) continue; // the save's own push already did it
2755
2842
  try {
2756
2843
  const saved = await updateSchedule(member.id, reset);
@@ -2783,7 +2870,7 @@ async function moveGroup(id, direction) {
2783
2870
  const pressed = _panel?.querySelector(`[data-action="${direction < 0 ? 'group-up' : 'group-down'}"]`);
2784
2871
  (pressed && !pressed.disabled ? pressed : _panel?.querySelector(`[data-action="${direction < 0 ? 'group-down' : 'group-up'}"]`))?.focus();
2785
2872
  } catch (err) {
2786
- showToast('Reorder failed: ' + err.message);
2873
+ showToast(t('schedules.toast.reorderFailed', { error: err.message }));
2787
2874
  }
2788
2875
  }
2789
2876
 
@@ -2798,6 +2885,7 @@ function openPromptScheduleModal() {
2798
2885
  document.body.appendChild(_promptBackdrop);
2799
2886
 
2800
2887
  const claudeMeta = getProviderMeta('claude-code');
2888
+ const antigravityMeta = getProviderMeta('antigravity');
2801
2889
  const opencodeMeta = getProviderMeta('opencode');
2802
2890
  const codexMeta = getProviderMeta('codex');
2803
2891
  _promptModal = document.createElement('div');
@@ -2810,11 +2898,11 @@ function openPromptScheduleModal() {
2810
2898
  <div class="sched-prompt-head-text">
2811
2899
  <span class="sched-prompt-icon">${ICONS.sparkle}</span>
2812
2900
  <div>
2813
- <h3 id="sched-prompt-title">Prompt a Schedule</h3>
2814
- <p>Start the schedule draft in a sidepanel.</p>
2901
+ <h3 id="sched-prompt-title">${te('schedules.prompt.title')}</h3>
2902
+ <p>${te('schedules.prompt.sub')}</p>
2815
2903
  </div>
2816
2904
  </div>
2817
- <button class="sched-prompt-close" data-action="prompt-modal-close" aria-label="Close">${ICONS.close}</button>
2905
+ <button class="sched-prompt-close" data-action="prompt-modal-close" aria-label="${te('schedules.prompt.close')}">${ICONS.close}</button>
2818
2906
  </header>
2819
2907
  <div class="sched-prompt-tiles">
2820
2908
  <button class="sched-prompt-tile" style="--provider-color:${claudeMeta.color};" data-action="prompt-modal-pick" data-runtime="claude-code">
@@ -2823,15 +2911,23 @@ function openPromptScheduleModal() {
2823
2911
  <span class="sched-prompt-tile-title">${claudeMeta.label}</span>
2824
2912
  <span class="sched-prompt-tile-sub">Anthropic</span>
2825
2913
  </span>
2826
- <span class="sched-prompt-pill sched-prompt-pill--active">Ready</span>
2914
+ <span class="sched-prompt-pill sched-prompt-pill--active">${te('schedules.prompt.ready')}</span>
2915
+ </button>
2916
+ <button class="sched-prompt-tile" style="--provider-color:${antigravityMeta.color};" data-action="prompt-modal-pick" data-runtime="antigravity">
2917
+ <span class="sched-prompt-tile-logo" aria-hidden="true">${antigravityMeta.icon}</span>
2918
+ <span class="sched-prompt-tile-body">
2919
+ <span class="sched-prompt-tile-title">${antigravityMeta.label}</span>
2920
+ <span class="sched-prompt-tile-sub">Google</span>
2921
+ </span>
2922
+ <span class="sched-prompt-pill sched-prompt-pill--active">${te('schedules.prompt.ready')}</span>
2827
2923
  </button>
2828
2924
  <button class="sched-prompt-tile disabled" style="--provider-color:${opencodeMeta.color};" disabled aria-disabled="true">
2829
2925
  <span class="sched-prompt-tile-logo" aria-hidden="true">${opencodeMeta.icon}</span>
2830
2926
  <span class="sched-prompt-tile-body">
2831
2927
  <span class="sched-prompt-tile-title">${opencodeMeta.label}</span>
2832
- <span class="sched-prompt-tile-sub">Multi-provider</span>
2928
+ <span class="sched-prompt-tile-sub">${te('schedules.prompt.multiProvider')}</span>
2833
2929
  </span>
2834
- <span class="sched-prompt-pill">Planned</span>
2930
+ <span class="sched-prompt-pill">${te('schedules.prompt.planned')}</span>
2835
2931
  </button>
2836
2932
  <button class="sched-prompt-tile disabled" style="--provider-color:${codexMeta.color};" disabled aria-disabled="true">
2837
2933
  <span class="sched-prompt-tile-logo" aria-hidden="true">${codexMeta.icon}</span>
@@ -2839,7 +2935,7 @@ function openPromptScheduleModal() {
2839
2935
  <span class="sched-prompt-tile-title">Codex CLI</span>
2840
2936
  <span class="sched-prompt-tile-sub">OpenAI</span>
2841
2937
  </span>
2842
- <span class="sched-prompt-pill">Planned</span>
2938
+ <span class="sched-prompt-pill">${te('schedules.prompt.planned')}</span>
2843
2939
  </button>
2844
2940
  </div>
2845
2941
  `;
@@ -2881,8 +2977,8 @@ function closePromptScheduleModal() {
2881
2977
  }
2882
2978
 
2883
2979
  async function handlePromptPick(runtime, triggerEl = null) {
2884
- if (runtime !== 'claude-code') {
2885
- showToast('Coming soon — Claude Code only for now');
2980
+ if (runtime !== 'claude-code' && runtime !== 'antigravity') {
2981
+ showToast(t('schedules.toast.comingSoon'));
2886
2982
  return;
2887
2983
  }
2888
2984
  if (triggerEl) {
@@ -2890,58 +2986,75 @@ async function handlePromptPick(runtime, triggerEl = null) {
2890
2986
  triggerEl.setAttribute('aria-busy', 'true');
2891
2987
  }
2892
2988
  try {
2893
- await sendToPanel('/synabun schedule', { newTab: true, tabLabel: 'Schedule Wizard', autoSubmit: true });
2989
+ if (runtime === 'antigravity') {
2990
+ // The Antigravity panel reads `/synabun …` as the bundled skill only on a server whose panel bridge says so
2991
+ // (lib/antigravity/panel-bridge.js 'synabun-command'); an older one would send the words as they are.
2992
+ if (!(await antigravityTakesSynabunCommands())) { showToast(t('schedules.toast.restartForAntigravity')); return; }
2993
+ await openAntigravityWithPrompt('/synabun schedule', { newTab: true, send: true });
2994
+ } else {
2995
+ await sendToPanel('/synabun schedule', { newTab: true, tabLabel: t('schedules.prompt.tabLabel'), autoSubmit: true });
2996
+ }
2894
2997
  } catch (err) {
2895
- showToast('Failed to open Claude panel: ' + err.message);
2998
+ showToast(t(runtime === 'antigravity' ? 'schedules.toast.antigravityPanelFailed' : 'schedules.toast.panelFailed', { error: err.message }));
2896
2999
  } finally {
2897
3000
  closePromptScheduleModal();
2898
3001
  }
2899
3002
  }
2900
3003
 
3004
+ /** Whether this server's Antigravity panel runs `/synabun …` (GET /api/antigravity/panel lists 'synabun-command'). */
3005
+ async function antigravityTakesSynabunCommands() {
3006
+ try {
3007
+ const res = await fetch('/api/antigravity/panel');
3008
+ if (!res.ok) return false;
3009
+ const body = await res.json();
3010
+ return Array.isArray(body?.capabilities) && body.capabilities.includes('synabun-command');
3011
+ } catch { return false; }
3012
+ }
3013
+
2901
3014
  // ── Timers: writes ──
2902
3015
 
2903
3016
  async function createQuickTimerFromForm(runNow) {
2904
3017
  const templateId = $('sched-qt-template')?.value;
2905
- if (!templateId) { markInvalid('sched-qt-template', 'Choose an automation first'); return; }
3018
+ if (!templateId) { markInvalid('sched-qt-template', t('schedules.toast.chooseAutomation')); return; }
2906
3019
  const customVal = Number($('sched-qt-custom-min')?.value);
2907
3020
  const minutes = customVal >= 1 ? customVal : _selectedQtMinutes;
2908
- if (!runNow && !minutes) { showToast('Choose a delay or enter minutes'); return; }
3021
+ if (!runNow && !minutes) { showToast(t('schedules.toast.chooseDelay')); return; }
2909
3022
  const template = getTemplate(templateId);
2910
3023
  const profile = $('sched-qt-profile')?.value || 'claude-code';
2911
3024
  const model = $('sched-qt-model')?.value || null;
2912
3025
  const contextMode = $('sched-qt-context')?.value || 'default';
2913
3026
  if (!modelChoiceAvailable(profile, model, _launchAccount)) {
2914
- showToast('This model is unavailable for the selected account'); return;
3027
+ showToast(t('schedules.toast.modelUnavailable')); return;
2915
3028
  }
2916
3029
  if (!contextChoiceAvailable(profile, model, contextMode, _launchAccount)) {
2917
- showToast('This context window is unavailable for the selected model and account'); return;
3030
+ showToast(t('schedules.toast.contextUnavailable')); return;
2918
3031
  }
2919
3032
  const effortRaw = $('sched-qt-effort')?.value || null;
2920
3033
  const effort = normalizeEffortForProfile(profile, effortRaw);
2921
- const mcpProfile = profile !== 'claude-code' ? ($('sched-qt-mcp')?.value || _launchMcpProfile || null) : null;
3034
+ const tools = strictToolHost(profile) ? readToolPicker($('sched-qt-mcp')) : null;
2922
3035
  const codexAccountId = profile === 'codex' ? ($('sched-qt-account')?.value || _launchAccount || 'default') : undefined;
2923
3036
  const usesBrowser = _qtUsesBrowser !== null ? _qtUsesBrowser : !!template?.usesBrowser;
2924
3037
  try {
2925
3038
  if (runNow) {
2926
3039
  const result = await triggerQuickTimerNow(templateId, {
2927
- profile, model, contextMode, effort, mcpProfile, usesBrowser, codexAccountId,
3040
+ profile, model, contextMode, effort, tools, usesBrowser, codexAccountId,
2928
3041
  sidepanelWindowId: getNativeLoopRouterWindowId(),
2929
3042
  sidepanelClaimToken: getNativeLoopRouterClaimToken(),
2930
3043
  });
2931
- showToast(`Running now: ${result.templateName}`);
3044
+ showToast(t('schedules.toast.runningNow', { name: result.templateName }));
2932
3045
  } else {
2933
- const result = await createQuickTimer(templateId, minutes, { profile, model, contextMode, effort, mcpProfile, usesBrowser, codexAccountId });
3046
+ const result = await createQuickTimer(templateId, minutes, { profile, model, contextMode, effort, tools, usesBrowser, codexAccountId });
2934
3047
  if (!_quickTimers.some(t => t.id === result.timerId)) {
2935
- _quickTimers.push({ id: result.timerId, templateId, templateName: result.templateName, firesAt: result.firesAt, minutes: result.minutes, profile: result.profile, model: result.model, contextMode: result.contextMode, effort: result.effort, mcpProfile: result.mcpProfile, usesBrowser: result.usesBrowser });
3048
+ _quickTimers.push({ id: result.timerId, templateId, templateName: result.templateName, firesAt: result.firesAt, minutes: result.minutes, profile: result.profile, model: result.model, contextMode: result.contextMode, effort: result.effort, tools: result.tools, usesBrowser: result.usesBrowser });
2936
3049
  }
2937
3050
  _selectedQtMinutes = null;
2938
3051
  _panel?.querySelectorAll('[data-action="qt-select"]').forEach(chip => chip.setAttribute('aria-pressed', 'false'));
2939
- showToast(`Timer set: ${result.templateName}`);
3052
+ showToast(t('schedules.toast.timerSet', { name: result.templateName }));
2940
3053
  }
2941
3054
  renderActiveTimersInto();
2942
3055
  renderRail();
2943
3056
  } catch (err) {
2944
- showToast((runNow ? 'Run now' : 'Timer') + ' failed: ' + err.message);
3057
+ showToast(t(runNow ? 'schedules.toast.runNowFailed' : 'schedules.toast.timerFailed', { error: err.message }));
2945
3058
  }
2946
3059
  }
2947
3060
 
@@ -2951,25 +3064,25 @@ async function cancelQuickTimerById(id) {
2951
3064
  _quickTimers = _quickTimers.filter(t => t.id !== id);
2952
3065
  renderActiveTimersInto();
2953
3066
  renderRail();
2954
- showToast('Timer cancelled');
3067
+ showToast(t('schedules.toast.timerCancelled'));
2955
3068
  } catch (err) {
2956
- showToast('Cancel failed: ' + err.message);
3069
+ showToast(t('schedules.toast.cancelFailed', { error: err.message }));
2957
3070
  }
2958
3071
  }
2959
3072
 
2960
3073
  async function createExistingScheduleTimer() {
2961
3074
  const scheduleId = $('sched-existing-timer-schedule')?.value;
2962
3075
  const minutes = Number($('sched-existing-timer-min')?.value);
2963
- if (!scheduleId) { markInvalid('sched-existing-timer-schedule', 'Choose a schedule'); return; }
2964
- if (!minutes || minutes < 1) { markInvalid('sched-existing-timer-min', 'Enter the minutes from now'); return; }
3076
+ if (!scheduleId) { markInvalid('sched-existing-timer-schedule', t('schedules.toast.chooseSchedule')); return; }
3077
+ if (!minutes || minutes < 1) { markInvalid('sched-existing-timer-min', t('schedules.toast.minutesRequired')); return; }
2965
3078
  try {
2966
3079
  const result = await startScheduleTimer(scheduleId, minutes);
2967
3080
  _scheduleTimerData[scheduleId] = { firesAt: result.firesAt, minutes: result.minutes };
2968
3081
  renderActiveTimersInto();
2969
3082
  renderRail();
2970
- showToast('Schedule timer set');
3083
+ showToast(t('schedules.toast.scheduleTimerSet'));
2971
3084
  } catch (err) {
2972
- showToast('Timer failed: ' + err.message);
3085
+ showToast(t('schedules.toast.timerFailed', { error: err.message }));
2973
3086
  }
2974
3087
  }
2975
3088
 
@@ -2979,9 +3092,9 @@ async function cancelExistingScheduleTimer(id) {
2979
3092
  delete _scheduleTimerData[id];
2980
3093
  renderActiveTimersInto();
2981
3094
  renderRail();
2982
- showToast('Timer cancelled');
3095
+ showToast(t('schedules.toast.timerCancelled'));
2983
3096
  } catch (err) {
2984
- showToast('Cancel failed: ' + err.message);
3097
+ showToast(t('schedules.toast.cancelFailed', { error: err.message }));
2985
3098
  }
2986
3099
  }
2987
3100
 
@@ -3033,7 +3146,7 @@ function setupSync() {
3033
3146
  refresh();
3034
3147
  });
3035
3148
  on('sync:schedule:fired', (data) => {
3036
- if (data?.scheduleName) showToast(`Schedule fired: ${data.scheduleName}`);
3149
+ if (data?.scheduleName) showToast(t('schedules.toast.fired', { name: data.scheduleName }));
3037
3150
  if (_panel) scheduleRefresh();
3038
3151
  });
3039
3152
  on('sync:schedule:completed', (data) => {
@@ -3047,7 +3160,7 @@ function setupSync() {
3047
3160
  }
3048
3161
  });
3049
3162
  on('sync:schedule:failed', (data) => {
3050
- if (data?.reason) showToast(`Schedule failed: ${data.reason}`);
3163
+ if (data?.reason) showToast(t('schedules.toast.scheduleFailed', { reason: data.reason }));
3051
3164
  if (_panel) scheduleRefresh();
3052
3165
  });
3053
3166
  on('sync:schedule:timer-set', (data) => {
@@ -3057,7 +3170,7 @@ function setupSync() {
3057
3170
  }
3058
3171
  });
3059
3172
  on('sync:schedule:timer-fired', (data) => {
3060
- if (data?.scheduleName) showToast(`Timer fired: ${data.scheduleName}`);
3173
+ if (data?.scheduleName) showToast(t('schedules.toast.timerFired', { name: data.scheduleName }));
3061
3174
  if (data?.scheduleId) delete _scheduleTimerData[data.scheduleId];
3062
3175
  if (_panel) { renderRail(); renderActiveTimersInto(); }
3063
3176
  });
@@ -3084,29 +3197,31 @@ function setupSync() {
3084
3197
  refresh();
3085
3198
  });
3086
3199
  on('sync:schedule-group:reordered', (data) => {
3087
- if (Array.isArray(data?.groups)) _groups = data.groups;
3200
+ if (Array.isArray(data?.groups)) _groups = migrateLaunchRecords(data.groups);
3088
3201
  refresh();
3089
3202
  });
3090
3203
  on('sync:quick-timer:set', (data) => {
3091
3204
  if (data?.timerId && !_quickTimers.some(t => t.id === data.timerId)) {
3092
- _quickTimers.push({ id: data.timerId, templateName: data.templateName, firesAt: data.firesAt, minutes: data.minutes, profile: data.profile, model: data.model, contextMode: data.contextMode, effort: data.effort, mcpProfile: data.mcpProfile, usesBrowser: data.usesBrowser });
3205
+ _quickTimers.push({ id: data.timerId, templateName: data.templateName, firesAt: data.firesAt, minutes: data.minutes, profile: data.profile, model: data.model, contextMode: data.contextMode, effort: data.effort, tools: data.tools, usesBrowser: data.usesBrowser });
3093
3206
  if (_panel) { renderRail(); renderActiveTimersInto(); }
3094
3207
  }
3095
3208
  });
3096
3209
  on('sync:quick-timer:fired', (data) => {
3097
- if (data?.templateName) showToast(`Timer fired: ${data.templateName}`);
3210
+ if (data?.templateName) showToast(t('schedules.toast.timerFired', { name: data.templateName }));
3098
3211
  if (data?.timerId) _quickTimers = _quickTimers.filter(t => t.id !== data.timerId);
3099
3212
  if (_panel) { renderRail(); renderActiveTimersInto(); }
3100
3213
  });
3101
3214
  on('sync:quick-timer:fired-now', (data) => {
3102
- if (data?.templateName) showToast(`Running now: ${data.templateName}`);
3215
+ if (data?.templateName) showToast(t('schedules.toast.runningNow', { name: data.templateName }));
3103
3216
  });
3104
3217
  on('sync:quick-timer:cancelled', (data) => {
3105
3218
  if (data?.timerId) _quickTimers = _quickTimers.filter(t => t.id !== data.timerId);
3106
3219
  if (_panel) { renderRail(); renderActiveTimersInto(); }
3107
3220
  });
3108
3221
  on('sync:quick-timer:failed', (data) => {
3109
- if (data?.reason) showToast(`Timer failed: ${data.reason}`);
3222
+ if (data?.reason) showToast(t('schedules.toast.timerFailed', { error: data.reason }));
3110
3223
  });
3111
3224
  }
3112
3225
 
3226
+
3227
+ configureToolPickerTranslation(t);