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