synabun 2.0.9 → 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.
- package/CHANGELOG.md +115 -0
- package/README.md +25 -2
- package/THIRD-PARTY-LICENSES.md +68 -4
- package/bin/synabun.js +71 -24
- package/docs/whatsapp.md +2 -2
- package/hooks/claude-code/pre-websearch.mjs +12 -1
- package/hooks/claude-code/shared.mjs +5 -0
- package/hooks/claude-code/stop.mjs +1 -1
- package/launcher.mjs +9 -3
- package/lib/claude-hooks.js +172 -18
- package/lib/client-config-repair.js +17 -8
- package/lib/desktop-app.js +7 -1
- package/lib/external-tools.js +1 -1
- package/lib/first-launch-protection.js +57 -12
- package/lib/mcp-dist.js +176 -0
- package/lib/release-npm-ci.mjs +138 -0
- package/lib/release-publish.mjs +91 -0
- package/lib/release-scan.mjs +50 -0
- package/lib/server-crash-record.js +123 -0
- package/lib/server-exit.js +318 -0
- package/lib/server-supervisor.js +508 -0
- package/lib/start-launcher.js +43 -5
- package/lib/tools-cli.js +45 -0
- package/lib/update-snapshot.js +228 -0
- package/lib/virustotal.mjs +192 -0
- package/lib/windows-crash-event.js +269 -0
- package/mcp-server/dist/http.d.ts +20 -1
- package/mcp-server/dist/http.d.ts.map +1 -1
- package/mcp-server/dist/http.js +59 -12
- package/mcp-server/dist/http.js.map +1 -1
- package/mcp-server/dist/index.d.ts +33 -27
- package/mcp-server/dist/index.d.ts.map +1 -1
- package/mcp-server/dist/index.js +128 -143
- package/mcp-server/dist/index.js.map +1 -1
- package/mcp-server/dist/preload.js +1 -1
- package/mcp-server/dist/preload.js.map +1 -1
- package/mcp-server/dist/services/codex-config-heal.d.ts +6 -2
- package/mcp-server/dist/services/codex-config-heal.d.ts.map +1 -1
- package/mcp-server/dist/services/codex-config-heal.js +81 -28
- package/mcp-server/dist/services/codex-config-heal.js.map +1 -1
- package/mcp-server/dist/services/memory-judge-backfill.d.ts +6 -0
- package/mcp-server/dist/services/memory-judge-backfill.d.ts.map +1 -1
- package/mcp-server/dist/services/memory-judge-backfill.js +7 -1
- package/mcp-server/dist/services/memory-judge-backfill.js.map +1 -1
- package/mcp-server/dist/services/neural-interface.d.ts +16 -10
- package/mcp-server/dist/services/neural-interface.d.ts.map +1 -1
- package/mcp-server/dist/services/neural-interface.js +67 -11
- package/mcp-server/dist/services/neural-interface.js.map +1 -1
- package/mcp-server/dist/services/server-instructions.d.ts +7 -2
- package/mcp-server/dist/services/server-instructions.d.ts.map +1 -1
- package/mcp-server/dist/services/server-instructions.js +20 -62
- package/mcp-server/dist/services/server-instructions.js.map +1 -1
- package/mcp-server/dist/services/tool-catalog.d.ts +120 -0
- package/mcp-server/dist/services/tool-catalog.d.ts.map +1 -0
- package/mcp-server/dist/services/tool-catalog.js +339 -0
- package/mcp-server/dist/services/tool-catalog.js.map +1 -0
- package/mcp-server/dist/services/tool-list-refresh-barrier.d.ts +12 -0
- package/mcp-server/dist/services/tool-list-refresh-barrier.d.ts.map +1 -0
- package/mcp-server/dist/services/tool-list-refresh-barrier.js +40 -0
- package/mcp-server/dist/services/tool-list-refresh-barrier.js.map +1 -0
- package/mcp-server/dist/services/tool-list.d.ts +108 -0
- package/mcp-server/dist/services/tool-list.d.ts.map +1 -0
- package/mcp-server/dist/services/tool-list.js +334 -0
- package/mcp-server/dist/services/tool-list.js.map +1 -0
- package/mcp-server/dist/services/tool-runtime.d.ts +161 -0
- package/mcp-server/dist/services/tool-runtime.d.ts.map +1 -0
- package/mcp-server/dist/services/tool-runtime.js +400 -0
- package/mcp-server/dist/services/tool-runtime.js.map +1 -0
- package/mcp-server/dist/services/whiteboard-geometry.d.ts +26 -2
- package/mcp-server/dist/services/whiteboard-geometry.d.ts.map +1 -1
- package/mcp-server/dist/services/whiteboard-geometry.js +91 -8
- package/mcp-server/dist/services/whiteboard-geometry.js.map +1 -1
- package/mcp-server/dist/tools/agents.d.ts +10 -10
- package/mcp-server/dist/tools/agents.d.ts.map +1 -1
- package/mcp-server/dist/tools/agents.js +22 -13
- package/mcp-server/dist/tools/agents.js.map +1 -1
- package/mcp-server/dist/tools/browser-batch.d.ts +3 -3
- package/mcp-server/dist/tools/image-tools.d.ts +1 -1
- package/mcp-server/dist/tools/image-tools.js +1 -1
- package/mcp-server/dist/tools/skill-tools.d.ts +505 -0
- package/mcp-server/dist/tools/skill-tools.d.ts.map +1 -0
- package/mcp-server/dist/tools/skill-tools.js +639 -0
- package/mcp-server/dist/tools/skill-tools.js.map +1 -0
- package/mcp-server/dist/tools/skill.d.ts +7 -0
- package/mcp-server/dist/tools/skill.d.ts.map +1 -0
- package/mcp-server/dist/tools/skill.js +11 -0
- package/mcp-server/dist/tools/skill.js.map +1 -0
- package/mcp-server/dist/tools/tool-groups.d.ts +68 -0
- package/mcp-server/dist/tools/tool-groups.d.ts.map +1 -0
- package/mcp-server/dist/tools/tool-groups.js +147 -0
- package/mcp-server/dist/tools/tool-groups.js.map +1 -0
- package/mcp-server/dist/tools/whiteboard-format.d.ts.map +1 -1
- package/mcp-server/dist/tools/whiteboard-format.js +7 -4
- package/mcp-server/dist/tools/whiteboard-format.js.map +1 -1
- package/mcp-server/dist/tools/whiteboard-tools.d.ts +64 -13
- package/mcp-server/dist/tools/whiteboard-tools.d.ts.map +1 -1
- package/mcp-server/dist/tools/whiteboard-tools.js +30 -5
- package/mcp-server/dist/tools/whiteboard-tools.js.map +1 -1
- package/mcp-server/package-lock.json +2 -2
- package/mcp-server/package.json +1 -1
- package/mcp-server/src/http.ts +75 -12
- package/mcp-server/src/index.ts +147 -154
- package/mcp-server/src/preload.ts +12 -12
- package/mcp-server/src/services/codex-config-heal.ts +80 -29
- package/mcp-server/src/services/memory-judge-backfill.ts +7 -1
- package/mcp-server/src/services/neural-interface.ts +79 -20
- package/mcp-server/src/services/server-instructions.ts +27 -63
- package/mcp-server/src/services/tool-catalog.ts +403 -0
- package/mcp-server/src/services/tool-list-refresh-barrier.ts +33 -0
- package/mcp-server/src/services/tool-list.ts +410 -0
- package/mcp-server/src/services/tool-runtime.ts +457 -0
- package/mcp-server/src/services/whiteboard-geometry.ts +94 -9
- package/mcp-server/src/tools/agents.ts +22 -14
- package/mcp-server/src/tools/image-tools.ts +1 -1
- package/mcp-server/src/tools/skill-tools.ts +589 -0
- package/mcp-server/src/tools/skill.ts +13 -0
- package/mcp-server/src/tools/tool-groups.ts +164 -0
- package/mcp-server/src/tools/whiteboard-format.ts +9 -3
- package/mcp-server/src/tools/whiteboard-tools.ts +39 -5
- package/neural-interface/i18n/en.json +3680 -206
- package/neural-interface/i18n/pt-BR.json +3628 -157
- package/neural-interface/lib/account-usage.js +5 -3
- package/neural-interface/lib/app-controls-api.js +161 -0
- package/neural-interface/lib/assistant-api.js +148 -35
- package/neural-interface/lib/assistant-brains/claude.js +4 -3
- package/neural-interface/lib/assistant-brains/codex-browser-policy-hook.mjs +12 -2
- package/neural-interface/lib/assistant-brains/codex-route-gate-hook.mjs +32 -9
- package/neural-interface/lib/assistant-brains/codex.js +9 -5
- package/neural-interface/lib/assistant-brains/opencode.js +122 -44
- package/neural-interface/lib/assistant-brains/opencode2-browser-policy/index.js +59 -0
- package/neural-interface/lib/assistant-brains/opencode2-route-gate/index.js +97 -0
- package/neural-interface/lib/assistant-catalog.js +23 -9
- package/neural-interface/lib/assistant-dispatch.js +874 -156
- package/neural-interface/lib/assistant-envelope.js +22 -2
- package/neural-interface/lib/assistant-persona.js +80 -11
- package/neural-interface/lib/assistant-plan-permissions.js +10 -9
- package/neural-interface/lib/assistant-route-gate-codex.js +3 -0
- package/neural-interface/lib/assistant-route-gate.js +6 -4
- package/neural-interface/lib/assistant-router.js +13 -8
- package/neural-interface/lib/assistant-runtime.js +183 -26
- package/neural-interface/lib/assistant-task-prompt.js +78 -14
- package/neural-interface/lib/assistant-tools.js +51 -0
- package/neural-interface/lib/backup-service.js +620 -3
- package/neural-interface/lib/backup-zip-reader.js +10 -5
- package/neural-interface/lib/backup-zip-writer.js +257 -0
- package/neural-interface/lib/browser-execution.js +10 -2
- package/neural-interface/lib/browser-guard-cdp.js +83 -0
- package/neural-interface/lib/browser-network-guard.js +284 -0
- package/neural-interface/lib/browser-policy-roots.js +64 -0
- package/neural-interface/lib/browser-tool-policy.js +186 -31
- package/neural-interface/lib/claude-agent-bridge.js +4 -2
- package/neural-interface/lib/claude-hooks-status.js +103 -0
- package/neural-interface/lib/claude-model-catalog.js +64 -14
- package/neural-interface/lib/claude-temporary.js +5 -11
- package/neural-interface/lib/cli-install.js +18 -138
- package/neural-interface/lib/cli-resolver.js +4 -1
- package/neural-interface/lib/cli-update-planner.js +32 -31
- package/neural-interface/lib/cloudflared/tunnel.js +42 -2
- package/neural-interface/lib/codex-mcp-approvals.js +11 -0
- package/neural-interface/lib/codex-settings-schema.js +109 -47
- package/neural-interface/lib/codex-worker-app-server.js +172 -0
- package/neural-interface/lib/confined-fs.js +30 -3
- package/neural-interface/lib/crash-notice-api.js +98 -0
- package/neural-interface/lib/editor-completion-opencode.js +164 -67
- package/neural-interface/lib/exec-loop-command.js +4 -1
- package/neural-interface/lib/http-guards.js +8 -2
- package/neural-interface/lib/install-details.js +4 -1
- package/neural-interface/lib/launch-cache.js +293 -0
- package/neural-interface/lib/login-start.js +524 -0
- package/neural-interface/lib/mcp-agent-servers.js +67 -0
- package/neural-interface/lib/mcp-client-config.js +0 -29
- package/neural-interface/lib/mcp-external-servers.js +323 -0
- package/neural-interface/lib/{mcp-profile-refresh-coordinator.js → mcp-tools-refresh-coordinator.js} +4 -4
- package/neural-interface/lib/morelogin.js +30 -0
- package/neural-interface/lib/native-loop-providers.js +565 -50
- package/neural-interface/lib/native-loop-runtime.js +287 -41
- package/neural-interface/lib/opencode-config.js +557 -0
- package/neural-interface/lib/opencode-db-schema.js +58 -0
- package/neural-interface/lib/opencode-history.js +49 -204
- package/neural-interface/lib/opencode-mcp-ready.js +108 -0
- package/neural-interface/lib/opencode-native-boundary.js +44 -0
- package/neural-interface/lib/opencode-protocol.js +310 -0
- package/neural-interface/lib/opencode-provider-view.js +81 -0
- package/neural-interface/lib/opencode-runtime-config.js +32 -15
- package/neural-interface/lib/opencode-serve-records.js +624 -0
- package/neural-interface/lib/opencode-v2-client.js +53 -131
- package/neural-interface/lib/opencode-v2-ws-requests.js +35 -195
- package/neural-interface/lib/opencode-worker-activity.js +229 -0
- package/neural-interface/lib/opencode-worker-data.js +27 -0
- package/neural-interface/lib/opencode2-client.js +796 -0
- package/neural-interface/lib/opencode2-proxy.js +76 -0
- package/neural-interface/lib/opencode2-translate.js +1057 -0
- package/neural-interface/lib/owner-authority.js +249 -0
- package/neural-interface/lib/page-session.js +275 -0
- package/neural-interface/lib/peer-identity.js +763 -0
- package/neural-interface/lib/process-identity.js +361 -0
- package/neural-interface/lib/request-gate.js +17 -2
- package/neural-interface/lib/rulesets/api.js +1 -1
- package/neural-interface/lib/rulesets/fs-safe.js +1 -1
- package/neural-interface/lib/rulesets/installer.js +92 -27
- package/neural-interface/lib/rulesets/json-edit.js +1 -1
- package/neural-interface/lib/session-cache-builder.js +18 -10
- package/neural-interface/lib/session-title-generator.js +58 -18
- package/neural-interface/lib/skills/api-v2.js +495 -0
- package/neural-interface/lib/skills/bundle.js +418 -0
- package/neural-interface/lib/skills/catalog.js +294 -0
- package/neural-interface/lib/skills/creation-api.js +191 -0
- package/neural-interface/lib/skills/creation-brief.js +247 -0
- package/neural-interface/lib/skills/creation-sources.js +267 -0
- package/neural-interface/lib/skills/creation.js +843 -0
- package/neural-interface/lib/skills/deploy.js +564 -0
- package/neural-interface/lib/skills/digest.js +119 -0
- package/neural-interface/lib/skills/discover.js +111 -0
- package/neural-interface/lib/skills/draft-confinement.js +214 -0
- package/neural-interface/lib/skills/eval-confinement.js +397 -0
- package/neural-interface/lib/skills/eval-sandbox.js +280 -0
- package/neural-interface/lib/skills/eval-trace.js +119 -0
- package/neural-interface/lib/skills/evals-api.js +212 -0
- package/neural-interface/lib/skills/evals.js +1573 -0
- package/neural-interface/lib/skills/export.js +200 -0
- package/neural-interface/lib/skills/frontmatter.js +64 -0
- package/neural-interface/lib/skills/health.js +75 -0
- package/neural-interface/lib/skills/json-edit.js +165 -0
- package/neural-interface/lib/skills/known-bridge-digests.json +387 -0
- package/neural-interface/lib/skills/known-bridges.js +96 -0
- package/neural-interface/lib/skills/library.js +823 -0
- package/neural-interface/lib/skills/local-store.js +21 -0
- package/neural-interface/lib/skills/migration.js +433 -0
- package/neural-interface/lib/skills/operations.js +45 -0
- package/neural-interface/lib/skills/ownership.js +427 -0
- package/neural-interface/lib/skills/packages.js +88 -0
- package/neural-interface/lib/skills/reviews.js +212 -0
- package/neural-interface/lib/skills/revision.js +80 -0
- package/neural-interface/lib/skills/roots.js +262 -0
- package/neural-interface/lib/skills/runtime-checks.js +355 -0
- package/neural-interface/lib/skills/scan-assets.js +252 -0
- package/neural-interface/lib/skills/scan-context.js +26 -0
- package/neural-interface/lib/skills/scan-shell.js +324 -0
- package/neural-interface/lib/skills/scan.js +364 -0
- package/neural-interface/lib/skills/scanners.js +54 -0
- package/neural-interface/lib/skills/service.js +522 -0
- package/neural-interface/lib/skills/telemetry.js +113 -0
- package/neural-interface/lib/sqlite-integrity-worker.js +22 -0
- package/neural-interface/lib/style-guide/api.js +4 -3
- package/neural-interface/lib/tool-lists.js +124 -0
- package/neural-interface/lib/untrusted-file-headers.js +27 -0
- package/neural-interface/lib/whatsapp/bridge.js +6 -3
- package/neural-interface/lib/worker-confinement.js +1192 -0
- package/neural-interface/lib/worker-guard-setting.js +56 -0
- package/neural-interface/lib/worker-mcp-channel.js +175 -0
- package/neural-interface/lib/worker-relay-ports.js +227 -0
- package/neural-interface/lib/worker-resume.js +229 -0
- package/neural-interface/lib/worker-sandbox.js +299 -0
- package/neural-interface/package-lock.json +1354 -50
- package/neural-interface/package.json +6 -2
- package/neural-interface/public/index.html +5 -3
- package/neural-interface/public/index2d.html +19 -11
- package/neural-interface/public/onboarding.html +2 -2
- package/neural-interface/public/shared/account-menu.js +8 -5
- package/neural-interface/public/shared/agent-runtime-options.js +76 -16
- package/neural-interface/public/shared/api.js +1527 -1336
- package/neural-interface/public/shared/app-frame.js +192 -0
- package/neural-interface/public/shared/assistant/asst-agents-dock.js +205 -19
- package/neural-interface/public/shared/assistant/asst-api.js +11 -3
- package/neural-interface/public/shared/assistant/asst-ask-styles.js +270 -0
- package/neural-interface/public/shared/assistant/asst-ask-tail.js +118 -0
- package/neural-interface/public/shared/assistant/asst-ask.js +397 -0
- package/neural-interface/public/shared/assistant/asst-brain-picker.js +241 -112
- package/neural-interface/public/shared/assistant/asst-composer.js +1 -1
- package/neural-interface/public/shared/assistant/asst-control.js +157 -26
- package/neural-interface/public/shared/assistant/asst-followups-card.js +383 -0
- package/neural-interface/public/shared/assistant/asst-followups.js +417 -0
- package/neural-interface/public/shared/assistant/asst-hosts.js +49 -2
- package/neural-interface/public/shared/assistant/asst-limit-dock.js +40 -0
- package/neural-interface/public/shared/assistant/asst-limit-wait.js +294 -0
- package/neural-interface/public/shared/assistant/asst-limit.js +168 -0
- package/neural-interface/public/shared/assistant/asst-mascot.js +49 -12
- package/neural-interface/public/shared/assistant/asst-menu.js +84 -12
- package/neural-interface/public/shared/assistant/asst-panel.js +303 -44
- package/neural-interface/public/shared/assistant/asst-rack-styles.js +93 -58
- package/neural-interface/public/shared/assistant/asst-redact.js +186 -0
- package/neural-interface/public/shared/assistant/asst-render.js +770 -100
- package/neural-interface/public/shared/assistant/asst-route.js +83 -12
- package/neural-interface/public/shared/assistant/asst-run-stage.js +231 -0
- package/neural-interface/public/shared/assistant/asst-sanitize.js +193 -3
- package/neural-interface/public/shared/assistant/asst-scene-styles.js +273 -0
- package/neural-interface/public/shared/assistant/asst-scenes.js +530 -0
- package/neural-interface/public/shared/assistant/asst-sidepanel-styles.js +3 -15
- package/neural-interface/public/shared/assistant/asst-sidepanel.js +67 -4
- package/neural-interface/public/shared/assistant/asst-slash.js +9 -7
- package/neural-interface/public/shared/assistant/asst-state.js +25 -15
- package/neural-interface/public/shared/assistant/asst-stop.js +127 -0
- package/neural-interface/public/shared/assistant/asst-styles.js +102 -21
- package/neural-interface/public/shared/assistant/asst-tool-kinds.js +165 -13
- package/neural-interface/public/shared/automation-context.js +5 -2
- package/neural-interface/public/shared/cc-dropdown.js +550 -0
- package/neural-interface/public/shared/cdx/cdx-context-menu.js +10 -9
- package/neural-interface/public/shared/cdx/cdx-copy.js +16 -0
- package/neural-interface/public/shared/cdx/cdx-output-model.js +22 -1
- package/neural-interface/public/shared/cdx/cdx-output.js +8 -7
- package/neural-interface/public/shared/cdx/cdx-panel.js +58 -150
- package/neural-interface/public/shared/cdx/cdx-protocol.js +28 -1
- package/neural-interface/public/shared/cdx/cdx-render.js +62 -32
- package/neural-interface/public/shared/cdx/cdx-requests.js +3 -3
- package/neural-interface/public/shared/cdx/cdx-skill-hints.js +54 -0
- package/neural-interface/public/shared/cdx/cdx-styles.js +15 -3
- package/neural-interface/public/shared/cdx/cdx-tabs.js +201 -74
- package/neural-interface/public/shared/cdx/cdx-ws.js +0 -1
- package/neural-interface/public/shared/cli-install-view.js +4 -8
- package/neural-interface/public/shared/cli-status.js +2 -2
- package/neural-interface/public/shared/constants.js +2 -2
- package/neural-interface/public/shared/context-menu-areas.js +34 -0
- package/neural-interface/public/shared/context-menu-logic.js +243 -0
- package/neural-interface/public/shared/context-menu-model.js +366 -0
- package/neural-interface/public/shared/context-menu.js +372 -0
- package/neural-interface/public/shared/cp/cp-context-menu.js +10 -9
- package/neural-interface/public/shared/cp/cp-markdown.js +4 -57
- package/neural-interface/public/shared/crash-notice-view.js +168 -0
- package/neural-interface/public/shared/data-cache.js +223 -0
- package/neural-interface/public/shared/desktop-bridge.js +11 -0
- package/neural-interface/public/shared/desktop-titlebar-view.js +78 -1
- package/neural-interface/public/shared/desktop-titlebar.js +214 -28
- package/neural-interface/public/shared/desktop-update-view.js +35 -5
- package/neural-interface/public/shared/editor/editor-panel-logic.js +2 -2
- package/neural-interface/public/shared/file-links.js +73 -8
- package/neural-interface/public/shared/html-shell.js +78 -86
- package/neural-interface/public/shared/i18n.js +2 -0
- package/neural-interface/public/shared/launch-prefetch.js +59 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-approvals.js +7 -7
- package/neural-interface/public/shared/ocp-v2/ocp-v2-binding.js +4 -4
- package/neural-interface/public/shared/ocp-v2/ocp-v2-caps.js +3 -2
- package/neural-interface/public/shared/ocp-v2/ocp-v2-changes-logic.js +3 -101
- package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +9 -2
- package/neural-interface/public/shared/ocp-v2/ocp-v2-compact-button.js +1 -3
- package/neural-interface/public/shared/ocp-v2/ocp-v2-composer-logic.js +2 -6
- package/neural-interface/public/shared/ocp-v2/ocp-v2-context-menu.js +11 -11
- package/neural-interface/public/shared/ocp-v2/ocp-v2-context-model.js +0 -3
- package/neural-interface/public/shared/ocp-v2/ocp-v2-events.js +8 -4
- package/neural-interface/public/shared/ocp-v2/ocp-v2-manager.js +7 -7
- package/neural-interface/public/shared/ocp-v2/ocp-v2-modelpicker.js +54 -17
- package/neural-interface/public/shared/ocp-v2/ocp-v2-outcome-notifications.js +16 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +140 -254
- package/neural-interface/public/shared/ocp-v2/ocp-v2-plan.js +1 -2
- package/neural-interface/public/shared/ocp-v2/ocp-v2-projectbar.js +97 -203
- package/neural-interface/public/shared/ocp-v2/ocp-v2-providers.js +11 -9
- package/neural-interface/public/shared/ocp-v2/ocp-v2-rehydrate.js +15 -4
- package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +7 -22
- package/neural-interface/public/shared/ocp-v2/ocp-v2-send-logic.js +401 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +20 -13
- package/neural-interface/public/shared/ocp-v2/ocp-v2-session-actions.js +8 -61
- package/neural-interface/public/shared/ocp-v2/ocp-v2-sessions-logic.js +53 -82
- package/neural-interface/public/shared/ocp-v2/ocp-v2-slash-hints.js +16 -15
- package/neural-interface/public/shared/ocp-v2/ocp-v2-state.js +8 -21
- package/neural-interface/public/shared/ocp-v2/ocp-v2-status-logic.js +1 -99
- package/neural-interface/public/shared/ocp-v2/ocp-v2-status.js +5 -35
- package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +48 -40
- package/neural-interface/public/shared/ocp-v2/{ocp-v2-profile-inheritance.js → ocp-v2-tools-inheritance.js} +16 -16
- package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +205 -62
- package/neural-interface/public/shared/opencode-providers.js +39 -0
- package/neural-interface/public/shared/page-session.js +96 -0
- package/neural-interface/public/shared/registry.js +5 -3
- package/neural-interface/public/shared/retired-2d.js +3 -0
- package/neural-interface/public/shared/schedules-view.js +452 -0
- package/neural-interface/public/shared/settings/settings-app-pane.js +335 -0
- package/neural-interface/public/shared/settings/settings-app.js +243 -0
- package/neural-interface/public/shared/settings/settings-claude-hooks.js +135 -0
- package/neural-interface/public/shared/settings/settings-data.js +90 -0
- package/neural-interface/public/shared/settings/settings-ia.js +23 -6
- package/neural-interface/public/shared/settings/settings-opencode.js +89 -0
- package/neural-interface/public/shared/settings/settings-skill-folders-pane.js +265 -0
- package/neural-interface/public/shared/settings/settings-skill-folders.js +130 -0
- package/neural-interface/public/shared/settings/settings-worker-guard.js +72 -0
- package/neural-interface/public/shared/sidepanel-session-status.js +49 -0
- package/neural-interface/public/shared/skills/ss-args.js +96 -0
- package/neural-interface/public/shared/skills/ss-creation.js +219 -0
- package/neural-interface/public/shared/skills/ss-dialog.js +130 -0
- package/neural-interface/public/shared/skills/ss-doc.js +173 -0
- package/neural-interface/public/shared/skills/ss-eval-adapters.js +269 -0
- package/neural-interface/public/shared/skills/ss-eval-grade.js +359 -0
- package/neural-interface/public/shared/skills/ss-eval-optimise.js +153 -0
- package/neural-interface/public/shared/skills/ss-eval-plan.js +315 -0
- package/neural-interface/public/shared/skills/ss-evals.js +417 -0
- package/neural-interface/public/shared/skills/ss-form.js +159 -0
- package/neural-interface/public/shared/skills/ss-frontmatter.js +376 -0
- package/neural-interface/public/shared/skills/ss-ids.js +32 -0
- package/neural-interface/public/shared/skills/ss-lint.js +141 -0
- package/neural-interface/public/shared/skills/ss-presets.js +117 -0
- package/neural-interface/public/shared/skills/ss-profiles.js +475 -0
- package/neural-interface/public/shared/skills/ss-resources.js +165 -0
- package/neural-interface/public/shared/skills/ss-review-view.js +190 -0
- package/neural-interface/public/shared/skills/ss-tabs.js +79 -0
- package/neural-interface/public/shared/skills/ss-unicode.js +135 -0
- package/neural-interface/public/shared/skills/studio/sx-actions.js +222 -0
- package/neural-interface/public/shared/skills/studio/sx-cm.js +101 -0
- package/neural-interface/public/shared/skills/studio/sx-create-ai-view.js +749 -0
- package/neural-interface/public/shared/skills/studio/sx-create-ai.js +236 -0
- package/neural-interface/public/shared/skills/studio/sx-create-view.js +254 -0
- package/neural-interface/public/shared/skills/studio/sx-create.js +126 -0
- package/neural-interface/public/shared/skills/studio/sx-diff-view.js +43 -0
- package/neural-interface/public/shared/skills/studio/sx-diff.js +133 -0
- package/neural-interface/public/shared/skills/studio/sx-docs.js +480 -0
- package/neural-interface/public/shared/skills/studio/sx-dom.js +229 -0
- package/neural-interface/public/shared/skills/studio/sx-edit-view.js +848 -0
- package/neural-interface/public/shared/skills/studio/sx-editor-model.js +193 -0
- package/neural-interface/public/shared/skills/studio/sx-errors.js +22 -0
- package/neural-interface/public/shared/skills/studio/sx-geometry.js +82 -0
- package/neural-interface/public/shared/skills/studio/sx-import.js +262 -0
- package/neural-interface/public/shared/skills/studio/sx-inspector.js +308 -0
- package/neural-interface/public/shared/skills/studio/sx-install-flow.js +279 -0
- package/neural-interface/public/shared/skills/studio/sx-install.js +296 -0
- package/neural-interface/public/shared/skills/studio/sx-library-view.js +639 -0
- package/neural-interface/public/shared/skills/studio/sx-library.js +475 -0
- package/neural-interface/public/shared/skills/studio/sx-main.js +681 -0
- package/neural-interface/public/shared/skills/studio/sx-operations-view.js +98 -0
- package/neural-interface/public/shared/skills/studio/sx-operations.js +11 -0
- package/neural-interface/public/shared/skills/studio/sx-plan.js +158 -0
- package/neural-interface/public/shared/skills/studio/sx-publish-view.js +621 -0
- package/neural-interface/public/shared/skills/studio/sx-review-view.js +369 -0
- package/neural-interface/public/shared/skills/studio/sx-review.js +148 -0
- package/neural-interface/public/shared/skills/studio/sx-scan.js +35 -0
- package/neural-interface/public/shared/skills/studio/sx-snippets.js +96 -0
- package/neural-interface/public/shared/skills/studio/sx-subject.js +248 -0
- package/neural-interface/public/shared/skills/studio/sx-test-view.js +994 -0
- package/neural-interface/public/shared/skills/studio/sx-test.js +328 -0
- package/neural-interface/public/shared/skills/studio/sx-versions-view.js +241 -0
- package/neural-interface/public/shared/skills/studio/sx-workspace-view.js +425 -0
- package/neural-interface/public/shared/styleguide/sg-panel.js +4 -2
- package/neural-interface/public/shared/styles.css +2065 -2150
- package/neural-interface/public/shared/synabun-mascot.js +1115 -9
- package/neural-interface/public/shared/tool-list-picker.js +55 -0
- package/neural-interface/public/shared/tool-list-storage.js +24 -0
- package/neural-interface/public/shared/tool-list-view.js +112 -0
- package/neural-interface/public/shared/tour/tour-actor.js +268 -0
- package/neural-interface/public/shared/tour/tour-beats.js +336 -0
- package/neural-interface/public/shared/tour/tour-engine.js +884 -0
- package/neural-interface/public/shared/tour/tour-ink.js +390 -0
- package/neural-interface/public/shared/tour/tour-stage.js +440 -0
- package/neural-interface/public/shared/tour/tour-state.js +40 -0
- package/neural-interface/public/shared/tour/tour-tips.js +99 -0
- package/neural-interface/public/shared/tour/tour.css +144 -0
- package/neural-interface/public/shared/ui-assistant.js +27 -5
- package/neural-interface/public/shared/ui-automation-studio.js +155 -152
- package/neural-interface/public/shared/ui-bookmarks.js +2 -1
- package/neural-interface/public/shared/ui-claude-panel.js +153 -13
- package/neural-interface/public/shared/ui-codex-panel.js +1 -0
- package/neural-interface/public/shared/ui-command-runner.js +6 -3
- package/neural-interface/public/shared/ui-context-menu.js +295 -0
- package/neural-interface/public/shared/ui-cost-widget.js +57 -59
- package/neural-interface/public/shared/ui-crash-notice.js +187 -0
- package/neural-interface/public/shared/ui-detail.js +21 -9
- package/neural-interface/public/shared/ui-file-links.js +1 -0
- package/neural-interface/public/shared/ui-image-gallery.js +56 -52
- package/neural-interface/public/shared/ui-invite.js +18 -2
- package/neural-interface/public/shared/ui-keybinds.js +26 -1
- package/neural-interface/public/shared/ui-link.js +9 -9
- package/neural-interface/public/shared/ui-menubar.js +229 -40
- package/neural-interface/public/shared/ui-native-loop-router-state.js +5 -0
- package/neural-interface/public/shared/ui-native-loop-router.js +9 -1
- package/neural-interface/public/shared/ui-navbar.js +4 -24
- package/neural-interface/public/shared/ui-opencode-panel-v2.js +1 -0
- package/neural-interface/public/shared/ui-panels.js +96 -65
- package/neural-interface/public/shared/ui-schedules-studio.js +2267 -953
- package/neural-interface/public/shared/ui-sessions.js +20 -7
- package/neural-interface/public/shared/ui-settings.js +679 -496
- package/neural-interface/public/shared/ui-sidebar.js +4 -4
- package/neural-interface/public/shared/ui-sidepanel-session-entry.js +13 -0
- package/neural-interface/public/shared/ui-sidepanel-sessions.js +56 -5
- package/neural-interface/public/shared/ui-sidepanel-state.js +23 -12
- package/neural-interface/public/shared/ui-sidepanel-tray.js +84 -8
- package/neural-interface/public/shared/ui-sidepanel-windows.js +16 -11
- package/neural-interface/public/shared/ui-skills.js +42 -2781
- package/neural-interface/public/shared/ui-styleguide.js +5 -2
- package/neural-interface/public/shared/ui-sync.js +22 -5
- package/neural-interface/public/shared/ui-terminal.js +159 -199
- package/neural-interface/public/shared/ui-tool-updates.js +3 -1
- package/neural-interface/public/shared/ui-tooltip.js +10 -5
- package/neural-interface/public/shared/ui-trash.js +14 -5
- package/neural-interface/public/shared/ui-tutorial-layout.js +3 -117
- package/neural-interface/public/shared/ui-tutorial-preview.js +103 -0
- package/neural-interface/public/shared/ui-tutorial-steps.js +3 -61
- package/neural-interface/public/shared/ui-tutorial.css +47 -100
- package/neural-interface/public/shared/ui-tutorial.js +285 -499
- package/neural-interface/public/shared/ui-update.js +121 -16
- package/neural-interface/public/shared/ui-whiteboard.js +313 -59
- package/neural-interface/public/shared/ui-worker-end.js +4 -0
- package/neural-interface/public/shared/ui-workspaces.js +3 -2
- package/neural-interface/public/shared/view-variant.js +66 -0
- package/neural-interface/public/shared/whatsapp/wa-view.js +1 -1
- package/neural-interface/public/shared/whiteboard-list.js +172 -0
- package/neural-interface/public/shared/worker-end.js +10 -3
- package/neural-interface/public/shared/worker-restart.js +12 -0
- package/neural-interface/public/sidepanel-session.html +1 -1
- package/neural-interface/public/variant/2d/background.js +2 -1
- package/neural-interface/public/variant/2d/context-menu.js +8 -2
- package/neural-interface/public/variant/2d/graph.js +42 -20
- package/neural-interface/public/variant/2d/hulls.js +14 -9
- package/neural-interface/public/variant/2d/lasso.js +11 -7
- package/neural-interface/public/variant/2d/main.js +36 -2
- package/neural-interface/public/variant/2d/minimap.js +11 -13
- package/neural-interface/public/variant/3d/main.js +72 -6
- package/neural-interface/public/variant/3d/map-controls.js +24 -9
- package/neural-interface/public/vendor/caveat/OFL.txt +93 -0
- package/neural-interface/public/vendor/caveat/caveat-latin-400-normal.woff2 +0 -0
- package/neural-interface/public/vendor/caveat/caveat-latin-700-normal.woff2 +0 -0
- package/neural-interface/public/vendor/caveat/caveat.css +17 -0
- package/neural-interface/public/vendor/caveat/manifest.json +12 -0
- package/neural-interface/public/vendor/highlight/LICENSE +29 -0
- package/neural-interface/public/vendor/highlight/github-dark-dimmed-11.12.0.min.css +9 -0
- package/neural-interface/public/vendor/highlight/highlight-11.12.0.min.js +1262 -0
- package/neural-interface/public/vendor/highlight/manifest.json +5 -0
- package/neural-interface/public/vendor/html2canvas/LICENSE +22 -0
- package/neural-interface/public/vendor/html2canvas/html2canvas-1.4.1.min.js +20 -0
- package/neural-interface/public/vendor/html2canvas/manifest.json +4 -0
- package/neural-interface/public/vendor/marked/LICENSE.md +44 -0
- package/neural-interface/public/vendor/marked/manifest.json +4 -0
- package/neural-interface/public/vendor/marked/marked-14.1.4.esm.js +2502 -0
- package/neural-interface/public/vendor/mermaid/LICENSE +21 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/abnfDiagram-PAR5IXJI.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/architecture-7GRP2DOG-M7G3SWCO.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/architectureDiagram-2PNWDENW.mjs +36 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/blockDiagram-PUWGUBOA.mjs +129 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/c4Diagram-3NLNHZP5.mjs +38 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-2NMSUPJH.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-32MCUW2P.mjs +206 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-3NPNU5GP.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-3SMGFRGC.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-4CAJECRU.mjs +14 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-4OADUZ5Q.mjs +10 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-5IMINLNL.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-5IQCK5RY.mjs +3 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-5MVFYWFU.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-AQ6EADP3.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-AZZRMDJM.mjs +15 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-CLS4B6BI.mjs +168 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-DZP67EKU.mjs +70 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-EICP7K5X.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-GDY2UAB4.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-H7VHZCWX.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-IJBDOHL6.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-JFFVMJIA.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-JQRUD6KW.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-JZGDI5JK.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-KD6SPIPN.mjs +2 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-NF366YI6.mjs +231 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-NUDPKBEF.mjs +32 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-OJINMHTS.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-Q6NGPIAH.mjs +2 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-QJSWEUOL.mjs +132 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-R7JDMUKV.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-RCXAM2GK.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-RT4MAWR3.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-SFMINX4W.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-SLDQ463B.mjs +88 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-STOV2HOB.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-T5OCTHI4.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-TCEHX7R3.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-VQC2EELJ.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-VRZKL3EH.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-VTHDQ7BV.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-VY2OSTJR.mjs +2 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-WUBWJARI.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-YBMQ5JGJ.mjs +2 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-YDMLL4PJ.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-Z6V44ARX.mjs +62 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-ZJJ7VQJP.mjs +339 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-ZNLK7HRR.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-ZQN7UHIL.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-ZS6VBONO.mjs +174 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-ZUNWM646.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/classDiagram-FGAMPOII.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/classDiagram-v2-AI5RJEK4.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/cose-bilkent-Q72C2OJ3.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/cynefin-OW5HDTMX-6OCTWOGG.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/cynefinDiagram-2OOWUY4M.mjs +62 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/dagre-MPVFI544.mjs +4 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/diagram-AE4NWXFW.mjs +24 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/diagram-BUJID5UG.mjs +41 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/diagram-C44CZ5NI.mjs +24 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/diagram-EYOLEXXS.mjs +30 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/diagram-P7B6PWOJ.mjs +3 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/ebnfDiagram-FTPZ3PV6.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/erDiagram-H3ERR26V.mjs +99 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/eventmodeling-NTZA5JFV-WKSWQHFS.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/flowDiagram-YHGXBVSY.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/ganttDiagram-RWFTB7ED.mjs +292 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/gitGraph-4MIJSDKK-JH2HKYUO.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/gitGraphDiagram-MAR6GKOP.mjs +106 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/info-A6RAGUB7-3TX3OFWW.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/infoDiagram-LV547AFH.mjs +2 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/ishikawaDiagram-PUJQGNDS.mjs +70 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/journeyDiagram-EW5QJT4W.mjs +139 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/kanban-definition-JSQERAZB.mjs +89 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/katex-CZ4GXH2S.mjs +257 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/mindmap-definition-VBJCLQLM.mjs +96 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/packet-AYTQ26CC-MKII6P3Y.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/pegDiagram-7WILOQTJ.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/pie-WAS4IAKB-6B4OPMJ5.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/pieDiagram-EFA7CBSB.mjs +39 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/quadrantDiagram-WARVYA7P.mjs +7 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/radar-RG4KPBEZ-UPTR3XUL.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/railroad-74A4TZTK-MEFUPFMS.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/railroad-abnf-HS5TGJTU-R5YG2S5D.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/railroad-ebnf-LZEXJU2U-G5QEX2L6.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/railroad-peg-WCYAUIDC-H5I2ZRY2.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/railroadDiagram-5NIQVNLQ.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/requirementDiagram-SXREGOKZ.mjs +84 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/sankeyDiagram-VV74M2VP.mjs +40 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/sequenceDiagram-52ZFIFDL.mjs +162 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/sizeCapture-FGA5AGYP.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/stateDiagram-3QAKPIVI.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/stateDiagram-v2-HN2P4RDZ.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/swimlanes-QLVP42QD.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/swimlanesDiagram-JKPDBFCN.mjs +8 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/timeline-definition-GS45XAXW.mjs +120 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/treeView-Q6P3EWNA-ZRJGANNQ.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/treemap-WGGIJYW6-K64VN5AS.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/vennDiagram-QIQU2ZRJ.mjs +34 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/wardley-WFR3VGLG-D67OWCCH.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/wardleyDiagram-U6YNM66N.mjs +78 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/xychartDiagram-XJAZEDNM.mjs +7 -0
- package/neural-interface/public/vendor/mermaid/manifest.json +4 -0
- package/neural-interface/public/vendor/mermaid/mermaid-11.17.2.esm.min.mjs +14 -0
- package/neural-interface/public/vendor/yaml/LICENSE +13 -0
- package/neural-interface/public/vendor/yaml/manifest.json +4 -0
- package/neural-interface/public/vendor/yaml/yaml-2.9.1.min.js +137 -0
- package/neural-interface/scripts/build-vendor.mjs +116 -3
- package/neural-interface/server.js +4834 -2541
- package/neural-interface/templates/rulesets/core.md +1 -1
- package/neural-interface/templates/rulesets/legacy-hashes.json +18 -0
- package/neural-interface/templates/rulesets/manifest.json +8 -8
- package/npm-shrinkwrap.json +18 -12
- package/package.json +16 -3
- package/postinstall.js +9 -4
- package/preuninstall.js +1 -1
- package/setup.js +97 -322
- package/skills/skill-forge/SKILL.md +177 -0
- package/skills/skill-forge/evals/evals.json +54 -0
- package/skills/skill-forge/references/package.md +105 -0
- package/skills/skill-forge/references/review-and-recovery.md +63 -0
- package/skills/skill-forge/references/runtime-profiles.md +20 -0
- package/skills/skill-forge/references/sources-and-capabilities.md +51 -0
- package/skills/synabun/SKILL.md +10 -7
- package/skills/synabun/modules/audit.md +1 -5
- package/skills/synabun/modules/changelog.md +1 -5
- package/skills/synabun/modules/idea.md +1 -1
- package/skills/synabun/modules/memorize.md +6 -9
- package/skills/synabun/modules/schedule.md +5 -5
- package/mcp-server/dist/services/profiles.d.ts +0 -120
- package/mcp-server/dist/services/profiles.d.ts.map +0 -1
- package/mcp-server/dist/services/profiles.js +0 -387
- package/mcp-server/dist/services/profiles.js.map +0 -1
- package/mcp-server/dist/tools/leonardo-browser-tools.d.ts +0 -97
- package/mcp-server/dist/tools/leonardo-browser-tools.d.ts.map +0 -1
- package/mcp-server/dist/tools/leonardo-browser-tools.js +0 -228
- package/mcp-server/dist/tools/leonardo-browser-tools.js.map +0 -1
- package/mcp-server/dist/tools/leonardo.d.ts +0 -13
- package/mcp-server/dist/tools/leonardo.d.ts.map +0 -1
- package/mcp-server/dist/tools/leonardo.js +0 -21
- package/mcp-server/dist/tools/leonardo.js.map +0 -1
- package/mcp-server/dist/tools/profile.d.ts +0 -17
- package/mcp-server/dist/tools/profile.d.ts.map +0 -1
- package/mcp-server/dist/tools/profile.js +0 -114
- package/mcp-server/dist/tools/profile.js.map +0 -1
- package/mcp-server/src/services/profiles.ts +0 -428
- package/mcp-server/src/tools/leonardo-browser-tools.ts +0 -291
- package/mcp-server/src/tools/leonardo.ts +0 -30
- package/mcp-server/src/tools/profile.ts +0 -122
- package/neural-interface/lib/assistant-brains/opencode-browser-policy.js +0 -47
- package/neural-interface/lib/assistant-brains/opencode-route-gate.js +0 -62
- package/neural-interface/lib/mcp-profile-servers.js +0 -120
- package/neural-interface/public/shared/ui-tutorial-draw.js +0 -311
- package/skills/leonardo/SKILL.md +0 -168
- package/skills/leonardo/icon.svg +0 -1
- package/skills/leonardo/modules/browser-guide.md +0 -378
- package/skills/leonardo/modules/image-prompter.md +0 -283
- package/skills/leonardo/modules/model-advisor.md +0 -100
- package/skills/leonardo/modules/prompt-library.md +0 -110
- package/skills/leonardo/modules/style-guide.md +0 -186
- package/skills/leonardo/modules/video-prompter.md +0 -317
|
@@ -1,8 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
import { migrateStoredTools } from './tool-list-storage.js';
|
|
2
|
+
import { configureToolPickerTranslation, toolPickerHTML, readToolPicker, updateToolPicker, taskToolHost as strictToolHost, toolWord } from './tool-list-picker.js';
|
|
3
|
+
// SynaBun Neural Interface — the Schedules window.
|
|
4
|
+
// A rail of views (Up next, All, Needs attention, Paused, Timers, Activity) and of groups, one
|
|
5
|
+
// list of schedules with their details on demand, and the editors in place of the list. What a
|
|
6
|
+
// view lists and how a schedule's state reads is decided in schedules-view.js (DOM-free).
|
|
2
7
|
|
|
3
8
|
import { emit, on } from './state.js';
|
|
4
9
|
import { storage } from './storage.js';
|
|
10
|
+
import { frameRect, clampBox } from './app-frame.js';
|
|
11
|
+
import { t, tp, getLocale } from './i18n.js';
|
|
12
|
+
import { KEYS } from './constants.js';
|
|
5
13
|
import { isGuest, hasPermission, showGuestToast } from './ui-sync.js';
|
|
14
|
+
import { registerLaunchPrefetch } from './launch-prefetch.js';
|
|
6
15
|
import { sendToPanel } from './ui-claude-panel.js';
|
|
7
16
|
import { getProviderMeta } from './provider-icons.js';
|
|
8
17
|
import {
|
|
@@ -12,10 +21,10 @@ import {
|
|
|
12
21
|
import {
|
|
13
22
|
CLI_PROFILES,
|
|
14
23
|
ensureModelsForProfile,
|
|
15
|
-
|
|
24
|
+
fetchToolCatalog,
|
|
16
25
|
fetchCodexAccounts,
|
|
17
26
|
getCachedCodexAccounts,
|
|
18
|
-
|
|
27
|
+
getCachedToolCatalog,
|
|
19
28
|
getEffortLevelsForProfile,
|
|
20
29
|
getModelsForProfile,
|
|
21
30
|
formatModelOptionLabel,
|
|
@@ -33,6 +42,7 @@ import {
|
|
|
33
42
|
import {
|
|
34
43
|
fetchLoopTemplates,
|
|
35
44
|
fetchSchedules,
|
|
45
|
+
fetchSchedule,
|
|
36
46
|
createSchedule,
|
|
37
47
|
updateSchedule,
|
|
38
48
|
deleteSchedule,
|
|
@@ -50,26 +60,81 @@ import {
|
|
|
50
60
|
cancelQuickTimer,
|
|
51
61
|
triggerQuickTimerNow,
|
|
52
62
|
} from './api.js';
|
|
63
|
+
import {
|
|
64
|
+
LIST_SCOPES,
|
|
65
|
+
SORTS,
|
|
66
|
+
VIEW_SCOPES,
|
|
67
|
+
dayLabel,
|
|
68
|
+
describeCron,
|
|
69
|
+
formatAgo,
|
|
70
|
+
formatCountdown,
|
|
71
|
+
fullDateTime,
|
|
72
|
+
groupIdOf,
|
|
73
|
+
groupSummary,
|
|
74
|
+
inScope,
|
|
75
|
+
lastRunState,
|
|
76
|
+
launchFacts,
|
|
77
|
+
listJoin,
|
|
78
|
+
matchesQuery,
|
|
79
|
+
oneLine,
|
|
80
|
+
scopeCounts,
|
|
81
|
+
setSchedulesViewTranslator,
|
|
82
|
+
shortWhen,
|
|
83
|
+
sortSchedules,
|
|
84
|
+
weekdayName,
|
|
85
|
+
whenLabel,
|
|
86
|
+
} from './schedules-view.js';
|
|
87
|
+
import {
|
|
88
|
+
closeDropdowns,
|
|
89
|
+
controlOf,
|
|
90
|
+
dropdownHTML,
|
|
91
|
+
setDropdownItems,
|
|
92
|
+
setDropdownValue,
|
|
93
|
+
wireDropdowns,
|
|
94
|
+
} from './cc-dropdown.js';
|
|
53
95
|
|
|
54
96
|
const $ = (id) => document.getElementById(id);
|
|
55
97
|
const PANEL_ID = 'schedules-studio-panel';
|
|
56
98
|
const POLL_INTERVAL = 1000;
|
|
99
|
+
const LAYOUT_KEY = KEYS.PANEL_PREFIX + 'schedules-window';
|
|
100
|
+
const SCOPE_KEY = 'sched-view-scope';
|
|
101
|
+
const SORT_KEY = 'sched-view-sort';
|
|
102
|
+
const COLLAPSED_KEY = 'sched-collapsed-groups';
|
|
103
|
+
const REOPEN_KEY = 'synabun-schedules-reopen'; // sessionStorage, for one reload
|
|
104
|
+
const DEFAULT_GROUP_COLOR = '#4fc3f7';
|
|
57
105
|
|
|
58
106
|
let _panel = null;
|
|
59
107
|
let _backdrop = null;
|
|
60
|
-
let
|
|
108
|
+
let _abort = null; // the window's document/window listeners
|
|
109
|
+
let _opener = null; // focus goes back here when the window closes
|
|
61
110
|
let _templates = [];
|
|
62
111
|
let _schedules = [];
|
|
63
112
|
let _groups = [];
|
|
64
113
|
let _scheduleTimerData = {};
|
|
65
114
|
let _quickTimers = [];
|
|
66
|
-
let
|
|
67
|
-
let
|
|
68
|
-
let
|
|
69
|
-
let
|
|
115
|
+
let _loading = false; // the window waits for a load (opening it, Try again)
|
|
116
|
+
let _pendingAction = null; // an editor asked for before the data is there opens once it is
|
|
117
|
+
let _loadError = null; // what the latest load could not fetch, as a sentence
|
|
118
|
+
let _loadSeq = 0;
|
|
119
|
+
const _loaded = { schedules: false, templates: false, groups: false }; // each list arrived once
|
|
120
|
+
let _timersSeq = 0;
|
|
121
|
+
let _timersLoaded = false;
|
|
122
|
+
let _timersError = null;
|
|
123
|
+
let _cliReport = null; // what a group's change of CLI did to its schedules (resetSchedulesForCli)
|
|
124
|
+
let _frame = null; // what the main area is built for: list, timers, activity, editor
|
|
125
|
+
let _scope = 'all';
|
|
126
|
+
let _sort = 'next';
|
|
127
|
+
let _query = '';
|
|
128
|
+
const _expanded = new Set();
|
|
129
|
+
const _collapsed = new Set();
|
|
130
|
+
let _scheduleEditor = null; // the draft while the schedule editor is open
|
|
131
|
+
let _groupEditorId = null; // the group being edited; null with a draft = a new group
|
|
70
132
|
let _groupEditorDraft = null;
|
|
133
|
+
let _confirm = null; // { kind: 'schedule' | 'group', id, opener }
|
|
71
134
|
let _draggingScheduleId = null;
|
|
72
135
|
let _pollTimer = null;
|
|
136
|
+
let _refreshTimer = null;
|
|
137
|
+
let _lastAutoRefresh = 0;
|
|
73
138
|
let _selectedQtMinutes = null;
|
|
74
139
|
let _qtUsesBrowser = null;
|
|
75
140
|
let _syncReady = false;
|
|
@@ -87,55 +152,108 @@ let _launchContextMode = _storedLaunchSelection.contextMode || 'default';
|
|
|
87
152
|
storage.setItem('as-launch-model', _launchModel || '');
|
|
88
153
|
storage.setItem('as-launch-context-mode', _launchContextMode);
|
|
89
154
|
let _launchEffort = storage.getItem('as-launch-effort') || 'off';
|
|
90
|
-
let
|
|
155
|
+
let _launchTools = (() => { try { return JSON.parse(storage.getItem('as-launch-tools') || 'null'); } catch { return null; } })();
|
|
91
156
|
let _launchAccount = storage.getItem('as-launch-account') || 'default'; // Codex ChatGPT account (CODEX_HOME)
|
|
92
157
|
let _codexAccountsRequested = false; // lazy-fetch guard for account selects
|
|
93
158
|
|
|
94
|
-
const _s = (d) => `<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">${d}</svg>`;
|
|
159
|
+
const _s = (d) => `<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${d}</svg>`;
|
|
95
160
|
|
|
96
161
|
const ICONS = {
|
|
97
162
|
clock: _s('<circle cx="8" cy="8" r="6"/><path d="M8 4.5V8l2.5 2.5"/>'),
|
|
163
|
+
next: _s('<path d="M2.5 8h7.5"/><path d="M7 4.5 10.5 8 7 11.5"/><path d="M13.5 3.5v9"/>'),
|
|
98
164
|
plus: _s('<path d="M8 3v10M3 8h10"/>'),
|
|
99
165
|
bolt: _s('<path d="M9 2L4 9h4l-1 5 5-7H8l1-5z"/>'),
|
|
100
166
|
edit: _s('<path d="M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z"/>'),
|
|
101
167
|
script: _s('<path d="M3.5 2h6.5l3 3v8.5a.5.5 0 0 1-.5.5h-9a.5.5 0 0 1-.5-.5v-11a.5.5 0 0 1 .5-.5z"/><path d="M5.5 7h5M5.5 9.5h5M5.5 12h3"/>'),
|
|
102
168
|
close: _s('<path d="M4 4l8 8M12 4l-8 8"/>'),
|
|
103
|
-
pause: _s('<path d="M5
|
|
169
|
+
pause: _s('<path d="M5.5 3.5v9M10.5 3.5v9"/>'),
|
|
104
170
|
play: _s('<path d="M5 3l8 5-8 5V3z"/>'),
|
|
105
171
|
back: _s('<path d="M10 3L5 8l5 5"/>'),
|
|
106
|
-
|
|
172
|
+
chevron: _s('<path d="M6 3.5 10.5 8 6 12.5"/>'),
|
|
173
|
+
up: _s('<path d="M8 12.5v-9M4.5 7 8 3.5 11.5 7"/>'),
|
|
174
|
+
down: _s('<path d="M8 3.5v9M4.5 9 8 12.5 11.5 9"/>'),
|
|
107
175
|
folder: _s('<path d="M2.5 5h4l1 1.5h6v6a1 1 0 0 1-1 1h-10a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1z"/>'),
|
|
108
|
-
list: _s('<path d="M5 4h8M5 8h8M5 12h8"/><path d="M2.5 4h.01M2.5 8h.01M2.5 12h.01"/>'),
|
|
176
|
+
list: _s('<path d="M5.5 4h8M5.5 8h8M5.5 12h8"/><path d="M2.5 4h.01M2.5 8h.01M2.5 12h.01"/>'),
|
|
177
|
+
alert: _s('<path d="M8 2.3 14.2 13H1.8L8 2.3z"/><path d="M8 6.5v3"/><path d="M8 11.4h.01"/>'),
|
|
178
|
+
timer: _s('<circle cx="8" cy="9" r="5"/><path d="M8 6.5V9l1.6 1"/><path d="M6.5 2h3"/>'),
|
|
179
|
+
activity: _s('<path d="M1.5 8h3l2-4.5 3 9 2-4.5h3"/>'),
|
|
180
|
+
search: _s('<circle cx="7" cy="7" r="4.5"/><path d="m10.5 10.5 3.5 3.5"/>'),
|
|
181
|
+
trash: _s('<path d="M2.5 4.5h11M6.5 4.5V3h3v1.5M4 4.5l.7 9h6.6l.7-9"/>'),
|
|
182
|
+
move: _s('<path d="M8 2v12M2 8h12"/><path d="M6 4 8 2l2 2M6 12l2 2 2-2M4 6 2 8l2 2M12 6l2 2-2 2"/>'),
|
|
109
183
|
eye: _s('<path d="M1.5 8s2.5-4 6.5-4 6.5 4 6.5 4-2.5 4-6.5 4-6.5-4-6.5-4z"/><circle cx="8" cy="8" r="2"/>'),
|
|
110
184
|
sparkle: _s('<path d="M8 2L9.2 6.8L14 8L9.2 9.2L8 14L6.8 9.2L2 8L6.8 6.8L8 2z"/>'),
|
|
111
185
|
lock: _s('<rect x="4" y="7" width="8" height="7" rx="1"/><path d="M5.5 7V5a2.5 2.5 0 0 1 5 0v2"/>'),
|
|
186
|
+
ok: _s('<circle cx="8" cy="8" r="6"/><path d="M5.5 8.2 7.2 9.9 10.6 6.3"/>'),
|
|
187
|
+
failed: _s('<circle cx="8" cy="8" r="6"/><path d="M5.9 5.9l4.2 4.2M10.1 5.9l-4.2 4.2"/>'),
|
|
188
|
+
started: _s('<circle cx="8" cy="8" r="6"/><path d="M6.9 5.7 10.3 8l-3.4 2.3z"/>'),
|
|
189
|
+
stopped: _s('<circle cx="8" cy="8" r="6"/><rect x="6" y="6" width="4" height="4" rx=".5"/>'),
|
|
190
|
+
never: _s('<circle cx="8" cy="8" r="6" stroke-dasharray="2.2 2.2"/>'),
|
|
112
191
|
};
|
|
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 };
|
|
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
|
+
|
|
202
|
+
const VIEWS = {
|
|
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 },
|
|
209
|
+
};
|
|
210
|
+
const viewLabel = (scope) => t(`schedules.views.${scope}`);
|
|
113
211
|
|
|
212
|
+
// A preset is a phrase (`key`) or a time of day, which reads the same in every language.
|
|
114
213
|
const CRON_PRESET_GROUPS = [
|
|
115
|
-
{ group: '
|
|
116
|
-
{
|
|
117
|
-
{
|
|
118
|
-
{
|
|
119
|
-
{
|
|
120
|
-
{
|
|
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 * * *' },
|
|
121
220
|
] },
|
|
122
|
-
{ group: '
|
|
123
|
-
{
|
|
124
|
-
{
|
|
125
|
-
{
|
|
126
|
-
{
|
|
127
|
-
{
|
|
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 * * *' },
|
|
128
227
|
] },
|
|
129
|
-
{ group: '
|
|
130
|
-
{
|
|
131
|
-
{
|
|
132
|
-
{
|
|
133
|
-
{
|
|
134
|
-
{
|
|
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' },
|
|
135
234
|
] },
|
|
136
235
|
];
|
|
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);
|
|
137
248
|
|
|
138
|
-
|
|
249
|
+
function readJson(key, fallback) {
|
|
250
|
+
try { const value = JSON.parse(storage.getItem(key) || 'null'); return value ?? fallback; } catch { return fallback; }
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function readScope() {
|
|
254
|
+
const saved = storage.getItem(SCOPE_KEY);
|
|
255
|
+
return saved && (VIEW_SCOPES.includes(saved) || saved === 'ungrouped' || saved.startsWith('group:')) ? saved : 'all';
|
|
256
|
+
}
|
|
139
257
|
|
|
140
258
|
function esc(value) {
|
|
141
259
|
return String(value ?? '').replace(/[&<>"']/g, (c) => ({
|
|
@@ -143,11 +261,18 @@ function esc(value) {
|
|
|
143
261
|
}[c]));
|
|
144
262
|
}
|
|
145
263
|
|
|
264
|
+
const domId = (value) => String(value ?? '').replace(/[^\w-]/g, '_');
|
|
265
|
+
// A choice of a popup (cc-dropdown.js). The one shown is the last marked `selected`, else the
|
|
266
|
+
// first, as in a <select>: a saved value a list lacks is added as a choice of its own, marked.
|
|
267
|
+
const opt = (value, label, selected = false) => ({ value: value ?? '', label: String(label ?? ''), selected: !!selected });
|
|
268
|
+
const cssColor = (value) => (value && CSS.supports('color', String(value)) ? String(value) : 'var(--stg-off)');
|
|
269
|
+
|
|
146
270
|
function showToast(msg) {
|
|
147
271
|
const existing = document.querySelector('.as-toast');
|
|
148
272
|
if (existing) existing.remove();
|
|
149
273
|
const toast = document.createElement('div');
|
|
150
274
|
toast.className = 'as-toast';
|
|
275
|
+
toast.setAttribute('role', 'status');
|
|
151
276
|
toast.textContent = msg;
|
|
152
277
|
document.body.appendChild(toast);
|
|
153
278
|
requestAnimationFrame(() => toast.classList.add('show'));
|
|
@@ -165,201 +290,362 @@ function getGroup(id) {
|
|
|
165
290
|
return _groups.find(g => g.id === id) || null;
|
|
166
291
|
}
|
|
167
292
|
|
|
168
|
-
function
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const pad = (n) => String(n).padStart(2, '0');
|
|
173
|
-
let dayPart = '';
|
|
174
|
-
if (dayOfWeek === '*') dayPart = 'Every day';
|
|
175
|
-
else if (dayOfWeek === '1-5') dayPart = 'Weekdays';
|
|
176
|
-
else if (dayOfWeek === '0,6') dayPart = 'Weekends';
|
|
177
|
-
else dayPart = dayOfWeek.split(',').map(d => DAY_NAMES[+d] || d).join(', ');
|
|
178
|
-
|
|
179
|
-
const minStep = minute.match(/^\*\/(\d+)$/);
|
|
180
|
-
if (minStep && hour === '*') return `${dayPart}, every ${minStep[1]} minutes`;
|
|
181
|
-
const hourStep = hour.match(/^\*\/(\d+)$/);
|
|
182
|
-
if (hourStep) return `${dayPart}, every ${hourStep[1]} hours at :${pad(+minute)}`;
|
|
183
|
-
if (hour.includes(',')) return `${dayPart} at ${hour.split(',').map(h => `${pad(+h)}:${pad(+minute)}`).join(', ')}`;
|
|
184
|
-
if (hour === '*') return `${dayPart}, every hour at :${pad(+minute)}`;
|
|
185
|
-
return `${dayPart} at ${pad(+hour)}:${pad(+minute)}`;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function formatNextRun(nextRun) {
|
|
189
|
-
if (!nextRun) return 'N/A';
|
|
190
|
-
const diffMs = new Date(nextRun) - Date.now();
|
|
191
|
-
if (diffMs <= 0) return 'Overdue';
|
|
192
|
-
const totalSec = Math.floor(diffMs / 1000);
|
|
193
|
-
const days = Math.floor(totalSec / 86400);
|
|
194
|
-
const hours = Math.floor((totalSec % 86400) / 3600);
|
|
195
|
-
const minutes = Math.floor((totalSec % 3600) / 60);
|
|
196
|
-
const seconds = totalSec % 60;
|
|
197
|
-
if (days > 0) return hours ? `${days}d ${hours}h` : `${days}d`;
|
|
198
|
-
if (hours > 0) return minutes ? `${hours}h ${minutes}m` : `${hours}h`;
|
|
199
|
-
if (minutes > 0) return seconds ? `${minutes}m ${seconds}s` : `${minutes}m`;
|
|
200
|
-
return `${seconds}s`;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function formatDateTime(value) {
|
|
204
|
-
if (!value) return 'Never';
|
|
205
|
-
try { return new Date(value).toLocaleString(); } catch { return value; }
|
|
293
|
+
function orderedGroups() {
|
|
294
|
+
return _groups.map((group, index) => ({ group, index }))
|
|
295
|
+
.sort((a, b) => ((a.group.order ?? a.index) - (b.group.order ?? b.index)) || (a.index - b.index))
|
|
296
|
+
.map(entry => entry.group);
|
|
206
297
|
}
|
|
207
298
|
|
|
299
|
+
const groupIds = () => new Set(_groups.map(g => g.id));
|
|
300
|
+
const editorOpen = () => !!_scheduleEditor || !!_groupEditorDraft;
|
|
301
|
+
// Until the schedules, the automations and the groups have each loaded once, the list cannot say
|
|
302
|
+
// what is where and an editor could only offer less than was saved: neither is shown.
|
|
303
|
+
const dataIncomplete = () => !_loaded.schedules || !_loaded.templates || !_loaded.groups;
|
|
304
|
+
const editorsWait = () => _loading || dataIncomplete();
|
|
305
|
+
const countsKnown = () => !_loading && !dataIncomplete();
|
|
306
|
+
const listScope = () => LIST_SCOPES.includes(_scope) || _scope === 'ungrouped' || !!groupIdOf(_scope);
|
|
307
|
+
|
|
208
308
|
function formatTimerCountdown(firesAtISO) {
|
|
209
309
|
const diff = new Date(firesAtISO) - Date.now();
|
|
210
|
-
if (diff <= 0) return 'firing';
|
|
310
|
+
if (diff <= 0) return t('schedules.timers.firing');
|
|
211
311
|
const mins = Math.ceil(diff / 60_000);
|
|
212
312
|
if (mins >= 60) {
|
|
213
313
|
const h = Math.floor(mins / 60);
|
|
214
314
|
const m = mins % 60;
|
|
215
|
-
return m ?
|
|
315
|
+
return m ? t('schedules.timers.countdownHours', { h, m }) : t('schedules.timers.countdownHour', { h });
|
|
216
316
|
}
|
|
217
|
-
return
|
|
317
|
+
return t('schedules.timers.countdownMinutes', { m: mins });
|
|
218
318
|
}
|
|
219
319
|
|
|
220
|
-
|
|
221
|
-
if (!schedule.enabled) return '<span class="sched-badge sched-badge--paused">Paused</span>';
|
|
222
|
-
if (schedule.lastRunResult === 'template_missing') return '<span class="sched-badge sched-badge--error">Missing Template</span>';
|
|
223
|
-
if (schedule.lastRunResult?.startsWith('error:')) return '<span class="sched-badge sched-badge--error">Error</span>';
|
|
224
|
-
return '<span class="sched-badge sched-badge--ok">Active</span>';
|
|
225
|
-
}
|
|
320
|
+
// ── Opening and closing ──
|
|
226
321
|
|
|
227
322
|
export function initSchedulesStudio() {
|
|
323
|
+
// Launch: the lists are read once the page has painted (launch-prefetch.js), so the first open paints from them.
|
|
324
|
+
registerLaunchPrefetch('schedules', () => (isGuest() && !hasPermission('automations') ? undefined : loadData()), { order: 74 });
|
|
228
325
|
on('automations:open-schedules', (opts) => openPanel(opts || {}));
|
|
229
326
|
on('schedules:open', (opts) => openPanel(opts || {}));
|
|
327
|
+
on('schedules:toggle', () => (_panel ? closePanel() : openPanel({})));
|
|
230
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; }
|
|
231
346
|
}
|
|
232
347
|
|
|
233
348
|
async function openPanel(opts = {}) {
|
|
234
349
|
if (isGuest() && !hasPermission('automations')) {
|
|
235
|
-
showGuestToast('
|
|
350
|
+
showGuestToast(t('schedules.window.guestDisabled'));
|
|
236
351
|
return;
|
|
237
352
|
}
|
|
238
353
|
if (_panel) {
|
|
239
|
-
|
|
240
|
-
|
|
354
|
+
// Rebuilt only when the request changes what shows: an open editor keeps what is typed in it.
|
|
355
|
+
if (applyOpenOptions(opts)) render({ frame: true });
|
|
356
|
+
focusInitial();
|
|
241
357
|
return;
|
|
242
358
|
}
|
|
243
359
|
|
|
360
|
+
_opener = document.activeElement;
|
|
361
|
+
// The view, order and folded groups the user left (UI state, synced like the rest).
|
|
362
|
+
_scope = readScope();
|
|
363
|
+
_sort = SORTS.includes(storage.getItem(SORT_KEY)) ? storage.getItem(SORT_KEY) : 'next';
|
|
364
|
+
_collapsed.clear();
|
|
365
|
+
for (const key of readJson(COLLAPSED_KEY, [])) if (typeof key === 'string') _collapsed.add(key);
|
|
244
366
|
_backdrop = document.createElement('div');
|
|
245
|
-
_backdrop.className = 'studio-backdrop';
|
|
367
|
+
_backdrop.className = 'studio-backdrop sched-backdrop';
|
|
246
368
|
document.body.appendChild(_backdrop);
|
|
247
369
|
|
|
248
|
-
_panel = document.createElement('
|
|
370
|
+
_panel = document.createElement('section');
|
|
249
371
|
_panel.className = 'schedules-studio-panel glass resizable';
|
|
250
372
|
_panel.id = PANEL_ID;
|
|
373
|
+
_panel.setAttribute('role', 'region');
|
|
374
|
+
_panel.setAttribute('aria-labelledby', 'sched-title');
|
|
251
375
|
_panel.innerHTML = buildPanelHTML();
|
|
252
376
|
document.body.appendChild(_panel);
|
|
253
|
-
|
|
254
|
-
|
|
377
|
+
document.body.classList.add('schedules-window-open');
|
|
378
|
+
placePanel();
|
|
255
379
|
|
|
256
380
|
wirePanel();
|
|
257
|
-
|
|
381
|
+
// Before the options: an editor asked for waits for the data (applyOpenOptions). Lists that arrived once (an
|
|
382
|
+
// earlier open, the launch read; sync keeps them while closed) paint at once, and the load below brings them
|
|
383
|
+
// up to date in place (loadFinished → refresh); only a first open shows "Loading schedules…".
|
|
384
|
+
_loading = dataIncomplete();
|
|
258
385
|
applyOpenOptions(opts);
|
|
259
|
-
render();
|
|
386
|
+
render({ frame: true });
|
|
387
|
+
if (!_loading) revealExpanded(); // what loadFinished does after a first load
|
|
388
|
+
emit('schedules:window', { open: true });
|
|
389
|
+
startPolling();
|
|
390
|
+
const panel = _panel;
|
|
260
391
|
requestAnimationFrame(() => {
|
|
392
|
+
if (_panel !== panel) return;
|
|
261
393
|
_backdrop?.classList.add('open');
|
|
262
|
-
|
|
394
|
+
panel.classList.add('open');
|
|
395
|
+
focusInitial();
|
|
263
396
|
});
|
|
264
|
-
|
|
397
|
+
await loadData(); // loadFinished() takes it from there
|
|
265
398
|
}
|
|
266
399
|
|
|
400
|
+
/** Applies what an open request asks for; true when that changes what the window shows. */
|
|
267
401
|
function applyOpenOptions(opts = {}) {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
if (opts.
|
|
271
|
-
|
|
272
|
-
|
|
402
|
+
const before = `${_scope}|${!!_scheduleEditor}|${!!_groupEditorDraft}`;
|
|
403
|
+
const fromTab = { cron: listScope() ? _scope : 'all', timers: 'timers', groups: 'all', activity: 'activity' };
|
|
404
|
+
if (opts.tab && fromTab[opts.tab]) setScope(fromTab[opts.tab], { keepEditors: true });
|
|
405
|
+
if (opts.scope) setScope(opts.scope, { keepEditors: true });
|
|
406
|
+
if (opts.groupId) setScope(`group:${opts.groupId}`, { keepEditors: true });
|
|
407
|
+
if ((opts.action === 'new-schedule' || opts.action === 'new-group') && editorsWait()) {
|
|
408
|
+
// An editor is built from the automations and groups: it opens once they have loaded.
|
|
409
|
+
_pendingAction = opts.action;
|
|
410
|
+
} else if (opts.action === 'new-schedule') {
|
|
411
|
+
closeEditors();
|
|
412
|
+
_scheduleEditor = { groupId: groupIdOf(_scope) };
|
|
273
413
|
} else if (opts.action === 'new-group') {
|
|
274
|
-
|
|
275
|
-
|
|
414
|
+
closeEditors();
|
|
415
|
+
openGroupEditor(null);
|
|
276
416
|
} else if (opts.action === 'quick-timer') {
|
|
277
|
-
|
|
278
|
-
|
|
417
|
+
closeEditors();
|
|
418
|
+
setScope('timers');
|
|
279
419
|
}
|
|
280
|
-
|
|
420
|
+
return before !== `${_scope}|${!!_scheduleEditor}|${!!_groupEditorDraft}` || !!opts.action;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function placePanel() {
|
|
424
|
+
const saved = readJson(LAYOUT_KEY, null);
|
|
425
|
+
if (saved && saved.w > 0 && saved.h > 0) {
|
|
426
|
+
_panel.style.width = saved.w + 'px';
|
|
427
|
+
_panel.style.height = saved.h + 'px';
|
|
428
|
+
}
|
|
429
|
+
const width = _panel.offsetWidth;
|
|
430
|
+
const height = _panel.offsetHeight;
|
|
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';
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function saveLayout() {
|
|
441
|
+
if (!_panel) return;
|
|
442
|
+
const rect = _panel.getBoundingClientRect();
|
|
443
|
+
try {
|
|
444
|
+
storage.setItem(LAYOUT_KEY, JSON.stringify({
|
|
445
|
+
x: Math.round(rect.left), y: Math.round(rect.top), w: Math.round(rect.width), h: Math.round(rect.height),
|
|
446
|
+
}));
|
|
447
|
+
} catch { /* the default layout is used next time */ }
|
|
281
448
|
}
|
|
282
449
|
|
|
283
450
|
function closePanel() {
|
|
284
451
|
if (!_panel) return;
|
|
452
|
+
saveLayout();
|
|
285
453
|
stopPolling();
|
|
454
|
+
clearTimeout(_refreshTimer);
|
|
286
455
|
closePromptScheduleModal();
|
|
287
|
-
|
|
288
|
-
|
|
456
|
+
_abort?.abort();
|
|
457
|
+
_abort = null;
|
|
289
458
|
_backdrop?.remove();
|
|
290
459
|
_backdrop = null;
|
|
291
460
|
_panel.remove();
|
|
292
461
|
_panel = null;
|
|
462
|
+
_frame = null;
|
|
463
|
+
_confirm = null;
|
|
464
|
+
_loading = false;
|
|
465
|
+
_pendingAction = null;
|
|
293
466
|
_scheduleEditor = null;
|
|
467
|
+
_groupEditorId = null;
|
|
468
|
+
_groupEditorDraft = null;
|
|
294
469
|
_draggingScheduleId = null;
|
|
470
|
+
document.body.classList.remove('schedules-window-open');
|
|
471
|
+
emit('schedules:window', { open: false });
|
|
472
|
+
const opener = _opener;
|
|
473
|
+
_opener = null;
|
|
474
|
+
if (opener?.isConnected && typeof opener.focus === 'function') opener.focus();
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function focusInitial() {
|
|
478
|
+
if (!_panel) return;
|
|
479
|
+
const failed = _loadError && dataIncomplete() ? _panel.querySelector('#sched-list [data-action="retry-load"]') : null;
|
|
480
|
+
const target = (_scheduleEditor && $('sched-ed-name')) || (_groupEditorDraft && $('sched-ge-name')) || failed
|
|
481
|
+
|| $('sched-search') || $('sched-load-note')?.querySelector('button') || $('sched-qt-template');
|
|
482
|
+
controlOf(target)?.focus({ preventScroll: true });
|
|
295
483
|
}
|
|
296
484
|
|
|
297
485
|
function buildPanelHTML() {
|
|
298
486
|
return `
|
|
299
|
-
|
|
300
|
-
<
|
|
301
|
-
<div class="resize-handle resize-handle-b" data-resize="b"></div>
|
|
302
|
-
<div class="resize-handle resize-handle-l" data-resize="l"></div>
|
|
303
|
-
<div class="resize-handle resize-handle-tl" data-resize="tl"></div>
|
|
304
|
-
<div class="resize-handle resize-handle-tr" data-resize="tr"></div>
|
|
305
|
-
<div class="resize-handle resize-handle-bl" data-resize="bl"></div>
|
|
306
|
-
<div class="resize-handle resize-handle-br" data-resize="br"></div>
|
|
307
|
-
|
|
308
|
-
<div class="sched-header drag-handle" data-drag="${PANEL_ID}">
|
|
487
|
+
${['t', 'r', 'b', 'l', 'tl', 'tr', 'bl', 'br'].map(edge => `<div aria-hidden="true" class="resize-handle resize-handle-${edge}" data-resize="${edge}"></div>`).join('')}
|
|
488
|
+
<header class="sched-header drag-handle" data-drag="${PANEL_ID}">
|
|
309
489
|
<div class="sched-header-left">
|
|
310
490
|
<span class="sched-header-icon">${ICONS.clock}</span>
|
|
311
|
-
<h3
|
|
491
|
+
<h3 id="sched-title">${te('schedules.window.title')}</h3>
|
|
312
492
|
<span class="sched-count" id="sched-total-count"></span>
|
|
313
493
|
</div>
|
|
314
|
-
<
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
</div>
|
|
318
|
-
<button class="backdrop-toggle-btn" id="sched-backdrop-toggle" data-tooltip="Toggle backdrop">${ICONS.eye}</button>
|
|
319
|
-
<button class="sched-close" id="sched-close">×</button>
|
|
320
|
-
</div>
|
|
321
|
-
|
|
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>
|
|
496
|
+
</header>
|
|
322
497
|
<div class="sched-body">
|
|
323
|
-
<
|
|
324
|
-
<
|
|
498
|
+
<nav class="sched-rail" id="sched-rail" aria-label="${te('schedules.window.railLabel')}"></nav>
|
|
499
|
+
<section class="sched-main" aria-labelledby="sched-view-title">
|
|
500
|
+
<!-- A window too narrow for the rail: its views and groups as a popup, and New group. Outside
|
|
501
|
+
the frame below, which each view and editor replaces, so it is there in every one. -->
|
|
502
|
+
<div class="sched-narrow-nav" id="sched-narrow-nav">
|
|
503
|
+
<span class="sched-sr" id="sched-scope-label">${te('schedules.window.viewLabel')}</span>
|
|
504
|
+
${dropdownHTML('sched-scope-select', [], { classes: 'sched-scope-select', labelledBy: 'sched-scope-label', inputAttrs: 'data-change="scope"' })}
|
|
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>
|
|
506
|
+
</div>
|
|
507
|
+
<div class="sched-frame" id="sched-main"></div>
|
|
508
|
+
</section>
|
|
325
509
|
</div>
|
|
510
|
+
<div class="sched-dialog-layer" id="sched-dialog-layer" hidden></div>
|
|
326
511
|
`;
|
|
327
512
|
}
|
|
328
513
|
|
|
329
514
|
function wirePanel() {
|
|
330
|
-
|
|
515
|
+
_abort = new AbortController();
|
|
516
|
+
const { signal } = _abort;
|
|
517
|
+
$('sched-close')?.addEventListener('click', closePanel, { signal });
|
|
331
518
|
$('sched-backdrop-toggle')?.addEventListener('click', () => {
|
|
332
|
-
_backdrop?.classList.toggle('backdrop-hidden');
|
|
333
|
-
$('sched-backdrop-toggle')
|
|
519
|
+
const hidden = !!_backdrop?.classList.toggle('backdrop-hidden');
|
|
520
|
+
const btn = $('sched-backdrop-toggle');
|
|
521
|
+
if (!btn) return;
|
|
522
|
+
btn.classList.toggle('active', hidden);
|
|
523
|
+
btn.setAttribute('aria-pressed', String(hidden));
|
|
524
|
+
const label = t(hidden ? 'schedules.window.showBackdrop' : 'schedules.window.hideBackdrop');
|
|
525
|
+
btn.setAttribute('aria-label', label);
|
|
526
|
+
btn.dataset.tooltip = label;
|
|
527
|
+
}, { signal });
|
|
528
|
+
|
|
529
|
+
_panel.addEventListener('click', handleClick, { signal });
|
|
530
|
+
_panel.addEventListener('change', handleChange, { signal });
|
|
531
|
+
_panel.addEventListener('input', handleInput, { signal });
|
|
532
|
+
_panel.addEventListener('dragstart', handleDragStart, { signal });
|
|
533
|
+
_panel.addEventListener('dragend', handleDragEnd, { signal });
|
|
534
|
+
_panel.addEventListener('dragover', handleDragOver, { signal });
|
|
535
|
+
_panel.addEventListener('dragleave', handleDragLeave, { signal });
|
|
536
|
+
_panel.addEventListener('drop', handleDrop, { signal });
|
|
537
|
+
_panel.addEventListener('keydown', handlePanelKeydown, { signal });
|
|
538
|
+
// The popups (cc-dropdown.js): one set of listeners for every one the window draws.
|
|
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 });
|
|
548
|
+
// Capture: the menubar's own Escape (it closes an open menu) has run by the time a bubbling
|
|
549
|
+
// listener hears the key, so a menu that was open is seen here first.
|
|
550
|
+
window.addEventListener('keydown', handleEscape, { capture: true, signal });
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// ── Data ──
|
|
554
|
+
// The list and the editors are built from three lists, the schedules, the automations and the
|
|
555
|
+
// groups, fetched together with a deadline each. A list that fails keeps what an earlier load
|
|
556
|
+
// brought (sync updates keep it current), so an open editor never loses a saved choice to it.
|
|
557
|
+
// The timers are fetched apart: a slow answer there holds the Timers view, nothing else.
|
|
558
|
+
|
|
559
|
+
const LOAD_DEADLINE = 15_000;
|
|
560
|
+
|
|
561
|
+
/** The request, or a failure (marked `timeout`) once it has had LOAD_DEADLINE to answer. */
|
|
562
|
+
function withDeadline(request) {
|
|
563
|
+
let timer;
|
|
564
|
+
const late = new Promise((_, reject) => {
|
|
565
|
+
timer = setTimeout(() => {
|
|
566
|
+
const err = new Error(t('schedules.load.noAnswer', { seconds: LOAD_DEADLINE / 1000 }));
|
|
567
|
+
err.timeout = true;
|
|
568
|
+
reject(err);
|
|
569
|
+
}, LOAD_DEADLINE);
|
|
334
570
|
});
|
|
335
|
-
|
|
336
|
-
_panel.addEventListener('click', handleClick);
|
|
337
|
-
_panel.addEventListener('change', handleChange);
|
|
338
|
-
_panel.addEventListener('dragstart', handleDragStart);
|
|
339
|
-
_panel.addEventListener('dragend', handleDragEnd);
|
|
340
|
-
_panel.addEventListener('dragover', handleDragOver);
|
|
341
|
-
_panel.addEventListener('drop', handleDrop);
|
|
342
|
-
|
|
343
|
-
const onEsc = (e) => {
|
|
344
|
-
if (e.key !== 'Escape' || !_panel) return;
|
|
345
|
-
if (_promptModal) return;
|
|
346
|
-
if (_scheduleEditor) {
|
|
347
|
-
_scheduleEditor = null;
|
|
348
|
-
render();
|
|
349
|
-
return;
|
|
350
|
-
}
|
|
351
|
-
closePanel();
|
|
352
|
-
};
|
|
353
|
-
document.addEventListener('keydown', onEsc);
|
|
354
|
-
_docListeners.push(['keydown', onEsc]);
|
|
571
|
+
return Promise.race([request, late]).finally(() => clearTimeout(timer));
|
|
355
572
|
}
|
|
356
573
|
|
|
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(/\.$/, '');
|
|
576
|
+
|
|
357
577
|
async function loadData() {
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
578
|
+
_launchTools = await migrateStoredTools(storage, 'as-launch-tools', 'as-launch-mcp-profile');
|
|
579
|
+
const seq = ++_loadSeq;
|
|
580
|
+
loadTimers();
|
|
581
|
+
const [schedules, templates, groups] = await Promise.allSettled([
|
|
582
|
+
withDeadline(fetchSchedules()), withDeadline(fetchLoopTemplates()), withDeadline(fetchScheduleGroups()),
|
|
583
|
+
]);
|
|
584
|
+
if (seq !== _loadSeq) return; // a newer load is under way: it finishes for both
|
|
585
|
+
const failed = [];
|
|
586
|
+
if (schedules.status === 'fulfilled' && Array.isArray(schedules.value)) {
|
|
587
|
+
_schedules = schedules.value;
|
|
588
|
+
_loaded.schedules = true;
|
|
589
|
+
} else failed.push(['schedules', failureReason(schedules)]);
|
|
590
|
+
if (templates.status === 'fulfilled' && Array.isArray(templates.value)) {
|
|
591
|
+
_templates = templates.value;
|
|
592
|
+
_loaded.templates = true;
|
|
593
|
+
} else failed.push(['automations', failureReason(templates)]);
|
|
594
|
+
if (groups.status === 'fulfilled' && Array.isArray(groups.value)) {
|
|
595
|
+
_groups = groups.value;
|
|
596
|
+
_loaded.groups = true;
|
|
597
|
+
if (groupIdOf(_scope) && !getGroup(groupIdOf(_scope))) setScope('all', { keepEditors: true });
|
|
598
|
+
} else failed.push(['groups', failureReason(groups)]);
|
|
599
|
+
_loadError = failed.length
|
|
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
|
+
})
|
|
604
|
+
: null;
|
|
605
|
+
loadFinished();
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/** The one end of every load: opening the window, Try again and a live refresh all finish here. */
|
|
609
|
+
function loadFinished() {
|
|
610
|
+
if (!_panel) return;
|
|
611
|
+
const gated = _loading;
|
|
612
|
+
_loading = false;
|
|
613
|
+
let opened = false;
|
|
614
|
+
if (_pendingAction && !dataIncomplete()) {
|
|
615
|
+
const action = _pendingAction;
|
|
616
|
+
_pendingAction = null;
|
|
617
|
+
applyOpenOptions({ action });
|
|
618
|
+
opened = true;
|
|
619
|
+
}
|
|
620
|
+
if (!gated && !opened) { refresh(); return; }
|
|
621
|
+
const focusWasHere = document.activeElement === document.body || _panel.contains(document.activeElement)
|
|
622
|
+
|| document.activeElement?.id === 'menubar-schedules-btn';
|
|
623
|
+
render({ frame: true });
|
|
624
|
+
if (gated) revealExpanded();
|
|
625
|
+
if (opened || (focusWasHere && !_panel.contains(document.activeElement))) focusInitial();
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/** The timers waiting to fire: only the Timers view and its count read them. */
|
|
629
|
+
async function loadTimers() {
|
|
630
|
+
const seq = ++_timersSeq;
|
|
631
|
+
const [timers, quick] = await Promise.allSettled([withDeadline(fetchScheduleTimers()), withDeadline(fetchQuickTimers())]);
|
|
632
|
+
if (seq !== _timersSeq) return;
|
|
633
|
+
const failed = [];
|
|
634
|
+
if (timers.status === 'fulfilled' && timers.value && typeof timers.value === 'object') _scheduleTimerData = timers.value;
|
|
635
|
+
else failed.push(failureReason(timers));
|
|
636
|
+
if (quick.status === 'fulfilled' && Array.isArray(quick.value)) _quickTimers = quick.value;
|
|
637
|
+
else failed.push(failureReason(quick));
|
|
638
|
+
_timersLoaded = true;
|
|
639
|
+
_timersError = failed.length ? t('schedules.load.timersFailed', { reasons: [...new Set(failed)].join('; ') }) : null;
|
|
640
|
+
if (!_panel) return;
|
|
641
|
+
renderRail();
|
|
642
|
+
renderActiveTimersInto();
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/** Fetch again, soon (several sync events tend to arrive together). */
|
|
646
|
+
function scheduleRefresh(delay = 400) {
|
|
647
|
+
clearTimeout(_refreshTimer);
|
|
648
|
+
_refreshTimer = setTimeout(() => { loadData(); }, delay);
|
|
363
649
|
}
|
|
364
650
|
|
|
365
651
|
function startPolling() {
|
|
@@ -376,242 +662,737 @@ function stopPolling() {
|
|
|
376
662
|
_pollTimer = null;
|
|
377
663
|
}
|
|
378
664
|
|
|
379
|
-
|
|
665
|
+
// ── Rendering ──
|
|
666
|
+
|
|
667
|
+
function setScope(scope, { keepEditors = false } = {}) {
|
|
668
|
+
_scope = scope;
|
|
669
|
+
if (!keepEditors) closeEditors();
|
|
670
|
+
try { storage.setItem(SCOPE_KEY, scope); } catch { /* not remembered */ }
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function closeEditors() {
|
|
674
|
+
_scheduleEditor = null;
|
|
675
|
+
_groupEditorId = null;
|
|
676
|
+
_groupEditorDraft = null;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/** Everything the window shows. `frame` rebuilds the main area's skeleton as well as its content. */
|
|
680
|
+
function render({ frame = false } = {}) {
|
|
380
681
|
if (!_panel) return;
|
|
381
682
|
const count = $('sched-total-count');
|
|
382
|
-
if (count) count.textContent = String(_schedules.length);
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
else renderMain();
|
|
683
|
+
if (count) count.textContent = countsKnown() ? String(_schedules.length) : '';
|
|
684
|
+
renderRail();
|
|
685
|
+
renderMain(frame);
|
|
386
686
|
}
|
|
387
687
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
if (!
|
|
688
|
+
/** A sync update: the editors keep what is being typed; everything else follows the data. */
|
|
689
|
+
function refresh() {
|
|
690
|
+
if (!_panel) return;
|
|
691
|
+
if (editorOpen()) {
|
|
692
|
+
const count = $('sched-total-count');
|
|
693
|
+
if (count) count.textContent = countsKnown() ? String(_schedules.length) : '';
|
|
694
|
+
renderRail();
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
render();
|
|
698
|
+
}
|
|
391
699
|
|
|
392
|
-
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
700
|
+
function renderRail() {
|
|
701
|
+
const rail = $('sched-rail');
|
|
702
|
+
if (!rail) return;
|
|
703
|
+
const now = Date.now();
|
|
704
|
+
const counts = scopeCounts(_schedules, now);
|
|
705
|
+
const timers = _quickTimers.length + Object.keys(_scheduleTimerData).length;
|
|
706
|
+
const ids = groupIds();
|
|
707
|
+
const view = (scope, count, { alert = false } = {}) => {
|
|
708
|
+
const { icon } = VIEWS[scope];
|
|
709
|
+
const current = _scope === scope;
|
|
710
|
+
const countHTML = count == null || !countsKnown() ? '' : `<span class="sched-nav-count${alert && count ? ' sched-nav-count--alert' : ''}">${count}</span>`;
|
|
711
|
+
return `<button type="button" class="sched-nav-item" data-action="set-scope" data-scope="${scope}"${current ? ' aria-current="page"' : ''}>
|
|
712
|
+
<span class="sched-nav-icon">${icon}</span><span class="sched-nav-label">${esc(viewLabel(scope))}</span>${countHTML}</button>`;
|
|
713
|
+
};
|
|
714
|
+
const groupRows = orderedGroups().map(group => {
|
|
715
|
+
const members = _schedules.filter(s => s.groupId === group.id);
|
|
716
|
+
const summary = groupSummary(members, now);
|
|
717
|
+
const scope = `group:${group.id}`;
|
|
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 });
|
|
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"' : ''}>
|
|
725
|
+
<span class="sched-nav-icon"><span class="sched-dot" style="--dot:${esc(cssColor(group.color))}"></span></span>
|
|
726
|
+
<span class="sched-nav-label">${esc(group.name)}</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>` : ''}
|
|
729
|
+
<span class="sched-nav-count">${summary.total}</span>
|
|
730
|
+
</button>`;
|
|
403
731
|
}).join('');
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
732
|
+
const ungrouped = _schedules.filter(s => !s.groupId || !ids.has(s.groupId)).length;
|
|
733
|
+
|
|
734
|
+
// Redrawn on every live update: the focused item and the scroll position stay.
|
|
735
|
+
const active = document.activeElement;
|
|
736
|
+
const focusKey = active && rail.contains(active)
|
|
737
|
+
? (active.dataset.scope ? `[data-scope="${CSS.escape(active.dataset.scope)}"]` : active.dataset.action ? `[data-action="${active.dataset.action}"]` : null)
|
|
738
|
+
: null;
|
|
739
|
+
const scroll = rail.scrollTop;
|
|
740
|
+
rail.innerHTML = `
|
|
741
|
+
${view('upcoming', counts.upcoming)}
|
|
742
|
+
${view('all', counts.all)}
|
|
743
|
+
${view('attention', counts.attention, { alert: true })}
|
|
744
|
+
${view('paused', counts.paused)}
|
|
745
|
+
<div class="sched-rail-sep" role="presentation"></div>
|
|
746
|
+
${view('timers', timers || null)}
|
|
747
|
+
${view('activity', null)}
|
|
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>
|
|
749
|
+
<div role="group" aria-labelledby="sched-rail-groups">
|
|
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>
|
|
753
|
+
</button>
|
|
409
754
|
</div>
|
|
410
|
-
<button class="sched-group-row${_selectedGroup === 'all' ? ' active' : ''}" data-action="select-group" data-group-id="all" data-drop-group="">
|
|
411
|
-
<span class="sched-group-icon">${ICONS.list}</span>
|
|
412
|
-
<span class="sched-group-name">All Schedules</span>
|
|
413
|
-
<span class="sched-group-count">${_schedules.length}</span>
|
|
414
|
-
</button>
|
|
415
|
-
<button class="sched-group-row${_selectedGroup === 'ungrouped' ? ' active' : ''}" data-action="select-group" data-group-id="ungrouped" data-drop-group="">
|
|
416
|
-
<span class="sched-group-icon">${ICONS.folder}</span>
|
|
417
|
-
<span class="sched-group-name">Ungrouped</span>
|
|
418
|
-
<span class="sched-group-count">${ungroupedCount}</span>
|
|
419
|
-
</button>
|
|
420
|
-
<div class="sched-sidebar-label">Groups</div>
|
|
421
|
-
<div class="sched-group-list">${groupRows || '<div class="sched-sidebar-empty">No schedule groups yet.</div>'}</div>
|
|
422
|
-
<button class="sched-sidebar-prompt" data-action="schedule-prompt" data-tooltip="Let AI build a schedule for you">${ICONS.sparkle} Prompt a Schedule</button>
|
|
423
|
-
<button class="sched-sidebar-new" data-action="group-new">${ICONS.plus} Create Group</button>
|
|
424
755
|
`;
|
|
756
|
+
rail.scrollTop = scroll;
|
|
757
|
+
if (focusKey) rail.querySelector(focusKey)?.focus({ preventScroll: true });
|
|
758
|
+
renderNarrowNav();
|
|
425
759
|
}
|
|
426
760
|
|
|
427
|
-
function renderMain() {
|
|
761
|
+
function renderMain(rebuild = false) {
|
|
428
762
|
const main = $('sched-main');
|
|
429
763
|
if (!main) return;
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
764
|
+
// An editor that is on screen gives its typed values to the draft before it is drawn again.
|
|
765
|
+
if (_groupEditorDraft) {
|
|
766
|
+
if (rebuild || _frame !== 'group-editor') {
|
|
767
|
+
if (_frame === 'group-editor') collectGroupEditorDraft();
|
|
768
|
+
renderGroupEditor(_groupEditorId ? getGroup(_groupEditorId) : null);
|
|
769
|
+
}
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
if (_scheduleEditor) {
|
|
773
|
+
if (rebuild || _frame !== 'schedule-editor') {
|
|
774
|
+
if (_frame === 'schedule-editor') collectScheduleEditorDraft();
|
|
775
|
+
renderScheduleEditor();
|
|
776
|
+
}
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
if (_scope === 'timers') {
|
|
780
|
+
if (rebuild || _frame !== 'timers') renderTimersView();
|
|
781
|
+
else { renderNote(); renderActiveTimersInto(); }
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
const frame = _scope === 'activity' ? 'activity' : 'list';
|
|
785
|
+
if (rebuild || _frame !== frame) buildListFrame(frame);
|
|
786
|
+
renderViewHead();
|
|
787
|
+
if (frame === 'activity') renderActivityList();
|
|
788
|
+
else renderList();
|
|
789
|
+
}
|
|
436
790
|
|
|
791
|
+
function buildListFrame(frame) {
|
|
792
|
+
const main = $('sched-main');
|
|
793
|
+
_frame = frame;
|
|
794
|
+
const activity = frame === 'activity';
|
|
795
|
+
// A rebuild (the data arriving, a view switch) must not drop a search being typed.
|
|
796
|
+
const search = document.activeElement?.id === 'sched-search' ? document.activeElement : null;
|
|
797
|
+
const caret = search ? [search.selectionStart, search.selectionEnd] : null;
|
|
437
798
|
main.innerHTML = `
|
|
438
|
-
<div class="sched-
|
|
439
|
-
<div class="sched-
|
|
799
|
+
<div class="sched-view-head" id="sched-view-head"></div>
|
|
800
|
+
<div class="sched-toolbar">
|
|
801
|
+
<div class="sched-search">
|
|
802
|
+
${ICONS.search}
|
|
803
|
+
<label class="sched-sr" for="sched-search">${activity ? te('schedules.list.searchRuns') : te('schedules.list.searchSchedules')}</label>
|
|
804
|
+
<input id="sched-search" type="search" autocomplete="off" spellcheck="false"
|
|
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>
|
|
807
|
+
<kbd aria-hidden="true">/</kbd>
|
|
808
|
+
</div>
|
|
809
|
+
${activity ? '' : `<span class="sched-sort" id="sched-sort-wrap"><span class="sched-sr" id="sched-sort-label">${te('schedules.list.order')}</span>
|
|
810
|
+
${dropdownHTML('sched-sort', [
|
|
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'),
|
|
814
|
+
], { labelledBy: 'sched-sort-label', inputAttrs: 'data-change="sort"' })}</span>`}
|
|
815
|
+
</div>
|
|
816
|
+
<div class="sched-load-note" id="sched-load-note" role="status"></div>
|
|
817
|
+
<div class="sched-list" id="sched-list" tabindex="-1"></div>
|
|
818
|
+
<footer class="sched-footer">
|
|
819
|
+
<span id="sched-status" role="status" aria-live="polite" aria-atomic="true"></span>
|
|
820
|
+
${activity ? '' : `<span class="sched-footer-hint">${ICONS.move}${te('schedules.list.dragHint')}</span>`}
|
|
821
|
+
</footer>
|
|
440
822
|
`;
|
|
823
|
+
if (caret) {
|
|
824
|
+
const input = $('sched-search');
|
|
825
|
+
input?.focus({ preventScroll: true });
|
|
826
|
+
try { input?.setSelectionRange(caret[0], caret[1]); } catch { /* type=search keeps its caret */ }
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function headActions({ group = null } = {}) {
|
|
831
|
+
// An editor needs the automations and groups: nothing opens one before they have loaded.
|
|
832
|
+
const wait = editorsWait() ? ' disabled' : '';
|
|
833
|
+
return `<div class="sched-view-actions">
|
|
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>
|
|
837
|
+
</div>`;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
function groupOverridesText(group) {
|
|
841
|
+
const parts = [];
|
|
842
|
+
if (group.profile) parts.push(CLI_PROFILES.find(c => c.id === group.profile)?.label || group.profile);
|
|
843
|
+
if (group.model) parts.push(group.model);
|
|
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'));
|
|
849
|
+
return parts.join(' · ');
|
|
850
|
+
}
|
|
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
|
+
|
|
862
|
+
function renderViewHead() {
|
|
863
|
+
const head = $('sched-view-head');
|
|
864
|
+
if (!head) return;
|
|
865
|
+
const now = Date.now();
|
|
866
|
+
const counts = scopeCounts(_schedules, now);
|
|
867
|
+
let title = VIEWS[_scope] ? viewLabel(_scope) : t('schedules.window.title');
|
|
868
|
+
let dot = '';
|
|
869
|
+
let sub = '';
|
|
870
|
+
let meta = '';
|
|
871
|
+
let group = null;
|
|
872
|
+
if (_scope === 'all') {
|
|
873
|
+
sub = countsLine({ total: counts.all, active: counts.active, paused: counts.paused, attention: counts.attention });
|
|
874
|
+
} else if (_scope === 'upcoming') {
|
|
875
|
+
// One row per schedule, at its next run (a schedule that runs three times today is listed once).
|
|
876
|
+
sub = counts.upcoming ? tp('schedules.head.upcoming', counts.upcoming) : t('schedules.head.upcomingNone');
|
|
877
|
+
} else if (_scope === 'attention') {
|
|
878
|
+
sub = t('schedules.head.attention');
|
|
879
|
+
} else if (_scope === 'paused') {
|
|
880
|
+
sub = t('schedules.head.paused');
|
|
881
|
+
} else if (_scope === 'activity') {
|
|
882
|
+
sub = t('schedules.head.activity');
|
|
883
|
+
} else if (_scope === 'ungrouped') {
|
|
884
|
+
title = viewLabel('ungrouped');
|
|
885
|
+
sub = t('schedules.head.ungrouped');
|
|
886
|
+
} else if (groupIdOf(_scope)) {
|
|
887
|
+
group = getGroup(groupIdOf(_scope));
|
|
888
|
+
if (group) {
|
|
889
|
+
title = group.name;
|
|
890
|
+
dot = `<span class="sched-dot" style="--dot:${esc(cssColor(group.color))}" aria-hidden="true"></span>`;
|
|
891
|
+
const summary = groupSummary(_schedules.filter(s => s.groupId === group.id), now);
|
|
892
|
+
sub = countsLine(summary);
|
|
893
|
+
if (group.mutualExclusion) sub = `${sub} · ${t('schedules.oneAtATime')}`;
|
|
894
|
+
const overrides = groupOverridesText(group);
|
|
895
|
+
if (overrides) meta = `<p class="sched-view-meta sched-group-overrides">${te('schedules.head.groupLaunch', { settings: overrides })}</p>`;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
if (_loading) sub = t('schedules.list.loading');
|
|
899
|
+
else if (dataIncomplete()) sub = '';
|
|
900
|
+
// A live update changes counts, not the head's controls: keep them (and the focus on them).
|
|
901
|
+
const key = `${_scope}|${title}|${dot}|${meta}|${editorsWait()}`;
|
|
902
|
+
if (head.dataset.key === key) {
|
|
903
|
+
const subEl = head.querySelector('.sched-view-sub');
|
|
904
|
+
if (subEl && subEl.textContent !== sub) subEl.textContent = sub;
|
|
905
|
+
} else {
|
|
906
|
+
head.dataset.key = key;
|
|
907
|
+
head.innerHTML = `
|
|
908
|
+
<div class="sched-view-titles">
|
|
909
|
+
<h2 id="sched-view-title">${dot}${esc(title)}</h2>
|
|
910
|
+
<p class="sched-view-sub">${esc(sub)}</p>
|
|
911
|
+
${meta}
|
|
912
|
+
</div>
|
|
913
|
+
${headActions({ group })}
|
|
914
|
+
`;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
441
917
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
918
|
+
/** The rail as a popup with its counts, and New group, for a window too narrow to show the rail. */
|
|
919
|
+
function renderNarrowNav() {
|
|
920
|
+
const scope = $('sched-scope-select');
|
|
921
|
+
if (scope) {
|
|
922
|
+
const counts = scopeCounts(_schedules, Date.now());
|
|
923
|
+
const timers = _quickTimers.length + Object.keys(_scheduleTimerData).length;
|
|
924
|
+
const n = (label, value) => (!countsKnown() || !value && value !== 0 ? label : t('schedules.rail.withCount', { label, count: value }));
|
|
925
|
+
const views = [['upcoming', counts.upcoming], ['all', counts.all], ['attention', counts.attention], ['paused', counts.paused], ['timers', timers || null], ['activity', null]]
|
|
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'))] }];
|
|
929
|
+
// Redrawn only when it changes (an open popup stays open, its highlighted choice too).
|
|
930
|
+
const key = JSON.stringify(entries);
|
|
931
|
+
if (scope._key !== key) { setDropdownItems(scope, entries); scope._key = key; }
|
|
932
|
+
setDropdownValue(scope, _scope);
|
|
933
|
+
}
|
|
934
|
+
const newGroup = $('sched-narrow-new-group');
|
|
935
|
+
if (newGroup) newGroup.disabled = editorsWait();
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
function setStatus(text) {
|
|
939
|
+
const status = $('sched-status');
|
|
940
|
+
if (status) status.textContent = text;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
/**
|
|
944
|
+
* The line above the list. A load that failed: without every list, the list views show the
|
|
945
|
+
* failure in place of the list; with an earlier copy of each, this line says so above the copy
|
|
946
|
+
* (and in Timers, which has none). Otherwise, on a group's page, what its change of CLI did to
|
|
947
|
+
* its schedules, until dismissed.
|
|
948
|
+
*/
|
|
949
|
+
function renderNote() {
|
|
950
|
+
const note = $('sched-load-note');
|
|
951
|
+
if (!note) return;
|
|
952
|
+
const listView = _frame === 'list' || _frame === 'activity';
|
|
953
|
+
const failed = _loading || !_loadError || (listView && dataIncomplete()) ? ''
|
|
954
|
+
: dataIncomplete() ? _loadError : t('schedules.note.showingBefore', { error: _loadError });
|
|
955
|
+
const report = !failed && _frame === 'list' ? cliReportNote() : null;
|
|
956
|
+
const kind = failed ? 'load' : report?.kind || '';
|
|
957
|
+
const text = failed || report?.text || '';
|
|
958
|
+
const key = `${kind}|${text}`;
|
|
959
|
+
if (note.dataset.key === key) return; // unchanged: a live update does not announce it again
|
|
960
|
+
note.dataset.key = key;
|
|
961
|
+
note.classList.toggle('sched-load-note--ok', kind === 'ok');
|
|
962
|
+
note.classList.toggle('sched-load-note--busy', kind === 'busy');
|
|
963
|
+
const icon = kind === 'ok' ? ICONS.ok : kind === 'busy' ? ICONS.clock : ICONS.alert;
|
|
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>`;
|
|
966
|
+
note.innerHTML = text ? `${icon}<span>${esc(text)}</span>${action}` : '';
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
/** What the last change of a group's CLI did to its schedules, for that group's page. */
|
|
970
|
+
function cliReportNote() {
|
|
971
|
+
const r = _cliReport;
|
|
972
|
+
if (!r || _scope !== `group:${r.groupId}`) return null;
|
|
973
|
+
if (r.pending) return { kind: 'busy', text: t('schedules.cliReport.clearing') };
|
|
974
|
+
const cli = r.profile ? CLI_PROFILES.find(p => p.id === r.profile)?.label || r.profile : null;
|
|
975
|
+
const kept = r.changed.length + r.failed.length;
|
|
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));
|
|
983
|
+
return { kind: kept ? 'warn' : 'ok', text: parts.join(' ') };
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
function loadFailedHTML() {
|
|
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>`);
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
/** Keep the place: scroll position and the focused control survive a re-render of the list. */
|
|
993
|
+
function captureListState(list) {
|
|
994
|
+
const active = document.activeElement;
|
|
995
|
+
const inside = active && list.contains(active);
|
|
996
|
+
return {
|
|
997
|
+
scroll: list.scrollTop,
|
|
998
|
+
focus: inside ? {
|
|
999
|
+
id: active.id || null,
|
|
1000
|
+
scheduleId: active.closest('[data-schedule-id]')?.dataset.scheduleId || null,
|
|
1001
|
+
groupId: active.closest('[data-group-section]')?.dataset.groupSection || null,
|
|
1002
|
+
action: active.dataset.action || active.dataset.change || null,
|
|
1003
|
+
} : null,
|
|
1004
|
+
};
|
|
446
1005
|
}
|
|
447
1006
|
|
|
448
|
-
function
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
1007
|
+
function restoreListState(list, saved) {
|
|
1008
|
+
list.scrollTop = saved.scroll;
|
|
1009
|
+
const f = saved.focus;
|
|
1010
|
+
if (!f) return;
|
|
1011
|
+
let target = f.id ? document.getElementById(f.id) : null;
|
|
1012
|
+
if (!target && f.scheduleId) {
|
|
1013
|
+
const row = [...list.querySelectorAll('[data-schedule-id]')].find(el => el.dataset.scheduleId === f.scheduleId);
|
|
1014
|
+
target = row && f.action ? controlOf(row.querySelector(`[data-action="${f.action}"], [data-change="${f.action}"]`)) : row?.querySelector('.sched-row-toggle');
|
|
1015
|
+
}
|
|
1016
|
+
if (!target && f.groupId) {
|
|
1017
|
+
const section = [...list.querySelectorAll('[data-group-section]')].find(el => el.dataset.groupSection === f.groupId);
|
|
1018
|
+
target = section?.querySelector(`[data-action="${f.action}"]`) || section?.querySelector('.sched-group-toggle');
|
|
1019
|
+
}
|
|
1020
|
+
target?.focus({ preventScroll: true });
|
|
452
1021
|
}
|
|
453
1022
|
|
|
454
|
-
function
|
|
455
|
-
return [
|
|
456
|
-
if (a.enabled !== b.enabled) return a.enabled ? -1 : 1;
|
|
457
|
-
const an = a.nextRun ? new Date(a.nextRun).getTime() : Infinity;
|
|
458
|
-
const bn = b.nextRun ? new Date(b.nextRun).getTime() : Infinity;
|
|
459
|
-
if (an !== bn) return an - bn;
|
|
460
|
-
return String(a.name || '').localeCompare(String(b.name || ''));
|
|
461
|
-
});
|
|
1023
|
+
function scheduleExtras(s) {
|
|
1024
|
+
return [getTemplate(s.templateId)?.name, getGroup(s.groupId)?.name];
|
|
462
1025
|
}
|
|
463
1026
|
|
|
464
|
-
function
|
|
465
|
-
const
|
|
466
|
-
|
|
1027
|
+
function visibleSchedules() {
|
|
1028
|
+
const now = Date.now();
|
|
1029
|
+
const ids = groupIds();
|
|
1030
|
+
return _schedules.filter(s => inScope(s, _scope, { now, groupIds: ids }) && matchesQuery(s, _query, scheduleExtras(s)));
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
function renderList() {
|
|
1034
|
+
const list = $('sched-list');
|
|
1035
|
+
if (!list) return;
|
|
1036
|
+
const saved = captureListState(list);
|
|
1037
|
+
const clear = _panel?.querySelector('[data-action="clear-search"]');
|
|
1038
|
+
if (clear) clear.hidden = !_query;
|
|
1039
|
+
// Up next is in time order by definition: no order to choose there.
|
|
1040
|
+
const sortWrap = $('sched-sort-wrap');
|
|
1041
|
+
if (sortWrap) sortWrap.hidden = _scope === 'upcoming';
|
|
1042
|
+
renderNote();
|
|
1043
|
+
|
|
1044
|
+
if (_loading) {
|
|
1045
|
+
list.innerHTML = emptyState(ICONS.clock, t('schedules.list.loading'), '');
|
|
1046
|
+
setStatus(t('schedules.list.statusLoading'));
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
if (dataIncomplete()) {
|
|
1050
|
+
list.innerHTML = loadFailedHTML();
|
|
1051
|
+
setStatus(t('schedules.list.statusNotLoaded'));
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
467
1054
|
|
|
468
|
-
const
|
|
469
|
-
const
|
|
470
|
-
const
|
|
471
|
-
const
|
|
472
|
-
|
|
473
|
-
|
|
1055
|
+
const now = Date.now();
|
|
1056
|
+
const shown = visibleSchedules();
|
|
1057
|
+
const scopeTotal = _schedules.filter(s => inScope(s, _scope, { now, groupIds: groupIds() })).length;
|
|
1058
|
+
const query = _query.trim();
|
|
1059
|
+
if (!_schedules.length) {
|
|
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) {
|
|
1064
|
+
const elsewhere = _scope !== 'all' && _schedules.some(s => matchesQuery(s, _query, scheduleExtras(s)));
|
|
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>` : ''}`);
|
|
1069
|
+
} else if (!shown.length) {
|
|
1070
|
+
list.innerHTML = scopeEmptyState();
|
|
1071
|
+
} else if (_scope === 'all') {
|
|
1072
|
+
list.innerHTML = renderGroupedList(shown, now);
|
|
1073
|
+
} else if (_scope === 'upcoming') {
|
|
1074
|
+
list.innerHTML = renderUpcomingList(shown, now);
|
|
1075
|
+
} else {
|
|
1076
|
+
const rows = sortSchedules(shown, _sort).map(s => rowHTML(s, { now, showGroup: !groupIdOf(_scope) })).join('');
|
|
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>`;
|
|
1078
|
+
}
|
|
1079
|
+
setStatus(query ? tp('schedules.list.statusMatch', shown.length, { query }) : tp('schedules.list.statusShowing', scopeTotal, { shown: shown.length }));
|
|
1080
|
+
restoreListState(list, saved);
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
function emptyState(icon, title, text, actions = '') {
|
|
1084
|
+
return `<div class="sched-empty">
|
|
1085
|
+
<div class="sched-empty-icon">${icon}</div>
|
|
1086
|
+
<h3>${esc(title)}</h3>
|
|
1087
|
+
${text ? `<p>${esc(text)}</p>` : ''}
|
|
1088
|
+
${actions ? `<div class="sched-empty-actions">${actions}</div>` : ''}
|
|
1089
|
+
</div>`;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
function scopeEmptyState() {
|
|
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>`);
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
/** All schedules: one box per group, in the groups' order, then the ungrouped ones. */
|
|
1102
|
+
function renderGroupedList(shown, now) {
|
|
1103
|
+
const ids = groupIds();
|
|
1104
|
+
const sections = orderedGroups().map(group => ({ group, items: shown.filter(s => s.groupId === group.id) }));
|
|
1105
|
+
sections.push({ group: null, items: shown.filter(s => !s.groupId || !ids.has(s.groupId)) });
|
|
1106
|
+
return sections.filter(section => section.items.length).map(({ group, items }) => {
|
|
1107
|
+
const key = group ? group.id : '__ungrouped';
|
|
1108
|
+
const collapsed = !_query.trim() && _collapsed.has(key);
|
|
1109
|
+
const listId = `sched-rows-${domId(key)}`;
|
|
1110
|
+
const all = group ? _schedules.filter(s => s.groupId === group.id) : items;
|
|
1111
|
+
const summary = groupSummary(all, now);
|
|
1112
|
+
const meta = [
|
|
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>` : '',
|
|
1116
|
+
].filter(Boolean).join('<span aria-hidden="true">·</span>');
|
|
1117
|
+
const name = group ? group.name : viewLabel('ungrouped');
|
|
1118
|
+
return `<section class="sched-box${collapsed ? ' is-collapsed' : ''}" data-group-section="${esc(key)}">
|
|
1119
|
+
<div class="sched-group-head" data-drop-group="${group ? esc(group.id) : ''}">
|
|
1120
|
+
<button type="button" class="sched-group-toggle" data-action="group-collapse" data-key="${esc(key)}" aria-expanded="${!collapsed}" aria-controls="${listId}">
|
|
1121
|
+
<span class="sched-group-chevron">${ICONS.chevron}</span>
|
|
1122
|
+
${group ? `<span class="sched-dot" style="--dot:${esc(cssColor(group.color))}" aria-hidden="true"></span>` : `<span class="sched-nav-icon">${ICONS.folder}</span>`}
|
|
1123
|
+
<span class="sched-group-name">${esc(name)}</span>
|
|
1124
|
+
<span class="sched-group-count">${items.length === all.length ? items.length : te('schedules.list.shownOf', { shown: items.length, total: all.length })}</span>
|
|
1125
|
+
</button>
|
|
1126
|
+
${meta ? `<span class="sched-group-meta">${meta}</span>` : ''}
|
|
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>` : ''}
|
|
1128
|
+
</div>
|
|
1129
|
+
<ul class="sched-rows" id="${listId}" aria-label="${esc(name)}"${collapsed ? ' hidden' : ''}>
|
|
1130
|
+
${sortSchedules(items, _sort).map(s => rowHTML(s, { now, showGroup: false })).join('')}
|
|
1131
|
+
</ul>
|
|
1132
|
+
</section>`;
|
|
1133
|
+
}).join('');
|
|
1134
|
+
}
|
|
474
1135
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
1136
|
+
/** Up next: the runs of the coming day in time order, under Today / Tomorrow. */
|
|
1137
|
+
function renderUpcomingList(shown, now) {
|
|
1138
|
+
const ordered = sortSchedules(shown, 'next');
|
|
1139
|
+
const days = new Map();
|
|
1140
|
+
for (const s of ordered) {
|
|
1141
|
+
const label = dayLabel(s.nextRun, now) || t('schedules.list.later');
|
|
1142
|
+
if (!days.has(label)) days.set(label, []);
|
|
1143
|
+
days.get(label).push(s);
|
|
1144
|
+
}
|
|
1145
|
+
return [...days.entries()].map(([label, items], i) => `
|
|
1146
|
+
<section class="sched-section" aria-labelledby="sched-day-${i}">
|
|
1147
|
+
<h3 class="sched-section-title" id="sched-day-${i}">${esc(label)}<span>${tpe('schedules.count.schedules', items.length)}</span></h3>
|
|
1148
|
+
<div class="sched-box"><ul class="sched-rows">${items.map(s => rowHTML(s, { now, showGroup: true })).join('')}</ul></div>
|
|
1149
|
+
</section>`).join('');
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
function nextRunHTML(s, now) {
|
|
1153
|
+
if (!s.enabled) return `<span class="sched-next" data-state="paused"><span class="sched-next-rel">${te('schedules.row.paused')}</span></span>`;
|
|
1154
|
+
const countdown = formatCountdown(s.nextRun, now);
|
|
1155
|
+
const when = s.nextRun ? whenLabel(s.nextRun, now) : '';
|
|
1156
|
+
return `<span class="sched-next" data-state="${countdown.state}" data-countdown="${esc(s.nextRun || '')}"${when ? ` title="${esc(when)}"` : ''}>
|
|
1157
|
+
<span class="sched-next-rel">${esc(countdown.text)}</span>${s.nextRun ? `<span class="sched-next-abs">${esc(shortWhen(s.nextRun, now))}</span>` : ''}</span>`;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
function lastRunHTML(s, now, run = lastRunState(s), { ago: withAgo = true } = {}) {
|
|
1161
|
+
const ago = withAgo && s.lastRun ? formatAgo(s.lastRun, now) : '';
|
|
1162
|
+
const title = s.lastRun ? fullDateTime(s.lastRun) : '';
|
|
1163
|
+
return `<span class="sched-last" data-kind="${run.kind}"${title ? ` title="${esc(title)}"` : ''}>
|
|
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>`;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
function rowHTML(s, { now = Date.now(), showGroup = false } = {}) {
|
|
1168
|
+
const run = lastRunState(s);
|
|
1169
|
+
const failing = run.kind === 'failed';
|
|
1170
|
+
const open = _expanded.has(s.id);
|
|
1171
|
+
const group = getGroup(s.groupId);
|
|
1172
|
+
const detailId = `sched-detail-${domId(s.id)}`;
|
|
1173
|
+
const groupBit = showGroup && group
|
|
1174
|
+
? `<span class="sched-row-group"><span class="sched-dot" style="--dot:${esc(cssColor(group.color))}" aria-hidden="true"></span>${esc(group.name)}</span>`
|
|
1175
|
+
: '';
|
|
1176
|
+
// The summary line drags (onto a group); the details stay selectable, an error message copyable.
|
|
1177
|
+
return `<li class="sched-row${s.enabled ? '' : ' is-paused'}${failing ? ' is-failing' : ''}${open ? ' is-open' : ''}" data-schedule-id="${esc(s.id)}">
|
|
1178
|
+
<div class="sched-row-line" data-action="schedule-expand" data-id="${esc(s.id)}" draggable="true">
|
|
1179
|
+
<span class="sched-switch-cell">
|
|
1180
|
+
<button type="button" class="sched-switch" role="switch" aria-checked="${!!s.enabled}" data-action="schedule-toggle" data-id="${esc(s.id)}"
|
|
1181
|
+
aria-label="${te('schedules.row.switchLabel', { name: s.name })}" data-tooltip="${s.enabled ? te('schedules.row.pause') : te('schedules.row.resume')}"></button>
|
|
1182
|
+
</span>
|
|
1183
|
+
<button type="button" class="sched-row-toggle" data-action="schedule-expand" data-id="${esc(s.id)}" aria-expanded="${open}"${open ? ` aria-controls="${detailId}"` : ''}>
|
|
1184
|
+
<span class="sched-row-name">${esc(s.name)}</span>
|
|
1185
|
+
<span class="sched-row-sub">${groupBit}${groupBit ? '<span aria-hidden="true">·</span>' : ''}<span>${esc(describeCron(s.cron))}</span></span>
|
|
1186
|
+
${failing ? `<span class="sched-row-reason">${esc(oneLine(run.reason))}</span>` : ''}
|
|
1187
|
+
</button>
|
|
1188
|
+
${nextRunHTML(s, now)}
|
|
1189
|
+
${lastRunHTML(s, now, run)}
|
|
1190
|
+
<span class="sched-row-chevron" aria-hidden="true">${ICONS.chevron}</span>
|
|
481
1191
|
</div>
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
1192
|
+
${open ? `<div class="sched-detail" id="${detailId}">${detailHTML(s, now, run)}</div>` : ''}
|
|
1193
|
+
</li>`;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
function detailHTML(s, now, run) {
|
|
1197
|
+
const template = getTemplate(s.templateId);
|
|
1198
|
+
const group = getGroup(s.groupId);
|
|
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>` : '');
|
|
1203
|
+
const next = s.enabled
|
|
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)
|
|
1207
|
+
: `<span class="${run.kind === 'failed' ? 'is-failed' : ''}">${esc(run.label)}</span>${s.lastRun ? ` · ${esc(whenLabel(s.lastRun, now))}` : ''}`;
|
|
1208
|
+
// Any entry counts: with day themes, the server runs a schedule only on the days that have one.
|
|
1209
|
+
const themes = [1, 2, 3, 4, 5, 6, 0].filter(d => s.dayThemes?.[String(d)]).map(d => weekdayName(d));
|
|
1210
|
+
const groupOptions = [
|
|
1211
|
+
opt('', viewLabel('ungrouped'), !s.groupId),
|
|
1212
|
+
...orderedGroups().map(g => opt(g.id, g.name, s.groupId === g.id)),
|
|
1213
|
+
];
|
|
1214
|
+
const fact = (label, value, cls = '') => `<div class="sched-fact"><dt>${esc(label)}</dt><dd${cls ? ` class="${cls}"` : ''}>${value}</dd></div>`;
|
|
1215
|
+
return `
|
|
1216
|
+
<div class="sched-detail-grid">
|
|
1217
|
+
<section class="sched-facts-block">
|
|
1218
|
+
<h4 class="sched-facts-title">${te('schedules.detail.runs')}</h4>
|
|
1219
|
+
<dl class="sched-facts">
|
|
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>`) : ''}
|
|
1228
|
+
</dl>
|
|
1229
|
+
</section>
|
|
1230
|
+
<section class="sched-facts-block">
|
|
1231
|
+
<h4 class="sched-facts-title">${te('schedules.detail.launchSettings')}</h4>
|
|
1232
|
+
<dl class="sched-facts">
|
|
1233
|
+
${facts.map(f => fact(f.label, `${esc(f.value)}${sourceNote(f.source)}`, f.mono ? 'sched-mono' : '')).join('')}
|
|
1234
|
+
</dl>
|
|
1235
|
+
</section>
|
|
485
1236
|
</div>
|
|
486
|
-
<div class="sched-
|
|
487
|
-
|
|
488
|
-
|
|
1237
|
+
<div class="sched-detail-bar">
|
|
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)}"` })}
|
|
1240
|
+
</div>
|
|
1241
|
+
<span class="sched-detail-spacer"></span>
|
|
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>
|
|
1246
|
+
</div>`;
|
|
489
1247
|
}
|
|
490
1248
|
|
|
491
|
-
function
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
1249
|
+
function renderActivityList() {
|
|
1250
|
+
const list = $('sched-list');
|
|
1251
|
+
if (!list) return;
|
|
1252
|
+
const saved = captureListState(list);
|
|
1253
|
+
const clear = _panel?.querySelector('[data-action="clear-search"]');
|
|
1254
|
+
if (clear) clear.hidden = !_query;
|
|
1255
|
+
renderNote();
|
|
1256
|
+
if (_loading || dataIncomplete()) {
|
|
1257
|
+
list.innerHTML = _loading ? emptyState(ICONS.clock, t('schedules.list.loading'), '') : loadFailedHTML();
|
|
500
1258
|
return;
|
|
501
1259
|
}
|
|
1260
|
+
const now = Date.now();
|
|
1261
|
+
const ran = _schedules.filter(s => s.lastRun);
|
|
1262
|
+
const shown = sortSchedules(ran.filter(s => matchesQuery(s, _query, scheduleExtras(s))), 'last');
|
|
1263
|
+
const query = _query.trim();
|
|
1264
|
+
if (!shown.length) {
|
|
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'));
|
|
1269
|
+
restoreListState(list, saved);
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
list.innerHTML = `<div class="sched-box"><ul class="sched-rows sched-rows--activity" aria-label="${te('schedules.activity.listLabel')}">${shown.map(s => {
|
|
1273
|
+
const run = lastRunState(s);
|
|
1274
|
+
const template = getTemplate(s.templateId);
|
|
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');
|
|
1277
|
+
return `<li class="sched-row" data-schedule-id="${esc(s.id)}">
|
|
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) })}">
|
|
1279
|
+
${lastRunHTML(s, now, run, { ago: false })}
|
|
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>
|
|
1281
|
+
<span class="sched-activity-when"><span>${esc(formatAgo(s.lastRun, now))}</span><span>${esc(whenLabel(s.lastRun, now))}</span></span>
|
|
1282
|
+
<span class="sched-activity-when sched-activity-next"><span>${s.enabled ? te('schedules.activity.next') : ''}</span><span>${esc(next)}</span></span>
|
|
1283
|
+
<span class="sched-row-chevron" aria-hidden="true">${ICONS.chevron}</span>
|
|
1284
|
+
</button>
|
|
1285
|
+
</li>`;
|
|
1286
|
+
}).join('')}</ul></div>`;
|
|
1287
|
+
setStatus(query ? tp('schedules.activity.statusMatch', shown.length, { query }) : tp('schedules.count.runs', shown.length));
|
|
1288
|
+
restoreListState(list, saved);
|
|
1289
|
+
}
|
|
502
1290
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
const group = getGroup(s.groupId);
|
|
511
|
-
const profile = s.profile ? CLI_PROFILES.find(p => p.id === s.profile)?.label || s.profile : 'Template default';
|
|
512
|
-
const runtimeParts = [
|
|
513
|
-
profile,
|
|
514
|
-
s.model,
|
|
515
|
-
s.contextMode && s.contextMode !== 'default' ? `context:${s.contextMode}` : '',
|
|
516
|
-
s.effort ? `think:${s.effort}` : '',
|
|
517
|
-
s.mcpProfile ? `mcp:${s.mcpProfile}` : '',
|
|
518
|
-
s.usesBrowser === true ? 'browser:on' : s.usesBrowser === false ? 'browser:off' : '',
|
|
519
|
-
].filter(Boolean);
|
|
520
|
-
return `
|
|
521
|
-
<article class="sched-card${s.enabled ? '' : ' is-paused'}" draggable="true" data-schedule-id="${esc(s.id)}">
|
|
522
|
-
<div class="sched-card-main">
|
|
523
|
-
<div class="sched-card-title-row">
|
|
524
|
-
<h4>${esc(s.name)}</h4>
|
|
525
|
-
${scheduleStatusBadge(s)}
|
|
526
|
-
</div>
|
|
527
|
-
<div class="sched-card-meta">${esc(tpl?.name || 'Missing template')} · ${esc(describeCronClient(s.cron))}</div>
|
|
528
|
-
<div class="sched-card-sub">
|
|
529
|
-
<span>Next: <span class="sched-countdown" data-countdown="${esc(s.nextRun || '')}">${esc(formatNextRun(s.nextRun))}</span></span>
|
|
530
|
-
<span>Runs: ${s.runCount || 0}</span>
|
|
531
|
-
<span>${esc(runtimeParts.join(' · '))}</span>
|
|
532
|
-
${group ? `<span>${esc(group.name)}</span>` : '<span>Ungrouped</span>'}
|
|
533
|
-
</div>
|
|
534
|
-
</div>
|
|
535
|
-
<div class="sched-card-controls">
|
|
536
|
-
<select class="sched-card-group-select" data-schedule-id="${esc(s.id)}" data-value="${esc(s.groupId || '')}">
|
|
537
|
-
${groupOptions}
|
|
538
|
-
</select>
|
|
539
|
-
<button class="sched-icon-btn" data-action="schedule-toggle" data-id="${esc(s.id)}" data-tooltip="${s.enabled ? 'Pause' : 'Enable'}">${s.enabled ? ICONS.pause : ICONS.play}</button>
|
|
540
|
-
<button class="sched-icon-btn" data-action="schedule-test" data-id="${esc(s.id)}" data-tooltip="Run now">${ICONS.bolt}</button>
|
|
541
|
-
<button class="sched-icon-btn" data-action="schedule-edit-prompt" data-id="${esc(s.id)}" data-template-id="${esc(s.templateId || '')}" data-tooltip="Edit prompt">${ICONS.script}</button>
|
|
542
|
-
<button class="sched-icon-btn" data-action="schedule-edit" data-id="${esc(s.id)}" data-tooltip="Edit schedule">${ICONS.edit}</button>
|
|
543
|
-
<button class="sched-icon-btn danger" data-action="schedule-delete" data-id="${esc(s.id)}" data-tooltip="Delete">${ICONS.close}</button>
|
|
544
|
-
</div>
|
|
545
|
-
</article>`;
|
|
546
|
-
}).join('');
|
|
1291
|
+
/** After a reopen, the schedule that was open shows again. */
|
|
1292
|
+
function revealExpanded() {
|
|
1293
|
+
const id = [..._expanded].at(-1);
|
|
1294
|
+
if (!id) return;
|
|
1295
|
+
const row = [..._panel.querySelectorAll('.sched-row')].find(el => el.dataset.scheduleId === id);
|
|
1296
|
+
row?.scrollIntoView({ block: 'nearest' });
|
|
1297
|
+
}
|
|
547
1298
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
1299
|
+
function revealSchedule(id) {
|
|
1300
|
+
const schedule = _schedules.find(s => s.id === id);
|
|
1301
|
+
if (!schedule) return;
|
|
1302
|
+
_query = '';
|
|
1303
|
+
setScope('all');
|
|
1304
|
+
if (schedule.groupId) _collapsed.delete(schedule.groupId);
|
|
1305
|
+
_expanded.add(id);
|
|
1306
|
+
render({ frame: true });
|
|
1307
|
+
const row = [..._panel.querySelectorAll('.sched-row')].find(el => el.dataset.scheduleId === id);
|
|
1308
|
+
row?.scrollIntoView({ block: 'center' });
|
|
1309
|
+
row?.querySelector('.sched-row-toggle')?.focus({ preventScroll: true });
|
|
551
1310
|
}
|
|
552
1311
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
1312
|
+
// ── Timers ──
|
|
1313
|
+
|
|
1314
|
+
function renderTimersView() {
|
|
1315
|
+
const main = $('sched-main');
|
|
1316
|
+
if (!main) return;
|
|
1317
|
+
_frame = 'timers';
|
|
556
1318
|
const selectedTemplate = $('sched-qt-template')?.value || '';
|
|
557
1319
|
const customMinutes = $('sched-qt-custom-min')?.value || '';
|
|
558
1320
|
const selectedSchedule = $('sched-existing-timer-schedule')?.value || '';
|
|
559
1321
|
const scheduleMinutes = $('sched-existing-timer-min')?.value || '';
|
|
560
|
-
const templateOptions = _templates.map(
|
|
561
|
-
const scheduleOptions = _schedules.map(s =>
|
|
562
|
-
const profileOptions = CLI_PROFILES.map(p =>
|
|
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))];
|
|
1324
|
+
const profileOptions = CLI_PROFILES.map(p => opt(p.id, p.label, p.id === _launchProfile));
|
|
563
1325
|
const modelOptions = renderModelOptions(_launchProfile, _launchModel, { accountId: _launchAccount });
|
|
564
1326
|
const contextOptions = renderContextOptions(_launchProfile, _launchModel, _launchContextMode, { accountId: _launchAccount });
|
|
565
1327
|
const effortOptions = renderEffortOptions(_launchProfile, _launchEffort);
|
|
566
|
-
const mcpOptions = renderMcpOptions(_launchMcpProfile);
|
|
567
1328
|
const accountOptions = renderAccountOptions(_launchAccount, false);
|
|
568
1329
|
const showEffort = profileSupportsEffort(_launchProfile);
|
|
569
|
-
const showMcp = _launchProfile
|
|
1330
|
+
const showMcp = strictToolHost(_launchProfile);
|
|
570
1331
|
const showAccount = _launchProfile === 'codex';
|
|
1332
|
+
const usesBrowser = _qtUsesBrowser !== null ? _qtUsesBrowser : !!getTemplate(selectedTemplate)?.usesBrowser;
|
|
571
1333
|
const qtPresetButtons = [5, 15, 30, 60, 120, 240]
|
|
572
|
-
.map(min => `<button class="sched-
|
|
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>`)
|
|
573
1335
|
.join('');
|
|
574
1336
|
|
|
575
1337
|
const modelLoadKey = `${_launchProfile}:${_launchAccount}`;
|
|
576
1338
|
if (isDynamicModelProfile(_launchProfile) && !_modelLoadRequests.has(modelLoadKey)) {
|
|
577
1339
|
_modelLoadRequests.add(modelLoadKey);
|
|
578
|
-
ensureModelsForProfile(_launchProfile, false, _launchAccount).then(() => { if (_panel &&
|
|
579
|
-
}
|
|
580
|
-
if (showAccount) ensureCodexAccountsLoaded(() => { if (_panel &&
|
|
581
|
-
if (showMcp && !
|
|
582
|
-
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
<label class="sched-field wide"><span>Automation</span><select id="sched-qt-template"><option value="">Select automation...</option>${templateOptions}</select></label>
|
|
591
|
-
<label class="sched-field"><span>CLI</span><select id="sched-qt-profile">${profileOptions}</select></label>
|
|
592
|
-
<label class="sched-field"><span>Model</span><select id="sched-qt-model">${modelOptions}</select></label>
|
|
593
|
-
<label class="sched-field"><span>Context</span><select id="sched-qt-context">${contextOptions}</select></label>
|
|
594
|
-
<label class="sched-field" style="${showEffort ? '' : 'display:none'}"><span>Think</span><select id="sched-qt-effort">${effortOptions}</select></label>
|
|
595
|
-
<label class="sched-field" style="${showMcp ? '' : 'display:none'}"><span>MCP</span><select id="sched-qt-mcp">${mcpOptions}</select></label>
|
|
596
|
-
<label class="sched-field" id="sched-qt-account-field" style="${showAccount ? '' : 'display:none'}"><span>OpenAI Account</span><select id="sched-qt-account">${accountOptions}</select></label>
|
|
597
|
-
<label class="sched-toggle"><input type="checkbox" id="sched-qt-browser" ${_qtUsesBrowser ? 'checked' : ''}><span>Uses browser</span></label>
|
|
598
|
-
</div>
|
|
599
|
-
<div class="sched-time-row">
|
|
600
|
-
${qtPresetButtons}
|
|
601
|
-
<input class="sched-min-input" id="sched-qt-custom-min" type="number" min="1" max="1440" value="${esc(customMinutes)}" placeholder="min">
|
|
602
|
-
<button class="sched-primary" data-action="qt-go">${ICONS.play} Go</button>
|
|
603
|
-
<button class="sched-secondary" data-action="qt-now">${ICONS.bolt} Run Now</button>
|
|
1340
|
+
ensureModelsForProfile(_launchProfile, false, _launchAccount).then(() => { if (_panel && _frame === 'timers') renderTimersView(); });
|
|
1341
|
+
}
|
|
1342
|
+
if (showAccount) ensureCodexAccountsLoaded(() => { if (_panel && _frame === 'timers') renderTimersView(); });
|
|
1343
|
+
if (showMcp && !getCachedToolCatalog()) {
|
|
1344
|
+
fetchToolCatalog().then(catalog => { if (catalog && _panel && _frame === 'timers') renderTimersView(); });
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
main.innerHTML = `
|
|
1348
|
+
<div class="sched-view-head">
|
|
1349
|
+
<div class="sched-view-titles">
|
|
1350
|
+
<h2 id="sched-view-title">${esc(viewLabel('timers'))}</h2>
|
|
1351
|
+
<p class="sched-view-sub">${te('schedules.timers.sub')}</p>
|
|
604
1352
|
</div>
|
|
605
|
-
</
|
|
606
|
-
<
|
|
607
|
-
|
|
608
|
-
<div class="sched-form
|
|
609
|
-
<
|
|
610
|
-
|
|
611
|
-
|
|
1353
|
+
</div>
|
|
1354
|
+
<div class="sched-load-note" id="sched-load-note" role="status"></div>
|
|
1355
|
+
<div class="sched-editor-body">
|
|
1356
|
+
<div class="sched-form">
|
|
1357
|
+
<section class="sched-form-section" aria-labelledby="sched-timers-active-title">
|
|
1358
|
+
<h3 class="sched-form-title" id="sched-timers-active-title">${te('schedules.timers.waiting')}</h3>
|
|
1359
|
+
<div id="sched-active-timers"></div>
|
|
1360
|
+
</section>
|
|
1361
|
+
<section class="sched-form-section" aria-labelledby="sched-qt-title">
|
|
1362
|
+
<h3 class="sched-form-title" id="sched-qt-title">${te('schedules.timers.runOnce')}</h3>
|
|
1363
|
+
<div class="sched-form-group">
|
|
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 })}
|
|
1374
|
+
</div>
|
|
1375
|
+
<div class="sched-form-actions">
|
|
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>
|
|
1378
|
+
</div>
|
|
1379
|
+
</section>
|
|
1380
|
+
<section class="sched-form-section" aria-labelledby="sched-st-title">
|
|
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>
|
|
1383
|
+
<div class="sched-form-group">
|
|
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">`)}
|
|
1386
|
+
</div>
|
|
1387
|
+
<div class="sched-form-actions">
|
|
1388
|
+
<button type="button" class="sched-btn sched-btn--primary" data-action="schedule-timer-create">${te('schedules.timers.set')}</button>
|
|
1389
|
+
</div>
|
|
1390
|
+
</section>
|
|
612
1391
|
</div>
|
|
613
|
-
</
|
|
1392
|
+
</div>
|
|
614
1393
|
`;
|
|
1394
|
+
renderNote();
|
|
1395
|
+
renderActiveTimersInto();
|
|
615
1396
|
|
|
616
1397
|
const qtProfile = $('sched-qt-profile');
|
|
617
1398
|
qtProfile?.addEventListener('change', () => {
|
|
@@ -624,15 +1405,15 @@ function renderTimersTab() {
|
|
|
624
1405
|
storage.setItem('as-launch-context-mode', _launchContextMode);
|
|
625
1406
|
_launchEffort = 'off';
|
|
626
1407
|
storage.setItem('as-launch-effort', _launchEffort);
|
|
627
|
-
|
|
1408
|
+
renderTimersView();
|
|
628
1409
|
});
|
|
629
1410
|
$('sched-qt-model')?.addEventListener('change', (e) => {
|
|
630
1411
|
_launchModel = e.target.value || null;
|
|
631
1412
|
storage.setItem('as-launch-model', _launchModel);
|
|
632
1413
|
_launchContextMode = 'default';
|
|
633
1414
|
storage.setItem('as-launch-context-mode', _launchContextMode);
|
|
634
|
-
const
|
|
635
|
-
if (
|
|
1415
|
+
const contextPopup = $('sched-qt-context');
|
|
1416
|
+
if (contextPopup) setDropdownItems(contextPopup, renderContextOptions(_launchProfile, _launchModel, _launchContextMode, { accountId: _launchAccount }));
|
|
636
1417
|
});
|
|
637
1418
|
$('sched-qt-context')?.addEventListener('change', (e) => {
|
|
638
1419
|
_launchContextMode = e.target.value || 'default';
|
|
@@ -643,8 +1424,9 @@ function renderTimersTab() {
|
|
|
643
1424
|
storage.setItem('as-launch-effort', _launchEffort);
|
|
644
1425
|
});
|
|
645
1426
|
$('sched-qt-mcp')?.addEventListener('change', (e) => {
|
|
646
|
-
|
|
647
|
-
storage.setItem('as-launch-
|
|
1427
|
+
_launchTools = readToolPicker(e.target);
|
|
1428
|
+
storage.setItem('as-launch-tools', JSON.stringify(_launchTools));
|
|
1429
|
+
updateToolPicker(e.target, _launchTools, { inheritLabel: toolWord('templateDefault'), usesBrowser });
|
|
648
1430
|
});
|
|
649
1431
|
$('sched-qt-account')?.addEventListener('change', (e) => {
|
|
650
1432
|
_launchAccount = e.target.value || 'default';
|
|
@@ -655,69 +1437,70 @@ function renderTimersTab() {
|
|
|
655
1437
|
const models = automationModelOptions('codex', getModelsForProfile('codex', _launchAccount));
|
|
656
1438
|
if (!models.some((model) => model.id === _launchModel)) _launchModel = (models.find((model) => model.isDefault) || models[0])?.id || null;
|
|
657
1439
|
storage.setItem('as-launch-model', _launchModel || '');
|
|
658
|
-
const
|
|
659
|
-
if (
|
|
660
|
-
const
|
|
661
|
-
if (
|
|
1440
|
+
const modelPopup = $('sched-qt-model');
|
|
1441
|
+
if (modelPopup) setDropdownItems(modelPopup, renderModelOptions('codex', _launchModel, { accountId: _launchAccount }));
|
|
1442
|
+
const contextPopup = $('sched-qt-context');
|
|
1443
|
+
if (contextPopup) setDropdownItems(contextPopup, renderContextOptions('codex', _launchModel, _launchContextMode, { accountId: _launchAccount }));
|
|
662
1444
|
});
|
|
663
1445
|
});
|
|
664
|
-
$('sched-qt-browser')?.addEventListener('change', (e) => {
|
|
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
|
+
});
|
|
665
1451
|
$('sched-qt-template')?.addEventListener('change', (e) => {
|
|
666
1452
|
const template = getTemplate(e.target.value);
|
|
667
1453
|
const browser = $('sched-qt-browser');
|
|
668
1454
|
if (browser && template) {
|
|
669
1455
|
browser.checked = !!template.usesBrowser;
|
|
670
1456
|
_qtUsesBrowser = null;
|
|
1457
|
+
const input = $('sched-qt-mcp');
|
|
1458
|
+
updateToolPicker(input, readToolPicker(input, { requested: true }), { inheritLabel: toolWord('templateDefault'), usesBrowser: browser.checked });
|
|
671
1459
|
}
|
|
672
1460
|
});
|
|
673
1461
|
}
|
|
674
1462
|
|
|
1463
|
+
// The choices of each launch popup (cc-dropdown.js entries, `opt`).
|
|
675
1464
|
function renderModelOptions(profile, selected, { showService = false, accountId = 'default' } = {}) {
|
|
676
1465
|
const models = automationModelOptions(profile, getModelsForProfile(profile, accountId));
|
|
677
1466
|
selected = normalizeAutomationSelection(profile, selected).model || '';
|
|
678
1467
|
if (!models.length) {
|
|
679
1468
|
// List not loaded yet — preserve any stored selection so saving the form cannot
|
|
680
|
-
// blank it (a blank
|
|
681
|
-
if (selected)
|
|
682
|
-
|
|
683
|
-
'<option value="">Template/default</option>',
|
|
684
|
-
`<option value="${esc(selected)}" selected>${esc(selected)}</option>`,
|
|
685
|
-
].join('');
|
|
686
|
-
}
|
|
687
|
-
return '<option value="" selected>Default</option>';
|
|
1469
|
+
// blank it (a blank popup makes save send model:null, which DELETES the override).
|
|
1470
|
+
if (selected) return [opt('', t('schedules.launch.templateOrDefault')), opt(selected, selected, true)];
|
|
1471
|
+
return [opt('', t('schedules.launch.default'), true)];
|
|
688
1472
|
}
|
|
689
1473
|
const known = !!selected && models.some(m => modelMatchesSelector(m, selected));
|
|
690
1474
|
const current = selected || '';
|
|
691
1475
|
const opts = [
|
|
692
|
-
|
|
693
|
-
...models.map(m => {
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
includeService: showService && profile === 'opencode',
|
|
697
|
-
});
|
|
698
|
-
return `<option value="${esc(val)}"${modelMatchesSelector(m, current) ? ' selected' : ''}>${esc(label)}</option>`;
|
|
699
|
-
}),
|
|
1476
|
+
opt('', t('schedules.launch.templateOrDefault'), !current),
|
|
1477
|
+
...models.map(m => opt(modelSelectorValue(m), formatModelOptionLabel(m, {
|
|
1478
|
+
includeService: showService && profile === 'opencode',
|
|
1479
|
+
}), modelMatchesSelector(m, current))),
|
|
700
1480
|
];
|
|
701
1481
|
// Preserve a stored model that isn't in the (still-loading or curated) list so the
|
|
702
1482
|
// dropdown never silently blanks it and a save can't wipe the persisted override.
|
|
703
|
-
if (selected && !known) {
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
1483
|
+
if (selected && !known) opts.push(opt(selected, t('schedules.launch.saved', { value: selected }), true));
|
|
1484
|
+
return opts;
|
|
1485
|
+
}
|
|
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;
|
|
707
1492
|
}
|
|
708
1493
|
|
|
709
1494
|
function renderContextOptions(profile, model, selected, { accountId = 'default', inherit = false } = {}) {
|
|
710
1495
|
const models = getModelsForProfile(profile, accountId);
|
|
711
1496
|
const choices = automationContextOptions(profile, model, models);
|
|
712
1497
|
const current = selected || (inherit ? '' : 'default');
|
|
713
|
-
const options = inherit ? [
|
|
714
|
-
for (const choice of choices)
|
|
715
|
-
options.push(`<option value="${esc(choice.id)}"${current === choice.id ? ' selected' : ''}>${esc(choice.label)}</option>`);
|
|
716
|
-
}
|
|
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));
|
|
717
1500
|
if (current && !choices.some((choice) => choice.id === current)) {
|
|
718
|
-
options.push(
|
|
1501
|
+
options.push(opt(current, t('schedules.launch.unavailable', { value: current }), true));
|
|
719
1502
|
}
|
|
720
|
-
return options
|
|
1503
|
+
return options;
|
|
721
1504
|
}
|
|
722
1505
|
|
|
723
1506
|
function contextChoiceAvailable(profile, model, contextMode, accountId = 'default') {
|
|
@@ -731,280 +1514,346 @@ function modelChoiceAvailable(profile, model, accountId = 'default') {
|
|
|
731
1514
|
return !models.length || models.some((entry) => entry.id === model);
|
|
732
1515
|
}
|
|
733
1516
|
|
|
734
|
-
|
|
1517
|
+
// `inheritLabel` adds the "no override" choice ('') of the editors, apart from an explicit 'off'
|
|
1518
|
+
// (Claude Code: no thinking), which is a value of its own: a run takes it over the group's level.
|
|
1519
|
+
function renderEffortOptions(profile, selected, { inheritLabel = null } = {}) {
|
|
735
1520
|
const levels = getEffortLevelsForProfile(profile);
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
}
|
|
748
|
-
const current = selected ? (presets[selected] ? selected : (presets.full ? 'full' : entries[0][0])) : null;
|
|
749
|
-
return entries.map(([id, p]) => {
|
|
750
|
-
const label = p.label || id;
|
|
751
|
-
const tools = p.tools != null ? ` - ${p.tools} tools` : '';
|
|
752
|
-
return `<option value="${esc(id)}"${id === current ? ' selected' : ''}>${esc(label)}${esc(tools)}</option>`;
|
|
753
|
-
}).join('');
|
|
1521
|
+
const value = selected || '';
|
|
1522
|
+
if (!inheritLabel && !levels.length) return [opt('off', t('schedules.launch.default'), true)];
|
|
1523
|
+
const known = !!value && levels.some(e => e.id === value);
|
|
1524
|
+
const opts = inheritLabel ? [opt('', inheritLabel, !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
|
+
}
|
|
1528
|
+
// A saved level this CLI does not list (set for another CLI, or by hand) stays selected,
|
|
1529
|
+
// so saving the form cannot drop it (the model, MCP and account popups do the same).
|
|
1530
|
+
if (value && !known) opts.push(opt(value, t('schedules.launch.saved', { value }), true));
|
|
1531
|
+
return opts;
|
|
754
1532
|
}
|
|
755
1533
|
|
|
756
|
-
|
|
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` });
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
// The choices of the Codex account popup. `allowInherit` adds a
|
|
757
1541
|
// "Template/group default" option (used by schedule/group forms); the timers
|
|
758
1542
|
// tab passes false so it always resolves to a concrete account.
|
|
759
1543
|
function renderAccountOptions(selected, allowInherit = false) {
|
|
760
1544
|
const accounts = getCachedCodexAccounts();
|
|
761
|
-
const
|
|
1545
|
+
const known = !!selected && accounts.some(a => a.id === selected);
|
|
1546
|
+
// A saved account the list does not show (the list has not loaded yet, or the account is gone)
|
|
1547
|
+
// stays selected: the editors collect the popup before they redraw, and a blank one would clear it.
|
|
1548
|
+
const keep = allowInherit && !!selected && !known;
|
|
1549
|
+
const current = known || keep ? selected : (allowInherit ? '' : 'default');
|
|
762
1550
|
const opts = accounts.map(a => {
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
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);
|
|
766
1556
|
});
|
|
767
|
-
if (allowInherit)
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
return opts.join('');
|
|
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));
|
|
1559
|
+
return opts;
|
|
771
1560
|
}
|
|
772
1561
|
|
|
773
|
-
// Kick off a one-time fetch of accounts; re-render the active
|
|
1562
|
+
// Kick off a one-time fetch of accounts; re-render the active view when ready.
|
|
774
1563
|
function ensureCodexAccountsLoaded(rerender) {
|
|
775
1564
|
if (_codexAccountsRequested) return;
|
|
776
1565
|
_codexAccountsRequested = true;
|
|
777
1566
|
fetchCodexAccounts().then(() => { if (_panel) rerender?.(); }).catch(() => {});
|
|
778
1567
|
}
|
|
779
1568
|
|
|
780
|
-
function
|
|
1569
|
+
function renderActiveTimersInto() {
|
|
1570
|
+
const box = $('sched-active-timers');
|
|
1571
|
+
if (!box) return;
|
|
781
1572
|
const scheduleTimers = Object.entries(_scheduleTimerData).map(([scheduleId, timer]) => {
|
|
782
1573
|
const schedule = _schedules.find(s => s.id === scheduleId);
|
|
1574
|
+
const name = schedule?.name || t('schedules.timers.schedule');
|
|
783
1575
|
return `
|
|
784
|
-
<div class="sched-
|
|
785
|
-
<span>${esc(
|
|
786
|
-
<strong>${formatTimerCountdown(timer.firesAt)}</strong>
|
|
787
|
-
<button class="sched-
|
|
1576
|
+
<div class="sched-timer-row" data-timer-id="${esc(scheduleId)}" data-timer-kind="schedule">
|
|
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>
|
|
788
1580
|
</div>`;
|
|
789
1581
|
}).join('');
|
|
790
1582
|
|
|
791
|
-
const quickTimers = _quickTimers.map(timer =>
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
1583
|
+
const quickTimers = _quickTimers.map(timer => {
|
|
1584
|
+
const meta = [
|
|
1585
|
+
timer.profile ? CLI_PROFILES.find(p => p.id === timer.profile)?.label || timer.profile : '',
|
|
1586
|
+
timer.model,
|
|
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(', ') }) : '',
|
|
1590
|
+
].filter(Boolean).join(' · ');
|
|
1591
|
+
return `
|
|
1592
|
+
<div class="sched-timer-row" data-timer-id="${esc(timer.id)}" data-timer-kind="quick">
|
|
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>
|
|
1596
|
+
</div>`;
|
|
1597
|
+
}).join('');
|
|
798
1598
|
|
|
799
|
-
|
|
800
|
-
|
|
1599
|
+
// The timers load on their own: until they answer (or when they could not), say so here.
|
|
1600
|
+
const note = _timersError
|
|
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>`
|
|
1602
|
+
: '';
|
|
1603
|
+
const empty = _timersError ? '' : `<p class="sched-form-intro">${_timersLoaded ? te('schedules.timers.none') : te('schedules.timers.loading')}</p>`;
|
|
1604
|
+
box.innerHTML = note + (scheduleTimers || quickTimers ? `<div class="sched-form-group">${quickTimers}${scheduleTimers}</div>` : empty);
|
|
801
1605
|
}
|
|
802
1606
|
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
1607
|
+
// ── Form pieces (Settings grouped rows) ──
|
|
1608
|
+
|
|
1609
|
+
function formRow(id, label, control, { help = '', hidden = false, top = false, rowId = '', toggle = false, labelId = '' } = {}) {
|
|
1610
|
+
// A switch sits at the end of its row, its explanation under the label (as in Settings).
|
|
1611
|
+
if (toggle) {
|
|
1612
|
+
return `<div class="sched-form-row sched-form-row--switch"${rowId ? ` id="${rowId}"` : ''}${hidden ? ' hidden' : ''}>
|
|
1613
|
+
<div class="sched-form-stack">
|
|
1614
|
+
<label class="sched-form-label" for="${id}">${esc(label)}</label>
|
|
1615
|
+
${help ? `<p class="sched-help" id="${id}-help">${help}</p>` : ''}
|
|
1616
|
+
</div>
|
|
1617
|
+
<div class="sched-form-control">${control}</div>
|
|
1618
|
+
</div>`;
|
|
808
1619
|
}
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
1620
|
+
// A popup is named by its label (aria-labelledby): its value lives in a hidden input no label can point at.
|
|
1621
|
+
const labelAttrs = labelId ? `id="${labelId}"` : `for="${id}"`;
|
|
1622
|
+
return `<div class="sched-form-row${top || help ? ' sched-form-row--top' : ''}"${rowId ? ` id="${rowId}"` : ''}${hidden ? ' hidden' : ''}>
|
|
1623
|
+
<label class="sched-form-label" ${labelAttrs}>${esc(label)}</label>
|
|
1624
|
+
<div class="sched-form-stack">
|
|
1625
|
+
<div class="sched-form-control">${control}</div>
|
|
1626
|
+
${help ? `<p class="sched-help" id="${id}-help">${help}</p>` : ''}
|
|
1627
|
+
</div>
|
|
1628
|
+
</div>`;
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
/** A row with a popup (cc-dropdown.js): its value is the hidden input `id`, its choices `entries`. */
|
|
1632
|
+
function dropdownRow(id, label, entries, rowOpts = {}) {
|
|
1633
|
+
return formRow(id, label, dropdownHTML(id, entries, { labelledBy: `${id}-label` }), { ...rowOpts, labelId: `${id}-label` });
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
function toggleHTML(id, checked, describedBy = '') {
|
|
1637
|
+
return `<label class="sched-toggle"><input type="checkbox" id="${id}" role="switch"${checked ? ' checked' : ''}${describedBy ? ` aria-describedby="${describedBy}"` : ''}><span class="sched-toggle-track" aria-hidden="true"></span></label>`;
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
// An editor is drawn again when a list it needs arrives (models, MCP presets, accounts), which
|
|
1641
|
+
// can be while the user types: the focused field (and its caret), the scroll position and an
|
|
1642
|
+
// opened Day themes section are put back as they were.
|
|
1643
|
+
function redrawEditor(main, html) {
|
|
1644
|
+
const active = document.activeElement;
|
|
1645
|
+
const focusId = active && main.contains(active) ? active.id : '';
|
|
1646
|
+
const caret = focusId && typeof active.selectionStart === 'number' ? [active.selectionStart, active.selectionEnd] : null;
|
|
1647
|
+
const scroll = main.querySelector('.sched-editor-body')?.scrollTop ?? 0;
|
|
1648
|
+
const opened = main.querySelector('details.sched-form-section')?.open;
|
|
1649
|
+
main.innerHTML = html;
|
|
1650
|
+
const body = main.querySelector('.sched-editor-body');
|
|
1651
|
+
if (body) body.scrollTop = scroll;
|
|
1652
|
+
const details = main.querySelector('details.sched-form-section');
|
|
1653
|
+
if (details && opened) details.open = true;
|
|
1654
|
+
const field = focusId ? document.getElementById(focusId) : null;
|
|
1655
|
+
if (field) {
|
|
1656
|
+
field.focus({ preventScroll: true });
|
|
1657
|
+
if (caret) try { field.setSelectionRange(caret[0], caret[1]); } catch { /* not a text field */ }
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
function editorHead({ back, title, sub, titleId }) {
|
|
1662
|
+
return `<header class="sched-editor-head">
|
|
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>
|
|
1664
|
+
<div class="sched-editor-titles"><h2 id="${titleId}">${esc(title)}</h2>${sub ? `<p>${esc(sub)}</p>` : ''}</div>
|
|
1665
|
+
</header>`;
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
// ── Group editor ──
|
|
1669
|
+
|
|
1670
|
+
function openGroupEditor(id) {
|
|
1671
|
+
_scheduleEditor = null;
|
|
1672
|
+
const group = id ? getGroup(id) : null;
|
|
1673
|
+
_groupEditorId = group ? group.id : null;
|
|
1674
|
+
// `_original` is the group as it was when the editor opened: a save sends only what the user
|
|
1675
|
+
// changed from it, so a change made elsewhere meanwhile (a sync update) is never undone.
|
|
1676
|
+
_groupEditorDraft = group
|
|
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 };
|
|
1679
|
+
ensureModelsForProfile(_groupEditorDraft.profile || 'claude-code', false, _groupEditorDraft.codexAccountId || 'default').then(() => {
|
|
1680
|
+
if (_panel && _groupEditorDraft) {
|
|
1681
|
+
collectGroupEditorDraft();
|
|
1682
|
+
renderGroupEditor(_groupEditorId ? getGroup(_groupEditorId) : null);
|
|
1683
|
+
}
|
|
1684
|
+
});
|
|
816
1685
|
}
|
|
817
1686
|
|
|
818
1687
|
function collectGroupEditorDraft() {
|
|
819
1688
|
if (!_groupEditorDraft || !$('sched-ge-name')) return;
|
|
820
1689
|
const browserRaw = $('sched-ge-browser')?.value || '';
|
|
821
1690
|
Object.assign(_groupEditorDraft, {
|
|
822
|
-
name: $('sched-ge-name')?.value
|
|
1691
|
+
name: $('sched-ge-name')?.value ?? _groupEditorDraft.name,
|
|
823
1692
|
color: $('sched-ge-color')?.value || _groupEditorDraft.color,
|
|
1693
|
+
mutualExclusion: $('sched-ge-mutex')?.checked ?? _groupEditorDraft.mutualExclusion,
|
|
824
1694
|
profile: $('sched-ge-profile')?.value || null,
|
|
825
1695
|
model: $('sched-ge-model')?.value || null,
|
|
826
1696
|
contextMode: $('sched-ge-context')?.value || null,
|
|
827
1697
|
effort: $('sched-ge-effort')?.value || null,
|
|
828
|
-
|
|
1698
|
+
tools: readEditorTools('sched-ge-mcp', _groupEditorDraft?.tools),
|
|
829
1699
|
codexAccountId: $('sched-ge-account')?.value || null,
|
|
830
1700
|
usesBrowser: browserRaw === '' ? undefined : browserRaw === 'true',
|
|
831
1701
|
});
|
|
832
1702
|
}
|
|
833
1703
|
|
|
834
1704
|
function renderGroupEditor(group) {
|
|
835
|
-
const
|
|
1705
|
+
const main = $('sched-main');
|
|
1706
|
+
if (!main || !_groupEditorDraft) return;
|
|
1707
|
+
_frame = 'group-editor';
|
|
1708
|
+
// A group deleted elsewhere while it is edited is still this group (a save then says it is gone).
|
|
1709
|
+
const creating = !_groupEditorId;
|
|
1710
|
+
const d = _groupEditorDraft;
|
|
836
1711
|
const profileForModels = d.profile || 'claude-code';
|
|
837
1712
|
const accountId = d.codexAccountId || 'default';
|
|
838
1713
|
const profileOptions = [
|
|
839
|
-
|
|
840
|
-
...CLI_PROFILES.map(p =>
|
|
841
|
-
]
|
|
1714
|
+
opt('', t('schedules.launch.noOverride'), !d.profile),
|
|
1715
|
+
...CLI_PROFILES.map(p => opt(p.id, p.label, d.profile === p.id)),
|
|
1716
|
+
];
|
|
842
1717
|
const modelOptions = renderModelOptions(profileForModels, d.model || '', { showService: true, accountId });
|
|
843
1718
|
const contextOptions = renderContextOptions(profileForModels, d.model, d.contextMode, { accountId, inherit: true });
|
|
844
|
-
const effortOptions = renderEffortOptions(profileForModels, d.effort || '');
|
|
845
|
-
const mcpOptions = renderMcpOptions(d.mcpProfile || '');
|
|
1719
|
+
const effortOptions = renderEffortOptions(profileForModels, d.effort || '', { inheritLabel: t('schedules.launch.noOverride') });
|
|
846
1720
|
const accountOptions = renderAccountOptions(d.codexAccountId || '', true);
|
|
847
1721
|
const showEffort = profileSupportsEffort(profileForModels);
|
|
848
|
-
const showMcp = profileForModels
|
|
1722
|
+
const showMcp = strictToolHost(profileForModels);
|
|
849
1723
|
const showAccount = profileForModels === 'codex';
|
|
850
|
-
|
|
851
|
-
if (
|
|
852
|
-
|
|
853
|
-
}
|
|
1724
|
+
const rerender = () => { if (_panel && _groupEditorDraft) { collectGroupEditorDraft(); renderGroupEditor(_groupEditorId ? getGroup(_groupEditorId) : null); } };
|
|
1725
|
+
if (showAccount) ensureCodexAccountsLoaded(rerender);
|
|
1726
|
+
if (showMcp && !getCachedToolCatalog()) fetchToolCatalog().then(catalog => { if (catalog) rerender(); });
|
|
854
1727
|
const browserValue = d.usesBrowser === true ? 'true' : d.usesBrowser === false ? 'false' : '';
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
<label class="sched-field wide"><span>Name</span><input id="sched-ge-name" type="text" value="${esc(d.name)}"></label>
|
|
859
|
-
<label class="sched-field"><span>Color</span><input id="sched-ge-color" type="color" value="${esc(d.color || '#4fc3f7')}"></label>
|
|
860
|
-
</div>
|
|
861
|
-
<div class="sched-section-head" style="margin-top:10px"><h4>Launch Overrides</h4><span>Applies to all schedules in this group (schedule-level overrides take priority)</span></div>
|
|
862
|
-
<div class="sched-form-grid">
|
|
863
|
-
<label class="sched-field"><span>CLI</span><select id="sched-ge-profile">${profileOptions}</select></label>
|
|
864
|
-
<label class="sched-field"><span>Model</span><select id="sched-ge-model">${modelOptions}</select></label>
|
|
865
|
-
<label class="sched-field"><span>Context</span><select id="sched-ge-context">${contextOptions}</select></label>
|
|
866
|
-
<label class="sched-field" style="${showEffort ? '' : 'display:none'}"><span>Think</span><select id="sched-ge-effort">
|
|
867
|
-
<option value=""${!d.effort ? ' selected' : ''}>No override</option>
|
|
868
|
-
${effortOptions}
|
|
869
|
-
</select></label>
|
|
870
|
-
<label class="sched-field" style="${showMcp ? '' : 'display:none'}"><span>MCP</span><select id="sched-ge-mcp">
|
|
871
|
-
<option value=""${!d.mcpProfile ? ' selected' : ''}>No override</option>
|
|
872
|
-
${mcpOptions}
|
|
873
|
-
</select></label>
|
|
874
|
-
<label class="sched-field" id="sched-ge-account-field" style="${showAccount ? '' : 'display:none'}"><span>OpenAI Account</span><select id="sched-ge-account">${accountOptions}</select></label>
|
|
875
|
-
<label class="sched-field"><span>Browser</span><select id="sched-ge-browser">
|
|
876
|
-
<option value=""${browserValue === '' ? ' selected' : ''}>No override</option>
|
|
877
|
-
<option value="true"${browserValue === 'true' ? ' selected' : ''}>Force on</option>
|
|
878
|
-
<option value="false"${browserValue === 'false' ? ' selected' : ''}>Force off</option>
|
|
879
|
-
</select></label>
|
|
880
|
-
</div>
|
|
881
|
-
<div class="sched-group-editor-actions">
|
|
882
|
-
<button class="sched-secondary" data-action="group-editor-cancel">Cancel</button>
|
|
883
|
-
<button class="sched-primary" data-action="group-editor-save" data-id="${esc(group.id)}">Save Changes</button>
|
|
884
|
-
</div>
|
|
885
|
-
</div>`;
|
|
886
|
-
}
|
|
1728
|
+
const ordered = orderedGroups();
|
|
1729
|
+
const position = group ? ordered.findIndex(g => g.id === group.id) : -1;
|
|
1730
|
+
const members = group ? _schedules.filter(s => s.groupId === group.id).length : 0;
|
|
887
1731
|
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
1732
|
+
redrawEditor(main, `
|
|
1733
|
+
<div class="sched-editor">
|
|
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' })}
|
|
1735
|
+
<div class="sched-editor-body">
|
|
1736
|
+
<div class="sched-form">
|
|
1737
|
+
<section class="sched-form-section" aria-labelledby="sched-ge-sec-group">
|
|
1738
|
+
<h3 class="sched-form-title" id="sched-ge-sec-group">${te('schedules.form.group')}</h3>
|
|
1739
|
+
<div class="sched-form-group">
|
|
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 })}
|
|
1744
|
+
</div>
|
|
1745
|
+
</section>
|
|
1746
|
+
<section class="sched-form-section" aria-labelledby="sched-ge-sec-launch">
|
|
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>
|
|
1749
|
+
<div class="sched-form-group">
|
|
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'),
|
|
1760
|
+
])}
|
|
1761
|
+
</div>
|
|
1762
|
+
</section>
|
|
1763
|
+
${!group ? '' : `
|
|
1764
|
+
<section class="sched-form-section" aria-labelledby="sched-ge-sec-order">
|
|
1765
|
+
<h3 class="sched-form-title" id="sched-ge-sec-order">${te('schedules.groupEditor.order')}</h3>
|
|
1766
|
+
<div class="sched-form-group">
|
|
1767
|
+
<div class="sched-form-row">
|
|
1768
|
+
<span class="sched-form-label">${te('schedules.groupEditor.position', { position: position + 1, count: ordered.length })}</span>
|
|
1769
|
+
<div class="sched-form-control">
|
|
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>
|
|
1772
|
+
</div>
|
|
1773
|
+
</div>
|
|
1774
|
+
</div>
|
|
1775
|
+
</section>
|
|
1776
|
+
<section class="sched-form-section" aria-labelledby="sched-ge-sec-delete">
|
|
1777
|
+
<h3 class="sched-form-title" id="sched-ge-sec-delete">${te('schedules.groupEditor.delete')}</h3>
|
|
1778
|
+
<div class="sched-form-group">
|
|
1779
|
+
<div class="sched-form-row">
|
|
1780
|
+
<span class="sched-form-label">${members ? tpe('schedules.groupEditor.membersStay', members) : te('schedules.groupEditor.empty')}</span>
|
|
1781
|
+
<div class="sched-form-control">
|
|
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>
|
|
1783
|
+
</div>
|
|
1784
|
+
</div>
|
|
1785
|
+
</div>
|
|
1786
|
+
</section>`}
|
|
913
1787
|
</div>
|
|
914
|
-
${isEditing ? renderGroupEditor(group) : ''}
|
|
915
|
-
</article>`;
|
|
916
|
-
}).join('');
|
|
917
|
-
|
|
918
|
-
content.innerHTML = `
|
|
919
|
-
<section class="sched-tool-section">
|
|
920
|
-
<div class="sched-section-head"><h4>Create Group</h4><span>Use groups to categorize schedules freely</span></div>
|
|
921
|
-
<div class="sched-form-grid compact">
|
|
922
|
-
<label class="sched-field wide"><span>Name</span><input id="sched-group-name" type="text" placeholder="e.g. Twitter cadence"></label>
|
|
923
|
-
<label class="sched-field"><span>Color</span><input id="sched-group-color" type="color" value="#4fc3f7"></label>
|
|
924
|
-
<label class="sched-mutex-toggle sched-mutex-create" data-tooltip="Only one schedule across all mutex-enabled groups can run at a time">
|
|
925
|
-
<input type="checkbox" id="sched-group-mutex"><span class="sched-mutex-label">${ICONS.lock} Mutex</span>
|
|
926
|
-
</label>
|
|
927
|
-
<button class="sched-primary align-end" data-action="group-save-new">${ICONS.plus} Create</button>
|
|
928
1788
|
</div>
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
$('sched-ge-context')
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
1789
|
+
<footer class="sched-editor-actions">
|
|
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>
|
|
1792
|
+
</footer>
|
|
1793
|
+
</div>`);
|
|
1794
|
+
wireGroupEditor();
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
function wireGroupEditor() {
|
|
1798
|
+
const profileSelect = $('sched-ge-profile');
|
|
1799
|
+
profileSelect?.addEventListener('change', () => {
|
|
1800
|
+
collectGroupEditorDraft();
|
|
1801
|
+
_groupEditorDraft.profile = profileSelect.value || null;
|
|
1802
|
+
_groupEditorDraft.model = null;
|
|
1803
|
+
_groupEditorDraft.contextMode = _groupEditorDraft.profile ? 'default' : null;
|
|
1804
|
+
_groupEditorDraft.effort = null;
|
|
1805
|
+
_groupEditorDraft.tools = null;
|
|
1806
|
+
_groupEditorDraft.codexAccountId = null;
|
|
1807
|
+
const rerender = () => { if (_panel && _groupEditorDraft) renderGroupEditor(_groupEditorId ? getGroup(_groupEditorId) : null); };
|
|
1808
|
+
ensureModelsForProfile(_groupEditorDraft.profile || 'claude-code', false, 'default').then(() => { collectGroupEditorDraft(); rerender(); });
|
|
1809
|
+
rerender();
|
|
1810
|
+
});
|
|
1811
|
+
$('sched-ge-model')?.addEventListener('change', (e) => {
|
|
1812
|
+
_groupEditorDraft.model = e.target.value || null;
|
|
1813
|
+
_groupEditorDraft.contextMode = _groupEditorDraft.model ? 'default' : null;
|
|
1814
|
+
const contextPopup = $('sched-ge-context');
|
|
1815
|
+
if (contextPopup) setDropdownItems(contextPopup, renderContextOptions(_groupEditorDraft.profile || 'claude-code', _groupEditorDraft.model, _groupEditorDraft.contextMode, { accountId: _groupEditorDraft.codexAccountId || 'default', inherit: true }));
|
|
1816
|
+
});
|
|
1817
|
+
$('sched-ge-context')?.addEventListener('change', (e) => { _groupEditorDraft.contextMode = e.target.value || null; });
|
|
1818
|
+
$('sched-ge-account')?.addEventListener('change', (e) => {
|
|
1819
|
+
_groupEditorDraft.codexAccountId = e.target.value || null;
|
|
1820
|
+
_groupEditorDraft.contextMode = 'default';
|
|
1821
|
+
ensureModelsForProfile('codex', false, _groupEditorDraft.codexAccountId || 'default').then(() => {
|
|
1822
|
+
if (!_groupEditorDraft) return;
|
|
1823
|
+
const account = _groupEditorDraft.codexAccountId || 'default';
|
|
1824
|
+
const modelPopup = $('sched-ge-model');
|
|
1825
|
+
if (modelPopup) setDropdownItems(modelPopup, renderModelOptions('codex', _groupEditorDraft.model, { accountId: account }));
|
|
1826
|
+
const contextPopup = $('sched-ge-context');
|
|
1827
|
+
if (contextPopup) setDropdownItems(contextPopup, renderContextOptions('codex', _groupEditorDraft.model, _groupEditorDraft.contextMode, { accountId: account, inherit: true }));
|
|
965
1828
|
});
|
|
966
|
-
}
|
|
1829
|
+
});
|
|
967
1830
|
}
|
|
968
1831
|
|
|
969
|
-
|
|
970
|
-
const content = $('sched-content');
|
|
971
|
-
if (!content) return;
|
|
972
|
-
const rows = [..._schedules]
|
|
973
|
-
.sort((a, b) => new Date(b.lastRun || 0) - new Date(a.lastRun || 0))
|
|
974
|
-
.map(s => `
|
|
975
|
-
<div class="sched-activity-row">
|
|
976
|
-
<div>
|
|
977
|
-
<strong>${esc(s.name)}</strong>
|
|
978
|
-
<span>${esc(getTemplate(s.templateId)?.name || 'Missing template')}</span>
|
|
979
|
-
</div>
|
|
980
|
-
${scheduleStatusBadge(s)}
|
|
981
|
-
<span>${esc(s.lastRunResult || 'not run')}</span>
|
|
982
|
-
<span>${esc(formatDateTime(s.lastRun))}</span>
|
|
983
|
-
<span>Next ${esc(formatNextRun(s.nextRun))}</span>
|
|
984
|
-
</div>
|
|
985
|
-
`).join('');
|
|
986
|
-
|
|
987
|
-
content.innerHTML = `
|
|
988
|
-
<div class="sched-section-head"><h4>Schedule Activity</h4><span>Last run state and upcoming runs</span></div>
|
|
989
|
-
<div class="sched-activity-list">${rows || '<div class="sched-empty">No activity yet.</div>'}</div>
|
|
990
|
-
`;
|
|
991
|
-
}
|
|
1832
|
+
// ── Schedule editor ──
|
|
992
1833
|
|
|
993
1834
|
function renderScheduleEditor() {
|
|
994
1835
|
const main = $('sched-main');
|
|
995
1836
|
if (!main) return;
|
|
1837
|
+
_frame = 'schedule-editor';
|
|
996
1838
|
const s = _scheduleEditor || {};
|
|
997
1839
|
const isEdit = !!s.id;
|
|
998
|
-
|
|
1840
|
+
// A saved automation or group the lists do not show (deleted elsewhere, or not in what loaded)
|
|
1841
|
+
// stays selected: a popup without it would show the first choice, and a save would move it.
|
|
1842
|
+
const templateOptions = [
|
|
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)] : []),
|
|
1846
|
+
];
|
|
999
1847
|
const groupOptions = [
|
|
1000
|
-
|
|
1001
|
-
...
|
|
1002
|
-
|
|
1848
|
+
opt('', viewLabel('ungrouped'), !s.groupId),
|
|
1849
|
+
...orderedGroups().map(g => opt(g.id, g.name, s.groupId === g.id)),
|
|
1850
|
+
...(s.groupId && !getGroup(s.groupId) ? [opt(s.groupId, t('schedules.launch.saved', { value: s.groupId }), true)] : []),
|
|
1851
|
+
];
|
|
1003
1852
|
const tz = s.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
1004
1853
|
const profileOptions = [
|
|
1005
|
-
|
|
1006
|
-
...CLI_PROFILES.map(p =>
|
|
1007
|
-
]
|
|
1854
|
+
opt('', t('schedules.launch.templateDefault'), !s.profile),
|
|
1855
|
+
...CLI_PROFILES.map(p => opt(p.id, p.label, s.profile === p.id)),
|
|
1856
|
+
];
|
|
1008
1857
|
const template = getTemplate(s.templateId);
|
|
1009
1858
|
const group = s.groupId ? getGroup(s.groupId) : null;
|
|
1010
1859
|
const profileForModels = s.profile || group?.profile || template?.profile || 'claude-code';
|
|
@@ -1012,25 +1861,39 @@ function renderScheduleEditor() {
|
|
|
1012
1861
|
const effectiveModel = s.model || group?.model || template?.model || null;
|
|
1013
1862
|
const modelOptions = renderModelOptions(profileForModels, s.model || '', { showService: true, accountId });
|
|
1014
1863
|
const contextOptions = renderContextOptions(profileForModels, effectiveModel, s.contextMode, { accountId, inherit: true });
|
|
1015
|
-
const effortOptions = renderEffortOptions(profileForModels, s.effort || '');
|
|
1016
|
-
const mcpOptions = renderMcpOptions(s.mcpProfile || '');
|
|
1864
|
+
const effortOptions = renderEffortOptions(profileForModels, s.effort || '', { inheritLabel: t('schedules.launch.templateOrDefault') });
|
|
1017
1865
|
const accountOptions = renderAccountOptions(s.codexAccountId || '', true);
|
|
1018
1866
|
const showEffort = profileSupportsEffort(profileForModels);
|
|
1019
|
-
const showMcp = profileForModels
|
|
1867
|
+
const showMcp = strictToolHost(profileForModels);
|
|
1020
1868
|
const showAccount = profileForModels === 'codex';
|
|
1021
1869
|
if (showAccount) ensureCodexAccountsLoaded(() => { if (_panel && _scheduleEditor) { collectScheduleEditorDraft(); renderScheduleEditor(); } });
|
|
1022
1870
|
const browserValue = s.usesBrowser === true ? 'true' : s.usesBrowser === false ? 'false' : '';
|
|
1023
|
-
const
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1871
|
+
const cron = s.cron || '0 9 * * *';
|
|
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('');
|
|
1028
1880
|
const dayOrder = [1, 2, 3, 4, 5, 6, 0];
|
|
1029
|
-
const
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1881
|
+
const hasThemes = dayOrder.some(idx => s.dayThemes?.[String(idx)]);
|
|
1882
|
+
// A textarea, not an input: a theme can span lines, and an input would drop its line breaks.
|
|
1883
|
+
// Clearing the text keeps the day's entry (and its label: the day still runs); Remove takes the
|
|
1884
|
+
// whole entry away.
|
|
1885
|
+
const dayThemes = dayOrder.map(idx => {
|
|
1886
|
+
const entry = s.dayThemes?.[String(idx)];
|
|
1887
|
+
const day = weekdayName(idx, { long: true });
|
|
1888
|
+
const help = entry?.label ? te('schedules.scheduleEditor.themeLabel', { label: entry.label }) : '';
|
|
1889
|
+
// A day without an entry keeps the button's room, so the fields line up.
|
|
1890
|
+
const remove = entry
|
|
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>`
|
|
1892
|
+
: '<span class="sched-icon-btn sched-day-remove sched-day-remove--room" aria-hidden="true"></span>';
|
|
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}`,
|
|
1895
|
+
{ help });
|
|
1896
|
+
}).join('');
|
|
1034
1897
|
|
|
1035
1898
|
const modelLoadKey = `${profileForModels}:${accountId}`;
|
|
1036
1899
|
if (isDynamicModelProfile(profileForModels) && !_modelLoadRequests.has(modelLoadKey)) {
|
|
@@ -1039,69 +1902,75 @@ function renderScheduleEditor() {
|
|
|
1039
1902
|
if (_panel && _scheduleEditor) { collectScheduleEditorDraft(); renderScheduleEditor(); }
|
|
1040
1903
|
});
|
|
1041
1904
|
}
|
|
1042
|
-
if (showMcp && !
|
|
1043
|
-
|
|
1905
|
+
if (showMcp && !getCachedToolCatalog()) {
|
|
1906
|
+
fetchToolCatalog().then(catalog => { if (catalog && _panel && _scheduleEditor) { collectScheduleEditorDraft(); renderScheduleEditor(); } });
|
|
1044
1907
|
}
|
|
1045
1908
|
|
|
1046
|
-
main
|
|
1909
|
+
redrawEditor(main, `
|
|
1047
1910
|
<div class="sched-editor">
|
|
1048
|
-
|
|
1049
|
-
<button class="sched-icon-btn" data-action="schedule-editor-close">${ICONS.back}</button>
|
|
1050
|
-
<div>
|
|
1051
|
-
<h3>${isEdit ? 'Edit Schedule' : 'New Schedule'}</h3>
|
|
1052
|
-
<span>${isEdit ? esc(s.name) : 'Create a recurring cron launch'}</span>
|
|
1053
|
-
</div>
|
|
1054
|
-
</div>
|
|
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' })}
|
|
1055
1912
|
<div class="sched-editor-body">
|
|
1056
|
-
<div class="sched-form
|
|
1057
|
-
<
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1913
|
+
<div class="sched-form">
|
|
1914
|
+
<section class="sched-form-section" aria-labelledby="sched-ed-sec-basics">
|
|
1915
|
+
<h3 class="sched-form-title" id="sched-ed-sec-basics">${te('schedules.timers.schedule')}</h3>
|
|
1916
|
+
<div class="sched-form-group">
|
|
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 })}
|
|
1922
|
+
</div>
|
|
1923
|
+
</section>
|
|
1924
|
+
|
|
1925
|
+
<section class="sched-form-section" aria-labelledby="sched-ed-sec-when">
|
|
1926
|
+
<h3 class="sched-form-title" id="sched-ed-sec-when">${te('schedules.scheduleEditor.when')}</h3>
|
|
1927
|
+
<div class="sched-form-group">
|
|
1928
|
+
<div class="sched-form-row sched-form-row--top">
|
|
1929
|
+
<label class="sched-form-label" for="sched-ed-cron">${te('schedules.scheduleEditor.cron')}</label>
|
|
1930
|
+
<div class="sched-form-stack">
|
|
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>
|
|
1932
|
+
<p class="sched-help" id="sched-ed-cron-desc" aria-live="polite">${esc(describeCron(cron))}</p>
|
|
1933
|
+
</div>
|
|
1934
|
+
</div>
|
|
1935
|
+
<div class="sched-form-row sched-form-row--top">
|
|
1936
|
+
<span class="sched-form-label">${te('schedules.scheduleEditor.presets')}</span>
|
|
1937
|
+
<div class="sched-presets">${presetsHTML}</div>
|
|
1938
|
+
</div>
|
|
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">`)}
|
|
1940
|
+
</div>
|
|
1941
|
+
</section>
|
|
1942
|
+
|
|
1943
|
+
<section class="sched-form-section" aria-labelledby="sched-ed-sec-launch">
|
|
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>
|
|
1946
|
+
<div class="sched-form-group">
|
|
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'),
|
|
1957
|
+
])}
|
|
1958
|
+
</div>
|
|
1959
|
+
</section>
|
|
1960
|
+
|
|
1961
|
+
<details class="sched-form-section"${hasThemes ? ' open' : ''}>
|
|
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>
|
|
1964
|
+
<div class="sched-form-group" id="sched-ed-daythemes">${dayThemes}</div>
|
|
1965
|
+
</details>
|
|
1061
1966
|
</div>
|
|
1062
|
-
|
|
1063
|
-
<section class="sched-editor-section">
|
|
1064
|
-
<div class="sched-section-head"><h4>Cron</h4><span id="sched-ed-cron-desc">${esc(describeCronClient(s.cron || '0 9 * * *'))}</span></div>
|
|
1065
|
-
<div class="sched-presets">${presetsHTML}</div>
|
|
1066
|
-
<input class="sched-cron-input" id="sched-ed-cron" type="text" value="${esc(s.cron || '0 9 * * *')}" placeholder="0 9 * * *">
|
|
1067
|
-
</section>
|
|
1068
|
-
|
|
1069
|
-
<section class="sched-editor-section">
|
|
1070
|
-
<div class="sched-section-head"><h4>Launch Overrides</h4><span>Leave blank to use automation defaults</span></div>
|
|
1071
|
-
<div class="sched-form-grid">
|
|
1072
|
-
<label class="sched-field"><span>CLI</span><select id="sched-ed-profile">${profileOptions}</select></label>
|
|
1073
|
-
<label class="sched-field"><span>Model</span><select id="sched-ed-model">${modelOptions}</select></label>
|
|
1074
|
-
<label class="sched-field"><span>Context</span><select id="sched-ed-context">${contextOptions}</select></label>
|
|
1075
|
-
<label class="sched-field" style="${showEffort ? '' : 'display:none'}"><span>Think</span><select id="sched-ed-effort">
|
|
1076
|
-
<option value=""${!s.effort ? ' selected' : ''}>Template/default</option>
|
|
1077
|
-
${effortOptions}
|
|
1078
|
-
</select></label>
|
|
1079
|
-
<label class="sched-field" style="${showMcp ? '' : 'display:none'}"><span>MCP</span><select id="sched-ed-mcp">
|
|
1080
|
-
<option value=""${!s.mcpProfile ? ' selected' : ''}>Template/default</option>
|
|
1081
|
-
${mcpOptions}
|
|
1082
|
-
</select></label>
|
|
1083
|
-
<label class="sched-field" id="sched-ed-account-field" style="${showAccount ? '' : 'display:none'}"><span>OpenAI Account</span><select id="sched-ed-account">${accountOptions}</select></label>
|
|
1084
|
-
<label class="sched-field"><span>Browser</span><select id="sched-ed-browser">
|
|
1085
|
-
<option value=""${browserValue === '' ? ' selected' : ''}>Template default</option>
|
|
1086
|
-
<option value="true"${browserValue === 'true' ? ' selected' : ''}>Force on</option>
|
|
1087
|
-
<option value="false"${browserValue === 'false' ? ' selected' : ''}>Force off</option>
|
|
1088
|
-
</select></label>
|
|
1089
|
-
</div>
|
|
1090
|
-
</section>
|
|
1091
|
-
|
|
1092
|
-
<section class="sched-editor-section">
|
|
1093
|
-
<div class="sched-section-head"><h4>Day Themes</h4><span>Optional context per weekday</span></div>
|
|
1094
|
-
<div class="sched-day-grid" id="sched-ed-daythemes">${dayThemes}</div>
|
|
1095
|
-
</section>
|
|
1096
|
-
|
|
1097
|
-
<label class="sched-toggle"><input id="sched-ed-enabled" type="checkbox" ${s.enabled !== false ? 'checked' : ''}><span>Enabled</span></label>
|
|
1098
|
-
</div>
|
|
1099
|
-
<div class="sched-editor-actions">
|
|
1100
|
-
<button class="sched-secondary" data-action="schedule-editor-close">Cancel</button>
|
|
1101
|
-
<button class="sched-primary" data-action="schedule-editor-save">${isEdit ? 'Save Changes' : 'Create Schedule'}</button>
|
|
1102
1967
|
</div>
|
|
1968
|
+
<footer class="sched-editor-actions">
|
|
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>
|
|
1971
|
+
</footer>
|
|
1103
1972
|
</div>
|
|
1104
|
-
|
|
1973
|
+
`);
|
|
1105
1974
|
|
|
1106
1975
|
$('sched-ed-cron')?.addEventListener('input', updateCronDesc);
|
|
1107
1976
|
$('sched-ed-template')?.addEventListener('change', (e) => {
|
|
@@ -1110,7 +1979,7 @@ function renderScheduleEditor() {
|
|
|
1110
1979
|
_scheduleEditor.model = null;
|
|
1111
1980
|
_scheduleEditor.contextMode = null;
|
|
1112
1981
|
_scheduleEditor.effort = null;
|
|
1113
|
-
_scheduleEditor.
|
|
1982
|
+
_scheduleEditor.tools = null;
|
|
1114
1983
|
_scheduleEditor.codexAccountId = null;
|
|
1115
1984
|
renderScheduleEditor();
|
|
1116
1985
|
});
|
|
@@ -1125,15 +1994,15 @@ function renderScheduleEditor() {
|
|
|
1125
1994
|
_scheduleEditor.model = null;
|
|
1126
1995
|
_scheduleEditor.contextMode = _scheduleEditor.profile ? 'default' : null;
|
|
1127
1996
|
_scheduleEditor.effort = null;
|
|
1128
|
-
_scheduleEditor.
|
|
1997
|
+
_scheduleEditor.tools = null;
|
|
1129
1998
|
_scheduleEditor.codexAccountId = null;
|
|
1130
1999
|
renderScheduleEditor();
|
|
1131
2000
|
});
|
|
1132
2001
|
$('sched-ed-model')?.addEventListener('change', (e) => {
|
|
1133
2002
|
_scheduleEditor.model = e.target.value || null;
|
|
1134
2003
|
_scheduleEditor.contextMode = _scheduleEditor.model ? 'default' : null;
|
|
1135
|
-
const
|
|
1136
|
-
if (
|
|
2004
|
+
const contextPopup = $('sched-ed-context');
|
|
2005
|
+
if (contextPopup) setDropdownItems(contextPopup, renderContextOptions(profileForModels, _scheduleEditor.model || group?.model || template?.model, _scheduleEditor.contextMode, { accountId, inherit: true }));
|
|
1137
2006
|
});
|
|
1138
2007
|
$('sched-ed-context')?.addEventListener('change', (e) => { _scheduleEditor.contextMode = e.target.value || null; });
|
|
1139
2008
|
$('sched-ed-account')?.addEventListener('change', (e) => {
|
|
@@ -1141,10 +2010,11 @@ function renderScheduleEditor() {
|
|
|
1141
2010
|
_scheduleEditor.contextMode = 'default';
|
|
1142
2011
|
const nextAccount = _scheduleEditor.codexAccountId || 'default';
|
|
1143
2012
|
ensureModelsForProfile('codex', false, nextAccount).then(() => {
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
2013
|
+
if (!_scheduleEditor) return;
|
|
2014
|
+
const modelPopup = $('sched-ed-model');
|
|
2015
|
+
if (modelPopup) setDropdownItems(modelPopup, renderModelOptions('codex', _scheduleEditor.model, { accountId: nextAccount, showService: true }));
|
|
2016
|
+
const contextPopup = $('sched-ed-context');
|
|
2017
|
+
if (contextPopup) setDropdownItems(contextPopup, renderContextOptions('codex', _scheduleEditor.model || effectiveModel, _scheduleEditor.contextMode, { accountId: nextAccount, inherit: true }));
|
|
1148
2018
|
});
|
|
1149
2019
|
});
|
|
1150
2020
|
}
|
|
@@ -1152,16 +2022,63 @@ function renderScheduleEditor() {
|
|
|
1152
2022
|
function updateCronDesc() {
|
|
1153
2023
|
const input = $('sched-ed-cron');
|
|
1154
2024
|
const desc = $('sched-ed-cron-desc');
|
|
1155
|
-
if (input
|
|
2025
|
+
if (!input) return;
|
|
2026
|
+
const value = input.value.trim();
|
|
2027
|
+
const valid = value.split(/\s+/).length === 5;
|
|
2028
|
+
if (!valid && value) input.setAttribute('aria-invalid', 'true');
|
|
2029
|
+
else input.removeAttribute('aria-invalid');
|
|
2030
|
+
if (desc) desc.textContent = valid ? describeCron(value) : t('schedules.scheduleEditor.cronHelp');
|
|
2031
|
+
_panel?.querySelectorAll('[data-action="cron-preset"]').forEach(btn => btn.setAttribute('aria-pressed', String(btn.dataset.cron === value)));
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
// ── Saving what was saved before: only what the user changed ──
|
|
2035
|
+
// The server reads a launch field as: absent = keep, null or '' = clear, a value = set (and a
|
|
2036
|
+
// group pushes every changed launch field onto its schedules). So an edit sends a field only when
|
|
2037
|
+
// the form's value differs from the saved one: a popup that cannot show a saved value yet, or a
|
|
2038
|
+
// save of an untouched form, never clears anything.
|
|
2039
|
+
const LAUNCH_FIELDS = ['profile', 'model', 'contextMode', 'effort', 'tools', 'codexAccountId', 'usesBrowser'];
|
|
2040
|
+
// What a CLI's choice decides: a model, context, thinking level, tool list or account of one CLI
|
|
2041
|
+
// means nothing to another, so a change of CLI resets them (the editors' CLI popups do the same).
|
|
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
|
+
|
|
2049
|
+
const blankToNull = (value) => (value === undefined || value === null || value === '' ? null : value);
|
|
2050
|
+
const sameValue = (a, b) => stableJson(blankToNull(a)) === stableJson(blankToNull(b));
|
|
2051
|
+
|
|
2052
|
+
function stableJson(value) {
|
|
2053
|
+
if (Array.isArray(value)) return `[${value.map(stableJson).join(',')}]`;
|
|
2054
|
+
if (value && typeof value === 'object') return `{${Object.keys(value).sort().map(k => `${JSON.stringify(k)}:${stableJson(value[k])}`).join(',')}}`;
|
|
2055
|
+
return JSON.stringify(value ?? null);
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
// The day themes as the form shows them, over the draft's. An entry the user did not touch is
|
|
2059
|
+
// kept exactly (its label too); an edited one keeps its other fields. A day's entry is what lets a
|
|
2060
|
+
// themed schedule run that day (server.js skips a day without one), so an entry goes only once
|
|
2061
|
+
// nothing is left in it, or when its Remove button takes it out of the draft.
|
|
2062
|
+
function collectDayThemes(saved) {
|
|
2063
|
+
const out = saved && typeof saved === 'object' ? JSON.parse(JSON.stringify(saved)) : {};
|
|
2064
|
+
_panel?.querySelectorAll('#sched-ed-daythemes textarea[data-day]').forEach(field => {
|
|
2065
|
+
const day = field.dataset.day;
|
|
2066
|
+
const value = field.value.trim();
|
|
2067
|
+
const before = out[day] && typeof out[day] === 'object' ? out[day] : null;
|
|
2068
|
+
if (value === String(before?.contextOverride ?? '').trim()) return;
|
|
2069
|
+
if (value) { out[day] = { ...(before || {}), contextOverride: value }; return; }
|
|
2070
|
+
if (!before) return;
|
|
2071
|
+
const { contextOverride, ...rest } = before;
|
|
2072
|
+
if (Object.keys(rest).length) out[day] = rest;
|
|
2073
|
+
else delete out[day];
|
|
2074
|
+
});
|
|
2075
|
+
return out;
|
|
1156
2076
|
}
|
|
1157
2077
|
|
|
1158
2078
|
function collectScheduleEditorDraft() {
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
const val = input.value.trim();
|
|
1163
|
-
if (val) dayThemes[input.dataset.day] = { contextOverride: val };
|
|
1164
|
-
});
|
|
2079
|
+
// Only from a form on screen: an absent popup would read as "no value" and wipe the draft.
|
|
2080
|
+
if (!_scheduleEditor || !$('sched-ed-name')) return;
|
|
2081
|
+
const dayThemes = collectDayThemes(_scheduleEditor.dayThemes);
|
|
1165
2082
|
const browserRaw = $('sched-ed-browser')?.value || '';
|
|
1166
2083
|
Object.assign(_scheduleEditor, {
|
|
1167
2084
|
name: $('sched-ed-name')?.value ?? _scheduleEditor.name,
|
|
@@ -1173,7 +2090,7 @@ function collectScheduleEditorDraft() {
|
|
|
1173
2090
|
model: $('sched-ed-model')?.value || null,
|
|
1174
2091
|
contextMode: $('sched-ed-context')?.value || null,
|
|
1175
2092
|
effort: $('sched-ed-effort')?.value || null,
|
|
1176
|
-
|
|
2093
|
+
tools: readEditorTools('sched-ed-mcp', _scheduleEditor?.tools),
|
|
1177
2094
|
codexAccountId: $('sched-ed-account')?.value || null,
|
|
1178
2095
|
usesBrowser: browserRaw === '' ? undefined : browserRaw === 'true',
|
|
1179
2096
|
enabled: $('sched-ed-enabled')?.checked ?? _scheduleEditor.enabled,
|
|
@@ -1181,107 +2098,201 @@ function collectScheduleEditorDraft() {
|
|
|
1181
2098
|
});
|
|
1182
2099
|
}
|
|
1183
2100
|
|
|
2101
|
+
function markInvalid(id, message) {
|
|
2102
|
+
const field = controlOf($(id)); // a popup's trigger, not its hidden input
|
|
2103
|
+
field?.setAttribute('aria-invalid', 'true');
|
|
2104
|
+
field?.focus();
|
|
2105
|
+
showToast(message);
|
|
2106
|
+
}
|
|
2107
|
+
|
|
1184
2108
|
async function saveScheduleFromEditor() {
|
|
2109
|
+
if (editorsWait()) { showToast(t('schedules.toast.notLoaded')); return; }
|
|
2110
|
+
const original = _scheduleEditor?._original || null;
|
|
1185
2111
|
const name = $('sched-ed-name')?.value?.trim();
|
|
1186
2112
|
const templateId = $('sched-ed-template')?.value;
|
|
1187
2113
|
const cron = $('sched-ed-cron')?.value?.trim();
|
|
1188
2114
|
const timezone = $('sched-ed-tz')?.value?.trim();
|
|
1189
2115
|
const groupId = $('sched-ed-group')?.value || null;
|
|
1190
|
-
const profile = $('sched-ed-profile')?.value || null;
|
|
1191
|
-
const model = $('sched-ed-model')?.value || null;
|
|
1192
|
-
const contextMode = $('sched-ed-context')?.value || null;
|
|
1193
|
-
const template = getTemplate(templateId);
|
|
1194
|
-
const group = groupId ? getGroup(groupId) : null;
|
|
1195
|
-
const effectiveProfile = profile || group?.profile || template?.profile || 'claude-code';
|
|
1196
|
-
const effort = normalizeEffortForProfile(effectiveProfile, $('sched-ed-effort')?.value || null);
|
|
1197
|
-
const mcpProfile = $('sched-ed-mcp')?.value || null;
|
|
1198
|
-
const codexAccountId = effectiveProfile === 'codex' ? ($('sched-ed-account')?.value || null) : null;
|
|
1199
|
-
const effectiveModel = model || group?.model || template?.model || null;
|
|
1200
|
-
if (!modelChoiceAvailable(effectiveProfile, effectiveModel, codexAccountId || group?.codexAccountId || template?.codexAccountId || 'default')) {
|
|
1201
|
-
showToast('This model is unavailable for the selected account'); return;
|
|
1202
|
-
}
|
|
1203
|
-
if (!contextChoiceAvailable(effectiveProfile, effectiveModel, contextMode, codexAccountId || group?.codexAccountId || template?.codexAccountId || 'default')) {
|
|
1204
|
-
showToast('This context window is unavailable for the selected model and account'); return;
|
|
1205
|
-
}
|
|
1206
|
-
const browserRaw = $('sched-ed-browser')?.value || '';
|
|
1207
2116
|
const enabled = $('sched-ed-enabled')?.checked ?? true;
|
|
2117
|
+
const browserRaw = $('sched-ed-browser')?.value || '';
|
|
2118
|
+
const launch = {
|
|
2119
|
+
profile: $('sched-ed-profile')?.value || null,
|
|
2120
|
+
model: $('sched-ed-model')?.value || null,
|
|
2121
|
+
contextMode: $('sched-ed-context')?.value || null,
|
|
2122
|
+
// 'off' is a choice of its own (no thinking), never the same as "no override".
|
|
2123
|
+
effort: $('sched-ed-effort')?.value || null,
|
|
2124
|
+
tools: readEditorTools('sched-ed-mcp', _scheduleEditor?.tools),
|
|
2125
|
+
// Read whatever the CLI: a saved account is kept while its row is hidden, never cleared by it.
|
|
2126
|
+
codexAccountId: $('sched-ed-account')?.value || null,
|
|
2127
|
+
usesBrowser: browserRaw === '' ? null : browserRaw === 'true',
|
|
2128
|
+
};
|
|
1208
2129
|
|
|
1209
|
-
if (!name) {
|
|
1210
|
-
if (!templateId) {
|
|
1211
|
-
if (!cron || cron.split(/\s+/).length !== 5) {
|
|
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; }
|
|
1212
2133
|
|
|
1213
|
-
const
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
2134
|
+
const template = getTemplate(templateId);
|
|
2135
|
+
const group = groupId ? getGroup(groupId) : null;
|
|
2136
|
+
// What the run would use is checked when the choice that decides it changed (or for a new schedule).
|
|
2137
|
+
const choiceChanged = !original || !sameValue(templateId, original.templateId) || !sameValue(groupId, original.groupId)
|
|
2138
|
+
|| ['profile', 'model', 'contextMode', 'codexAccountId'].some(k => !sameValue(launch[k], original[k]));
|
|
2139
|
+
if (choiceChanged) {
|
|
2140
|
+
const effectiveProfile = launch.profile || group?.profile || template?.profile || 'claude-code';
|
|
2141
|
+
const effectiveModel = launch.model || group?.model || template?.model || null;
|
|
2142
|
+
const effectiveAccount = launch.codexAccountId || group?.codexAccountId || template?.codexAccountId || 'default';
|
|
2143
|
+
if (!modelChoiceAvailable(effectiveProfile, effectiveModel, effectiveAccount)) {
|
|
2144
|
+
showToast(t('schedules.toast.modelUnavailable')); return;
|
|
2145
|
+
}
|
|
2146
|
+
if (!contextChoiceAvailable(effectiveProfile, effectiveModel, launch.contextMode, effectiveAccount)) {
|
|
2147
|
+
showToast(t('schedules.toast.contextUnavailable')); return;
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
1218
2150
|
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
2151
|
+
collectScheduleEditorDraft(); // the day themes as shown, without the ones removed
|
|
2152
|
+
const dayThemes = _scheduleEditor?.dayThemes || {};
|
|
2153
|
+
let payload;
|
|
2154
|
+
if (original) {
|
|
2155
|
+
payload = {};
|
|
2156
|
+
for (const [key, value] of Object.entries({ name, templateId, cron, groupId, enabled })) {
|
|
2157
|
+
if (!sameValue(value, original[key])) payload[key] = value;
|
|
2158
|
+
}
|
|
2159
|
+
// A schedule without a zone shows the browser's; leaving that as shown keeps it without one.
|
|
2160
|
+
const shownZone = original.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
2161
|
+
if (timezone !== shownZone) payload.timezone = timezone;
|
|
2162
|
+
if (stableJson(dayThemes) !== stableJson(original.dayThemes || {})) payload.dayThemes = dayThemes;
|
|
2163
|
+
for (const key of LAUNCH_FIELDS) if (!sameValue(launch[key], original[key])) payload[key] = launch[key];
|
|
2164
|
+
if (!Object.keys(payload).length) {
|
|
2165
|
+
_scheduleEditor = null;
|
|
2166
|
+
render({ frame: true });
|
|
2167
|
+
focusRow(original.id);
|
|
2168
|
+
showToast(t('schedules.toast.noChanges'));
|
|
2169
|
+
return;
|
|
2170
|
+
}
|
|
2171
|
+
} else {
|
|
2172
|
+
payload = { name, templateId, cron, timezone, groupId, enabled, dayThemes };
|
|
2173
|
+
for (const key of LAUNCH_FIELDS) if (launch[key] !== null) payload[key] = launch[key];
|
|
2174
|
+
}
|
|
1224
2175
|
|
|
2176
|
+
// The save belongs to this editor: once answered, it closes only this one (an editor or a view
|
|
2177
|
+
// opened while the save was on its way stays as it is).
|
|
2178
|
+
const draft = _scheduleEditor;
|
|
2179
|
+
const panel = _panel;
|
|
1225
2180
|
try {
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
2181
|
+
let saved;
|
|
2182
|
+
if (draft?.id) {
|
|
2183
|
+
saved = await updateSchedule(draft.id, payload);
|
|
2184
|
+
const idx = _schedules.findIndex(s => s.id === saved.id);
|
|
2185
|
+
if (idx >= 0) _schedules[idx] = saved;
|
|
2186
|
+
showToast(t('schedules.toast.updated'));
|
|
1231
2187
|
} else {
|
|
1232
|
-
|
|
1233
|
-
if (!_schedules.some(s => s.id ===
|
|
1234
|
-
showToast('
|
|
2188
|
+
saved = await createSchedule(payload);
|
|
2189
|
+
if (!_schedules.some(s => s.id === saved.id)) _schedules.push(saved);
|
|
2190
|
+
showToast(t('schedules.toast.created'));
|
|
1235
2191
|
}
|
|
2192
|
+
if (_panel !== panel || _scheduleEditor !== draft) { refresh(); return; }
|
|
1236
2193
|
_scheduleEditor = null;
|
|
1237
|
-
|
|
2194
|
+
if (saved?.id) _expanded.add(saved.id);
|
|
2195
|
+
render({ frame: true });
|
|
2196
|
+
if (saved?.id) {
|
|
2197
|
+
const row = [..._panel.querySelectorAll('.sched-row')].find(el => el.dataset.scheduleId === saved.id);
|
|
2198
|
+
row?.scrollIntoView({ block: 'nearest' });
|
|
2199
|
+
row?.querySelector('.sched-row-toggle')?.focus({ preventScroll: true });
|
|
2200
|
+
}
|
|
1238
2201
|
} catch (err) {
|
|
1239
|
-
showToast('
|
|
2202
|
+
showToast(t('schedules.toast.saveFailed', { error: err.message }));
|
|
1240
2203
|
}
|
|
1241
2204
|
}
|
|
1242
2205
|
|
|
2206
|
+
// ── Events ──
|
|
2207
|
+
|
|
1243
2208
|
async function handleClick(e) {
|
|
1244
2209
|
const el = e.target.closest('[data-action]');
|
|
1245
|
-
if (!el || !_panel?.contains(el)) return;
|
|
2210
|
+
if (!el || !_panel?.contains(el) || el.disabled) return;
|
|
1246
2211
|
const action = el.dataset.action;
|
|
1247
2212
|
const id = el.dataset.id;
|
|
1248
2213
|
|
|
1249
2214
|
switch (action) {
|
|
1250
|
-
case 'set-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
render();
|
|
2215
|
+
case 'set-scope':
|
|
2216
|
+
_query = _scope === el.dataset.scope ? _query : '';
|
|
2217
|
+
setScope(el.dataset.scope || 'all');
|
|
2218
|
+
render({ frame: true });
|
|
2219
|
+
break;
|
|
2220
|
+
case 'schedule-expand':
|
|
2221
|
+
toggleExpanded(id, el);
|
|
1254
2222
|
break;
|
|
1255
|
-
case '
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
2223
|
+
case 'group-collapse': {
|
|
2224
|
+
const key = el.dataset.key;
|
|
2225
|
+
if (!key) break;
|
|
2226
|
+
if (_collapsed.has(key)) _collapsed.delete(key); else _collapsed.add(key);
|
|
2227
|
+
try { storage.setItem(COLLAPSED_KEY, JSON.stringify([..._collapsed])); } catch { /* not remembered */ }
|
|
2228
|
+
renderList();
|
|
2229
|
+
break;
|
|
2230
|
+
}
|
|
2231
|
+
case 'clear-search':
|
|
2232
|
+
_query = '';
|
|
2233
|
+
{ const input = $('sched-search'); if (input) input.value = ''; }
|
|
2234
|
+
renderMain();
|
|
2235
|
+
$('sched-search')?.focus();
|
|
2236
|
+
break;
|
|
2237
|
+
case 'search-all':
|
|
2238
|
+
setScope('all');
|
|
2239
|
+
render({ frame: true });
|
|
2240
|
+
$('sched-search')?.focus();
|
|
2241
|
+
break;
|
|
2242
|
+
case 'retry-load':
|
|
2243
|
+
_loading = true;
|
|
2244
|
+
render({ frame: true });
|
|
2245
|
+
await loadData(); // loadFinished() redraws, and opens an editor asked for meanwhile
|
|
2246
|
+
break;
|
|
2247
|
+
case 'retry-timers':
|
|
2248
|
+
_timersError = null;
|
|
2249
|
+
_timersLoaded = false;
|
|
2250
|
+
renderActiveTimersInto();
|
|
2251
|
+
await loadTimers();
|
|
2252
|
+
break;
|
|
2253
|
+
case 'dismiss-report':
|
|
2254
|
+
_cliReport = null;
|
|
2255
|
+
renderNote();
|
|
2256
|
+
$('sched-search')?.focus();
|
|
2257
|
+
break;
|
|
2258
|
+
case 'reveal-schedule':
|
|
2259
|
+
revealSchedule(id);
|
|
1259
2260
|
break;
|
|
1260
2261
|
case 'schedule-new':
|
|
1261
|
-
|
|
1262
|
-
render();
|
|
2262
|
+
applyOpenOptions({ action: 'new-schedule' });
|
|
2263
|
+
render({ frame: true });
|
|
2264
|
+
$('sched-ed-name')?.focus();
|
|
1263
2265
|
break;
|
|
1264
2266
|
case 'schedule-edit': {
|
|
1265
|
-
const schedule = _schedules.find(s => s.id === id);
|
|
2267
|
+
const schedule = editorsWait() ? null : _schedules.find(s => s.id === id);
|
|
1266
2268
|
if (schedule) {
|
|
1267
|
-
|
|
1268
|
-
|
|
2269
|
+
_groupEditorId = null;
|
|
2270
|
+
_groupEditorDraft = null;
|
|
2271
|
+
// `_original` is the schedule as saved: a save sends only what differs from it.
|
|
2272
|
+
_scheduleEditor = { ...JSON.parse(JSON.stringify(schedule)), _original: JSON.parse(JSON.stringify(schedule)) };
|
|
2273
|
+
render({ frame: true });
|
|
2274
|
+
$('sched-ed-name')?.focus();
|
|
1269
2275
|
}
|
|
1270
2276
|
break;
|
|
1271
2277
|
}
|
|
1272
2278
|
case 'schedule-edit-prompt': {
|
|
1273
2279
|
const templateId = el.dataset.templateId;
|
|
1274
|
-
if (!templateId) { showToast('
|
|
2280
|
+
if (!templateId) { showToast(t('schedules.toast.noAutomation')); break; }
|
|
1275
2281
|
closePanel();
|
|
1276
2282
|
emit('automations:open', { templateId });
|
|
1277
2283
|
break;
|
|
1278
2284
|
}
|
|
1279
|
-
case 'schedule-editor-close':
|
|
2285
|
+
case 'schedule-editor-close': {
|
|
2286
|
+
const editedId = _scheduleEditor?.id;
|
|
1280
2287
|
_scheduleEditor = null;
|
|
1281
|
-
render();
|
|
2288
|
+
render({ frame: true });
|
|
2289
|
+
focusRow(editedId);
|
|
1282
2290
|
break;
|
|
2291
|
+
}
|
|
1283
2292
|
case 'schedule-editor-save':
|
|
1284
|
-
|
|
2293
|
+
// One save at a time: a second click while one is on its way would send it again.
|
|
2294
|
+
el.disabled = true;
|
|
2295
|
+
try { await saveScheduleFromEditor(); } finally { if (el.isConnected) el.disabled = false; }
|
|
1285
2296
|
break;
|
|
1286
2297
|
case 'cron-preset': {
|
|
1287
2298
|
const cronInput = $('sched-ed-cron');
|
|
@@ -1292,19 +2303,24 @@ async function handleClick(e) {
|
|
|
1292
2303
|
break;
|
|
1293
2304
|
}
|
|
1294
2305
|
case 'schedule-toggle':
|
|
1295
|
-
await toggleSchedule(id);
|
|
2306
|
+
await toggleSchedule(id, el);
|
|
1296
2307
|
break;
|
|
1297
2308
|
case 'schedule-test':
|
|
1298
|
-
await fireSchedule(id);
|
|
2309
|
+
await fireSchedule(id, el);
|
|
1299
2310
|
break;
|
|
1300
2311
|
case 'schedule-delete':
|
|
1301
|
-
|
|
2312
|
+
openConfirm({ kind: 'schedule', id, opener: el });
|
|
2313
|
+
break;
|
|
2314
|
+
case 'confirm-cancel':
|
|
2315
|
+
closeConfirm();
|
|
2316
|
+
break;
|
|
2317
|
+
case 'confirm-accept':
|
|
2318
|
+
await acceptConfirm(el);
|
|
1302
2319
|
break;
|
|
1303
2320
|
case 'group-new':
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
setTimeout(() => $('sched-group-name')?.focus(), 0);
|
|
2321
|
+
applyOpenOptions({ action: 'new-group' });
|
|
2322
|
+
render({ frame: true });
|
|
2323
|
+
$('sched-ge-name')?.focus();
|
|
1308
2324
|
break;
|
|
1309
2325
|
case 'schedule-prompt':
|
|
1310
2326
|
openPromptScheduleModal();
|
|
@@ -1315,25 +2331,39 @@ async function handleClick(e) {
|
|
|
1315
2331
|
case 'prompt-modal-pick':
|
|
1316
2332
|
handlePromptPick(el.dataset.runtime);
|
|
1317
2333
|
break;
|
|
1318
|
-
case 'group-save-new':
|
|
1319
|
-
await saveNewGroup();
|
|
1320
|
-
break;
|
|
1321
|
-
case 'group-mutex-toggle':
|
|
1322
|
-
await toggleGroupMutex(id, el.checked);
|
|
1323
|
-
break;
|
|
1324
2334
|
case 'group-edit':
|
|
1325
|
-
|
|
2335
|
+
if (editorsWait() || !getGroup(id)) break;
|
|
2336
|
+
openGroupEditor(id);
|
|
2337
|
+
render({ frame: true });
|
|
2338
|
+
$('sched-ge-name')?.focus();
|
|
2339
|
+
break;
|
|
2340
|
+
case 'day-theme-remove': {
|
|
2341
|
+
// The whole entry goes, label too: an entry, even an empty one, is what makes the day run.
|
|
2342
|
+
if (!_scheduleEditor) break;
|
|
2343
|
+
collectScheduleEditorDraft();
|
|
2344
|
+
const day = el.dataset.day;
|
|
2345
|
+
const themes = { ...(_scheduleEditor.dayThemes || {}) };
|
|
2346
|
+
delete themes[day];
|
|
2347
|
+
_scheduleEditor.dayThemes = themes;
|
|
2348
|
+
renderScheduleEditor();
|
|
2349
|
+
$(`sched-ed-day-${day}`)?.focus();
|
|
2350
|
+
showToast(t('schedules.toast.themeRemoved', { day: weekdayName(day, { long: true }) }));
|
|
1326
2351
|
break;
|
|
2352
|
+
}
|
|
1327
2353
|
case 'group-editor-save':
|
|
1328
|
-
|
|
2354
|
+
el.disabled = true;
|
|
2355
|
+
try { await saveGroupFromEditor(id || null); } finally { if (el.isConnected) el.disabled = false; }
|
|
1329
2356
|
break;
|
|
1330
|
-
case 'group-editor-cancel':
|
|
2357
|
+
case 'group-editor-cancel': {
|
|
2358
|
+
const editedId = _groupEditorId;
|
|
1331
2359
|
_groupEditorId = null;
|
|
1332
2360
|
_groupEditorDraft = null;
|
|
1333
|
-
|
|
2361
|
+
render({ frame: true });
|
|
2362
|
+
if (editedId) _panel?.querySelector(`.sched-nav-item[data-scope="group:${CSS.escape(editedId)}"]`)?.focus();
|
|
1334
2363
|
break;
|
|
2364
|
+
}
|
|
1335
2365
|
case 'group-delete':
|
|
1336
|
-
|
|
2366
|
+
openConfirm({ kind: 'group', id, opener: el });
|
|
1337
2367
|
break;
|
|
1338
2368
|
case 'group-up':
|
|
1339
2369
|
await moveGroup(id, -1);
|
|
@@ -1341,10 +2371,14 @@ async function handleClick(e) {
|
|
|
1341
2371
|
case 'group-down':
|
|
1342
2372
|
await moveGroup(id, 1);
|
|
1343
2373
|
break;
|
|
1344
|
-
case 'qt-select':
|
|
1345
|
-
|
|
1346
|
-
|
|
2374
|
+
case 'qt-select': {
|
|
2375
|
+
const minutes = Number(el.dataset.minutes);
|
|
2376
|
+
_selectedQtMinutes = _selectedQtMinutes === minutes ? null : minutes;
|
|
2377
|
+
_panel.querySelectorAll('[data-action="qt-select"]').forEach(chip => chip.setAttribute('aria-pressed', String(Number(chip.dataset.minutes) === _selectedQtMinutes)));
|
|
2378
|
+
const custom = $('sched-qt-custom-min');
|
|
2379
|
+
if (custom && _selectedQtMinutes) custom.value = '';
|
|
1347
2380
|
break;
|
|
2381
|
+
}
|
|
1348
2382
|
case 'qt-go':
|
|
1349
2383
|
await createQuickTimerFromForm(false);
|
|
1350
2384
|
break;
|
|
@@ -1363,42 +2397,154 @@ async function handleClick(e) {
|
|
|
1363
2397
|
}
|
|
1364
2398
|
}
|
|
1365
2399
|
|
|
2400
|
+
function toggleExpanded(id, el) {
|
|
2401
|
+
if (!id) return;
|
|
2402
|
+
if (_expanded.has(id)) _expanded.delete(id); else _expanded.add(id);
|
|
2403
|
+
const row = el.closest('.sched-row');
|
|
2404
|
+
if (!row) { renderList(); return; }
|
|
2405
|
+
// Redraw only this row, so the list keeps its place and the focus stays on the row.
|
|
2406
|
+
const schedule = _schedules.find(s => s.id === id);
|
|
2407
|
+
if (!schedule) return;
|
|
2408
|
+
const showGroup = !!row.querySelector('.sched-row-group');
|
|
2409
|
+
const template = document.createElement('template');
|
|
2410
|
+
template.innerHTML = rowHTML(schedule, { now: Date.now(), showGroup }).trim();
|
|
2411
|
+
const next = template.content.firstElementChild;
|
|
2412
|
+
row.replaceWith(next);
|
|
2413
|
+
next.querySelector('.sched-row-toggle')?.focus({ preventScroll: true });
|
|
2414
|
+
if (_expanded.has(id)) next.querySelector('.sched-detail')?.scrollIntoView({ block: 'nearest' });
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
function focusRow(id) {
|
|
2418
|
+
if (!id || !_panel) { $('sched-search')?.focus(); return; }
|
|
2419
|
+
const row = [..._panel.querySelectorAll('.sched-row')].find(el => el.dataset.scheduleId === id);
|
|
2420
|
+
if (row) {
|
|
2421
|
+
row.scrollIntoView({ block: 'nearest' });
|
|
2422
|
+
row.querySelector('.sched-row-toggle')?.focus({ preventScroll: true });
|
|
2423
|
+
} else {
|
|
2424
|
+
$('sched-search')?.focus();
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
|
|
1366
2428
|
async function handleChange(e) {
|
|
1367
|
-
const
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
2429
|
+
const el = e.target;
|
|
2430
|
+
const kind = el.dataset?.change;
|
|
2431
|
+
if (kind === 'sort') {
|
|
2432
|
+
_sort = SORTS.includes(el.value) ? el.value : 'next';
|
|
2433
|
+
try { storage.setItem(SORT_KEY, _sort); } catch { /* not remembered */ }
|
|
2434
|
+
renderList();
|
|
2435
|
+
} else if (kind === 'scope') {
|
|
2436
|
+
_query = '';
|
|
2437
|
+
setScope(el.value || 'all');
|
|
2438
|
+
render({ frame: true });
|
|
2439
|
+
controlOf($('sched-scope-select'))?.focus();
|
|
2440
|
+
} else if (kind === 'move-group') {
|
|
2441
|
+
await moveScheduleToGroup(el.dataset.id, el.value || null);
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2445
|
+
function handleInput(e) {
|
|
2446
|
+
if (e.target.id === 'sched-search') {
|
|
2447
|
+
_query = e.target.value;
|
|
2448
|
+
if (_scope === 'activity') renderActivityList(); else renderList();
|
|
2449
|
+
} else {
|
|
2450
|
+
// A popup's choice arrives on its hidden input; the mark is on its trigger.
|
|
2451
|
+
const field = controlOf(e.target);
|
|
2452
|
+
if (field.getAttribute('aria-invalid') === 'true' && field.id !== 'sched-ed-cron') field.removeAttribute('aria-invalid');
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
|
|
2456
|
+
function handlePanelKeydown(e) {
|
|
2457
|
+
if (_confirm && e.key === 'Tab') {
|
|
2458
|
+
// Keep the focus inside the dialog.
|
|
2459
|
+
const layer = $('sched-dialog-layer');
|
|
2460
|
+
const focusable = [...(layer?.querySelectorAll('button:not(:disabled)') || [])];
|
|
2461
|
+
if (!focusable.length) return;
|
|
2462
|
+
const index = focusable.indexOf(document.activeElement);
|
|
2463
|
+
const next = index < 0 ? 0 : (index + (e.shiftKey ? -1 : 1) + focusable.length) % focusable.length;
|
|
2464
|
+
e.preventDefault();
|
|
2465
|
+
focusable[next].focus();
|
|
2466
|
+
return;
|
|
2467
|
+
}
|
|
2468
|
+
const typing = e.target.closest('input, textarea, select, [contenteditable], [role="combobox"]');
|
|
2469
|
+
const find = (!typing && e.key === '/') || ((e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey && e.key.toLowerCase() === 'f');
|
|
2470
|
+
if (find && !editorOpen() && !_confirm && $('sched-search') && _scope !== 'timers') {
|
|
2471
|
+
e.preventDefault();
|
|
2472
|
+
e.stopPropagation();
|
|
2473
|
+
$('sched-search').focus();
|
|
2474
|
+
$('sched-search').select();
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
|
|
2478
|
+
function handleEscape(e) {
|
|
2479
|
+
if (e.key !== 'Escape' || !_panel || _promptModal) return;
|
|
2480
|
+
if (document.querySelector('.menubar-item.open')) return; // the menubar closes its menu first
|
|
2481
|
+
const target = e.target;
|
|
2482
|
+
// Escape in another window belongs to that window.
|
|
2483
|
+
if (target && target !== document.body && !_panel.contains(target) && !target.closest?.('#title-bar')) return;
|
|
2484
|
+
e.preventDefault();
|
|
2485
|
+
e.stopPropagation();
|
|
2486
|
+
// An open popup closes first, and the focus goes back to it (this runs before its own key handler).
|
|
2487
|
+
if (closeDropdowns(_panel, { focus: true })) return;
|
|
2488
|
+
if (_confirm) { closeConfirm(); return; }
|
|
2489
|
+
if (_scheduleEditor) {
|
|
2490
|
+
const editedId = _scheduleEditor.id;
|
|
2491
|
+
_scheduleEditor = null;
|
|
2492
|
+
render({ frame: true });
|
|
2493
|
+
focusRow(editedId);
|
|
2494
|
+
return;
|
|
2495
|
+
}
|
|
2496
|
+
if (_groupEditorDraft) {
|
|
2497
|
+
_groupEditorId = null;
|
|
2498
|
+
_groupEditorDraft = null;
|
|
2499
|
+
render({ frame: true });
|
|
2500
|
+
$('sched-search')?.focus();
|
|
2501
|
+
return;
|
|
1378
2502
|
}
|
|
2503
|
+
if (_query && target?.id === 'sched-search') {
|
|
2504
|
+
_query = '';
|
|
2505
|
+
target.value = '';
|
|
2506
|
+
renderMain();
|
|
2507
|
+
return;
|
|
2508
|
+
}
|
|
2509
|
+
closePanel();
|
|
1379
2510
|
}
|
|
1380
2511
|
|
|
2512
|
+
// ── Drag a schedule onto a group ──
|
|
2513
|
+
|
|
1381
2514
|
function handleDragStart(e) {
|
|
1382
|
-
const
|
|
1383
|
-
if (!
|
|
1384
|
-
_draggingScheduleId =
|
|
1385
|
-
|
|
2515
|
+
const row = e.target.closest?.('.sched-row-line[draggable="true"]')?.closest('.sched-row');
|
|
2516
|
+
if (!row) return;
|
|
2517
|
+
_draggingScheduleId = row.dataset.scheduleId;
|
|
2518
|
+
row.classList.add('is-dragging');
|
|
1386
2519
|
try { e.dataTransfer.setData('text/plain', _draggingScheduleId); } catch {}
|
|
1387
2520
|
e.dataTransfer.effectAllowed = 'move';
|
|
1388
2521
|
}
|
|
1389
2522
|
|
|
2523
|
+
function clearDropTargets() {
|
|
2524
|
+
_panel?.querySelectorAll('.drop-target').forEach(el => el.classList.remove('drop-target'));
|
|
2525
|
+
}
|
|
2526
|
+
|
|
1390
2527
|
function handleDragEnd() {
|
|
1391
2528
|
_draggingScheduleId = null;
|
|
1392
|
-
_panel?.querySelectorAll('.sched-
|
|
1393
|
-
|
|
2529
|
+
_panel?.querySelectorAll('.sched-row.is-dragging').forEach(el => el.classList.remove('is-dragging'));
|
|
2530
|
+
clearDropTargets();
|
|
1394
2531
|
}
|
|
1395
2532
|
|
|
1396
2533
|
function handleDragOver(e) {
|
|
1397
2534
|
if (!_draggingScheduleId) return;
|
|
1398
2535
|
const target = e.target.closest('[data-drop-group]');
|
|
1399
|
-
if (!target) return;
|
|
2536
|
+
if (!target) { clearDropTargets(); return; }
|
|
1400
2537
|
e.preventDefault();
|
|
1401
|
-
|
|
2538
|
+
e.dataTransfer.dropEffect = 'move';
|
|
2539
|
+
if (!target.classList.contains('drop-target')) {
|
|
2540
|
+
clearDropTargets();
|
|
2541
|
+
target.classList.add('drop-target');
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
|
|
2545
|
+
function handleDragLeave(e) {
|
|
2546
|
+
const target = e.target.closest?.('[data-drop-group]');
|
|
2547
|
+
if (target && !target.contains(e.relatedTarget)) target.classList.remove('drop-target');
|
|
1402
2548
|
}
|
|
1403
2549
|
|
|
1404
2550
|
async function handleDrop(e) {
|
|
@@ -1406,168 +2552,325 @@ async function handleDrop(e) {
|
|
|
1406
2552
|
const target = e.target.closest('[data-drop-group]');
|
|
1407
2553
|
if (!target) return;
|
|
1408
2554
|
e.preventDefault();
|
|
1409
|
-
const
|
|
2555
|
+
const id = _draggingScheduleId;
|
|
2556
|
+
handleDragEnd();
|
|
2557
|
+
await moveScheduleToGroup(id, target.dataset.dropGroup || null);
|
|
2558
|
+
}
|
|
2559
|
+
|
|
2560
|
+
// ── Writes ──
|
|
2561
|
+
|
|
2562
|
+
function replaceSchedule(updated) {
|
|
2563
|
+
const idx = _schedules.findIndex(s => s.id === updated.id);
|
|
2564
|
+
if (idx >= 0) _schedules[idx] = updated;
|
|
2565
|
+
else _schedules.push(updated);
|
|
2566
|
+
}
|
|
2567
|
+
|
|
2568
|
+
async function moveScheduleToGroup(id, groupId) {
|
|
2569
|
+
const schedule = _schedules.find(s => s.id === id);
|
|
2570
|
+
if (!schedule || (schedule.groupId || null) === (groupId || null)) return;
|
|
1410
2571
|
try {
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
_selectedGroup = groupId || 'ungrouped';
|
|
1415
|
-
showToast(groupId ? `Moved to ${getGroup(groupId)?.name || 'group'}` : 'Moved to Ungrouped');
|
|
1416
|
-
render();
|
|
2572
|
+
replaceSchedule(await updateSchedule(id, { groupId }));
|
|
2573
|
+
showToast(t('schedules.toast.movedTo', { group: groupId ? getGroup(groupId)?.name || t('schedules.toast.aGroup') : viewLabel('ungrouped') }));
|
|
2574
|
+
refresh();
|
|
1417
2575
|
} catch (err) {
|
|
1418
|
-
showToast('
|
|
1419
|
-
|
|
1420
|
-
handleDragEnd();
|
|
2576
|
+
showToast(t('schedules.toast.moveFailed', { error: err.message }));
|
|
2577
|
+
refresh();
|
|
1421
2578
|
}
|
|
1422
2579
|
}
|
|
1423
2580
|
|
|
1424
|
-
async function toggleSchedule(id) {
|
|
2581
|
+
async function toggleSchedule(id, el = null) {
|
|
1425
2582
|
const schedule = _schedules.find(s => s.id === id);
|
|
1426
2583
|
if (!schedule) return;
|
|
2584
|
+
el?.setAttribute('aria-busy', 'true');
|
|
1427
2585
|
try {
|
|
1428
|
-
|
|
1429
|
-
const
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
render();
|
|
2586
|
+
replaceSchedule(await updateSchedule(id, { enabled: !schedule.enabled }));
|
|
2587
|
+
const updated = _schedules.find(s => s.id === id);
|
|
2588
|
+
showToast(t(updated?.enabled ? 'schedules.toast.resumed' : 'schedules.toast.paused', { name: schedule.name }));
|
|
2589
|
+
refresh();
|
|
1433
2590
|
} catch (err) {
|
|
1434
|
-
|
|
2591
|
+
el?.removeAttribute('aria-busy');
|
|
2592
|
+
showToast(t('schedules.toast.toggleFailed', { error: err.message }));
|
|
1435
2593
|
}
|
|
1436
2594
|
}
|
|
1437
2595
|
|
|
1438
|
-
async function fireSchedule(id) {
|
|
2596
|
+
async function fireSchedule(id, el = null) {
|
|
2597
|
+
if (el) el.disabled = true;
|
|
1439
2598
|
try {
|
|
1440
2599
|
await testSchedule(id, {
|
|
1441
2600
|
sidepanelWindowId: getNativeLoopRouterWindowId(),
|
|
1442
2601
|
sidepanelClaimToken: getNativeLoopRouterClaimToken(),
|
|
1443
2602
|
});
|
|
1444
|
-
showToast('
|
|
2603
|
+
showToast(t('schedules.toast.launching'));
|
|
1445
2604
|
} catch (err) {
|
|
1446
|
-
showToast('
|
|
2605
|
+
showToast(t('schedules.toast.runFailed', { error: err.message }));
|
|
2606
|
+
} finally {
|
|
2607
|
+
if (el?.isConnected) el.disabled = false;
|
|
1447
2608
|
}
|
|
1448
2609
|
}
|
|
1449
2610
|
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
2611
|
+
// ── Confirmation (destructive actions) ──
|
|
2612
|
+
|
|
2613
|
+
function openConfirm({ kind, id, opener }) {
|
|
2614
|
+
const layer = $('sched-dialog-layer');
|
|
2615
|
+
if (!layer) return;
|
|
2616
|
+
let title, name, text, accept;
|
|
2617
|
+
if (kind === 'schedule') {
|
|
2618
|
+
const schedule = _schedules.find(s => s.id === id);
|
|
2619
|
+
if (!schedule) return;
|
|
2620
|
+
title = t('schedules.confirm.scheduleTitle');
|
|
2621
|
+
name = schedule.name;
|
|
2622
|
+
text = t('schedules.confirm.scheduleText');
|
|
2623
|
+
accept = t('schedules.confirm.scheduleAccept');
|
|
2624
|
+
} else {
|
|
2625
|
+
const group = getGroup(id);
|
|
2626
|
+
if (!group) return;
|
|
2627
|
+
const count = _schedules.filter(s => s.groupId === id).length;
|
|
2628
|
+
title = t('schedules.confirm.groupTitle');
|
|
2629
|
+
name = group.name;
|
|
2630
|
+
text = count ? tp('schedules.groupEditor.membersStay', count) : t('schedules.groupEditor.empty');
|
|
2631
|
+
accept = t('schedules.confirm.groupAccept');
|
|
1459
2632
|
}
|
|
2633
|
+
_confirm = { kind, id, opener };
|
|
2634
|
+
layer.innerHTML = `<div class="sched-dialog" role="alertdialog" aria-modal="true" aria-labelledby="sched-dialog-title" aria-describedby="sched-dialog-text">
|
|
2635
|
+
<h4 id="sched-dialog-title">${esc(title)}</h4>
|
|
2636
|
+
<div class="sched-dialog-body">
|
|
2637
|
+
<p class="sched-dialog-name">${esc(name)}</p>
|
|
2638
|
+
<p class="sched-help" id="sched-dialog-text">${esc(text)}</p>
|
|
2639
|
+
</div>
|
|
2640
|
+
<div class="sched-dialog-actions">
|
|
2641
|
+
<button type="button" class="sched-btn" data-action="confirm-cancel">${te('schedules.editor.cancel')}</button>
|
|
2642
|
+
<button type="button" class="sched-btn sched-btn--danger" data-action="confirm-accept">${ICONS.trash}<span>${esc(accept)}</span></button>
|
|
2643
|
+
</div>
|
|
2644
|
+
</div>`;
|
|
2645
|
+
layer.hidden = false;
|
|
2646
|
+
for (const part of _panel.querySelectorAll(':scope > .sched-header, :scope > .sched-body')) part.inert = true;
|
|
2647
|
+
layer.querySelector('[data-action="confirm-cancel"]')?.focus();
|
|
1460
2648
|
}
|
|
1461
2649
|
|
|
1462
|
-
|
|
1463
|
-
const
|
|
1464
|
-
|
|
1465
|
-
const
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
if (!_groups.some(g => g.id === group.id)) _groups.push(group);
|
|
1470
|
-
_selectedGroup = group.id;
|
|
1471
|
-
_tab = 'cron';
|
|
1472
|
-
render();
|
|
1473
|
-
showToast('Group created');
|
|
1474
|
-
} catch (err) {
|
|
1475
|
-
showToast('Group failed: ' + err.message);
|
|
1476
|
-
}
|
|
2650
|
+
function closeConfirm() {
|
|
2651
|
+
const layer = $('sched-dialog-layer');
|
|
2652
|
+
if (layer) { layer.hidden = true; layer.innerHTML = ''; }
|
|
2653
|
+
for (const part of _panel?.querySelectorAll(':scope > .sched-header, :scope > .sched-body') || []) part.inert = false;
|
|
2654
|
+
const opener = _confirm?.opener;
|
|
2655
|
+
_confirm = null;
|
|
2656
|
+
if (opener?.isConnected) opener.focus();
|
|
1477
2657
|
}
|
|
1478
2658
|
|
|
1479
|
-
async function
|
|
1480
|
-
if (
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
});
|
|
2659
|
+
async function acceptConfirm(button) {
|
|
2660
|
+
if (!_confirm) return;
|
|
2661
|
+
const { kind, id } = _confirm;
|
|
2662
|
+
button.disabled = true;
|
|
2663
|
+
try {
|
|
2664
|
+
if (kind === 'schedule') {
|
|
2665
|
+
await deleteSchedule(id);
|
|
2666
|
+
_schedules = _schedules.filter(s => s.id !== id);
|
|
2667
|
+
_expanded.delete(id);
|
|
2668
|
+
showToast(t('schedules.toast.deleted'));
|
|
2669
|
+
} else {
|
|
2670
|
+
await deleteScheduleGroup(id);
|
|
2671
|
+
_groups = _groups.filter(g => g.id !== id);
|
|
2672
|
+
for (const schedule of _schedules) if (schedule.groupId === id) schedule.groupId = null;
|
|
2673
|
+
if (_scope === `group:${id}`) setScope('ungrouped', { keepEditors: true });
|
|
2674
|
+
if (_groupEditorId === id) { _groupEditorId = null; _groupEditorDraft = null; }
|
|
2675
|
+
showToast(t('schedules.toast.groupDeleted'));
|
|
1494
2676
|
}
|
|
2677
|
+
_confirm.opener = null;
|
|
2678
|
+
closeConfirm();
|
|
2679
|
+
render({ frame: true });
|
|
2680
|
+
$('sched-search')?.focus();
|
|
2681
|
+
} catch (err) {
|
|
2682
|
+
button.disabled = false;
|
|
2683
|
+
showToast(t(kind === 'schedule' ? 'schedules.toast.deleteFailed' : 'schedules.toast.groupDeleteFailed', { error: err.message }));
|
|
1495
2684
|
}
|
|
1496
|
-
renderGroupsTab();
|
|
1497
2685
|
}
|
|
1498
2686
|
|
|
2687
|
+
// ── Groups ──
|
|
2688
|
+
|
|
1499
2689
|
async function saveGroupFromEditor(id) {
|
|
2690
|
+
if (editorsWait()) { showToast(t('schedules.toast.notLoaded')); return; }
|
|
2691
|
+
collectGroupEditorDraft();
|
|
1500
2692
|
const name = $('sched-ge-name')?.value?.trim();
|
|
1501
|
-
if (!name) {
|
|
2693
|
+
if (!name) { markInvalid('sched-ge-name', t('schedules.toast.groupNameRequired')); return; }
|
|
1502
2694
|
const color = $('sched-ge-color')?.value || null;
|
|
1503
|
-
const
|
|
1504
|
-
const model = $('sched-ge-model')?.value || null;
|
|
1505
|
-
const contextMode = $('sched-ge-context')?.value || null;
|
|
1506
|
-
const effectiveProfile = profile || 'claude-code';
|
|
1507
|
-
const effort = normalizeEffortForProfile(effectiveProfile, $('sched-ge-effort')?.value || null);
|
|
1508
|
-
const mcpProfile = $('sched-ge-mcp')?.value || null;
|
|
1509
|
-
const codexAccountId = effectiveProfile === 'codex' ? ($('sched-ge-account')?.value || null) : null;
|
|
1510
|
-
if (!modelChoiceAvailable(effectiveProfile, model, codexAccountId || 'default')) {
|
|
1511
|
-
showToast('This model is unavailable for the selected account'); return;
|
|
1512
|
-
}
|
|
1513
|
-
if (!contextChoiceAvailable(effectiveProfile, model, contextMode, codexAccountId || 'default')) {
|
|
1514
|
-
showToast('This context window is unavailable for the selected model and account'); return;
|
|
1515
|
-
}
|
|
2695
|
+
const mutualExclusion = !!$('sched-ge-mutex')?.checked;
|
|
1516
2696
|
const browserRaw = $('sched-ge-browser')?.value || '';
|
|
1517
|
-
const
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
2697
|
+
const launch = {
|
|
2698
|
+
profile: $('sched-ge-profile')?.value || null,
|
|
2699
|
+
model: $('sched-ge-model')?.value || null,
|
|
2700
|
+
contextMode: $('sched-ge-context')?.value || null,
|
|
2701
|
+
effort: $('sched-ge-effort')?.value || null, // 'off' stays explicit
|
|
2702
|
+
tools: readEditorTools('sched-ge-mcp', _groupEditorDraft?.tools),
|
|
2703
|
+
codexAccountId: $('sched-ge-account')?.value || null, // read whatever the CLI: hidden is not cleared
|
|
2704
|
+
usesBrowser: browserRaw === '' ? null : browserRaw === 'true',
|
|
2705
|
+
};
|
|
2706
|
+
// The group as it was when the editor opened, not as a sync update has made it since.
|
|
2707
|
+
const existing = id ? _groupEditorDraft?._original || getGroup(id) || {} : null;
|
|
2708
|
+
if (!existing || ['profile', 'model', 'contextMode', 'codexAccountId'].some(k => !sameValue(launch[k], existing[k]))) {
|
|
2709
|
+
const effectiveProfile = launch.profile || 'claude-code';
|
|
2710
|
+
if (!modelChoiceAvailable(effectiveProfile, launch.model, launch.codexAccountId || 'default')) {
|
|
2711
|
+
showToast(t('schedules.toast.modelUnavailable')); return;
|
|
2712
|
+
}
|
|
2713
|
+
if (!contextChoiceAvailable(effectiveProfile, launch.model, launch.contextMode, launch.codexAccountId || 'default')) {
|
|
2714
|
+
showToast(t('schedules.toast.contextUnavailable')); return;
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
|
|
2718
|
+
// The save belongs to this editor: once it is answered, the window goes to the group only while
|
|
2719
|
+
// this editor is still the one open. One opened meanwhile (or a closed window) is left as it is.
|
|
2720
|
+
const draft = _groupEditorDraft;
|
|
2721
|
+
const panel = _panel;
|
|
2722
|
+
const stillHere = () => !!draft && _panel === panel && _groupEditorDraft === draft;
|
|
2723
|
+
|
|
2724
|
+
if (!id) {
|
|
2725
|
+
try {
|
|
2726
|
+
const payload = { name, color, mutualExclusion };
|
|
2727
|
+
for (const key of LAUNCH_FIELDS) if (launch[key] !== null) payload[key] = launch[key];
|
|
2728
|
+
const group = await createScheduleGroup(payload);
|
|
2729
|
+
if (!_groups.some(g => g.id === group.id)) _groups.push(group);
|
|
2730
|
+
showToast(t('schedules.toast.groupCreated'));
|
|
2731
|
+
if (!stillHere()) { refresh(); return; }
|
|
2732
|
+
_groupEditorId = null;
|
|
2733
|
+
_groupEditorDraft = null;
|
|
2734
|
+
_query = '';
|
|
2735
|
+
setScope(`group:${group.id}`);
|
|
2736
|
+
render({ frame: true });
|
|
2737
|
+
_panel?.querySelector(`.sched-nav-item[data-scope="group:${CSS.escape(group.id)}"]`)?.focus();
|
|
2738
|
+
} catch (err) {
|
|
2739
|
+
showToast(t('schedules.toast.groupFailed', { error: err.message }));
|
|
2740
|
+
}
|
|
2741
|
+
return;
|
|
2742
|
+
}
|
|
2743
|
+
|
|
2744
|
+
// Send only what the user changed. The server pushes a changed group launch field onto every
|
|
2745
|
+
// schedule in the group, so an unchanged one sent anyway would overwrite their own choices.
|
|
2746
|
+
// The colour counts as changed when it differs from what showed (an unset one shows the default).
|
|
2747
|
+
const payload = {};
|
|
2748
|
+
if (name !== String(existing.name ?? '').trim()) payload.name = name;
|
|
2749
|
+
const shownColor = /^#[0-9a-f]{6}$/i.test(existing.color || '') ? existing.color.toLowerCase() : DEFAULT_GROUP_COLOR;
|
|
2750
|
+
if (color && color.toLowerCase() !== shownColor) payload.color = color;
|
|
2751
|
+
if (mutualExclusion !== !!existing.mutualExclusion) payload.mutualExclusion = mutualExclusion;
|
|
2752
|
+
for (const key of LAUNCH_FIELDS) if (!sameValue(launch[key], existing[key])) payload[key] = launch[key];
|
|
2753
|
+
// A new CLI: what the old one decided goes, explicitly (also what the group never set itself).
|
|
2754
|
+
const cliChanged = !sameValue(launch.profile, existing.profile);
|
|
2755
|
+
if (cliChanged) for (const key of CLI_DEPENDENT) payload[key] = launch[key];
|
|
2756
|
+
if (!Object.keys(payload).length) {
|
|
2757
|
+
_groupEditorId = null;
|
|
2758
|
+
_groupEditorDraft = null;
|
|
2759
|
+
render({ frame: true });
|
|
2760
|
+
_panel?.querySelector(`.sched-nav-item[data-scope="group:${CSS.escape(id)}"]`)?.focus();
|
|
2761
|
+
showToast(t('schedules.toast.noChanges'));
|
|
2762
|
+
return;
|
|
1527
2763
|
}
|
|
2764
|
+
// What each schedule of the group holds as the user saves: a reset afterwards writes only over
|
|
2765
|
+
// values that are still these.
|
|
2766
|
+
const members = cliChanged ? captureMembers(id, launch) : [];
|
|
1528
2767
|
try {
|
|
1529
2768
|
const updated = await updateScheduleGroup(id, payload);
|
|
1530
2769
|
const idx = _groups.findIndex(g => g.id === id);
|
|
1531
2770
|
if (idx >= 0) _groups[idx] = updated;
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
2771
|
+
if (stillHere()) {
|
|
2772
|
+
_groupEditorId = null;
|
|
2773
|
+
_groupEditorDraft = null;
|
|
2774
|
+
setScope(`group:${id}`);
|
|
2775
|
+
render({ frame: true });
|
|
2776
|
+
_panel?.querySelector(`.sched-nav-item[data-scope="group:${CSS.escape(id)}"]`)?.focus();
|
|
2777
|
+
}
|
|
2778
|
+
if (!cliChanged) {
|
|
2779
|
+
showToast(t('schedules.toast.groupUpdated'));
|
|
2780
|
+
} else {
|
|
2781
|
+
_cliReport = { groupId: id, profile: launch.profile, pending: true };
|
|
2782
|
+
renderNote();
|
|
2783
|
+
const result = await resetSchedulesForCli(id, launch, members);
|
|
2784
|
+
_cliReport = { groupId: id, profile: launch.profile, ...result };
|
|
2785
|
+
renderNote();
|
|
2786
|
+
const kept = result.changed.length + result.failed.length;
|
|
2787
|
+
showToast(kept ? tp('schedules.toast.groupUpdatedKept', kept) : t('schedules.toast.groupUpdated'));
|
|
2788
|
+
}
|
|
2789
|
+
// The schedules show what the save pushed onto them. From here on nothing is navigated:
|
|
2790
|
+
// the user may have opened something else meanwhile, and it stays as it is.
|
|
1536
2791
|
await loadData();
|
|
1537
|
-
render();
|
|
1538
|
-
showToast('Group updated');
|
|
1539
2792
|
} catch (err) {
|
|
1540
|
-
showToast('
|
|
2793
|
+
showToast(t('schedules.toast.groupUpdateFailed', { error: err.message }));
|
|
1541
2794
|
}
|
|
1542
2795
|
}
|
|
1543
2796
|
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
2797
|
+
const RESET_KEYS = ['profile', ...CLI_DEPENDENT];
|
|
2798
|
+
|
|
2799
|
+
/** The schedules of a group that hold a value of their own the group's new launch values replace. */
|
|
2800
|
+
function captureMembers(groupId, launch) {
|
|
2801
|
+
return _schedules.filter(s => s.groupId === groupId)
|
|
2802
|
+
.map(s => ({ id: s.id, name: s.name || s.id, values: Object.fromEntries(RESET_KEYS.map(k => [k, blankToNull(s[k])])) }))
|
|
2803
|
+
.filter(m => RESET_KEYS.some(k => m.values[k] !== null && !sameValue(m.values[k], launch[k])));
|
|
2804
|
+
}
|
|
2805
|
+
|
|
2806
|
+
// server.js pushes a group field onto the group's schedules only when the group's own value
|
|
2807
|
+
// changed. A schedule's own model, context, thinking, tools or account under a group that had
|
|
2808
|
+
// none of its own would survive a change of CLI and launch the new CLI with the old one's model.
|
|
2809
|
+
// So after the group is saved, each schedule that was in it when the user saved is read again
|
|
2810
|
+
// right before its write, and gets the group's new values only while it is still in the group and
|
|
2811
|
+
// each of those values is still what it was at the save (or what the save itself pushed onto it).
|
|
2812
|
+
// One that moved or took a newer value meanwhile is left alone; one that cannot be read is not
|
|
2813
|
+
// written, and when a read gets no answer at all the rest are not tried. Never from a cached list.
|
|
2814
|
+
async function resetSchedulesForCli(groupId, launch, members) {
|
|
2815
|
+
const report = { reset: [], changed: [], failed: [] };
|
|
2816
|
+
const target = (key) => blankToNull(launch[key]);
|
|
2817
|
+
for (let i = 0; i < members.length; i++) {
|
|
2818
|
+
const member = members[i];
|
|
2819
|
+
let fresh;
|
|
2820
|
+
try {
|
|
2821
|
+
fresh = await withDeadline(fetchSchedule(member.id));
|
|
2822
|
+
} catch (err) {
|
|
2823
|
+
if (err?.timeout) { report.failed.push(...members.slice(i).map(m => m.name)); break; }
|
|
2824
|
+
report.failed.push(member.name);
|
|
2825
|
+
continue;
|
|
2826
|
+
}
|
|
2827
|
+
const name = fresh?.name || member.name;
|
|
2828
|
+
const moved = !fresh || fresh.groupId !== groupId;
|
|
2829
|
+
const changed = moved || RESET_KEYS.some(key => {
|
|
2830
|
+
const now = blankToNull(fresh[key]);
|
|
2831
|
+
return !sameValue(now, member.values[key]) && !sameValue(now, target(key));
|
|
2832
|
+
});
|
|
2833
|
+
if (changed) { report.changed.push(name); continue; }
|
|
2834
|
+
const reset = {};
|
|
2835
|
+
for (const key of RESET_KEYS) if (blankToNull(fresh[key]) !== null && !sameValue(fresh[key], target(key))) reset[key] = launch[key];
|
|
2836
|
+
if (!Object.keys(reset).length) continue; // the save's own push already did it
|
|
2837
|
+
try {
|
|
2838
|
+
const saved = await updateSchedule(member.id, reset);
|
|
2839
|
+
const idx = _schedules.findIndex(s => s.id === saved?.id);
|
|
2840
|
+
if (idx >= 0) _schedules[idx] = saved;
|
|
2841
|
+
report.reset.push(name);
|
|
2842
|
+
} catch {
|
|
2843
|
+
report.failed.push(name);
|
|
2844
|
+
}
|
|
1553
2845
|
}
|
|
2846
|
+
return report;
|
|
1554
2847
|
}
|
|
1555
2848
|
|
|
1556
|
-
async function
|
|
1557
|
-
const
|
|
1558
|
-
|
|
2849
|
+
async function moveGroup(id, direction) {
|
|
2850
|
+
const ordered = orderedGroups();
|
|
2851
|
+
const idx = ordered.findIndex(g => g.id === id);
|
|
2852
|
+
const next = idx + direction;
|
|
2853
|
+
if (idx < 0 || next < 0 || next >= ordered.length) return;
|
|
2854
|
+
const copy = [...ordered];
|
|
2855
|
+
const [moved] = copy.splice(idx, 1);
|
|
2856
|
+
copy.splice(next, 0, moved);
|
|
1559
2857
|
try {
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
2858
|
+
collectGroupEditorDraft();
|
|
2859
|
+
await reorderScheduleGroups(copy.map(g => g.id));
|
|
2860
|
+
_groups = copy.map((g, i) => ({ ...g, order: i }));
|
|
2861
|
+
renderRail();
|
|
2862
|
+
if (_groupEditorDraft) renderGroupEditor(getGroup(id));
|
|
2863
|
+
else renderMain(true);
|
|
2864
|
+
// Keep the focus on the button that was pressed, or on the other one once it reached an end.
|
|
2865
|
+
const pressed = _panel?.querySelector(`[data-action="${direction < 0 ? 'group-up' : 'group-down'}"]`);
|
|
2866
|
+
(pressed && !pressed.disabled ? pressed : _panel?.querySelector(`[data-action="${direction < 0 ? 'group-down' : 'group-up'}"]`))?.focus();
|
|
1566
2867
|
} catch (err) {
|
|
1567
|
-
showToast('
|
|
2868
|
+
showToast(t('schedules.toast.reorderFailed', { error: err.message }));
|
|
1568
2869
|
}
|
|
1569
2870
|
}
|
|
1570
2871
|
|
|
2872
|
+
// ── Prompt a Schedule ──
|
|
2873
|
+
|
|
1571
2874
|
function openPromptScheduleModal() {
|
|
1572
2875
|
if (_promptModal) return;
|
|
1573
2876
|
|
|
@@ -1581,16 +2884,19 @@ function openPromptScheduleModal() {
|
|
|
1581
2884
|
const codexMeta = getProviderMeta('codex');
|
|
1582
2885
|
_promptModal = document.createElement('div');
|
|
1583
2886
|
_promptModal.className = 'sched-prompt-modal glass';
|
|
2887
|
+
_promptModal.setAttribute('role', 'dialog');
|
|
2888
|
+
_promptModal.setAttribute('aria-modal', 'true');
|
|
2889
|
+
_promptModal.setAttribute('aria-labelledby', 'sched-prompt-title');
|
|
1584
2890
|
_promptModal.innerHTML = `
|
|
1585
2891
|
<header class="sched-prompt-head">
|
|
1586
2892
|
<div class="sched-prompt-head-text">
|
|
1587
2893
|
<span class="sched-prompt-icon">${ICONS.sparkle}</span>
|
|
1588
2894
|
<div>
|
|
1589
|
-
<h3
|
|
1590
|
-
<p
|
|
2895
|
+
<h3 id="sched-prompt-title">${te('schedules.prompt.title')}</h3>
|
|
2896
|
+
<p>${te('schedules.prompt.sub')}</p>
|
|
1591
2897
|
</div>
|
|
1592
2898
|
</div>
|
|
1593
|
-
<button class="sched-prompt-close" data-action="prompt-modal-close" aria-label="
|
|
2899
|
+
<button class="sched-prompt-close" data-action="prompt-modal-close" aria-label="${te('schedules.prompt.close')}">${ICONS.close}</button>
|
|
1594
2900
|
</header>
|
|
1595
2901
|
<div class="sched-prompt-tiles">
|
|
1596
2902
|
<button class="sched-prompt-tile" style="--provider-color:${claudeMeta.color};" data-action="prompt-modal-pick" data-runtime="claude-code">
|
|
@@ -1599,15 +2905,15 @@ function openPromptScheduleModal() {
|
|
|
1599
2905
|
<span class="sched-prompt-tile-title">${claudeMeta.label}</span>
|
|
1600
2906
|
<span class="sched-prompt-tile-sub">Anthropic</span>
|
|
1601
2907
|
</span>
|
|
1602
|
-
<span class="sched-prompt-pill sched-prompt-pill--active"
|
|
2908
|
+
<span class="sched-prompt-pill sched-prompt-pill--active">${te('schedules.prompt.ready')}</span>
|
|
1603
2909
|
</button>
|
|
1604
2910
|
<button class="sched-prompt-tile disabled" style="--provider-color:${opencodeMeta.color};" disabled aria-disabled="true">
|
|
1605
2911
|
<span class="sched-prompt-tile-logo" aria-hidden="true">${opencodeMeta.icon}</span>
|
|
1606
2912
|
<span class="sched-prompt-tile-body">
|
|
1607
2913
|
<span class="sched-prompt-tile-title">${opencodeMeta.label}</span>
|
|
1608
|
-
<span class="sched-prompt-tile-sub"
|
|
2914
|
+
<span class="sched-prompt-tile-sub">${te('schedules.prompt.multiProvider')}</span>
|
|
1609
2915
|
</span>
|
|
1610
|
-
<span class="sched-prompt-pill"
|
|
2916
|
+
<span class="sched-prompt-pill">${te('schedules.prompt.planned')}</span>
|
|
1611
2917
|
</button>
|
|
1612
2918
|
<button class="sched-prompt-tile disabled" style="--provider-color:${codexMeta.color};" disabled aria-disabled="true">
|
|
1613
2919
|
<span class="sched-prompt-tile-logo" aria-hidden="true">${codexMeta.icon}</span>
|
|
@@ -1615,14 +2921,14 @@ function openPromptScheduleModal() {
|
|
|
1615
2921
|
<span class="sched-prompt-tile-title">Codex CLI</span>
|
|
1616
2922
|
<span class="sched-prompt-tile-sub">OpenAI</span>
|
|
1617
2923
|
</span>
|
|
1618
|
-
<span class="sched-prompt-pill"
|
|
2924
|
+
<span class="sched-prompt-pill">${te('schedules.prompt.planned')}</span>
|
|
1619
2925
|
</button>
|
|
1620
2926
|
</div>
|
|
1621
2927
|
`;
|
|
1622
2928
|
_promptModal.addEventListener('click', handlePromptModalClick);
|
|
1623
2929
|
document.body.appendChild(_promptModal);
|
|
1624
2930
|
|
|
1625
|
-
_promptKeyHandler = (e) => { if (e.key === 'Escape') closePromptScheduleModal(); };
|
|
2931
|
+
_promptKeyHandler = (e) => { if (e.key === 'Escape') { e.stopPropagation(); closePromptScheduleModal(); } };
|
|
1626
2932
|
document.addEventListener('keydown', _promptKeyHandler);
|
|
1627
2933
|
|
|
1628
2934
|
requestAnimationFrame(() => {
|
|
@@ -1648,15 +2954,17 @@ function closePromptScheduleModal() {
|
|
|
1648
2954
|
document.removeEventListener('keydown', _promptKeyHandler);
|
|
1649
2955
|
_promptKeyHandler = null;
|
|
1650
2956
|
}
|
|
2957
|
+
const hadModal = !!_promptModal;
|
|
1651
2958
|
_promptBackdrop?.remove();
|
|
1652
2959
|
_promptModal?.remove();
|
|
1653
2960
|
_promptBackdrop = null;
|
|
1654
2961
|
_promptModal = null;
|
|
2962
|
+
if (hadModal && _panel) _panel.querySelector('[data-action="schedule-prompt"]')?.focus();
|
|
1655
2963
|
}
|
|
1656
2964
|
|
|
1657
2965
|
async function handlePromptPick(runtime, triggerEl = null) {
|
|
1658
2966
|
if (runtime !== 'claude-code') {
|
|
1659
|
-
showToast('
|
|
2967
|
+
showToast(t('schedules.toast.comingSoon'));
|
|
1660
2968
|
return;
|
|
1661
2969
|
}
|
|
1662
2970
|
if (triggerEl) {
|
|
@@ -1664,70 +2972,58 @@ async function handlePromptPick(runtime, triggerEl = null) {
|
|
|
1664
2972
|
triggerEl.setAttribute('aria-busy', 'true');
|
|
1665
2973
|
}
|
|
1666
2974
|
try {
|
|
1667
|
-
await sendToPanel('/synabun schedule', { newTab: true, tabLabel: '
|
|
2975
|
+
await sendToPanel('/synabun schedule', { newTab: true, tabLabel: t('schedules.prompt.tabLabel'), autoSubmit: true });
|
|
1668
2976
|
} catch (err) {
|
|
1669
|
-
showToast('
|
|
2977
|
+
showToast(t('schedules.toast.panelFailed', { error: err.message }));
|
|
1670
2978
|
} finally {
|
|
1671
2979
|
closePromptScheduleModal();
|
|
1672
2980
|
}
|
|
1673
2981
|
}
|
|
1674
2982
|
|
|
1675
|
-
|
|
1676
|
-
const idx = _groups.findIndex(g => g.id === id);
|
|
1677
|
-
const next = idx + direction;
|
|
1678
|
-
if (idx < 0 || next < 0 || next >= _groups.length) return;
|
|
1679
|
-
const copy = [..._groups];
|
|
1680
|
-
const [moved] = copy.splice(idx, 1);
|
|
1681
|
-
copy.splice(next, 0, moved);
|
|
1682
|
-
try {
|
|
1683
|
-
await reorderScheduleGroups(copy.map(g => g.id));
|
|
1684
|
-
_groups = copy.map((g, i) => ({ ...g, order: i }));
|
|
1685
|
-
render();
|
|
1686
|
-
} catch (err) {
|
|
1687
|
-
showToast('Reorder failed: ' + err.message);
|
|
1688
|
-
}
|
|
1689
|
-
}
|
|
2983
|
+
// ── Timers: writes ──
|
|
1690
2984
|
|
|
1691
2985
|
async function createQuickTimerFromForm(runNow) {
|
|
1692
2986
|
const templateId = $('sched-qt-template')?.value;
|
|
1693
|
-
if (!templateId) {
|
|
2987
|
+
if (!templateId) { markInvalid('sched-qt-template', t('schedules.toast.chooseAutomation')); return; }
|
|
1694
2988
|
const customVal = Number($('sched-qt-custom-min')?.value);
|
|
1695
2989
|
const minutes = customVal >= 1 ? customVal : _selectedQtMinutes;
|
|
1696
|
-
if (!runNow && !minutes) { showToast('
|
|
2990
|
+
if (!runNow && !minutes) { showToast(t('schedules.toast.chooseDelay')); return; }
|
|
1697
2991
|
const template = getTemplate(templateId);
|
|
1698
2992
|
const profile = $('sched-qt-profile')?.value || 'claude-code';
|
|
1699
2993
|
const model = $('sched-qt-model')?.value || null;
|
|
1700
2994
|
const contextMode = $('sched-qt-context')?.value || 'default';
|
|
1701
2995
|
if (!modelChoiceAvailable(profile, model, _launchAccount)) {
|
|
1702
|
-
showToast('
|
|
2996
|
+
showToast(t('schedules.toast.modelUnavailable')); return;
|
|
1703
2997
|
}
|
|
1704
2998
|
if (!contextChoiceAvailable(profile, model, contextMode, _launchAccount)) {
|
|
1705
|
-
showToast('
|
|
2999
|
+
showToast(t('schedules.toast.contextUnavailable')); return;
|
|
1706
3000
|
}
|
|
1707
3001
|
const effortRaw = $('sched-qt-effort')?.value || null;
|
|
1708
3002
|
const effort = normalizeEffortForProfile(profile, effortRaw);
|
|
1709
|
-
const
|
|
3003
|
+
const tools = strictToolHost(profile) ? readToolPicker($('sched-qt-mcp')) : null;
|
|
1710
3004
|
const codexAccountId = profile === 'codex' ? ($('sched-qt-account')?.value || _launchAccount || 'default') : undefined;
|
|
1711
3005
|
const usesBrowser = _qtUsesBrowser !== null ? _qtUsesBrowser : !!template?.usesBrowser;
|
|
1712
3006
|
try {
|
|
1713
3007
|
if (runNow) {
|
|
1714
3008
|
const result = await triggerQuickTimerNow(templateId, {
|
|
1715
|
-
profile, model, contextMode, effort,
|
|
3009
|
+
profile, model, contextMode, effort, tools, usesBrowser, codexAccountId,
|
|
1716
3010
|
sidepanelWindowId: getNativeLoopRouterWindowId(),
|
|
1717
3011
|
sidepanelClaimToken: getNativeLoopRouterClaimToken(),
|
|
1718
3012
|
});
|
|
1719
|
-
showToast(
|
|
3013
|
+
showToast(t('schedules.toast.runningNow', { name: result.templateName }));
|
|
1720
3014
|
} else {
|
|
1721
|
-
const result = await createQuickTimer(templateId, minutes, { profile, model, contextMode, effort,
|
|
3015
|
+
const result = await createQuickTimer(templateId, minutes, { profile, model, contextMode, effort, tools, usesBrowser, codexAccountId });
|
|
1722
3016
|
if (!_quickTimers.some(t => t.id === result.timerId)) {
|
|
1723
|
-
_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,
|
|
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 });
|
|
1724
3018
|
}
|
|
1725
3019
|
_selectedQtMinutes = null;
|
|
1726
|
-
|
|
3020
|
+
_panel?.querySelectorAll('[data-action="qt-select"]').forEach(chip => chip.setAttribute('aria-pressed', 'false'));
|
|
3021
|
+
showToast(t('schedules.toast.timerSet', { name: result.templateName }));
|
|
1727
3022
|
}
|
|
1728
|
-
|
|
3023
|
+
renderActiveTimersInto();
|
|
3024
|
+
renderRail();
|
|
1729
3025
|
} catch (err) {
|
|
1730
|
-
showToast((runNow ? '
|
|
3026
|
+
showToast(t(runNow ? 'schedules.toast.runNowFailed' : 'schedules.toast.timerFailed', { error: err.message }));
|
|
1731
3027
|
}
|
|
1732
3028
|
}
|
|
1733
3029
|
|
|
@@ -1735,25 +3031,27 @@ async function cancelQuickTimerById(id) {
|
|
|
1735
3031
|
try {
|
|
1736
3032
|
await cancelQuickTimer(id);
|
|
1737
3033
|
_quickTimers = _quickTimers.filter(t => t.id !== id);
|
|
1738
|
-
|
|
1739
|
-
|
|
3034
|
+
renderActiveTimersInto();
|
|
3035
|
+
renderRail();
|
|
3036
|
+
showToast(t('schedules.toast.timerCancelled'));
|
|
1740
3037
|
} catch (err) {
|
|
1741
|
-
showToast('
|
|
3038
|
+
showToast(t('schedules.toast.cancelFailed', { error: err.message }));
|
|
1742
3039
|
}
|
|
1743
3040
|
}
|
|
1744
3041
|
|
|
1745
3042
|
async function createExistingScheduleTimer() {
|
|
1746
3043
|
const scheduleId = $('sched-existing-timer-schedule')?.value;
|
|
1747
3044
|
const minutes = Number($('sched-existing-timer-min')?.value);
|
|
1748
|
-
if (!scheduleId) {
|
|
1749
|
-
if (!minutes || minutes < 1) {
|
|
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; }
|
|
1750
3047
|
try {
|
|
1751
3048
|
const result = await startScheduleTimer(scheduleId, minutes);
|
|
1752
3049
|
_scheduleTimerData[scheduleId] = { firesAt: result.firesAt, minutes: result.minutes };
|
|
1753
|
-
|
|
1754
|
-
|
|
3050
|
+
renderActiveTimersInto();
|
|
3051
|
+
renderRail();
|
|
3052
|
+
showToast(t('schedules.toast.scheduleTimerSet'));
|
|
1755
3053
|
} catch (err) {
|
|
1756
|
-
showToast('
|
|
3054
|
+
showToast(t('schedules.toast.timerFailed', { error: err.message }));
|
|
1757
3055
|
}
|
|
1758
3056
|
}
|
|
1759
3057
|
|
|
@@ -1761,28 +3059,41 @@ async function cancelExistingScheduleTimer(id) {
|
|
|
1761
3059
|
try {
|
|
1762
3060
|
await cancelScheduleTimer(id);
|
|
1763
3061
|
delete _scheduleTimerData[id];
|
|
1764
|
-
|
|
1765
|
-
|
|
3062
|
+
renderActiveTimersInto();
|
|
3063
|
+
renderRail();
|
|
3064
|
+
showToast(t('schedules.toast.timerCancelled'));
|
|
1766
3065
|
} catch (err) {
|
|
1767
|
-
showToast('
|
|
3066
|
+
showToast(t('schedules.toast.cancelFailed', { error: err.message }));
|
|
1768
3067
|
}
|
|
1769
3068
|
}
|
|
1770
3069
|
|
|
3070
|
+
// ── Every second: countdowns ──
|
|
3071
|
+
|
|
1771
3072
|
function updateTimerCountdowns() {
|
|
1772
|
-
_panel
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
if (!timer) timer = _scheduleTimerData[id];
|
|
1777
|
-
const strong = row.querySelector('strong');
|
|
1778
|
-
if (timer && strong) strong.textContent = formatTimerCountdown(timer.firesAt);
|
|
3073
|
+
if (!_panel) return;
|
|
3074
|
+
const now = Date.now();
|
|
3075
|
+
_panel.querySelectorAll('.sched-timer-count[data-fires-at]').forEach(el => {
|
|
3076
|
+
el.textContent = formatTimerCountdown(el.dataset.firesAt);
|
|
1779
3077
|
});
|
|
1780
|
-
|
|
3078
|
+
let stale = false;
|
|
3079
|
+
_panel.querySelectorAll('[data-countdown]').forEach(el => {
|
|
1781
3080
|
const iso = el.dataset.countdown;
|
|
1782
|
-
|
|
3081
|
+
if (!iso) return;
|
|
3082
|
+
const countdown = formatCountdown(iso, now);
|
|
3083
|
+
const rel = el.querySelector('.sched-next-rel') || el;
|
|
3084
|
+
if (rel.textContent !== countdown.text) rel.textContent = countdown.text;
|
|
3085
|
+
if (el.classList.contains('sched-next')) el.dataset.state = countdown.state;
|
|
3086
|
+
if (Date.parse(iso) < now - 90_000) stale = true;
|
|
1783
3087
|
});
|
|
3088
|
+
// A run's time has passed and no update came: ask the server, at most once a minute.
|
|
3089
|
+
if (stale && now - _lastAutoRefresh > 60_000 && !editorOpen()) {
|
|
3090
|
+
_lastAutoRefresh = now;
|
|
3091
|
+
scheduleRefresh(0);
|
|
3092
|
+
}
|
|
1784
3093
|
}
|
|
1785
3094
|
|
|
3095
|
+
// ── Live updates over the sync socket ──
|
|
3096
|
+
|
|
1786
3097
|
function setupSync() {
|
|
1787
3098
|
if (_syncReady) return;
|
|
1788
3099
|
_syncReady = true;
|
|
@@ -1790,25 +3101,25 @@ function setupSync() {
|
|
|
1790
3101
|
on('sync:schedule:created', (data) => {
|
|
1791
3102
|
if (!data?.schedule) return;
|
|
1792
3103
|
if (!_schedules.some(s => s.id === data.schedule.id)) _schedules.push(data.schedule);
|
|
1793
|
-
|
|
3104
|
+
refresh();
|
|
1794
3105
|
});
|
|
1795
3106
|
on('sync:schedule:updated', (data) => {
|
|
1796
3107
|
if (!data?.schedule) return;
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
else _schedules.push(data.schedule);
|
|
1800
|
-
if (_panel) render();
|
|
3108
|
+
replaceSchedule(data.schedule);
|
|
3109
|
+
refresh();
|
|
1801
3110
|
});
|
|
1802
3111
|
on('sync:schedule:deleted', (data) => {
|
|
1803
3112
|
if (!data?.scheduleId) return;
|
|
1804
3113
|
_schedules = _schedules.filter(s => s.id !== data.scheduleId);
|
|
1805
|
-
|
|
3114
|
+
_expanded.delete(data.scheduleId);
|
|
3115
|
+
refresh();
|
|
1806
3116
|
});
|
|
1807
3117
|
on('sync:schedule:fired', (data) => {
|
|
1808
|
-
if (data?.scheduleName) showToast(
|
|
3118
|
+
if (data?.scheduleName) showToast(t('schedules.toast.fired', { name: data.scheduleName }));
|
|
3119
|
+
if (_panel) scheduleRefresh();
|
|
1809
3120
|
});
|
|
1810
3121
|
on('sync:schedule:completed', (data) => {
|
|
1811
|
-
|
|
3122
|
+
if (_panel) scheduleRefresh();
|
|
1812
3123
|
if (data?.terminalSessionId && data.surface !== 'sidepanel') {
|
|
1813
3124
|
emit('terminal:attach-floating', {
|
|
1814
3125
|
terminalSessionId: data.terminalSessionId,
|
|
@@ -1818,65 +3129,68 @@ function setupSync() {
|
|
|
1818
3129
|
}
|
|
1819
3130
|
});
|
|
1820
3131
|
on('sync:schedule:failed', (data) => {
|
|
1821
|
-
if (data?.reason) showToast(
|
|
1822
|
-
|
|
3132
|
+
if (data?.reason) showToast(t('schedules.toast.scheduleFailed', { reason: data.reason }));
|
|
3133
|
+
if (_panel) scheduleRefresh();
|
|
1823
3134
|
});
|
|
1824
3135
|
on('sync:schedule:timer-set', (data) => {
|
|
1825
3136
|
if (data?.scheduleId && data?.firesAt) {
|
|
1826
3137
|
_scheduleTimerData[data.scheduleId] = { firesAt: data.firesAt, minutes: data.minutes };
|
|
1827
|
-
if (_panel
|
|
3138
|
+
if (_panel) { renderRail(); renderActiveTimersInto(); }
|
|
1828
3139
|
}
|
|
1829
3140
|
});
|
|
1830
3141
|
on('sync:schedule:timer-fired', (data) => {
|
|
1831
|
-
if (data?.scheduleName) showToast(
|
|
3142
|
+
if (data?.scheduleName) showToast(t('schedules.toast.timerFired', { name: data.scheduleName }));
|
|
1832
3143
|
if (data?.scheduleId) delete _scheduleTimerData[data.scheduleId];
|
|
1833
|
-
if (_panel)
|
|
3144
|
+
if (_panel) { renderRail(); renderActiveTimersInto(); }
|
|
1834
3145
|
});
|
|
1835
3146
|
on('sync:schedule:timer-cancelled', (data) => {
|
|
1836
3147
|
if (data?.scheduleId) delete _scheduleTimerData[data.scheduleId];
|
|
1837
|
-
if (_panel
|
|
3148
|
+
if (_panel) { renderRail(); renderActiveTimersInto(); }
|
|
1838
3149
|
});
|
|
1839
3150
|
on('sync:schedule-group:created', (data) => {
|
|
1840
3151
|
if (data?.group && !_groups.some(g => g.id === data.group.id)) _groups.push(data.group);
|
|
1841
|
-
|
|
3152
|
+
refresh();
|
|
1842
3153
|
});
|
|
1843
3154
|
on('sync:schedule-group:updated', (data) => {
|
|
1844
3155
|
if (!data?.group) return;
|
|
1845
3156
|
const idx = _groups.findIndex(g => g.id === data.group.id);
|
|
1846
3157
|
if (idx >= 0) _groups[idx] = data.group;
|
|
1847
3158
|
else _groups.push(data.group);
|
|
1848
|
-
|
|
3159
|
+
refresh();
|
|
1849
3160
|
});
|
|
1850
3161
|
on('sync:schedule-group:deleted', (data) => {
|
|
1851
3162
|
if (!data?.groupId) return;
|
|
1852
3163
|
_groups = _groups.filter(g => g.id !== data.groupId);
|
|
1853
3164
|
for (const schedule of _schedules) if (schedule.groupId === data.groupId) schedule.groupId = null;
|
|
1854
|
-
if (
|
|
1855
|
-
|
|
3165
|
+
if (_scope === `group:${data.groupId}`) setScope('ungrouped', { keepEditors: true });
|
|
3166
|
+
refresh();
|
|
1856
3167
|
});
|
|
1857
3168
|
on('sync:schedule-group:reordered', (data) => {
|
|
1858
3169
|
if (Array.isArray(data?.groups)) _groups = data.groups;
|
|
1859
|
-
|
|
3170
|
+
refresh();
|
|
1860
3171
|
});
|
|
1861
3172
|
on('sync:quick-timer:set', (data) => {
|
|
1862
3173
|
if (data?.timerId && !_quickTimers.some(t => t.id === data.timerId)) {
|
|
1863
|
-
_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,
|
|
1864
|
-
if (_panel
|
|
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 });
|
|
3175
|
+
if (_panel) { renderRail(); renderActiveTimersInto(); }
|
|
1865
3176
|
}
|
|
1866
3177
|
});
|
|
1867
3178
|
on('sync:quick-timer:fired', (data) => {
|
|
1868
|
-
if (data?.templateName) showToast(
|
|
3179
|
+
if (data?.templateName) showToast(t('schedules.toast.timerFired', { name: data.templateName }));
|
|
1869
3180
|
if (data?.timerId) _quickTimers = _quickTimers.filter(t => t.id !== data.timerId);
|
|
1870
|
-
if (_panel
|
|
3181
|
+
if (_panel) { renderRail(); renderActiveTimersInto(); }
|
|
1871
3182
|
});
|
|
1872
3183
|
on('sync:quick-timer:fired-now', (data) => {
|
|
1873
|
-
if (data?.templateName) showToast(
|
|
3184
|
+
if (data?.templateName) showToast(t('schedules.toast.runningNow', { name: data.templateName }));
|
|
1874
3185
|
});
|
|
1875
3186
|
on('sync:quick-timer:cancelled', (data) => {
|
|
1876
3187
|
if (data?.timerId) _quickTimers = _quickTimers.filter(t => t.id !== data.timerId);
|
|
1877
|
-
if (_panel
|
|
3188
|
+
if (_panel) { renderRail(); renderActiveTimersInto(); }
|
|
1878
3189
|
});
|
|
1879
3190
|
on('sync:quick-timer:failed', (data) => {
|
|
1880
|
-
if (data?.reason) showToast(
|
|
3191
|
+
if (data?.reason) showToast(t('schedules.toast.timerFailed', { error: data.reason }));
|
|
1881
3192
|
});
|
|
1882
3193
|
}
|
|
3194
|
+
|
|
3195
|
+
|
|
3196
|
+
configureToolPickerTranslation(t);
|