mixdog 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +31 -0
- package/.claude-plugin/plugin.json +20 -0
- package/.gitattributes +34 -0
- package/.mcp.json +14 -0
- package/ARCHITECTURE.md +77 -0
- package/CHANGELOG.md +7 -0
- package/CONTRIBUTING.md +45 -0
- package/DATA-FLOW.md +79 -0
- package/LICENSE +21 -0
- package/README.md +389 -0
- package/SECURITY.md +138 -0
- package/UNINSTALL.md +112 -0
- package/agents/maintenance.md +5 -0
- package/agents/memory-classification.md +30 -0
- package/agents/scheduler-task.md +18 -0
- package/agents/webhook-handler.md +27 -0
- package/agents/worker.md +24 -0
- package/bin/bridge +133 -0
- package/bin/statusline-launcher.mjs +78 -0
- package/bin/statusline-lib.mjs +550 -0
- package/bin/statusline.mjs +607 -0
- package/bun.lock +802 -0
- package/commands/config.md +16 -0
- package/commands/doctor.md +13 -0
- package/commands/setup.md +17 -0
- package/defaults/cycle3-review-prompt.md +90 -0
- package/defaults/hidden-roles.json +65 -0
- package/defaults/memory-chunk-prompt.md +63 -0
- package/defaults/memory-promote-prompt.md +135 -0
- package/defaults/mixdog-config.template.json +27 -0
- package/defaults/user-workflow.json +8 -0
- package/defaults/user-workflow.md +12 -0
- package/hooks/hooks.json +73 -0
- package/hooks/lib/active-instance.cjs +77 -0
- package/hooks/lib/permission-evaluator.cjs +411 -0
- package/hooks/lib/permission-route.cjs +63 -0
- package/hooks/lib/permission-rules.cjs +170 -0
- package/hooks/lib/settings-loader.cjs +116 -0
- package/hooks/post-tool-use.cjs +84 -0
- package/hooks/pre-mcp-sandbox.cjs +158 -0
- package/hooks/pre-tool-subagent.cjs +253 -0
- package/hooks/session-start.cjs +1372 -0
- package/hooks/turn-timer.cjs +82 -0
- package/lib/claude-md-writer.cjs +386 -0
- package/lib/config-cjs.cjs +61 -0
- package/lib/hook-pipe-path.cjs +10 -0
- package/lib/keychain-cjs.cjs +263 -0
- package/lib/plugin-paths.cjs +61 -0
- package/lib/rules-builder.cjs +241 -0
- package/lib/text-utils.cjs +61 -0
- package/native/README.md +117 -0
- package/native/prebuilt/linux-aarch64/mixdog-shim +0 -0
- package/native/prebuilt/linux-x86_64/mixdog-shim +0 -0
- package/native/prebuilt/macos-aarch64/mixdog-shim +0 -0
- package/native/prebuilt/macos-x86_64/mixdog-shim +0 -0
- package/native/prebuilt/windows-x86_64/mixdog-shim.exe +0 -0
- package/package.json +107 -0
- package/prompts/code-review.txt +16 -0
- package/prompts/security-audit.txt +17 -0
- package/rules/bridge/00-common.md +39 -0
- package/rules/bridge/20-skip-protocol.md +18 -0
- package/rules/bridge/30-explorer.md +33 -0
- package/rules/bridge/40-cycle1-agent.md +52 -0
- package/rules/bridge/41-cycle2-agent.md +62 -0
- package/rules/bridge/42-cycle3-agent.md +44 -0
- package/rules/lead/00-tool-lead.md +61 -0
- package/rules/lead/01-general.md +23 -0
- package/rules/lead/02-channels.md +49 -0
- package/rules/lead/03-team.md +27 -0
- package/rules/lead/04-workflow.md +20 -0
- package/rules/shared/00-language.md +14 -0
- package/rules/shared/01-tool.md +138 -0
- package/scripts/bootstrap.mjs +184 -0
- package/scripts/bridge-unify-smoke.mjs +308 -0
- package/scripts/build-runtime-linux.sh +348 -0
- package/scripts/build-runtime-macos.sh +217 -0
- package/scripts/build-runtime-windows.ps1 +242 -0
- package/scripts/builtin-utils-smoke.mjs +392 -0
- package/scripts/check-json.mjs +45 -0
- package/scripts/check-syntax-changed.mjs +102 -0
- package/scripts/check-syntax.mjs +58 -0
- package/scripts/code-graph-batch.test.mjs +33 -0
- package/scripts/config-preserve-smoke.mjs +180 -0
- package/scripts/doctor.mjs +484 -0
- package/scripts/edit-normalize-fuzz.mjs +130 -0
- package/scripts/edit-normalize-smoke.mjs +401 -0
- package/scripts/edit-operation-smoke.mjs +369 -0
- package/scripts/edit2-smoke.mjs +63 -0
- package/scripts/fuzzy-e2e.mjs +28 -0
- package/scripts/fuzzy-smoke.mjs +26 -0
- package/scripts/generate-runtime-manifest.mjs +166 -0
- package/scripts/guard-smoke.mjs +66 -0
- package/scripts/hidden-role-schema-smoke.mjs +162 -0
- package/scripts/hook-routing-smoke.mjs +29 -0
- package/scripts/inject-input.ps1 +204 -0
- package/scripts/io-complex-smoke.mjs +667 -0
- package/scripts/io-explore-bench.mjs +424 -0
- package/scripts/io-guardrails-smoke.mjs +205 -0
- package/scripts/io-mini-bench-baseline.json +11 -0
- package/scripts/io-mini-bench.mjs +216 -0
- package/scripts/io-route-harness.mjs +933 -0
- package/scripts/io-telemetry-report.mjs +691 -0
- package/scripts/mutation-bench.mjs +564 -0
- package/scripts/mutation-io-smoke.mjs +1081 -0
- package/scripts/native-patch-bridge-smoke.mjs +288 -0
- package/scripts/native-patch-smoke.mjs +304 -0
- package/scripts/patch-interior-context-smoke.mjs +49 -0
- package/scripts/patch-newline-utf8-smoke.mjs +157 -0
- package/scripts/perf-hook-smoke.mjs +71 -0
- package/scripts/permission-eval-smoke.mjs +426 -0
- package/scripts/prep-patch.mjs +53 -0
- package/scripts/prep-shim.mjs +96 -0
- package/scripts/provider-cache-smoke.mjs +687 -0
- package/scripts/report-runtime-health.mjs +132 -0
- package/scripts/run-mcp.mjs +1547 -0
- package/scripts/salvage-v4a-shatter.test.mjs +58 -0
- package/scripts/scoped-cache-io-smoke.mjs +103 -0
- package/scripts/shell-policy-round3-smoke.mjs +46 -0
- package/scripts/smoke-runtime-negative.ps1 +100 -0
- package/scripts/smoke-runtime-negative.sh +95 -0
- package/scripts/stall-policy-smoke.mjs +50 -0
- package/scripts/start-memory-worker.mjs +23 -0
- package/scripts/statusline-launcher-smoke.mjs +82 -0
- package/scripts/stress-atomic-write.mjs +1028 -0
- package/scripts/test-config-rmw-restore.mjs +122 -0
- package/scripts/test-fault-inject.mjs +164 -0
- package/scripts/test-large-file.mjs +174 -0
- package/scripts/tool-edge-smoke.mjs +209 -0
- package/scripts/uninstall.mjs +201 -0
- package/scripts/webhook-selfheal-smoke.mjs +29 -0
- package/scripts/write-overwrite-guard-smoke.mjs +56 -0
- package/server-main.mjs +3055 -0
- package/server.mjs +468 -0
- package/setup/config-merge.mjs +254 -0
- package/setup/install.mjs +120 -0
- package/setup/launch-core.mjs +507 -0
- package/setup/launch.mjs +101 -0
- package/setup/setup-server.mjs +3206 -0
- package/setup/setup.html +3693 -0
- package/skills/retro-skill-proposer/SKILL.md +92 -0
- package/skills/schedule-add/SKILL.md +77 -0
- package/skills/setup/SKILL.md +346 -0
- package/skills/webhook-add/SKILL.md +81 -0
- package/src/agent/bridge-stall-watchdog.mjs +337 -0
- package/src/agent/index.mjs +2138 -0
- package/src/agent/orchestrator/activity-bus.mjs +38 -0
- package/src/agent/orchestrator/ai-wrapped-dispatch.mjs +1010 -0
- package/src/agent/orchestrator/bridge-retry.mjs +220 -0
- package/src/agent/orchestrator/bridge-trace.mjs +583 -0
- package/src/agent/orchestrator/cache-mtime.mjs +58 -0
- package/src/agent/orchestrator/config.mjs +358 -0
- package/src/agent/orchestrator/context/collect.mjs +651 -0
- package/src/agent/orchestrator/dispatch-persist.mjs +549 -0
- package/src/agent/orchestrator/drain-registry.mjs +50 -0
- package/src/agent/orchestrator/explore-validator.mjs +8 -0
- package/src/agent/orchestrator/internal-roles.mjs +118 -0
- package/src/agent/orchestrator/internal-tools.mjs +88 -0
- package/src/agent/orchestrator/jobs.mjs +116 -0
- package/src/agent/orchestrator/mcp/client.mjs +364 -0
- package/src/agent/orchestrator/providers/anthropic-betas.mjs +21 -0
- package/src/agent/orchestrator/providers/anthropic-oauth.mjs +1745 -0
- package/src/agent/orchestrator/providers/anthropic.mjs +437 -0
- package/src/agent/orchestrator/providers/gemini.mjs +1175 -0
- package/src/agent/orchestrator/providers/grok-oauth.mjs +782 -0
- package/src/agent/orchestrator/providers/model-catalog.mjs +241 -0
- package/src/agent/orchestrator/providers/openai-compat.mjs +1467 -0
- package/src/agent/orchestrator/providers/openai-oauth-ws.mjs +1890 -0
- package/src/agent/orchestrator/providers/openai-oauth.mjs +1307 -0
- package/src/agent/orchestrator/providers/openai-ws.mjs +104 -0
- package/src/agent/orchestrator/providers/registry.mjs +192 -0
- package/src/agent/orchestrator/providers/retry-classifier.mjs +325 -0
- package/src/agent/orchestrator/session/abort-lookup.mjs +13 -0
- package/src/agent/orchestrator/session/cache/post-edit-marks.mjs +42 -0
- package/src/agent/orchestrator/session/cache/prefetch-cache.mjs +142 -0
- package/src/agent/orchestrator/session/cache/read-cache.mjs +319 -0
- package/src/agent/orchestrator/session/cache/scoped-cache-outcome.mjs +11 -0
- package/src/agent/orchestrator/session/cache/scoped-cache.mjs +361 -0
- package/src/agent/orchestrator/session/cache/util.mjs +49 -0
- package/src/agent/orchestrator/session/loop.mjs +1478 -0
- package/src/agent/orchestrator/session/manager.mjs +1975 -0
- package/src/agent/orchestrator/session/read-dedup.mjs +6 -0
- package/src/agent/orchestrator/session/result-classification.mjs +65 -0
- package/src/agent/orchestrator/session/save-session-worker.mjs +18 -0
- package/src/agent/orchestrator/session/store.mjs +624 -0
- package/src/agent/orchestrator/session/stream-watchdog.mjs +130 -0
- package/src/agent/orchestrator/session/tool-result-offload.mjs +166 -0
- package/src/agent/orchestrator/session/trim.mjs +491 -0
- package/src/agent/orchestrator/smart-bridge/CACHE-SHARD.md +115 -0
- package/src/agent/orchestrator/smart-bridge/bridge-llm.mjs +327 -0
- package/src/agent/orchestrator/smart-bridge/cache-obs.mjs +150 -0
- package/src/agent/orchestrator/smart-bridge/cache-strategy.mjs +228 -0
- package/src/agent/orchestrator/smart-bridge/index.mjs +215 -0
- package/src/agent/orchestrator/smart-bridge/profiles.mjs +37 -0
- package/src/agent/orchestrator/smart-bridge/registry.mjs +348 -0
- package/src/agent/orchestrator/smart-bridge/session-builder.mjs +116 -0
- package/src/agent/orchestrator/stall-policy.mjs +195 -0
- package/src/agent/orchestrator/tool-loop-guard.mjs +75 -0
- package/src/agent/orchestrator/tools/bash-policy-scan.mjs +77 -0
- package/src/agent/orchestrator/tools/bash-session.mjs +721 -0
- package/src/agent/orchestrator/tools/builtin/advisory-lock.mjs +171 -0
- package/src/agent/orchestrator/tools/builtin/arg-guard.mjs +455 -0
- package/src/agent/orchestrator/tools/builtin/atomic-write.mjs +236 -0
- package/src/agent/orchestrator/tools/builtin/bash-tool.mjs +480 -0
- package/src/agent/orchestrator/tools/builtin/binary-file.mjs +76 -0
- package/src/agent/orchestrator/tools/builtin/builtin-tools.mjs +256 -0
- package/src/agent/orchestrator/tools/builtin/cache-layers.mjs +386 -0
- package/src/agent/orchestrator/tools/builtin/cwd-utils.mjs +37 -0
- package/src/agent/orchestrator/tools/builtin/device-paths.mjs +154 -0
- package/src/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +292 -0
- package/src/agent/orchestrator/tools/builtin/diff-utils.mjs +109 -0
- package/src/agent/orchestrator/tools/builtin/edit-base-guard.mjs +58 -0
- package/src/agent/orchestrator/tools/builtin/edit-byte-plan.mjs +240 -0
- package/src/agent/orchestrator/tools/builtin/edit-byte-utils.mjs +113 -0
- package/src/agent/orchestrator/tools/builtin/edit-commit.mjs +74 -0
- package/src/agent/orchestrator/tools/builtin/edit-context-utils.mjs +242 -0
- package/src/agent/orchestrator/tools/builtin/edit-diagnostics.mjs +211 -0
- package/src/agent/orchestrator/tools/builtin/edit-engine.mjs +1364 -0
- package/src/agent/orchestrator/tools/builtin/edit-failure-context.mjs +126 -0
- package/src/agent/orchestrator/tools/builtin/edit-hint.mjs +141 -0
- package/src/agent/orchestrator/tools/builtin/edit-match-utils.mjs +194 -0
- package/src/agent/orchestrator/tools/builtin/edit-partial-write.mjs +60 -0
- package/src/agent/orchestrator/tools/builtin/edit-stale-refresh.mjs +168 -0
- package/src/agent/orchestrator/tools/builtin/edit-tool.mjs +173 -0
- package/src/agent/orchestrator/tools/builtin/edit-utf8-guard.mjs +48 -0
- package/src/agent/orchestrator/tools/builtin/fs-reachability.mjs +48 -0
- package/src/agent/orchestrator/tools/builtin/fuzzy-match.mjs +99 -0
- package/src/agent/orchestrator/tools/builtin/glob-walk.mjs +170 -0
- package/src/agent/orchestrator/tools/builtin/grep-formatting.mjs +113 -0
- package/src/agent/orchestrator/tools/builtin/hash-utils.mjs +6 -0
- package/src/agent/orchestrator/tools/builtin/list-formatting.mjs +7 -0
- package/src/agent/orchestrator/tools/builtin/list-tool.mjs +593 -0
- package/src/agent/orchestrator/tools/builtin/native-edit-runner.mjs +89 -0
- package/src/agent/orchestrator/tools/builtin/notebook-edit-tool.mjs +300 -0
- package/src/agent/orchestrator/tools/builtin/open-config-tool.mjs +26 -0
- package/src/agent/orchestrator/tools/builtin/path-diagnostics.mjs +152 -0
- package/src/agent/orchestrator/tools/builtin/path-locks.mjs +35 -0
- package/src/agent/orchestrator/tools/builtin/path-utils.mjs +201 -0
- package/src/agent/orchestrator/tools/builtin/read-args.mjs +103 -0
- package/src/agent/orchestrator/tools/builtin/read-batch.mjs +172 -0
- package/src/agent/orchestrator/tools/builtin/read-constants.mjs +40 -0
- package/src/agent/orchestrator/tools/builtin/read-formatting.mjs +118 -0
- package/src/agent/orchestrator/tools/builtin/read-image-resize.mjs +189 -0
- package/src/agent/orchestrator/tools/builtin/read-image.mjs +88 -0
- package/src/agent/orchestrator/tools/builtin/read-lines.mjs +12 -0
- package/src/agent/orchestrator/tools/builtin/read-mode-tool.mjs +455 -0
- package/src/agent/orchestrator/tools/builtin/read-open.mjs +190 -0
- package/src/agent/orchestrator/tools/builtin/read-range-index.mjs +271 -0
- package/src/agent/orchestrator/tools/builtin/read-ranges.mjs +26 -0
- package/src/agent/orchestrator/tools/builtin/read-single-tool.mjs +728 -0
- package/src/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +173 -0
- package/src/agent/orchestrator/tools/builtin/read-special-files.mjs +268 -0
- package/src/agent/orchestrator/tools/builtin/read-streaming.mjs +602 -0
- package/src/agent/orchestrator/tools/builtin/read-tool.mjs +530 -0
- package/src/agent/orchestrator/tools/builtin/read-windows.mjs +107 -0
- package/src/agent/orchestrator/tools/builtin/rename-tool.mjs +196 -0
- package/src/agent/orchestrator/tools/builtin/rg-runner.mjs +422 -0
- package/src/agent/orchestrator/tools/builtin/search-builders.mjs +158 -0
- package/src/agent/orchestrator/tools/builtin/search-tool.mjs +869 -0
- package/src/agent/orchestrator/tools/builtin/shell-analysis.mjs +653 -0
- package/src/agent/orchestrator/tools/builtin/shell-jobs.mjs +936 -0
- package/src/agent/orchestrator/tools/builtin/shell-output.mjs +36 -0
- package/src/agent/orchestrator/tools/builtin/shell-runtime.mjs +214 -0
- package/src/agent/orchestrator/tools/builtin/snapshot-helpers.mjs +143 -0
- package/src/agent/orchestrator/tools/builtin/snapshot-store.mjs +206 -0
- package/src/agent/orchestrator/tools/builtin/snapshot-validation.mjs +98 -0
- package/src/agent/orchestrator/tools/builtin/text-stats.mjs +69 -0
- package/src/agent/orchestrator/tools/builtin/windows-roots.mjs +23 -0
- package/src/agent/orchestrator/tools/builtin/write-tool.mjs +401 -0
- package/src/agent/orchestrator/tools/builtin.mjs +500 -0
- package/src/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +39 -0
- package/src/agent/orchestrator/tools/code-graph-tool-defs.mjs +24 -0
- package/src/agent/orchestrator/tools/code-graph.mjs +4095 -0
- package/src/agent/orchestrator/tools/cwd-tool.mjs +298 -0
- package/src/agent/orchestrator/tools/destructive-warning.mjs +323 -0
- package/src/agent/orchestrator/tools/edit-normalize.mjs +603 -0
- package/src/agent/orchestrator/tools/env-scrub.mjs +100 -0
- package/src/agent/orchestrator/tools/graph-binary-fetcher.mjs +144 -0
- package/src/agent/orchestrator/tools/graph-manifest.json +26 -0
- package/src/agent/orchestrator/tools/host-input.mjs +204 -0
- package/src/agent/orchestrator/tools/mutation-content-cache.mjs +67 -0
- package/src/agent/orchestrator/tools/mutation-planner.mjs +75 -0
- package/src/agent/orchestrator/tools/next-call-utils.mjs +48 -0
- package/src/agent/orchestrator/tools/patch-binary-fetcher.mjs +133 -0
- package/src/agent/orchestrator/tools/patch-manifest.json +26 -0
- package/src/agent/orchestrator/tools/patch-tool-defs.mjs +20 -0
- package/src/agent/orchestrator/tools/patch.mjs +2754 -0
- package/src/agent/orchestrator/tools/progress-message.mjs +118 -0
- package/src/agent/orchestrator/tools/result-compression.mjs +279 -0
- package/src/agent/orchestrator/tools/shell-command.mjs +865 -0
- package/src/agent/orchestrator/tools/shell-exec-policy.mjs +89 -0
- package/src/agent/orchestrator/tools/shell-policy-danger-target.mjs +27 -0
- package/src/agent/orchestrator/tools/shell-policy-imports.mjs +7 -0
- package/src/agent/orchestrator/tools/shell-policy.mjs +345 -0
- package/src/agent/orchestrator/tools/shell-snapshot.mjs +313 -0
- package/src/agent/orchestrator/workflow-store.mjs +93 -0
- package/src/agent/tool-defs.mjs +103 -0
- package/src/channels/backends/discord.mjs +784 -0
- package/src/channels/data/voice-runtime-manifest.json +138 -0
- package/src/channels/index.mjs +3229 -0
- package/src/channels/lib/cli-worker-host.mjs +12 -0
- package/src/channels/lib/config-lock.mjs +13 -0
- package/src/channels/lib/config.mjs +292 -0
- package/src/channels/lib/drop-trace.mjs +71 -0
- package/src/channels/lib/event-pipeline.mjs +81 -0
- package/src/channels/lib/event-queue.mjs +345 -0
- package/src/channels/lib/executor.mjs +168 -0
- package/src/channels/lib/format.mjs +188 -0
- package/src/channels/lib/holidays.mjs +138 -0
- package/src/channels/lib/hook-pipe-server.mjs +802 -0
- package/src/channels/lib/interaction-workflows.mjs +184 -0
- package/src/channels/lib/memory-client.mjs +149 -0
- package/src/channels/lib/output-forwarder.mjs +765 -0
- package/src/channels/lib/runtime-paths.mjs +479 -0
- package/src/channels/lib/scheduler.mjs +723 -0
- package/src/channels/lib/session-control.mjs +36 -0
- package/src/channels/lib/session-discovery.mjs +103 -0
- package/src/channels/lib/settings.mjs +11 -0
- package/src/channels/lib/state-file.mjs +68 -0
- package/src/channels/lib/status-snapshot.mjs +219 -0
- package/src/channels/lib/tool-format.mjs +140 -0
- package/src/channels/lib/transcript-discovery.mjs +195 -0
- package/src/channels/lib/voice-runtime-fetcher.mjs +734 -0
- package/src/channels/lib/webhook.mjs +1179 -0
- package/src/channels/lib/whisper-server.mjs +477 -0
- package/src/channels/tool-defs.mjs +170 -0
- package/src/daemon/host.mjs +118 -0
- package/src/daemon/mcp-transport.mjs +47 -0
- package/src/daemon/session.mjs +100 -0
- package/src/daemon/thin-client.mjs +71 -0
- package/src/daemon/transport.mjs +163 -0
- package/src/memory/data/runtime-manifest.json +40 -0
- package/src/memory/index.mjs +3305 -0
- package/src/memory/lib/agent-ipc.mjs +93 -0
- package/src/memory/lib/bridge-trace-queries.mjs +120 -0
- package/src/memory/lib/core-memory-store.mjs +330 -0
- package/src/memory/lib/embedding-provider.mjs +269 -0
- package/src/memory/lib/embedding-worker.mjs +323 -0
- package/src/memory/lib/llm-worker-host.mjs +17 -0
- package/src/memory/lib/memory-cycle.mjs +11 -0
- package/src/memory/lib/memory-cycle1.mjs +641 -0
- package/src/memory/lib/memory-cycle2.mjs +1284 -0
- package/src/memory/lib/memory-cycle3.mjs +540 -0
- package/src/memory/lib/memory-embed.mjs +299 -0
- package/src/memory/lib/memory-extraction.mjs +5 -0
- package/src/memory/lib/memory-maintenance-store.mjs +32 -0
- package/src/memory/lib/memory-ops-policy.mjs +190 -0
- package/src/memory/lib/memory-recall-id-patch.mjs +15 -0
- package/src/memory/lib/memory-recall-read-query.mjs +7 -0
- package/src/memory/lib/memory-recall-scope-filter.mjs +63 -0
- package/src/memory/lib/memory-recall-store.mjs +621 -0
- package/src/memory/lib/memory-retrievers.mjs +112 -0
- package/src/memory/lib/memory-score.mjs +71 -0
- package/src/memory/lib/memory-text-utils.mjs +58 -0
- package/src/memory/lib/memory.mjs +412 -0
- package/src/memory/lib/model-profile.mjs +85 -0
- package/src/memory/lib/pg/adapter.mjs +308 -0
- package/src/memory/lib/pg/process.mjs +360 -0
- package/src/memory/lib/pg/supervisor.mjs +396 -0
- package/src/memory/lib/project-id-resolver.mjs +86 -0
- package/src/memory/lib/runtime-fetcher.mjs +442 -0
- package/src/memory/lib/trace-store.mjs +728 -0
- package/src/memory/tool-defs.mjs +79 -0
- package/src/search/index.mjs +1173 -0
- package/src/search/lib/backends/anthropic-oauth.mjs +98 -0
- package/src/search/lib/backends/exa.mjs +50 -0
- package/src/search/lib/backends/firecrawl.mjs +61 -0
- package/src/search/lib/backends/gemini-api.mjs +83 -0
- package/src/search/lib/backends/grok-oauth.mjs +86 -0
- package/src/search/lib/backends/index.mjs +150 -0
- package/src/search/lib/backends/openai-api.mjs +144 -0
- package/src/search/lib/backends/openai-oauth.mjs +98 -0
- package/src/search/lib/backends/openai-web-search.mjs +76 -0
- package/src/search/lib/backends/tavily.mjs +55 -0
- package/src/search/lib/backends/xai-api.mjs +113 -0
- package/src/search/lib/cache.mjs +131 -0
- package/src/search/lib/config.mjs +192 -0
- package/src/search/lib/formatter.mjs +115 -0
- package/src/search/lib/provider-usage.mjs +67 -0
- package/src/search/lib/providers.mjs +47 -0
- package/src/search/lib/search-intent.mjs +109 -0
- package/src/search/lib/setup-handler.mjs +261 -0
- package/src/search/lib/state.mjs +201 -0
- package/src/search/lib/web-tools.mjs +1207 -0
- package/src/search/tool-defs.mjs +83 -0
- package/src/setup/defender-exclusion.mjs +183 -0
- package/src/shared/abort-controller.mjs +15 -0
- package/src/shared/atomic-file.mjs +420 -0
- package/src/shared/config.mjs +350 -0
- package/src/shared/daemon-recycle.mjs +108 -0
- package/src/shared/disable-claude-builtins.mjs +88 -0
- package/src/shared/err-text.mjs +12 -0
- package/src/shared/llm/cost.mjs +66 -0
- package/src/shared/llm/http-agent.mjs +123 -0
- package/src/shared/llm/index.mjs +41 -0
- package/src/shared/llm/pid-cleanup.mjs +27 -0
- package/src/shared/llm/usage-log.mjs +47 -0
- package/src/shared/plugin-paths.mjs +58 -0
- package/src/shared/schedules-store.mjs +70 -0
- package/src/shared/seed.mjs +119 -0
- package/src/shared/user-cwd.mjs +213 -0
- package/src/shared/user-data-guard.mjs +238 -0
- package/src/status/aggregator.mjs +584 -0
- package/src/status/server.mjs +413 -0
- package/tools.json +1653 -0
package/setup/setup.html
ADDED
|
@@ -0,0 +1,3693 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<meta name="google" content="notranslate">
|
|
7
|
+
<title>MIXDOG CONFIG</title>
|
|
8
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
|
|
9
|
+
<style>
|
|
10
|
+
:root {
|
|
11
|
+
--bg: #18181b;
|
|
12
|
+
--bg-raised: #222228;
|
|
13
|
+
--bg-input: #18181b;
|
|
14
|
+
--border: rgba(255,255,255,0.15);
|
|
15
|
+
--border-focus: rgba(255,255,255,0.4);
|
|
16
|
+
--text-1: #fff;
|
|
17
|
+
--text-2: #ccc;
|
|
18
|
+
--text-3: #aaa;
|
|
19
|
+
--text-4: #777;
|
|
20
|
+
--accent: #8b91f0;
|
|
21
|
+
--green: #3fb950;
|
|
22
|
+
--green-dim: rgba(63,185,80,0.12);
|
|
23
|
+
--red: #e5534b;
|
|
24
|
+
--red-dim: rgba(229,83,75,0.12);
|
|
25
|
+
--orange: #d29922;
|
|
26
|
+
--orange-dim: rgba(210,153,34,0.12);
|
|
27
|
+
--radius: 8px;
|
|
28
|
+
--cat-w: 150px;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
32
|
+
body {
|
|
33
|
+
font-family: 'Inter', -apple-system, sans-serif;
|
|
34
|
+
background: var(--bg); color: var(--text-2);
|
|
35
|
+
-webkit-font-smoothing: antialiased;
|
|
36
|
+
margin: 0; overflow: hidden; height: 100vh;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.layout { display: flex; height: 100vh; }
|
|
40
|
+
|
|
41
|
+
/* Sidebar */
|
|
42
|
+
.cat-bar {
|
|
43
|
+
width: var(--cat-w); flex-shrink: 0;
|
|
44
|
+
background: #141416;
|
|
45
|
+
border-right: 1px solid var(--border);
|
|
46
|
+
display: flex; flex-direction: column;
|
|
47
|
+
padding: 16px 0; overflow-y: auto;
|
|
48
|
+
}
|
|
49
|
+
.cat-item {
|
|
50
|
+
padding: 8px 14px; cursor: pointer;
|
|
51
|
+
transition: all 0.15s; font-size: 13px;
|
|
52
|
+
color: var(--text-3); font-weight: 500;
|
|
53
|
+
border-left: 2px solid transparent;
|
|
54
|
+
}
|
|
55
|
+
.cat-item:hover { background: rgba(255,255,255,0.03); color: var(--text-2); }
|
|
56
|
+
.cat-item.active {
|
|
57
|
+
background: rgba(139,145,240,0.08);
|
|
58
|
+
color: var(--text-1);
|
|
59
|
+
border-left-color: var(--accent);
|
|
60
|
+
}
|
|
61
|
+
.cat-sep {
|
|
62
|
+
height: 1px; background: var(--border);
|
|
63
|
+
margin: 14px 14px 2px;
|
|
64
|
+
}
|
|
65
|
+
.cat-label {
|
|
66
|
+
padding: 10px 14px 6px;
|
|
67
|
+
font-size: 11px; font-weight: 700;
|
|
68
|
+
color: var(--text-3); text-transform: uppercase;
|
|
69
|
+
letter-spacing: 0.8px;
|
|
70
|
+
}
|
|
71
|
+
.cat-label:first-child {
|
|
72
|
+
padding-top: 4px;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* Main */
|
|
76
|
+
.main {
|
|
77
|
+
flex: 1; overflow-y: auto; padding: 28px 24px 20px;
|
|
78
|
+
opacity: 0;
|
|
79
|
+
transition: opacity 0.18s ease-out;
|
|
80
|
+
}
|
|
81
|
+
.main.ready { opacity: 1; }
|
|
82
|
+
/* Centered loading indicator shown until the loader chain resolves and
|
|
83
|
+
`.main` gains the `ready` class. Prevents the brief "empty defaults"
|
|
84
|
+
flash that the user reported on first entry. */
|
|
85
|
+
#boot-spinner {
|
|
86
|
+
position: fixed; left: 0; right: 0; top: 0; bottom: 0;
|
|
87
|
+
display: flex; align-items: center; justify-content: center;
|
|
88
|
+
background: var(--bg);
|
|
89
|
+
color: var(--text-3); font-size: 12px;
|
|
90
|
+
z-index: 9990;
|
|
91
|
+
transition: opacity 0.18s ease-out;
|
|
92
|
+
}
|
|
93
|
+
#boot-spinner.hidden { opacity: 0; pointer-events: none; }
|
|
94
|
+
#boot-spinner .dot {
|
|
95
|
+
width: 8px; height: 8px; border-radius: 50%;
|
|
96
|
+
background: var(--accent); margin: 0 4px;
|
|
97
|
+
animation: bootpulse 1s ease-in-out infinite;
|
|
98
|
+
}
|
|
99
|
+
#boot-spinner .dot:nth-child(2) { animation-delay: 0.15s; }
|
|
100
|
+
#boot-spinner .dot:nth-child(3) { animation-delay: 0.3s; }
|
|
101
|
+
@keyframes bootpulse {
|
|
102
|
+
0%, 60%, 100% { opacity: 0.25; transform: scale(0.85); }
|
|
103
|
+
30% { opacity: 1; transform: scale(1.1); }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.panel { display: none; }
|
|
107
|
+
.panel.active { display: block; }
|
|
108
|
+
|
|
109
|
+
.hdr { margin-bottom: 24px; display: flex; align-items: center; justify-content: space-between; }
|
|
110
|
+
.hdr h1 { font-size: 20px; font-weight: 600; color: var(--text-1); letter-spacing: -0.3px; }
|
|
111
|
+
|
|
112
|
+
/* Sections */
|
|
113
|
+
.sec { margin-bottom: 20px; }
|
|
114
|
+
.sec-title {
|
|
115
|
+
font-size: 12px; font-weight: 600; color: var(--text-2);
|
|
116
|
+
margin-bottom: 8px; padding: 0 2px;
|
|
117
|
+
}
|
|
118
|
+
.sec-body {
|
|
119
|
+
background: var(--bg-raised);
|
|
120
|
+
border: 1px solid var(--border);
|
|
121
|
+
border-radius: var(--radius);
|
|
122
|
+
overflow: visible;
|
|
123
|
+
min-height: 1px;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/* Rows */
|
|
127
|
+
.r {
|
|
128
|
+
display: flex; align-items: center;
|
|
129
|
+
padding: 0 14px; min-height: 44px;
|
|
130
|
+
border-bottom: 1px solid var(--border);
|
|
131
|
+
transition: background 0.15s;
|
|
132
|
+
}
|
|
133
|
+
.r:last-child { border-bottom: none; }
|
|
134
|
+
.r:hover { background: rgba(255,255,255,0.015); cursor: default; }
|
|
135
|
+
.r.dragging { opacity: 0.4; }
|
|
136
|
+
.r.drag-over { border-top: 2px solid var(--accent); }
|
|
137
|
+
.r.r-opt {
|
|
138
|
+
align-items: flex-start;
|
|
139
|
+
min-height: auto;
|
|
140
|
+
padding: 14px 16px;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.r-name {
|
|
144
|
+
width: 130px; font-size: 13px; color: var(--text-1);
|
|
145
|
+
font-weight: 500; flex-shrink: 0; white-space: nowrap;
|
|
146
|
+
}
|
|
147
|
+
.r-name-wide {
|
|
148
|
+
width: 140px; font-size: 13px; color: var(--text-1);
|
|
149
|
+
font-weight: 500; flex-shrink: 0; white-space: nowrap;
|
|
150
|
+
}
|
|
151
|
+
.r-name-wide .tip { vertical-align: middle; margin-left: 4px; }
|
|
152
|
+
|
|
153
|
+
/* Inputs */
|
|
154
|
+
.r-input {
|
|
155
|
+
flex: 1; height: 34px; padding: 0 10px;
|
|
156
|
+
background: rgba(0,0,0,0.25); border: 1px solid var(--border);
|
|
157
|
+
border-radius: 5px; color: var(--text-1);
|
|
158
|
+
font-size: 12px; font-family: 'SF Mono', 'Cascadia Code', monospace;
|
|
159
|
+
transition: border-color 0.15s;
|
|
160
|
+
}
|
|
161
|
+
.r-input:focus { outline: none; border-color: var(--border-focus); }
|
|
162
|
+
.r-input::placeholder { color: #777; }
|
|
163
|
+
.r-input.warn { border-color: var(--red); }
|
|
164
|
+
.r-input.valid { border-color: var(--green); }
|
|
165
|
+
|
|
166
|
+
/* Selects */
|
|
167
|
+
.r-select {
|
|
168
|
+
flex: 1; height: 34px; padding: 0 8px;
|
|
169
|
+
background: rgba(0,0,0,0.25); border: 1px solid var(--border);
|
|
170
|
+
border-radius: 5px; color: var(--text-1);
|
|
171
|
+
font-size: 12px; font-family: 'Inter', sans-serif;
|
|
172
|
+
transition: border-color 0.15s; cursor: pointer;
|
|
173
|
+
-webkit-appearance: none; appearance: none;
|
|
174
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23666'/%3E%3C/svg%3E");
|
|
175
|
+
background-repeat: no-repeat;
|
|
176
|
+
background-position: right 10px center;
|
|
177
|
+
padding-right: 28px;
|
|
178
|
+
}
|
|
179
|
+
.r-select:focus { outline: none; border-color: var(--border-focus); }
|
|
180
|
+
.r-select option { background: var(--bg-raised); color: var(--text-1); }
|
|
181
|
+
.r-select:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
182
|
+
.r-select.md { width: 140px; flex: none; }
|
|
183
|
+
|
|
184
|
+
/* Toggle */
|
|
185
|
+
.r-toggle {
|
|
186
|
+
width: 40px; height: 22px; border-radius: 11px;
|
|
187
|
+
background: rgba(255,255,255,0.12); cursor: pointer; position: relative;
|
|
188
|
+
transition: background 0.2s; flex-shrink: 0;
|
|
189
|
+
}
|
|
190
|
+
.r-toggle.on { background: var(--accent); }
|
|
191
|
+
.r-toggle::after {
|
|
192
|
+
content: ''; position: absolute;
|
|
193
|
+
width: 16px; height: 16px; border-radius: 50%;
|
|
194
|
+
background: var(--text-3); top: 3px; left: 3px;
|
|
195
|
+
transition: all 0.2s;
|
|
196
|
+
}
|
|
197
|
+
.r-toggle.on::after { transform: translateX(18px); background: #fff; }
|
|
198
|
+
|
|
199
|
+
/* Radio */
|
|
200
|
+
.r-radio {
|
|
201
|
+
width: 17px; height: 17px; border-radius: 50%;
|
|
202
|
+
border: 1.5px solid var(--text-4); cursor: pointer;
|
|
203
|
+
display: flex; align-items: center; justify-content: center;
|
|
204
|
+
transition: all 0.15s; flex-shrink: 0;
|
|
205
|
+
}
|
|
206
|
+
.r-radio.on { border-color: var(--accent); }
|
|
207
|
+
.r-radio.on::after {
|
|
208
|
+
content: ''; width: 9px; height: 9px; border-radius: 50%;
|
|
209
|
+
background: var(--accent);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/* General panel — injection mode picker */
|
|
213
|
+
.sec-desc {
|
|
214
|
+
font-size: 12px; color: var(--text-3);
|
|
215
|
+
margin-bottom: 22px; padding: 0 2px; line-height: 1.5;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/* Modules panel — keep description grouped with its toggle row.
|
|
219
|
+
Default `.r` adds a bottom border between sibling rows, which would
|
|
220
|
+
visually cut the description away from its toggle. Move the border
|
|
221
|
+
onto the description instead so each {toggle, desc} pair reads as a
|
|
222
|
+
single block, with the separator falling between modules. */
|
|
223
|
+
#panel-general-modules .r.toggle-row { border-bottom: none; }
|
|
224
|
+
#panel-general-modules .sec-desc {
|
|
225
|
+
margin-bottom: 0;
|
|
226
|
+
border-bottom: 1px solid var(--border);
|
|
227
|
+
}
|
|
228
|
+
#panel-general-modules .sec-desc:last-of-type { border-bottom: none; }
|
|
229
|
+
.opt {
|
|
230
|
+
display: flex; align-items: flex-start; gap: 12px;
|
|
231
|
+
cursor: pointer; width: 100%;
|
|
232
|
+
}
|
|
233
|
+
.opt-body { flex: 1; display: flex; flex-direction: column; gap: 3px; }
|
|
234
|
+
.opt-title {
|
|
235
|
+
font-size: 13px; font-weight: 500; color: var(--text-1);
|
|
236
|
+
line-height: 1.3;
|
|
237
|
+
}
|
|
238
|
+
.opt-sub { font-size: 12px; color: var(--text-3); line-height: 1.5; }
|
|
239
|
+
.opt-sub code { font-family: 'SF Mono', 'Cascadia Code', monospace; font-size: 11px; padding: 1px 6px; background: rgba(0,0,0,0.25); border: 1px solid var(--border); border-radius: 4px; color: var(--text-2); vertical-align: middle; display: inline-block; line-height: 1.4; }
|
|
240
|
+
.r.is-centered { align-items: center; }
|
|
241
|
+
.r-value {
|
|
242
|
+
flex: 1; height: 34px; padding: 0 10px;
|
|
243
|
+
background: rgba(0,0,0,0.25); border: 1px solid var(--border);
|
|
244
|
+
border-radius: 5px; color: var(--text-2);
|
|
245
|
+
font-size: 12px; font-family: 'SF Mono', 'Cascadia Code', monospace;
|
|
246
|
+
display: flex; align-items: center; user-select: text;
|
|
247
|
+
}
|
|
248
|
+
.note-box {
|
|
249
|
+
display: flex; align-items: flex-start; gap: 10px;
|
|
250
|
+
padding: 12px 16px;
|
|
251
|
+
background: rgba(210,153,34,0.12);
|
|
252
|
+
font-size: 12px; line-height: 1.5; color: #d29922;
|
|
253
|
+
}
|
|
254
|
+
.note-box .note-icon {
|
|
255
|
+
flex-shrink: 0; font-weight: 700; width: 16px; text-align: center;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/* Slider */
|
|
259
|
+
.r-slider {
|
|
260
|
+
flex: 1; -webkit-appearance: none; appearance: none;
|
|
261
|
+
height: 4px; background: var(--text-4); border-radius: 2px;
|
|
262
|
+
outline: none;
|
|
263
|
+
}
|
|
264
|
+
.r-slider::-webkit-slider-thumb {
|
|
265
|
+
-webkit-appearance: none; width: 16px; height: 16px;
|
|
266
|
+
border-radius: 50%; background: var(--accent); cursor: pointer;
|
|
267
|
+
}
|
|
268
|
+
.slider-val {
|
|
269
|
+
width: 24px; text-align: center; font-size: 13px;
|
|
270
|
+
color: var(--text-1); font-weight: 600; flex-shrink: 0;
|
|
271
|
+
}
|
|
272
|
+
.slider-labels {
|
|
273
|
+
display: flex; justify-content: space-between;
|
|
274
|
+
font-size: 13px; color: var(--text-3);
|
|
275
|
+
padding: 2px 4px 0;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/* Password */
|
|
279
|
+
.pw-wrap {
|
|
280
|
+
flex: 1; display: flex; align-items: center;
|
|
281
|
+
position: relative;
|
|
282
|
+
}
|
|
283
|
+
.pw-wrap .r-input { flex: 1; padding-right: 34px; }
|
|
284
|
+
.pw-toggle {
|
|
285
|
+
position: absolute; right: 8px; cursor: pointer;
|
|
286
|
+
font-size: 14px; color: var(--text-3);
|
|
287
|
+
transition: color 0.15s; user-select: none;
|
|
288
|
+
}
|
|
289
|
+
.pw-toggle:hover { color: var(--text-1); }
|
|
290
|
+
|
|
291
|
+
/* Tags */
|
|
292
|
+
.tag-container {
|
|
293
|
+
flex: 1; display: flex; flex-wrap: wrap; gap: 4px;
|
|
294
|
+
min-height: 34px; padding: 4px 8px;
|
|
295
|
+
background: rgba(0,0,0,0.25); border: 1px solid var(--border);
|
|
296
|
+
border-radius: 5px; align-items: center; cursor: text;
|
|
297
|
+
}
|
|
298
|
+
.tag-container:focus-within { border-color: var(--border-focus); }
|
|
299
|
+
.tag-item {
|
|
300
|
+
display: flex; align-items: center; gap: 4px;
|
|
301
|
+
background: rgba(139,145,240,0.15); color: var(--accent);
|
|
302
|
+
padding: 2px 8px; border-radius: 4px; font-size: 12px;
|
|
303
|
+
font-family: 'SF Mono', 'Cascadia Code', monospace;
|
|
304
|
+
}
|
|
305
|
+
.tag-item .tag-x {
|
|
306
|
+
cursor: pointer; font-size: 13px; color: var(--text-3);
|
|
307
|
+
transition: color 0.15s;
|
|
308
|
+
}
|
|
309
|
+
.tag-item .tag-x:hover { color: var(--red); }
|
|
310
|
+
.tag-input {
|
|
311
|
+
border: none; background: none; color: var(--text-1);
|
|
312
|
+
font-size: 12px; font-family: 'SF Mono', 'Cascadia Code', monospace;
|
|
313
|
+
outline: none; min-width: 60px; flex: 1;
|
|
314
|
+
}
|
|
315
|
+
.tag-input::placeholder { color: #777; }
|
|
316
|
+
|
|
317
|
+
/* Save */
|
|
318
|
+
.save-area { margin-top: 12px; }
|
|
319
|
+
.save {
|
|
320
|
+
width: 100%; height: 44px;
|
|
321
|
+
background: var(--accent); color: #fff;
|
|
322
|
+
border: none; border-radius: var(--radius);
|
|
323
|
+
font-family: 'Inter', sans-serif;
|
|
324
|
+
font-size: 13px; font-weight: 600;
|
|
325
|
+
cursor: pointer; transition: all 0.15s;
|
|
326
|
+
}
|
|
327
|
+
.save:hover { filter: brightness(1.1); }
|
|
328
|
+
.save.done { background: var(--bg-raised); color: var(--green); pointer-events: none; }
|
|
329
|
+
.warn-msg {
|
|
330
|
+
text-align: center; font-size: 12px; color: var(--red);
|
|
331
|
+
margin-bottom: 4px; height: 16px;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/* Dynamic list */
|
|
335
|
+
.dyn-list { padding: 8px 14px; }
|
|
336
|
+
.dyn-item {
|
|
337
|
+
display: flex; align-items: center; gap: 8px;
|
|
338
|
+
padding: 8px 0; border-bottom: 1px solid var(--border);
|
|
339
|
+
flex-wrap: nowrap;
|
|
340
|
+
}
|
|
341
|
+
.dyn-item:last-child { border-bottom: none; }
|
|
342
|
+
.dyn-item .r-input { height: 34px; font-size: 12px; overflow: hidden; text-overflow: ellipsis; }
|
|
343
|
+
.dyn-item .r-select { height: 34px; font-size: 12px; }
|
|
344
|
+
.dyn-add {
|
|
345
|
+
font-size: 13px; color: var(--accent); cursor: pointer;
|
|
346
|
+
padding: 4px 0; transition: color 0.15s;
|
|
347
|
+
}
|
|
348
|
+
.dyn-add:hover { filter: brightness(1.1); }
|
|
349
|
+
.dyn-remove {
|
|
350
|
+
font-size: 13px; color: var(--text-4); cursor: pointer;
|
|
351
|
+
transition: color 0.15s; flex-shrink: 0; width: 20px;
|
|
352
|
+
text-align: center;
|
|
353
|
+
}
|
|
354
|
+
.dyn-remove:hover { color: var(--red); }
|
|
355
|
+
|
|
356
|
+
/* Tooltip */
|
|
357
|
+
.tip {
|
|
358
|
+
position: relative; display: inline-flex; align-items: center;
|
|
359
|
+
cursor: help; color: var(--text-4); font-size: 12px;
|
|
360
|
+
width: 16px; height: 16px; justify-content: center;
|
|
361
|
+
border-radius: 50%; border: 1px solid var(--text-4);
|
|
362
|
+
flex-shrink: 0; font-weight: 600;
|
|
363
|
+
transition: color 0.15s, border-color 0.15s;
|
|
364
|
+
}
|
|
365
|
+
.tip:hover { color: var(--text-1); border-color: var(--text-3); }
|
|
366
|
+
.tip .tip-text {
|
|
367
|
+
display: none; position: absolute; top: calc(100% + 8px); left: 0;
|
|
368
|
+
background: var(--bg-raised); color: var(--text-1);
|
|
369
|
+
font-size: 12px; font-weight: 400; padding: 10px 14px;
|
|
370
|
+
border-radius: 5px; z-index: 9999;
|
|
371
|
+
box-shadow: 0 4px 16px rgba(0,0,0,0.5); pointer-events: none;
|
|
372
|
+
max-width: 280px; min-width: 180px; white-space: normal; line-height: 1.5;
|
|
373
|
+
border: 1px solid var(--border);
|
|
374
|
+
}
|
|
375
|
+
.tip:hover .tip-text { display: block; }
|
|
376
|
+
|
|
377
|
+
/* Cards */
|
|
378
|
+
.card-list { padding: 0; display: flex; flex-direction: column; gap: 0; }
|
|
379
|
+
.card {
|
|
380
|
+
background: var(--bg-raised); border: 1px solid var(--border);
|
|
381
|
+
border-radius: var(--radius); position: relative;
|
|
382
|
+
transition: border-color 0.15s; margin: 8px 14px;
|
|
383
|
+
overflow: hidden;
|
|
384
|
+
}
|
|
385
|
+
.card:hover { border-color: rgba(255,255,255,0.25); }
|
|
386
|
+
.card .r { padding: 0 14px; min-height: 44px; border-bottom: 1px solid var(--border); display: flex; align-items: center; }
|
|
387
|
+
.card .r:last-child { border-bottom: none; }
|
|
388
|
+
.card-hdr {
|
|
389
|
+
display: flex; align-items: center; padding: 12px 14px;
|
|
390
|
+
cursor: pointer; user-select: none;
|
|
391
|
+
border-bottom: 1px solid var(--border);
|
|
392
|
+
transition: background 0.15s;
|
|
393
|
+
}
|
|
394
|
+
.card-hdr:hover { background: rgba(255,255,255,0.02); }
|
|
395
|
+
.card-hdr::before {
|
|
396
|
+
content: '\25B8'; color: var(--text-2); margin-right: 10px;
|
|
397
|
+
font-size: 12px; transition: transform 0.15s;
|
|
398
|
+
}
|
|
399
|
+
.card:not(.collapsed) .card-hdr::before { content: '\25BE'; }
|
|
400
|
+
.card.collapsed .card-hdr::before { transform: none; }
|
|
401
|
+
.card-title { font-size: 14px; font-weight: 600; color: var(--text-1); flex: 1; }
|
|
402
|
+
.card-hdr-right { display: flex; align-items: center; gap: 10px; }
|
|
403
|
+
.card-hdr-right .card-remove {
|
|
404
|
+
position: static; font-size: 18px; color: var(--text-3);
|
|
405
|
+
cursor: pointer; transition: color 0.15s; line-height: 1;
|
|
406
|
+
}
|
|
407
|
+
.card-hdr-right .card-remove:hover { color: var(--red); }
|
|
408
|
+
.card-content { transition: none; }
|
|
409
|
+
.card.collapsed .card-content { display: none; }
|
|
410
|
+
.card.collapsed .card-hdr { border-bottom: none; }
|
|
411
|
+
.card-remove {
|
|
412
|
+
position: absolute; top: 10px; right: 10px;
|
|
413
|
+
font-size: 14px; color: var(--text-4); cursor: pointer;
|
|
414
|
+
transition: color 0.15s; z-index: 1; line-height: 1;
|
|
415
|
+
width: 20px; height: 20px; display: flex;
|
|
416
|
+
align-items: center; justify-content: center;
|
|
417
|
+
}
|
|
418
|
+
.card-remove:hover { color: var(--red); }
|
|
419
|
+
|
|
420
|
+
/* Header button */
|
|
421
|
+
.hdr-btn {
|
|
422
|
+
height: 30px; padding: 0 14px;
|
|
423
|
+
background: var(--accent); color: #fff;
|
|
424
|
+
border: none; border-radius: 5px;
|
|
425
|
+
font-family: 'Inter', sans-serif;
|
|
426
|
+
font-size: 12px; font-weight: 600;
|
|
427
|
+
cursor: pointer; transition: filter 0.15s;
|
|
428
|
+
}
|
|
429
|
+
.hdr-btn:hover { filter: brightness(1.1); }
|
|
430
|
+
|
|
431
|
+
/* Empty state */
|
|
432
|
+
.empty {
|
|
433
|
+
padding: 24px; text-align: center;
|
|
434
|
+
font-size: 12px; color: var(--text-4);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/* CLI status */
|
|
438
|
+
.cli-status {
|
|
439
|
+
padding: 14px; display: flex; align-items: center; gap: 10px;
|
|
440
|
+
font-size: 13px; line-height: 1.6;
|
|
441
|
+
}
|
|
442
|
+
.cli-status.ok { color: var(--green); }
|
|
443
|
+
.cli-status.warn { color: var(--orange); }
|
|
444
|
+
.cli-status a {
|
|
445
|
+
color: var(--accent); text-decoration: none;
|
|
446
|
+
transition: color 0.15s;
|
|
447
|
+
}
|
|
448
|
+
.cli-status a:hover { color: var(--text-1); }
|
|
449
|
+
|
|
450
|
+
/* Time inputs */
|
|
451
|
+
.time-row {
|
|
452
|
+
display: flex; align-items: center; gap: 8px;
|
|
453
|
+
}
|
|
454
|
+
.time-row .r-input { width: 70px; flex: none; text-align: center; }
|
|
455
|
+
.time-sep { color: var(--text-3); font-size: 13px; }
|
|
456
|
+
|
|
457
|
+
/* Scrollbar */
|
|
458
|
+
.main::-webkit-scrollbar, .cat-bar::-webkit-scrollbar { width: 6px; }
|
|
459
|
+
.main::-webkit-scrollbar-track, .cat-bar::-webkit-scrollbar-track { background: transparent; }
|
|
460
|
+
.main::-webkit-scrollbar-thumb, .cat-bar::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 3px; }
|
|
461
|
+
.main::-webkit-scrollbar-thumb:hover, .cat-bar::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.2); }
|
|
462
|
+
|
|
463
|
+
/* Collapsible */
|
|
464
|
+
.collapsible-toggle {
|
|
465
|
+
display: flex; align-items: center; gap: 8px;
|
|
466
|
+
padding: 12px 14px; cursor: pointer; user-select: none;
|
|
467
|
+
font-size: 13px; color: var(--text-1);
|
|
468
|
+
transition: background 0.15s;
|
|
469
|
+
}
|
|
470
|
+
.collapsible-toggle:hover { background: rgba(255,255,255,0.015); }
|
|
471
|
+
.collapsible-toggle .arrow {
|
|
472
|
+
font-size: 10px; color: var(--text-3); transition: transform 0.2s;
|
|
473
|
+
}
|
|
474
|
+
.collapsible-toggle.open .arrow { transform: rotate(90deg); }
|
|
475
|
+
.collapsible-body { display: none; }
|
|
476
|
+
.collapsible-body.open { display: block; }
|
|
477
|
+
|
|
478
|
+
.hidden { display: none !important; }
|
|
479
|
+
|
|
480
|
+
/* Close confirmation modal */
|
|
481
|
+
.modal-overlay {
|
|
482
|
+
position: fixed; inset: 0;
|
|
483
|
+
background: rgba(0,0,0,0.55);
|
|
484
|
+
display: none; align-items: center; justify-content: center;
|
|
485
|
+
z-index: 1000; backdrop-filter: blur(4px);
|
|
486
|
+
}
|
|
487
|
+
.modal-box {
|
|
488
|
+
background: var(--bg-raised);
|
|
489
|
+
border: 1px solid var(--border);
|
|
490
|
+
border-radius: 10px;
|
|
491
|
+
padding: 28px 24px 20px;
|
|
492
|
+
max-width: 380px; width: 90%;
|
|
493
|
+
box-shadow: 0 16px 48px rgba(0,0,0,0.4);
|
|
494
|
+
}
|
|
495
|
+
.modal-title {
|
|
496
|
+
font-size: 15px; font-weight: 600; color: var(--text-1);
|
|
497
|
+
margin-bottom: 6px;
|
|
498
|
+
}
|
|
499
|
+
.modal-msg {
|
|
500
|
+
font-size: 12px; color: var(--text-3);
|
|
501
|
+
margin-bottom: 20px; line-height: 1.5;
|
|
502
|
+
}
|
|
503
|
+
.modal-actions {
|
|
504
|
+
display: flex; flex-direction: column; gap: 6px;
|
|
505
|
+
}
|
|
506
|
+
.modal-actions .mbtn {
|
|
507
|
+
height: 36px; width: 100%;
|
|
508
|
+
border: none; border-radius: 6px;
|
|
509
|
+
font-family: 'Inter', sans-serif;
|
|
510
|
+
font-size: 12px; font-weight: 600;
|
|
511
|
+
cursor: pointer; transition: all 0.15s;
|
|
512
|
+
}
|
|
513
|
+
.modal-actions .mbtn.primary { background: var(--accent); color: #fff; }
|
|
514
|
+
.modal-actions .mbtn.primary:hover { filter: brightness(1.1); }
|
|
515
|
+
.modal-actions .mbtn.secondary { background: rgba(255,255,255,0.05); color: var(--text-2); border: 1px solid var(--border); }
|
|
516
|
+
.modal-actions .mbtn.danger-outline {
|
|
517
|
+
background: transparent; color: var(--red, #e5534b);
|
|
518
|
+
border: 1px solid rgba(229,83,75,0.3);
|
|
519
|
+
}
|
|
520
|
+
.modal-actions .mbtn.danger-outline:hover {
|
|
521
|
+
background: rgba(229,83,75,0.12); border-color: var(--red, #e5534b);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/* -- Agent module styles -- */
|
|
525
|
+
.r-tag {
|
|
526
|
+
font-size: 11px; padding: 3px 0; border-radius: 4px;
|
|
527
|
+
font-weight: 500; margin-left: auto; flex-shrink: 0;
|
|
528
|
+
width: 64px; text-align: center; display: inline-flex; align-items: center; justify-content: center;
|
|
529
|
+
}
|
|
530
|
+
.r-tag.ok { background: var(--green-dim); color: var(--green); }
|
|
531
|
+
.r-tag.no { background: var(--red-dim); color: var(--red); }
|
|
532
|
+
.r-tag.oauth { background: var(--orange-dim); color: var(--orange); }
|
|
533
|
+
.r-tag.env { background: rgba(139,145,240,0.08); color: var(--accent); }
|
|
534
|
+
.r-tag.running { background: var(--green-dim); color: var(--green); }
|
|
535
|
+
.r-tag.not-running { background: var(--red-dim); color: var(--red); }
|
|
536
|
+
|
|
537
|
+
.input-group {
|
|
538
|
+
display: flex; width: 280px; flex: 0 0 280px; align-items: center; gap: 0;
|
|
539
|
+
}
|
|
540
|
+
.input-group .r-input {
|
|
541
|
+
flex: 1; width: auto;
|
|
542
|
+
border-top-right-radius: 0; border-bottom-right-radius: 0;
|
|
543
|
+
border-right: none;
|
|
544
|
+
}
|
|
545
|
+
.input-group .eye-btn {
|
|
546
|
+
height: 34px; width: 36px; flex-shrink: 0;
|
|
547
|
+
background: rgba(0,0,0,0.25); border: 1px solid var(--border);
|
|
548
|
+
border-left: none;
|
|
549
|
+
border-radius: 0 5px 5px 0;
|
|
550
|
+
color: var(--text-3); cursor: pointer;
|
|
551
|
+
display: flex; align-items: center; justify-content: center;
|
|
552
|
+
font-size: 14px; transition: color 0.15s;
|
|
553
|
+
}
|
|
554
|
+
.input-group .eye-btn:hover { color: var(--text-1); }
|
|
555
|
+
|
|
556
|
+
.r-link {
|
|
557
|
+
margin-left: auto; font-size: 11px; color: #bbb;
|
|
558
|
+
text-decoration: none; white-space: nowrap; flex-shrink: 0;
|
|
559
|
+
transition: color 0.15s; display: inline-flex; align-items: center; justify-content: center;
|
|
560
|
+
width: 64px;
|
|
561
|
+
}
|
|
562
|
+
.r-link:hover { color: #fff; }
|
|
563
|
+
|
|
564
|
+
#panel-agent-providers .r-tag,
|
|
565
|
+
#panel-agent-providers .r-link {
|
|
566
|
+
margin-right: -6px;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
.r-validation {
|
|
570
|
+
font-size: 11px; flex-shrink: 0;
|
|
571
|
+
white-space: nowrap; transition: opacity 0.2s;
|
|
572
|
+
}
|
|
573
|
+
.r-validation:empty { display: none; }
|
|
574
|
+
.r-validation:not(:empty) { margin-left: 8px; }
|
|
575
|
+
.r-validation.checking { color: var(--text-3); }
|
|
576
|
+
.r-validation.ok { color: var(--green); }
|
|
577
|
+
.r-validation.fail { color: var(--red); }
|
|
578
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
579
|
+
.r-validation .spinner {
|
|
580
|
+
display: inline-block; width: 12px; height: 12px;
|
|
581
|
+
border: 2px solid var(--text-4); border-top-color: var(--text-2);
|
|
582
|
+
border-radius: 50%; animation: spin 0.6s linear infinite;
|
|
583
|
+
vertical-align: middle; margin-right: 4px;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
.r-detail {
|
|
587
|
+
font-size: 11px; color: var(--text-3);
|
|
588
|
+
font-family: 'SF Mono', 'Cascadia Code', monospace;
|
|
589
|
+
margin-left: 8px; white-space: nowrap;
|
|
590
|
+
overflow: hidden; text-overflow: ellipsis;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
.radio-group { display: flex; gap: 4px; flex: 1; }
|
|
594
|
+
.radio-btn {
|
|
595
|
+
flex: 1; height: 30px; padding: 0 10px;
|
|
596
|
+
background: rgba(0,0,0,0.25); border: 1px solid var(--border);
|
|
597
|
+
border-radius: 5px; color: var(--text-3);
|
|
598
|
+
font-size: 12px; font-weight: 500; cursor: pointer;
|
|
599
|
+
display: flex; align-items: center; justify-content: center;
|
|
600
|
+
transition: all 0.15s;
|
|
601
|
+
}
|
|
602
|
+
.radio-btn:hover { color: var(--text-2); }
|
|
603
|
+
.radio-btn.on {
|
|
604
|
+
background: rgba(139,145,240,0.15);
|
|
605
|
+
border-color: var(--accent);
|
|
606
|
+
color: var(--text-1);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
.preset-card {
|
|
610
|
+
padding: 12px 14px;
|
|
611
|
+
border-bottom: 1px solid var(--border);
|
|
612
|
+
display: flex; align-items: center; gap: 10px;
|
|
613
|
+
min-height: 48px;
|
|
614
|
+
}
|
|
615
|
+
.preset-card:last-child { border-bottom: none; }
|
|
616
|
+
.preset-card:hover { background: rgba(255,255,255,0.015); }
|
|
617
|
+
.preset-card[draggable] { cursor: grab; }
|
|
618
|
+
.preset-card.dragging { opacity: 0.4; }
|
|
619
|
+
.preset-card.drag-over { border-top: 2px solid var(--accent); }
|
|
620
|
+
.preset-info { flex: 1; min-width: 0; }
|
|
621
|
+
.preset-id { font-size: 13px; font-weight: 600; color: var(--text-1); }
|
|
622
|
+
.preset-meta {
|
|
623
|
+
font-size: 11px; color: var(--text-3); margin-top: 2px;
|
|
624
|
+
font-family: 'SF Mono', 'Cascadia Code', monospace;
|
|
625
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
626
|
+
}
|
|
627
|
+
.preset-actions { display: flex; gap: 6px; flex-shrink: 0; }
|
|
628
|
+
.icon-btn {
|
|
629
|
+
height: 28px; padding: 0 10px; min-width: 64px;
|
|
630
|
+
background: rgba(0,0,0,0.25); border: 1px solid var(--border);
|
|
631
|
+
border-radius: 4px; color: var(--text-2);
|
|
632
|
+
font-size: 11px; font-weight: 500; cursor: pointer;
|
|
633
|
+
transition: all 0.15s;
|
|
634
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
635
|
+
}
|
|
636
|
+
.icon-btn:hover { color: var(--text-1); border-color: var(--border-focus); }
|
|
637
|
+
.icon-btn.edit-btn { border-color: rgba(139,145,240,0.3); color: var(--accent); }
|
|
638
|
+
.icon-btn.edit-btn:hover { background: rgba(139,145,240,0.1); border-color: var(--accent); color: var(--accent); }
|
|
639
|
+
.icon-btn.danger { border-color: rgba(229,83,75,0.3); color: var(--red); }
|
|
640
|
+
.icon-btn.danger:hover { background: var(--red-dim); border-color: var(--red); }
|
|
641
|
+
.icon-btn.builtin { color: var(--green); border-color: var(--green); cursor: default; opacity: 0.7; }
|
|
642
|
+
|
|
643
|
+
.form-row {
|
|
644
|
+
display: flex; align-items: center;
|
|
645
|
+
padding: 0 14px; min-height: 48px;
|
|
646
|
+
border-bottom: 1px solid var(--border);
|
|
647
|
+
}
|
|
648
|
+
.form-row:last-child { border-bottom: none; }
|
|
649
|
+
.form-row .r-name { width: 90px; }
|
|
650
|
+
.form-row .r-input { flex: 1; width: auto; }
|
|
651
|
+
.form-actions {
|
|
652
|
+
display: flex; gap: 8px; padding: 12px 14px;
|
|
653
|
+
border-top: 1px solid var(--border);
|
|
654
|
+
background: rgba(0,0,0,0.15);
|
|
655
|
+
}
|
|
656
|
+
.form-actions .btn {
|
|
657
|
+
flex: 1; height: 36px;
|
|
658
|
+
border: none; border-radius: 5px;
|
|
659
|
+
font-family: 'Inter', sans-serif;
|
|
660
|
+
font-size: 12px; font-weight: 600;
|
|
661
|
+
cursor: pointer; transition: all 0.15s;
|
|
662
|
+
}
|
|
663
|
+
.btn.primary { background: var(--accent); color: #fff; }
|
|
664
|
+
.btn.primary:hover { filter: brightness(1.1); }
|
|
665
|
+
.btn.secondary { background: rgba(255,255,255,0.05); color: var(--text-2); border: 1px solid var(--border); }
|
|
666
|
+
.btn.secondary:hover { color: var(--text-1); }
|
|
667
|
+
|
|
668
|
+
.advanced-toggle {
|
|
669
|
+
display: flex; align-items: center; gap: 8px;
|
|
670
|
+
padding: 0 14px; min-height: 40px;
|
|
671
|
+
border-bottom: 1px solid var(--border);
|
|
672
|
+
cursor: pointer; user-select: none;
|
|
673
|
+
font-size: 12px; color: var(--text-3);
|
|
674
|
+
transition: color 0.15s;
|
|
675
|
+
}
|
|
676
|
+
.advanced-toggle:hover { color: var(--text-2); }
|
|
677
|
+
.advanced-toggle .arrow { transition: transform 0.2s; display: inline-block; font-size: 10px; }
|
|
678
|
+
.advanced-toggle .arrow.open { transform: rotate(90deg); }
|
|
679
|
+
.advanced-content { display: none; }
|
|
680
|
+
.advanced-content.open { display: block; }
|
|
681
|
+
|
|
682
|
+
/* -- Search module styles -- */
|
|
683
|
+
.r .drag-handle {
|
|
684
|
+
width: 16px; margin-right: 6px; cursor: grab; color: #888;
|
|
685
|
+
font-size: 12px; text-align: center; flex-shrink: 0;
|
|
686
|
+
user-select: none;
|
|
687
|
+
}
|
|
688
|
+
.r .drag-handle:active { cursor: grabbing; }
|
|
689
|
+
|
|
690
|
+
.r-check {
|
|
691
|
+
width: 17px; height: 17px; border-radius: 4px;
|
|
692
|
+
border: 1.5px solid #666;
|
|
693
|
+
margin-right: 10px; cursor: pointer;
|
|
694
|
+
display: flex; align-items: center; justify-content: center;
|
|
695
|
+
transition: all 0.15s; flex-shrink: 0;
|
|
696
|
+
background: transparent;
|
|
697
|
+
}
|
|
698
|
+
.r-check.on { background: var(--accent); border-color: var(--accent); }
|
|
699
|
+
.r-check.on::after {
|
|
700
|
+
content: '';
|
|
701
|
+
width: 8px; height: 5px;
|
|
702
|
+
border-left: 1.5px solid #fff; border-bottom: 1.5px solid #fff;
|
|
703
|
+
transform: rotate(-45deg) translateY(-1px);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
.modes { display: flex; overflow: hidden; }
|
|
707
|
+
.mode {
|
|
708
|
+
flex: 1; padding: 11px 14px;
|
|
709
|
+
cursor: pointer; transition: background 0.15s;
|
|
710
|
+
border-right: 1px solid var(--border);
|
|
711
|
+
}
|
|
712
|
+
.mode:last-child { border-right: none; }
|
|
713
|
+
.mode:hover { background: rgba(255,255,255,0.015); }
|
|
714
|
+
.mode.on { background: rgba(139,145,240,0.12); }
|
|
715
|
+
.mode .m-title { font-size: 13px; font-weight: 600; color: #999; }
|
|
716
|
+
.mode.on .m-title { color: #fff; }
|
|
717
|
+
.mode .m-desc { font-size: 11px; color: #777; margin-top: 2px; }
|
|
718
|
+
.mode .m-badge {
|
|
719
|
+
display: inline-block; font-size: 10px; font-weight: 600;
|
|
720
|
+
color: var(--accent); margin-left: 4px;
|
|
721
|
+
}
|
|
722
|
+
.priority-hint {
|
|
723
|
+
font-size: 11px; color: var(--text-4);
|
|
724
|
+
padding: 5px 2px 0; margin-top: 0;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/* -- Memory module styles -- */
|
|
728
|
+
.toggle-row { display: flex; align-items: center; justify-content: space-between; }
|
|
729
|
+
.toggle { position: relative; width: 40px; height: 22px; flex-shrink: 0; }
|
|
730
|
+
.toggle input { opacity: 0; width: 0; height: 0; }
|
|
731
|
+
.toggle-track {
|
|
732
|
+
position: absolute; inset: 0; cursor: pointer;
|
|
733
|
+
background: rgba(255,255,255,0.12); border-radius: 11px; transition: background 0.2s;
|
|
734
|
+
}
|
|
735
|
+
.toggle-track::after {
|
|
736
|
+
content: ''; position: absolute; left: 3px; top: 3px;
|
|
737
|
+
width: 16px; height: 16px; border-radius: 50%;
|
|
738
|
+
background: var(--text-3); transition: all 0.2s;
|
|
739
|
+
}
|
|
740
|
+
.toggle input:checked + .toggle-track { background: var(--accent); }
|
|
741
|
+
.toggle input:checked + .toggle-track::after { transform: translateX(18px); background: #fff; }
|
|
742
|
+
.r-unit { font-size: 11px; color: var(--text-4); margin-left: 8px; flex-shrink: 0; min-width: 30px; text-align: right; }
|
|
743
|
+
.settings-body.disabled { opacity: 0.35; pointer-events: none; }
|
|
744
|
+
.readonly-info {
|
|
745
|
+
color: var(--text-3); font-size: 13px; padding: 8px 12px;
|
|
746
|
+
background: var(--bg-raised); border-radius: 6px; border: 1px solid var(--border);
|
|
747
|
+
}
|
|
748
|
+
.file-label { font-size: 13px; font-weight: 600; color: var(--text-1); margin-bottom: 6px; display: flex; align-items: center; }
|
|
749
|
+
.file-label .file-hint { font-weight: 400; color: var(--text-3); font-size: 11px; margin-left: 6px; }
|
|
750
|
+
.file-open { margin-left: auto; font-size: 11px; color: var(--accent); cursor: pointer; font-weight: 500; text-decoration: none; }
|
|
751
|
+
.file-open:hover { color: #fff; }
|
|
752
|
+
.file-area {
|
|
753
|
+
width: 100%; height: 200px; padding: 10px;
|
|
754
|
+
background: var(--bg-input); border: 1px solid var(--border);
|
|
755
|
+
border-radius: 5px; color: var(--text-1);
|
|
756
|
+
font-size: 12px; font-family: 'SF Mono', 'Cascadia Code', monospace;
|
|
757
|
+
line-height: 1.5; resize: vertical; transition: border-color 0.15s;
|
|
758
|
+
}
|
|
759
|
+
.file-area:focus { outline: none; border-color: var(--border-focus); }
|
|
760
|
+
.file-group { margin-bottom: 16px; }
|
|
761
|
+
.cm-item {
|
|
762
|
+
padding: 10px 14px;
|
|
763
|
+
border-bottom: 1px solid var(--border);
|
|
764
|
+
}
|
|
765
|
+
.cm-item:last-child { border-bottom: none; }
|
|
766
|
+
.cm-item.archived { opacity: 0.4; }
|
|
767
|
+
.cm-header {
|
|
768
|
+
display: flex; align-items: center; gap: 8px;
|
|
769
|
+
margin-bottom: 4px;
|
|
770
|
+
}
|
|
771
|
+
.cm-topic {
|
|
772
|
+
font-size: 14px; font-weight: 600; color: var(--text-1);
|
|
773
|
+
flex: 1; min-width: 0;
|
|
774
|
+
}
|
|
775
|
+
.cm-element {
|
|
776
|
+
font-size: 12px; color: var(--text-3);
|
|
777
|
+
line-height: 1.4; word-break: break-word;
|
|
778
|
+
}
|
|
779
|
+
.cm-actions { flex-shrink: 0; }
|
|
780
|
+
</style>
|
|
781
|
+
</head>
|
|
782
|
+
<body class="notranslate" translate="no">
|
|
783
|
+
<div id="boot-spinner"><span class="dot"></span><span class="dot"></span><span class="dot"></span></div>
|
|
784
|
+
<div class="layout">
|
|
785
|
+
|
|
786
|
+
<!-- Category sidebar -->
|
|
787
|
+
<div class="cat-bar">
|
|
788
|
+
<div class="cat-label">General</div>
|
|
789
|
+
<div class="cat-item active" data-panel="mem-files" onclick="switchCat(this)">Profile</div>
|
|
790
|
+
<div class="cat-item" data-panel="workflow-custom" onclick="switchCat(this)">Custom Workflow</div>
|
|
791
|
+
<div class="cat-item" data-panel="general-modules" onclick="switchCat(this)">Default</div>
|
|
792
|
+
<div class="cat-sep"></div>
|
|
793
|
+
<div class="cat-label">Channels</div>
|
|
794
|
+
<div class="cat-item" data-panel="connection" onclick="switchCat(this)">Discord</div>
|
|
795
|
+
<div class="cat-item" data-panel="schedules" onclick="switchCat(this)">Schedules</div>
|
|
796
|
+
<div class="cat-item" data-panel="webhooks" onclick="switchCat(this)">Webhooks</div>
|
|
797
|
+
<div class="cat-sep"></div>
|
|
798
|
+
<div class="cat-label">Agent</div>
|
|
799
|
+
<div class="cat-item" data-panel="agent-providers" onclick="switchCat(this)">Providers</div>
|
|
800
|
+
<div class="cat-item" data-panel="agent-presets" onclick="switchCat(this)">Presets</div>
|
|
801
|
+
<div class="cat-item" data-panel="agent-maintenance" onclick="switchCat(this)">Maintenance</div>
|
|
802
|
+
<div class="cat-sep"></div>
|
|
803
|
+
<div class="cat-label">Memory</div>
|
|
804
|
+
<div class="cat-item" data-panel="mem-settings" onclick="switchCat(this)">Settings</div>
|
|
805
|
+
<div class="cat-sep"></div>
|
|
806
|
+
<div class="cat-label">Search</div>
|
|
807
|
+
<div class="cat-item" data-panel="search-apikeys" onclick="switchCat(this)">Search Provider</div>
|
|
808
|
+
</div>
|
|
809
|
+
|
|
810
|
+
<!-- Main -->
|
|
811
|
+
<div class="main">
|
|
812
|
+
|
|
813
|
+
<!-- ============ CHANNELS MODULE ============ -->
|
|
814
|
+
|
|
815
|
+
<!-- DISCORD (merged: Connection + Channels + Access + DND + Voice) -->
|
|
816
|
+
<div class="panel" id="panel-connection">
|
|
817
|
+
<div class="hdr"><h1>Discord</h1></div>
|
|
818
|
+
|
|
819
|
+
<div class="sec">
|
|
820
|
+
<div class="sec-title">Discord</div>
|
|
821
|
+
<div class="sec-body">
|
|
822
|
+
<div class="r">
|
|
823
|
+
<span class="r-name-wide">Bot token <span class="tip">?<span class="tip-text">Bot token from the Discord Developer Portal</span></span></span>
|
|
824
|
+
<div class="pw-wrap">
|
|
825
|
+
<input class="r-input" id="ch-discord-token" type="password" placeholder="Bot token" autocomplete="new-password">
|
|
826
|
+
<span class="pw-toggle" onclick="togglePw(this)">👁</span>
|
|
827
|
+
</div>
|
|
828
|
+
</div>
|
|
829
|
+
<div class="r">
|
|
830
|
+
<span class="r-name-wide">Application ID <span class="tip">?<span class="tip-text">Application ID from the Discord Developer Portal</span></span></span>
|
|
831
|
+
<input class="r-input" id="ch-discord-appid" type="text" placeholder="Application ID" autocomplete="off">
|
|
832
|
+
</div>
|
|
833
|
+
</div>
|
|
834
|
+
</div>
|
|
835
|
+
|
|
836
|
+
<div class="sec">
|
|
837
|
+
<div class="sec-title">Channels <span class="tip" style="vertical-align:middle;">?<span class="tip-text">Add Discord channels for the bot to operate in. Select the radio button to set the main channel</span></span></div>
|
|
838
|
+
<div class="sec-body">
|
|
839
|
+
<div id="ch-channel-list" class="dyn-list"></div>
|
|
840
|
+
<div style="padding: 0 16px 10px;">
|
|
841
|
+
<span class="dyn-add" onclick="addChannel()">+ Add channel</span>
|
|
842
|
+
</div>
|
|
843
|
+
</div>
|
|
844
|
+
</div>
|
|
845
|
+
|
|
846
|
+
<div class="sec">
|
|
847
|
+
<div class="sec-title">Access — DM & mentions</div>
|
|
848
|
+
<div class="sec-body">
|
|
849
|
+
<div class="r">
|
|
850
|
+
<span class="r-name-wide">DM policy <span class="tip">?<span class="tip-text">allowlist: only listed users / disabled: block all DMs</span></span></span>
|
|
851
|
+
<select class="r-select" id="ch-dm-policy">
|
|
852
|
+
<option value="allowlist">allowlist</option>
|
|
853
|
+
<option value="disabled">disabled</option>
|
|
854
|
+
</select>
|
|
855
|
+
</div>
|
|
856
|
+
<div class="r" style="min-height:auto;padding:10px 14px;">
|
|
857
|
+
<span class="r-name-wide">Allow from <span class="tip">?<span class="tip-text">Leave empty to allow all users. Add user IDs to restrict responses</span></span></span>
|
|
858
|
+
<div class="tag-container" id="ch-allowfrom"></div>
|
|
859
|
+
</div>
|
|
860
|
+
<div class="r" style="min-height:auto;padding:10px 14px;">
|
|
861
|
+
<span class="r-name-wide">Mention patterns <span class="tip">?<span class="tip-text">Additional keywords the bot responds to (e.g. mixdog, hey bot)</span></span></span>
|
|
862
|
+
<div class="tag-container" id="ch-mentionpatterns"></div>
|
|
863
|
+
</div>
|
|
864
|
+
<div class="r">
|
|
865
|
+
<span class="r-name-wide">Ack reaction <span class="tip">?<span class="tip-text">React with an emoji to acknowledge received messages (default emoji when toggled on)</span></span></span>
|
|
866
|
+
<div class="r-toggle" id="ch-ackreaction" onclick="this.classList.toggle('on')"></div>
|
|
867
|
+
</div>
|
|
868
|
+
</div>
|
|
869
|
+
</div>
|
|
870
|
+
|
|
871
|
+
<div class="sec">
|
|
872
|
+
<div class="sec-title">Do Not Disturb — quiet hours</div>
|
|
873
|
+
<div class="sec-body">
|
|
874
|
+
<div class="r">
|
|
875
|
+
<span class="r-name-wide">Schedule <span class="tip">?<span class="tip-text">Time window during which opted-in systems stay quiet</span></span></span>
|
|
876
|
+
<select class="r-select" id="dnd-quiet-preset" style="width:160px;flex:none;" onchange="onQuietPresetChange()">
|
|
877
|
+
<option value="none">None</option>
|
|
878
|
+
<option value="23:00-09:00">23:00 - 09:00</option>
|
|
879
|
+
<option value="23:00-07:00">23:00 - 07:00</option>
|
|
880
|
+
<option value="22:00-08:00">22:00 - 08:00</option>
|
|
881
|
+
<option value="00:00-09:00">00:00 - 09:00</option>
|
|
882
|
+
<option value="custom">Custom</option>
|
|
883
|
+
</select>
|
|
884
|
+
<div class="time-row hidden" id="dnd-quiet-custom">
|
|
885
|
+
<input class="r-input" id="dnd-quiet-start" type="text" placeholder="HH:MM">
|
|
886
|
+
<span class="time-sep">-</span>
|
|
887
|
+
<input class="r-input" id="dnd-quiet-end" type="text" placeholder="HH:MM">
|
|
888
|
+
</div>
|
|
889
|
+
</div>
|
|
890
|
+
<div class="r">
|
|
891
|
+
<span class="r-name-wide">Holidays <span class="tip">?<span class="tip-text">Skip during public holidays as well</span></span></span>
|
|
892
|
+
<div class="r-toggle" id="dnd-holidays" onclick="this.classList.toggle('on')"></div>
|
|
893
|
+
</div>
|
|
894
|
+
</div>
|
|
895
|
+
</div>
|
|
896
|
+
|
|
897
|
+
<div class="sec">
|
|
898
|
+
<div class="sec-title">Do Not Disturb — apply to <span class="tip">?<span class="tip-text">Which systems respect the quiet window above</span></span></div>
|
|
899
|
+
<div class="sec-body">
|
|
900
|
+
<div class="r">
|
|
901
|
+
<span class="r-name-wide">Webhook <span class="tip">?<span class="tip-text">If on, incoming webhook events during quiet hours are dropped (no dispatch, no queue)</span></span></span>
|
|
902
|
+
<div class="r-toggle" id="dnd-respect-webhook" onclick="this.classList.toggle('on')"></div>
|
|
903
|
+
</div>
|
|
904
|
+
<div class="r">
|
|
905
|
+
<span class="r-name-wide">Schedule <span class="tip">?<span class="tip-text">If on, scheduled tasks during quiet hours are skipped</span></span></span>
|
|
906
|
+
<div class="r-toggle on" id="dnd-respect-schedule" onclick="this.classList.toggle('on')"></div>
|
|
907
|
+
</div>
|
|
908
|
+
</div>
|
|
909
|
+
</div>
|
|
910
|
+
|
|
911
|
+
<div class="sec">
|
|
912
|
+
<div class="sec-title">Voice</div>
|
|
913
|
+
<div class="sec-body" id="voice-section"></div>
|
|
914
|
+
</div>
|
|
915
|
+
|
|
916
|
+
<div class="save-area">
|
|
917
|
+
<div class="warn-msg" id="warn-connection"></div>
|
|
918
|
+
<button class="save" onclick="savePanel()">Save</button>
|
|
919
|
+
</div>
|
|
920
|
+
</div>
|
|
921
|
+
|
|
922
|
+
<!-- SCHEDULES -->
|
|
923
|
+
<div class="panel" id="panel-schedules">
|
|
924
|
+
<div class="hdr">
|
|
925
|
+
<h1>Schedules</h1>
|
|
926
|
+
<button class="hdr-btn" onclick="addScheduleCard()">+ New Schedule</button>
|
|
927
|
+
</div>
|
|
928
|
+
<div class="sec">
|
|
929
|
+
<div class="sec-title">Scheduled Tasks</div>
|
|
930
|
+
<div class="sec-body">
|
|
931
|
+
<div id="sched-tasks" class="card-list"></div>
|
|
932
|
+
<div class="empty" id="sched-empty">No schedules yet. Click <strong>+ New Schedule</strong> to create one.</div>
|
|
933
|
+
</div>
|
|
934
|
+
</div>
|
|
935
|
+
<div class="save-area">
|
|
936
|
+
<div class="warn-msg" id="warn-schedules"></div>
|
|
937
|
+
<button class="save" onclick="savePanel()">Save</button>
|
|
938
|
+
</div>
|
|
939
|
+
</div>
|
|
940
|
+
|
|
941
|
+
<!-- WEBHOOKS -->
|
|
942
|
+
<div class="panel" id="panel-webhooks">
|
|
943
|
+
<div class="hdr">
|
|
944
|
+
<h1>Webhooks</h1>
|
|
945
|
+
<button class="hdr-btn" onclick="addEventCard()">+ New Webhook</button>
|
|
946
|
+
</div>
|
|
947
|
+
<div class="sec">
|
|
948
|
+
<div class="sec-title">Webhook Endpoints</div>
|
|
949
|
+
<div class="sec-body">
|
|
950
|
+
<div id="event-rules" class="card-list"></div>
|
|
951
|
+
<div class="empty" id="webhook-empty">No webhooks yet. Click <strong>+ New Webhook</strong> to create one.</div>
|
|
952
|
+
</div>
|
|
953
|
+
</div>
|
|
954
|
+
<div class="sec">
|
|
955
|
+
<div class="sec-title">Webhook</div>
|
|
956
|
+
<div class="sec-body" id="webhook-section"></div>
|
|
957
|
+
<div class="sec-desc" style="padding:6px 14px 10px">ngrok tunnel for inbound events. <strong>Custom Domain requires an Auth Token</strong> from your ngrok dashboard — without it the named domain will not bind and incoming events are dropped.</div>
|
|
958
|
+
</div>
|
|
959
|
+
<div class="save-area">
|
|
960
|
+
<div class="warn-msg" id="warn-webhooks"></div>
|
|
961
|
+
<button class="save" onclick="savePanel()">Save</button>
|
|
962
|
+
</div>
|
|
963
|
+
</div>
|
|
964
|
+
|
|
965
|
+
|
|
966
|
+
<!-- ============ AGENT MODULE ============ -->
|
|
967
|
+
|
|
968
|
+
<!-- AGENT PROVIDERS -->
|
|
969
|
+
<div class="panel" id="panel-agent-providers">
|
|
970
|
+
<div class="hdr"><h1>Providers</h1></div>
|
|
971
|
+
|
|
972
|
+
<div class="sec">
|
|
973
|
+
<div class="sec-title">API Key</div>
|
|
974
|
+
<div class="sec-body" id="ag-api-sec"></div>
|
|
975
|
+
</div>
|
|
976
|
+
|
|
977
|
+
<div class="sec">
|
|
978
|
+
<div class="sec-title">OAuth</div>
|
|
979
|
+
<div class="sec-body" id="ag-oauth-sec"></div>
|
|
980
|
+
</div>
|
|
981
|
+
|
|
982
|
+
<div class="sec">
|
|
983
|
+
<div class="sec-title">Local</div>
|
|
984
|
+
<div class="sec-body" id="ag-local-sec"></div>
|
|
985
|
+
</div>
|
|
986
|
+
|
|
987
|
+
<div class="save-area">
|
|
988
|
+
<div class="warn-msg" id="warn-agent-providers"></div>
|
|
989
|
+
<button class="save" onclick="agSaveProviders()">Save</button>
|
|
990
|
+
</div>
|
|
991
|
+
</div>
|
|
992
|
+
|
|
993
|
+
<!-- AGENT PRESETS -->
|
|
994
|
+
<div class="panel" id="panel-agent-presets">
|
|
995
|
+
<div class="hdr">
|
|
996
|
+
<h1>Presets</h1>
|
|
997
|
+
<button class="hdr-btn" onclick="agOpenPresetForm(null)">+ New Preset</button>
|
|
998
|
+
</div>
|
|
999
|
+
|
|
1000
|
+
<div class="sec">
|
|
1001
|
+
<div class="sec-title">Saved Presets</div>
|
|
1002
|
+
<div class="sec-body" id="ag-preset-list">
|
|
1003
|
+
<div class="empty">Loading...</div>
|
|
1004
|
+
</div>
|
|
1005
|
+
</div>
|
|
1006
|
+
|
|
1007
|
+
<div class="sec" id="ag-preset-form-sec" style="display:none">
|
|
1008
|
+
<div class="sec-title" id="ag-preset-form-title">New Preset</div>
|
|
1009
|
+
<div class="sec-body">
|
|
1010
|
+
<div class="form-row">
|
|
1011
|
+
<span class="r-name">Name</span>
|
|
1012
|
+
<input class="r-input" id="ag-pf-name" placeholder="my-preset" maxlength="40">
|
|
1013
|
+
</div>
|
|
1014
|
+
<div class="form-row">
|
|
1015
|
+
<span class="r-name">Provider</span>
|
|
1016
|
+
<select class="r-select" id="ag-pf-provider" onchange="agOnProviderChange()"></select>
|
|
1017
|
+
</div>
|
|
1018
|
+
<div class="form-row">
|
|
1019
|
+
<span class="r-name">Model</span>
|
|
1020
|
+
<select class="r-select" id="ag-pf-model" disabled><option value="">Select a provider first</option></select>
|
|
1021
|
+
</div>
|
|
1022
|
+
<div class="form-row" id="ag-pf-effort-row">
|
|
1023
|
+
<span class="r-name">Effort</span>
|
|
1024
|
+
<select class="r-select" id="ag-pf-effort"></select>
|
|
1025
|
+
</div>
|
|
1026
|
+
<div class="form-row" id="ag-pf-fast-row" style="display:none">
|
|
1027
|
+
<span class="r-name">Fast Mode</span>
|
|
1028
|
+
<div class="r-toggle" id="ag-pf-fast" onclick="this.classList.toggle('on')"></div>
|
|
1029
|
+
<span style="flex:1;font-size:11px;color:var(--text-4);margin-left:10px">Speed optimized output (premium)</span>
|
|
1030
|
+
</div>
|
|
1031
|
+
<div class="advanced-toggle" onclick="toggleAdvanced(this)">
|
|
1032
|
+
<span class="arrow">▶</span> Advanced Options
|
|
1033
|
+
</div>
|
|
1034
|
+
<div class="advanced-content" id="ag-pf-access-section">
|
|
1035
|
+
<div class="form-row">
|
|
1036
|
+
<span class="r-name">Access</span>
|
|
1037
|
+
<div class="radio-group" id="ag-pf-tools">
|
|
1038
|
+
<div class="radio-btn on" data-v="full" onclick="agSelectTools(this)">Read & Write</div>
|
|
1039
|
+
<div class="radio-btn" data-v="readonly" onclick="agSelectTools(this)">Read Only</div>
|
|
1040
|
+
<div class="radio-btn" data-v="mcp" onclick="agSelectTools(this)">None</div>
|
|
1041
|
+
</div>
|
|
1042
|
+
</div>
|
|
1043
|
+
</div>
|
|
1044
|
+
<div class="form-actions">
|
|
1045
|
+
<button class="btn secondary" onclick="agClosePresetForm()">Cancel</button>
|
|
1046
|
+
<button class="btn primary" onclick="agSavePreset()">Save Preset</button>
|
|
1047
|
+
</div>
|
|
1048
|
+
</div>
|
|
1049
|
+
</div>
|
|
1050
|
+
|
|
1051
|
+
<div class="warn-msg" id="warn-agent-presets"></div>
|
|
1052
|
+
</div>
|
|
1053
|
+
|
|
1054
|
+
<!-- AGENT MAINTENANCE -->
|
|
1055
|
+
<div class="panel" id="panel-agent-maintenance">
|
|
1056
|
+
<div class="hdr"><h1>Maintenance Presets</h1></div>
|
|
1057
|
+
<div class="sec">
|
|
1058
|
+
<div class="sec-title">Task Presets</div>
|
|
1059
|
+
<div class="sec-desc">Assign which model preset handles each background maintenance task.</div>
|
|
1060
|
+
<div class="sec-body" id="ag-maint-tasks"></div>
|
|
1061
|
+
</div>
|
|
1062
|
+
<div class="save-area">
|
|
1063
|
+
<div class="warn-msg" id="warn-ag-maint"></div>
|
|
1064
|
+
<button class="save" onclick="saveAgMaintenance()">Save</button>
|
|
1065
|
+
</div>
|
|
1066
|
+
</div>
|
|
1067
|
+
|
|
1068
|
+
<!-- ============ MEMORY MODULE ============ -->
|
|
1069
|
+
|
|
1070
|
+
<!-- MEMORY SETTINGS -->
|
|
1071
|
+
<div class="panel" id="panel-mem-settings">
|
|
1072
|
+
<div class="hdr"><h1>Settings</h1></div>
|
|
1073
|
+
<div id="mem-settings-body" class="settings-body">
|
|
1074
|
+
<div class="sec">
|
|
1075
|
+
<div class="sec-title">Extraction</div>
|
|
1076
|
+
<div class="sec-body">
|
|
1077
|
+
<div class="r">
|
|
1078
|
+
<span class="r-name-wide">Interval</span>
|
|
1079
|
+
<select class="r-select" id="mem-set-retention">
|
|
1080
|
+
<option value="1m">1 min</option>
|
|
1081
|
+
<option value="5m">5 min</option>
|
|
1082
|
+
<option value="10m" selected>10 min</option>
|
|
1083
|
+
<option value="30m">30 min</option>
|
|
1084
|
+
<option value="1h">1 hour</option>
|
|
1085
|
+
</select>
|
|
1086
|
+
</div>
|
|
1087
|
+
</div>
|
|
1088
|
+
</div>
|
|
1089
|
+
<div class="sec">
|
|
1090
|
+
<div class="sec-title">Consolidation</div>
|
|
1091
|
+
<div class="sec-body">
|
|
1092
|
+
<div class="r">
|
|
1093
|
+
<span class="r-name-wide">Interval</span>
|
|
1094
|
+
<input class="r-input" id="mem-set-interval" type="text" placeholder="1h">
|
|
1095
|
+
</div>
|
|
1096
|
+
</div>
|
|
1097
|
+
</div>
|
|
1098
|
+
<div class="sec">
|
|
1099
|
+
<div class="sec-title">Backfill</div>
|
|
1100
|
+
<div class="sec-body">
|
|
1101
|
+
<div class="r">
|
|
1102
|
+
<span class="r-name-wide">Window</span>
|
|
1103
|
+
<select class="r-select" id="mem-set-backfill-window" style="flex:1">
|
|
1104
|
+
<option value="1d">1 day</option>
|
|
1105
|
+
<option value="3d">3 days</option>
|
|
1106
|
+
<option value="7d" selected>7 days</option>
|
|
1107
|
+
<option value="30d">30 days</option>
|
|
1108
|
+
<option value="all">Unlimited</option>
|
|
1109
|
+
</select>
|
|
1110
|
+
<button class="save" id="mem-btn-backfill" onclick="memRunBackfill()" style="width:auto;height:34px;padding:0 16px;margin-left:8px;font-size:12px">Run</button>
|
|
1111
|
+
</div>
|
|
1112
|
+
</div>
|
|
1113
|
+
</div>
|
|
1114
|
+
<div class="sec">
|
|
1115
|
+
<div class="sec-title" style="color:#e5534b">Danger zone</div>
|
|
1116
|
+
<div class="sec-body">
|
|
1117
|
+
<div class="r" style="flex-direction:column;align-items:stretch;gap:8px">
|
|
1118
|
+
<div style="font-size:12px;color:var(--text-3);line-height:1.5">Delete generated memory entries (active + archived + pending). User Core Memory is preserved. Type <code>DELETE ALL MEMORY</code> below to enable the button.</div>
|
|
1119
|
+
<div style="display:flex;gap:8px;align-items:center">
|
|
1120
|
+
<input class="r-input" id="mem-del-confirm" type="text" placeholder="DELETE ALL MEMORY" style="flex:1" oninput="memDeleteConfirmCheck()">
|
|
1121
|
+
<button class="save" id="mem-btn-delete" onclick="memDeleteAll()" disabled style="width:auto;height:34px;padding:0 16px;font-size:12px;background:#e5534b;opacity:0.4;cursor:not-allowed">Delete all</button>
|
|
1122
|
+
</div>
|
|
1123
|
+
<div class="warn-msg" id="warn-mem-delete"></div>
|
|
1124
|
+
</div>
|
|
1125
|
+
</div>
|
|
1126
|
+
</div>
|
|
1127
|
+
</div>
|
|
1128
|
+
<div class="save-area">
|
|
1129
|
+
<div class="warn-msg" id="warn-mem-settings"></div>
|
|
1130
|
+
<button class="save" onclick="memSaveConfig()">Save</button>
|
|
1131
|
+
</div>
|
|
1132
|
+
</div>
|
|
1133
|
+
|
|
1134
|
+
<!-- MEMORY FILES (sidebar = General > Profile) -->
|
|
1135
|
+
<div class="panel active" id="panel-mem-files">
|
|
1136
|
+
<div class="hdr"><h1>Profile</h1></div>
|
|
1137
|
+
|
|
1138
|
+
<div class="sec">
|
|
1139
|
+
<div class="sec-title">User</div>
|
|
1140
|
+
<div class="sec-body">
|
|
1141
|
+
<div class="r">
|
|
1142
|
+
<span class="r-name-wide">Title</span>
|
|
1143
|
+
<input class="r-input" id="mem-set-user-title" type="text" placeholder="Boss">
|
|
1144
|
+
</div>
|
|
1145
|
+
</div>
|
|
1146
|
+
</div>
|
|
1147
|
+
<div class="file-group">
|
|
1148
|
+
<div class="file-label">bot.md<span class="file-hint">Bot persona and behavior</span><span class="file-open" onclick="memOpenFile('bot.md')">Open</span></div>
|
|
1149
|
+
<textarea class="file-area" id="mem-file-bot"></textarea>
|
|
1150
|
+
</div>
|
|
1151
|
+
<div class="file-group">
|
|
1152
|
+
<div class="file-label">user.md<span class="file-hint">User information</span><span class="file-open" onclick="memOpenFile('user.md')">Open</span></div>
|
|
1153
|
+
<textarea class="file-area" id="mem-file-user-profile"></textarea>
|
|
1154
|
+
</div>
|
|
1155
|
+
<div class="sec">
|
|
1156
|
+
<div class="sec-title" style="display:flex;align-items:center;justify-content:space-between">
|
|
1157
|
+
<span>User Core Memory <span style="font-weight:400;color:var(--text-4);font-size:11px">(curated startup context)</span></span>
|
|
1158
|
+
<button class="hdr-btn" style="font-size:11px;padding:3px 10px" onclick="memAddCoreMemory()">+ Add</button>
|
|
1159
|
+
</div>
|
|
1160
|
+
<div class="sec-body" id="mem-core-memory-list">
|
|
1161
|
+
<div class="empty">Loading...</div>
|
|
1162
|
+
</div>
|
|
1163
|
+
</div>
|
|
1164
|
+
<div class="save-area">
|
|
1165
|
+
<div class="warn-msg" id="warn-mem-files"></div>
|
|
1166
|
+
<button class="save" onclick="memSaveFiles()">Save</button>
|
|
1167
|
+
</div>
|
|
1168
|
+
</div>
|
|
1169
|
+
|
|
1170
|
+
<!-- ============ SEARCH MODULE ============ -->
|
|
1171
|
+
|
|
1172
|
+
<!-- SEARCH -->
|
|
1173
|
+
<div class="panel" id="panel-search-apikeys">
|
|
1174
|
+
<div class="hdr">
|
|
1175
|
+
<h1>Search</h1>
|
|
1176
|
+
</div>
|
|
1177
|
+
|
|
1178
|
+
<div class="sec">
|
|
1179
|
+
<div class="sec-title">Active provider</div>
|
|
1180
|
+
<div class="sec-body">
|
|
1181
|
+
<div class="r">
|
|
1182
|
+
<span class="r-name" style="width:90px">Provider</span>
|
|
1183
|
+
<select class="r-select" id="sr-provider" style="flex:1;width:auto"></select>
|
|
1184
|
+
</div>
|
|
1185
|
+
</div>
|
|
1186
|
+
<div class="sec-desc" id="sr-provider-hint" style="padding:6px 14px 10px"></div>
|
|
1187
|
+
</div>
|
|
1188
|
+
|
|
1189
|
+
<div class="sec" id="sr-model-presets-sec" style="display:none">
|
|
1190
|
+
<div class="sec-title">Model presets</div>
|
|
1191
|
+
<div class="sec-body" id="sr-model-presets-body"></div>
|
|
1192
|
+
<div class="sec-desc" style="padding:6px 14px 10px">Models used when search dispatches to each provider family. Only families with resolved credentials appear. Anthropic is fixed to claude-haiku-4-5.</div>
|
|
1193
|
+
</div>
|
|
1194
|
+
|
|
1195
|
+
<div class="sec">
|
|
1196
|
+
<div class="sec-title">API Keys</div>
|
|
1197
|
+
<div class="sec-body" id="sr-search-sec"></div>
|
|
1198
|
+
<div class="sec-desc" style="padding:6px 14px 10px">Other backends (anthropic-oauth / openai-oauth / openai-api / xai-api) reuse credentials registered in the Agent panel.</div>
|
|
1199
|
+
</div>
|
|
1200
|
+
|
|
1201
|
+
<div class="save-area">
|
|
1202
|
+
<div class="warn-msg" id="warn-search-apikeys"></div>
|
|
1203
|
+
<button class="save" onclick="srSavePanel()">Save</button>
|
|
1204
|
+
</div>
|
|
1205
|
+
</div>
|
|
1206
|
+
|
|
1207
|
+
<!-- ============ GENERAL — DEFAULT (Modules + Injection + Security merged) ============ -->
|
|
1208
|
+
<div class="panel" id="panel-general-modules">
|
|
1209
|
+
<div class="hdr"><h1>Default</h1></div>
|
|
1210
|
+
|
|
1211
|
+
<div class="sec">
|
|
1212
|
+
<div class="sec-title">Modules</div>
|
|
1213
|
+
<div class="sec-body">
|
|
1214
|
+
<div class="r toggle-row">
|
|
1215
|
+
<span class="r-name-wide">Channels</span>
|
|
1216
|
+
<label class="toggle">
|
|
1217
|
+
<input type="checkbox" id="mod-channels-enabled" checked onchange="saveModulesConfig()">
|
|
1218
|
+
<span class="toggle-track"></span>
|
|
1219
|
+
</label>
|
|
1220
|
+
</div>
|
|
1221
|
+
<div class="sec-desc" style="padding:0 14px 10px">Discord I/O, scheduler, webhook.</div>
|
|
1222
|
+
|
|
1223
|
+
<div class="r toggle-row">
|
|
1224
|
+
<span class="r-name-wide">Memory</span>
|
|
1225
|
+
<label class="toggle">
|
|
1226
|
+
<input type="checkbox" id="mod-memory-enabled" checked onchange="saveModulesConfig()">
|
|
1227
|
+
<span class="toggle-track"></span>
|
|
1228
|
+
</label>
|
|
1229
|
+
</div>
|
|
1230
|
+
<div class="sec-desc" style="padding:0 14px 10px">Persistent memory DB + core-memory context injection. (Required for channels' core-memory inject and agent <code>recall</code> / <code>memory</code>.)</div>
|
|
1231
|
+
|
|
1232
|
+
<div class="r toggle-row">
|
|
1233
|
+
<span class="r-name-wide">Search</span>
|
|
1234
|
+
<label class="toggle">
|
|
1235
|
+
<input type="checkbox" id="mod-search-enabled" checked onchange="saveModulesConfig()">
|
|
1236
|
+
<span class="toggle-track"></span>
|
|
1237
|
+
</label>
|
|
1238
|
+
</div>
|
|
1239
|
+
<div class="sec-desc" style="padding:0 14px 10px">Web search + URL scrape + GitHub. (Required for agent <code>search</code>.)</div>
|
|
1240
|
+
|
|
1241
|
+
<div class="r toggle-row">
|
|
1242
|
+
<span class="r-name-wide">Agent</span>
|
|
1243
|
+
<label class="toggle">
|
|
1244
|
+
<input type="checkbox" id="mod-agent-enabled" checked onchange="saveModulesConfig()">
|
|
1245
|
+
<span class="toggle-track"></span>
|
|
1246
|
+
</label>
|
|
1247
|
+
</div>
|
|
1248
|
+
<div class="sec-desc" style="padding:0 14px 10px">External model orchestration (<code>bridge</code>, sessions).</div>
|
|
1249
|
+
|
|
1250
|
+
</div>
|
|
1251
|
+
</div>
|
|
1252
|
+
|
|
1253
|
+
<div class="sec">
|
|
1254
|
+
<div class="sec-title">Injection Mode</div>
|
|
1255
|
+
<div class="sec-body">
|
|
1256
|
+
<div class="r r-opt">
|
|
1257
|
+
<label class="opt" onclick="genSelectMode('hook')">
|
|
1258
|
+
<div class="r-radio" id="gen-radio-hook" style="margin-top:2px"></div>
|
|
1259
|
+
<div class="opt-body">
|
|
1260
|
+
<div class="opt-title">Hook (immediate)</div>
|
|
1261
|
+
<div class="opt-sub">Injects via SessionStart hook. Applies immediately, no restart required, and does not write to CLAUDE.md.</div>
|
|
1262
|
+
</div>
|
|
1263
|
+
</label>
|
|
1264
|
+
</div>
|
|
1265
|
+
<div class="r r-opt">
|
|
1266
|
+
<label class="opt" onclick="genSelectMode('claude_md')">
|
|
1267
|
+
<div class="r-radio" id="gen-radio-claude-md" style="margin-top:2px"></div>
|
|
1268
|
+
<div class="opt-body">
|
|
1269
|
+
<div class="opt-title">CLAUDE.md (default, strong enforcement, next session)</div>
|
|
1270
|
+
<div class="opt-sub">Writes a marker-delimited managed block into <code id="gen-target-path">~/.claude/CLAUDE.md</code>. Strongest enforcement; enable only if you want a persistent global rule block.</div>
|
|
1271
|
+
</div>
|
|
1272
|
+
</label>
|
|
1273
|
+
</div>
|
|
1274
|
+
<div class="note-box">
|
|
1275
|
+
<span class="note-icon">!</span>
|
|
1276
|
+
<span>After enabling CLAUDE.md mode, restart Claude Code once so the first session picks up the managed block.</span>
|
|
1277
|
+
</div>
|
|
1278
|
+
</div>
|
|
1279
|
+
</div>
|
|
1280
|
+
|
|
1281
|
+
<div class="sec">
|
|
1282
|
+
<div class="sec-title">Security</div>
|
|
1283
|
+
<div class="sec-body">
|
|
1284
|
+
<div class="r toggle-row">
|
|
1285
|
+
<span class="r-name-wide">Allow HOME-wide write access</span>
|
|
1286
|
+
<label class="toggle">
|
|
1287
|
+
<input type="checkbox" id="sec-home-access" onchange="saveSecurityConfig()">
|
|
1288
|
+
<span class="toggle-track"></span>
|
|
1289
|
+
</label>
|
|
1290
|
+
</div>
|
|
1291
|
+
<div class="sec-desc" style="padding:0 14px 12px">When OFF (default), file tools are limited to the working directory. When ON, tools may edit anywhere under your home folder. This only controls the in-process path gate; sub-agent Edit/Write to HOME paths always go through Discord approval regardless.</div>
|
|
1292
|
+
</div>
|
|
1293
|
+
</div>
|
|
1294
|
+
|
|
1295
|
+
<div class="save-area">
|
|
1296
|
+
<div class="warn-msg" id="warn-general-modules"></div>
|
|
1297
|
+
<button class="save" onclick="genSavePanel()">Save</button>
|
|
1298
|
+
</div>
|
|
1299
|
+
</div>
|
|
1300
|
+
|
|
1301
|
+
|
|
1302
|
+
<!-- CUSTOM WORKFLOW -->
|
|
1303
|
+
<div class="panel" id="panel-workflow-custom">
|
|
1304
|
+
<div class="hdr"><h1>Custom Workflow</h1></div>
|
|
1305
|
+
<div class="sec">
|
|
1306
|
+
<div class="sec-desc">Define your own role→preset mapping and a free-form description. Injected into every session after the system workflow rules.</div>
|
|
1307
|
+
<div class="sec-body">
|
|
1308
|
+
<div style="padding:12px 14px;border-top:1px solid var(--border)">
|
|
1309
|
+
<div style="display:flex;align-items:center;margin-bottom:6px">
|
|
1310
|
+
<span style="font-size:11px;color:var(--text-3)">Description</span>
|
|
1311
|
+
<span class="file-open" onclick="wfOpenDescription()">Open</span>
|
|
1312
|
+
</div>
|
|
1313
|
+
<textarea id="wf-description" class="r-input" placeholder="Free-form workflow description (markdown)." style="width:100%;height:150px;resize:vertical;font-family:inherit;padding:10px;line-height:1.5"></textarea>
|
|
1314
|
+
</div>
|
|
1315
|
+
<div class="r r-opt">
|
|
1316
|
+
<div class="opt-body" style="width:100%">
|
|
1317
|
+
<div class="opt-title">Roles</div>
|
|
1318
|
+
<div id="wf-roles" style="display:flex;flex-direction:column;gap:6px;margin-top:6px;"></div>
|
|
1319
|
+
<button onclick="wfAddRole()" style="margin-top:8px;height:30px;padding:0 14px;background:rgba(139,145,240,0.15);border:1px solid var(--accent);border-radius:5px;color:var(--accent);font-size:12px;cursor:pointer;">+ Add Role</button>
|
|
1320
|
+
</div>
|
|
1321
|
+
</div>
|
|
1322
|
+
</div>
|
|
1323
|
+
</div>
|
|
1324
|
+
<div class="save-area">
|
|
1325
|
+
<div class="warn-msg" id="warn-workflow-custom"></div>
|
|
1326
|
+
<button class="save" onclick="wfSavePanel()">Save</button>
|
|
1327
|
+
</div>
|
|
1328
|
+
</div>
|
|
1329
|
+
|
|
1330
|
+
</div>
|
|
1331
|
+
</div>
|
|
1332
|
+
|
|
1333
|
+
<script>
|
|
1334
|
+
// ============================================================
|
|
1335
|
+
// SHARED GLOBALS
|
|
1336
|
+
// ============================================================
|
|
1337
|
+
let isDirty = false;
|
|
1338
|
+
let myGeneration = 0;
|
|
1339
|
+
const DEFAULT_HOLIDAY_COUNTRY = 'KR';
|
|
1340
|
+
|
|
1341
|
+
document.addEventListener('input', () => { isDirty = true; });
|
|
1342
|
+
document.addEventListener('change', () => { isDirty = true; });
|
|
1343
|
+
window.addEventListener('beforeunload', () => {
|
|
1344
|
+
if (isDirty) {
|
|
1345
|
+
const data = buildChannelsData();
|
|
1346
|
+
navigator.sendBeacon('/config', JSON.stringify(data));
|
|
1347
|
+
}
|
|
1348
|
+
navigator.sendBeacon('/close', '');
|
|
1349
|
+
});
|
|
1350
|
+
|
|
1351
|
+
function requestClose() {
|
|
1352
|
+
if (isDirty) {
|
|
1353
|
+
document.getElementById('close-modal').style.display = 'flex';
|
|
1354
|
+
} else {
|
|
1355
|
+
navigator.sendBeacon('/close', '');
|
|
1356
|
+
window.close();
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
function discardAndClose() {
|
|
1361
|
+
isDirty = false;
|
|
1362
|
+
navigator.sendBeacon('/close', '');
|
|
1363
|
+
window.close();
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
function cancelClose() {
|
|
1367
|
+
document.getElementById('close-modal').style.display = 'none';
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
async function saveAndClose() {
|
|
1371
|
+
try {
|
|
1372
|
+
await savePanel();
|
|
1373
|
+
isDirty = false;
|
|
1374
|
+
navigator.sendBeacon('/close', '');
|
|
1375
|
+
window.close();
|
|
1376
|
+
} catch (e) {
|
|
1377
|
+
alert('Save failed — window NOT closed: ' + (e.message || e));
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
// Shared data containers
|
|
1382
|
+
let chConfig = null;
|
|
1383
|
+
let chExtraData = { bot: {} };
|
|
1384
|
+
let cliStatus = { whisper: { installed: false }, ngrok: { installed: false } };
|
|
1385
|
+
|
|
1386
|
+
let agConfig = {}, agAuth = {}, agPresets = [];
|
|
1387
|
+
let agEditingPresetId = null;
|
|
1388
|
+
|
|
1389
|
+
let memConfig = {}, memAuth = {};
|
|
1390
|
+
|
|
1391
|
+
let srConfig = null;
|
|
1392
|
+
|
|
1393
|
+
// ============================================================
|
|
1394
|
+
// SHARED UTILITIES
|
|
1395
|
+
// ============================================================
|
|
1396
|
+
|
|
1397
|
+
function switchCat(el) {
|
|
1398
|
+
document.querySelectorAll('.cat-item').forEach(c => c.classList.remove('active'));
|
|
1399
|
+
el.classList.add('active');
|
|
1400
|
+
document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
|
|
1401
|
+
document.getElementById('panel-' + el.dataset.panel).classList.add('active');
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
function togglePw(el) {
|
|
1405
|
+
const inp = el.previousElementSibling;
|
|
1406
|
+
inp.type = inp.type === 'password' ? 'text' : 'password';
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
function toggleVis(btn) {
|
|
1410
|
+
const inp = btn.parentElement.querySelector('.r-input');
|
|
1411
|
+
if (inp.type === 'password') { inp.type = 'text'; btn.innerHTML = '👁‍🗨'; }
|
|
1412
|
+
else { inp.type = 'password'; btn.innerHTML = '👁'; }
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
function toggleAdvanced(el) {
|
|
1416
|
+
const arrow = el.querySelector('.arrow');
|
|
1417
|
+
const content = el.nextElementSibling;
|
|
1418
|
+
arrow.classList.toggle('open');
|
|
1419
|
+
content.classList.toggle('open');
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
function escapeAttr(v) {
|
|
1423
|
+
return String(v ?? '').replace(/&/g,'&').replace(/"/g,'"').replace(/</g,'<');
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
function escapeHtml(v) {
|
|
1427
|
+
return String(v ?? '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
document.addEventListener('click', (e) => {
|
|
1431
|
+
if (e.target.closest('.r-toggle, .r-radio, .dyn-add, .dyn-remove, .card-remove, .tag-x, .mode, .r-check')) {
|
|
1432
|
+
isDirty = true;
|
|
1433
|
+
}
|
|
1434
|
+
});
|
|
1435
|
+
|
|
1436
|
+
// ============================================================
|
|
1437
|
+
// INIT
|
|
1438
|
+
// ============================================================
|
|
1439
|
+
|
|
1440
|
+
// ============================================================
|
|
1441
|
+
// GENERAL MODULE — Prompt Injection
|
|
1442
|
+
// ============================================================
|
|
1443
|
+
|
|
1444
|
+
let genConfig = { mode: 'claude_md', targetPath: '~/.claude/CLAUDE.md' };
|
|
1445
|
+
|
|
1446
|
+
async function genLoadConfig() {
|
|
1447
|
+
try {
|
|
1448
|
+
const r = await fetch('/general/config').then(r => r.json());
|
|
1449
|
+
const pi = (r && r.promptInjection) || {};
|
|
1450
|
+
genConfig.mode = (pi.mode === 'hook') ? 'hook' : 'claude_md';
|
|
1451
|
+
genConfig.targetPath = pi.targetPath || '~/.claude/CLAUDE.md';
|
|
1452
|
+
} catch {
|
|
1453
|
+
genConfig.mode = 'claude_md';
|
|
1454
|
+
genConfig.targetPath = '~/.claude/CLAUDE.md';
|
|
1455
|
+
}
|
|
1456
|
+
genRenderRadios();
|
|
1457
|
+
const tp = document.getElementById('gen-target-path');
|
|
1458
|
+
if (tp) tp.textContent = genConfig.targetPath;
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
function genSelectMode(mode) {
|
|
1462
|
+
genConfig.mode = mode;
|
|
1463
|
+
isDirty = true;
|
|
1464
|
+
genRenderRadios();
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
function genRenderRadios() {
|
|
1468
|
+
const rh = document.getElementById('gen-radio-hook');
|
|
1469
|
+
const rc = document.getElementById('gen-radio-claude-md');
|
|
1470
|
+
if (rh) rh.classList.toggle('on', genConfig.mode === 'hook');
|
|
1471
|
+
if (rc) rc.classList.toggle('on', genConfig.mode === 'claude_md');
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
async function genSavePanel() {
|
|
1475
|
+
const btn = document.querySelector('#panel-general-modules .save');
|
|
1476
|
+
const warnEl = document.getElementById('warn-general-modules');
|
|
1477
|
+
warnEl.textContent = '';
|
|
1478
|
+
btn.textContent = 'Saving...';
|
|
1479
|
+
btn.disabled = true;
|
|
1480
|
+
try {
|
|
1481
|
+
const res = await fetch('/general/save', {
|
|
1482
|
+
method: 'POST',
|
|
1483
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1484
|
+
body: JSON.stringify({ mode: genConfig.mode, targetPath: genConfig.targetPath }),
|
|
1485
|
+
});
|
|
1486
|
+
if (!res.ok) throw new Error('save failed');
|
|
1487
|
+
btn.textContent = '\u2713 Saved';
|
|
1488
|
+
btn.classList.add('done');
|
|
1489
|
+
isDirty = false;
|
|
1490
|
+
setTimeout(() => {
|
|
1491
|
+
btn.textContent = 'Save';
|
|
1492
|
+
btn.classList.remove('done');
|
|
1493
|
+
btn.disabled = false;
|
|
1494
|
+
}, 1500);
|
|
1495
|
+
} catch (e) {
|
|
1496
|
+
warnEl.textContent = 'Save failed';
|
|
1497
|
+
btn.textContent = 'Save';
|
|
1498
|
+
btn.disabled = false;
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
// ============================================================
|
|
1503
|
+
// GENERAL MODULES (B6) — enable/disable per-module
|
|
1504
|
+
// ============================================================
|
|
1505
|
+
async function loadModulesConfig() {
|
|
1506
|
+
try {
|
|
1507
|
+
const r = await fetch('/modules').then(r => r.json());
|
|
1508
|
+
for (const name of ['channels', 'memory', 'search', 'agent']) {
|
|
1509
|
+
const el = document.getElementById('mod-' + name + '-enabled');
|
|
1510
|
+
if (el) el.checked = !(r && r[name] && r[name].enabled === false);
|
|
1511
|
+
}
|
|
1512
|
+
} catch {
|
|
1513
|
+
// On load failure, leave all toggles at their default (checked).
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
async function saveModulesConfig() {
|
|
1518
|
+
const warnEl = document.getElementById('warn-general-modules');
|
|
1519
|
+
if (warnEl) warnEl.textContent = '';
|
|
1520
|
+
const payload = {};
|
|
1521
|
+
for (const name of ['channels', 'memory', 'search', 'agent']) {
|
|
1522
|
+
const el = document.getElementById('mod-' + name + '-enabled');
|
|
1523
|
+
payload[name] = { enabled: el ? !!el.checked : true };
|
|
1524
|
+
}
|
|
1525
|
+
try {
|
|
1526
|
+
const res = await fetch('/modules', {
|
|
1527
|
+
method: 'POST',
|
|
1528
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1529
|
+
body: JSON.stringify(payload),
|
|
1530
|
+
});
|
|
1531
|
+
if (!res.ok) throw new Error('save failed');
|
|
1532
|
+
isDirty = false;
|
|
1533
|
+
} catch (e) {
|
|
1534
|
+
if (warnEl) warnEl.textContent = 'Save failed — check setup-server log.';
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
// ============================================================
|
|
1539
|
+
// GENERAL SECURITY (B2) — capability toggles
|
|
1540
|
+
// ============================================================
|
|
1541
|
+
async function loadSecurityConfig() {
|
|
1542
|
+
try {
|
|
1543
|
+
const r = await fetch('/capabilities').then(r => r.json());
|
|
1544
|
+
const el = document.getElementById('sec-home-access');
|
|
1545
|
+
if (el) el.checked = !!(r && r.homeAccess === true);
|
|
1546
|
+
} catch {
|
|
1547
|
+
// On load failure, leave toggle at default (off).
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
async function saveSecurityConfig() {
|
|
1552
|
+
const warnEl = document.getElementById('warn-general-modules');
|
|
1553
|
+
if (warnEl) warnEl.textContent = '';
|
|
1554
|
+
const el = document.getElementById('sec-home-access');
|
|
1555
|
+
const payload = { homeAccess: el ? !!el.checked : false };
|
|
1556
|
+
try {
|
|
1557
|
+
const res = await fetch('/capabilities', {
|
|
1558
|
+
method: 'POST',
|
|
1559
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1560
|
+
body: JSON.stringify(payload),
|
|
1561
|
+
});
|
|
1562
|
+
if (!res.ok) throw new Error('save failed');
|
|
1563
|
+
isDirty = false;
|
|
1564
|
+
} catch (e) {
|
|
1565
|
+
if (warnEl) warnEl.textContent = 'Save failed — check setup-server log.';
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
// MD Library module removed — role MD editing is now in the Custom
|
|
1570
|
+
// Workflow panel, Common MD is plugin-fixed (rules/agent.md) and not
|
|
1571
|
+
// user-editable, and Project MDs are no longer part of the UI.
|
|
1572
|
+
|
|
1573
|
+
// ============================================================
|
|
1574
|
+
// CUSTOM WORKFLOW MODULE
|
|
1575
|
+
// ============================================================
|
|
1576
|
+
|
|
1577
|
+
let wfConfig = { roles: [] };
|
|
1578
|
+
let wfMdContent = "";
|
|
1579
|
+
|
|
1580
|
+
async function wfLoadConfig() {
|
|
1581
|
+
try {
|
|
1582
|
+
const [jsonRes, mdRes, rolesMdRes] = await Promise.all([
|
|
1583
|
+
fetch('/workflow/load'),
|
|
1584
|
+
fetch('/workflow/md'),
|
|
1585
|
+
fetch('/md/role'),
|
|
1586
|
+
agPresets.length ? Promise.resolve() : agLoadPresets(),
|
|
1587
|
+
]);
|
|
1588
|
+
const data = await jsonRes.json();
|
|
1589
|
+
wfConfig.roles = Array.isArray(data.roles) ? data.roles : [];
|
|
1590
|
+
wfMdContent = mdRes.ok ? await mdRes.text() : "";
|
|
1591
|
+
const rolesMdData = rolesMdRes.ok ? await rolesMdRes.json() : { items: [] };
|
|
1592
|
+
const mdByName = {};
|
|
1593
|
+
for (const it of (rolesMdData.items || [])) mdByName[it.name] = it.body || '';
|
|
1594
|
+
for (const r of wfConfig.roles) {
|
|
1595
|
+
r.md = mdByName[(r.name || '').trim().toLowerCase()] || '';
|
|
1596
|
+
}
|
|
1597
|
+
} catch {
|
|
1598
|
+
wfConfig.roles = [];
|
|
1599
|
+
wfMdContent = "";
|
|
1600
|
+
}
|
|
1601
|
+
document.getElementById('wf-description').value = wfMdContent;
|
|
1602
|
+
wfRenderRoles();
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
function wfRenderRoles() {
|
|
1606
|
+
const host = document.getElementById('wf-roles');
|
|
1607
|
+
host.innerHTML = '';
|
|
1608
|
+
wfConfig.roles.forEach((role, idx) => {
|
|
1609
|
+
const card = document.createElement('div');
|
|
1610
|
+
card.style.cssText = 'border:1px solid var(--border);border-radius:6px;background:rgba(255,255,255,0.02);';
|
|
1611
|
+
|
|
1612
|
+
const header = document.createElement('div');
|
|
1613
|
+
header.style.cssText = 'display:flex;gap:6px;align-items:center;padding:6px;';
|
|
1614
|
+
|
|
1615
|
+
const toggleBtn = document.createElement('button');
|
|
1616
|
+
toggleBtn.textContent = role._open ? '▼' : '▶';
|
|
1617
|
+
toggleBtn.style.cssText = 'width:24px;height:30px;background:transparent;border:1px solid var(--border);border-radius:5px;color:var(--text-3);cursor:pointer;font-size:10px;';
|
|
1618
|
+
|
|
1619
|
+
const nameInput = document.createElement('input');
|
|
1620
|
+
nameInput.type = 'text';
|
|
1621
|
+
nameInput.value = role.name || '';
|
|
1622
|
+
nameInput.placeholder = 'role name';
|
|
1623
|
+
nameInput.className = 'r-input';
|
|
1624
|
+
nameInput.style.cssText = 'flex:1;height:30px;';
|
|
1625
|
+
nameInput.oninput = (e) => { wfConfig.roles[idx].name = e.target.value; isDirty = true; };
|
|
1626
|
+
|
|
1627
|
+
const presetSelect = document.createElement('select');
|
|
1628
|
+
presetSelect.className = 'r-select md';
|
|
1629
|
+
presetSelect.style.cssText = 'width:160px;height:30px;';
|
|
1630
|
+
agPresets.forEach(p => {
|
|
1631
|
+
const opt = document.createElement('option');
|
|
1632
|
+
opt.value = p.id;
|
|
1633
|
+
opt.textContent = p.name || p.id;
|
|
1634
|
+
if (p.id === role.preset || p.name === role.preset) opt.selected = true;
|
|
1635
|
+
presetSelect.appendChild(opt);
|
|
1636
|
+
});
|
|
1637
|
+
presetSelect.onchange = (e) => { wfConfig.roles[idx].preset = e.target.value; isDirty = true; };
|
|
1638
|
+
|
|
1639
|
+
const delBtn = document.createElement('button');
|
|
1640
|
+
delBtn.textContent = '×';
|
|
1641
|
+
delBtn.style.cssText = 'width:30px;height:30px;background:transparent;border:1px solid var(--border);border-radius:5px;color:var(--text-3);cursor:pointer;font-size:14px;';
|
|
1642
|
+
delBtn.onclick = () => wfDeleteRole(idx);
|
|
1643
|
+
|
|
1644
|
+
header.appendChild(toggleBtn);
|
|
1645
|
+
header.appendChild(nameInput);
|
|
1646
|
+
header.appendChild(presetSelect);
|
|
1647
|
+
header.appendChild(delBtn);
|
|
1648
|
+
|
|
1649
|
+
const body = document.createElement('div');
|
|
1650
|
+
body.style.cssText = 'padding:0 6px 6px 6px;display:' + (role._open ? 'block' : 'none') + ';';
|
|
1651
|
+
const mdTextarea = document.createElement('textarea');
|
|
1652
|
+
mdTextarea.className = 'r-input';
|
|
1653
|
+
mdTextarea.placeholder = 'Role-specific MD (injected into Tier 3 # agent-role slot)';
|
|
1654
|
+
mdTextarea.style.cssText = 'width:100%;min-height:120px;font-family:monospace;font-size:12px;resize:vertical;';
|
|
1655
|
+
mdTextarea.value = role.md || '';
|
|
1656
|
+
mdTextarea.oninput = (e) => { wfConfig.roles[idx].md = e.target.value; isDirty = true; };
|
|
1657
|
+
body.appendChild(mdTextarea);
|
|
1658
|
+
|
|
1659
|
+
toggleBtn.onclick = () => {
|
|
1660
|
+
wfConfig.roles[idx]._open = !wfConfig.roles[idx]._open;
|
|
1661
|
+
wfRenderRoles();
|
|
1662
|
+
};
|
|
1663
|
+
|
|
1664
|
+
card.appendChild(header);
|
|
1665
|
+
card.appendChild(body);
|
|
1666
|
+
host.appendChild(card);
|
|
1667
|
+
});
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
function wfAddRole() {
|
|
1671
|
+
wfConfig.roles.push({ name: '', preset: agPresets[0]?.id || 'opus-max', md: '', _open: true });
|
|
1672
|
+
isDirty = true;
|
|
1673
|
+
wfRenderRoles();
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
function wfDeleteRole(idx) {
|
|
1677
|
+
const name = (wfConfig.roles[idx].name || '').trim().toLowerCase();
|
|
1678
|
+
wfConfig.roles.splice(idx, 1);
|
|
1679
|
+
isDirty = true;
|
|
1680
|
+
// Best-effort: remove the role MD file on disk too.
|
|
1681
|
+
if (name) {
|
|
1682
|
+
fetch('/md/role?name=' + encodeURIComponent(name), { method: 'DELETE' }).catch(() => {});
|
|
1683
|
+
}
|
|
1684
|
+
wfRenderRoles();
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
async function wfSavePanel() {
|
|
1688
|
+
const btn = document.querySelector('#panel-workflow-custom .save');
|
|
1689
|
+
const warnEl = document.getElementById('warn-workflow-custom');
|
|
1690
|
+
warnEl.textContent = '';
|
|
1691
|
+
const mdText = document.getElementById('wf-description').value;
|
|
1692
|
+
try {
|
|
1693
|
+
// Drop blank-name rows entirely so nothing partially persists:
|
|
1694
|
+
// user-workflow.json gets only named roles, and per-role MD posts
|
|
1695
|
+
// only run for the same set. This avoids silent data loss where a
|
|
1696
|
+
// user typed an MD body before naming the role.
|
|
1697
|
+
const validRoles = wfConfig.roles.filter(r => (r.name || '').trim());
|
|
1698
|
+
const rolesPayload = validRoles.map(r => {
|
|
1699
|
+
const { _open, md, ...rest } = r;
|
|
1700
|
+
return rest;
|
|
1701
|
+
});
|
|
1702
|
+
const roleMdPosts = validRoles.map(r => fetch('/md/role', {
|
|
1703
|
+
method: 'POST',
|
|
1704
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1705
|
+
body: JSON.stringify({ name: r.name, body: r.md || '' }),
|
|
1706
|
+
}));
|
|
1707
|
+
const [r1, r2, ...mdResults] = await Promise.all([
|
|
1708
|
+
fetch('/workflow/save', {
|
|
1709
|
+
method: 'POST',
|
|
1710
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1711
|
+
body: JSON.stringify({ roles: rolesPayload })
|
|
1712
|
+
}),
|
|
1713
|
+
fetch('/workflow/md', {
|
|
1714
|
+
method: 'POST',
|
|
1715
|
+
headers: { 'Content-Type': 'text/plain' },
|
|
1716
|
+
body: mdText
|
|
1717
|
+
}),
|
|
1718
|
+
...roleMdPosts,
|
|
1719
|
+
]);
|
|
1720
|
+
if (!r1.ok) throw new Error(await r1.text());
|
|
1721
|
+
if (!r2.ok) throw new Error(await r2.text());
|
|
1722
|
+
for (const mr of mdResults) {
|
|
1723
|
+
if (!mr.ok) throw new Error(await mr.text());
|
|
1724
|
+
}
|
|
1725
|
+
wfMdContent = mdText;
|
|
1726
|
+
btn.classList.add('done');
|
|
1727
|
+
btn.textContent = 'Saved';
|
|
1728
|
+
setTimeout(() => { btn.classList.remove('done'); btn.textContent = 'Save'; }, 1500);
|
|
1729
|
+
isDirty = false;
|
|
1730
|
+
} catch (e) {
|
|
1731
|
+
warnEl.textContent = 'Save failed: ' + (e.message || e);
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
async function wfOpenDescription() {
|
|
1736
|
+
try {
|
|
1737
|
+
const _wod = await fetch('/workflow/md', { method: 'POST', headers: { 'Content-Type': 'text/plain' }, body: document.getElementById('wf-description').value });
|
|
1738
|
+
if (!_wod.ok) throw new Error('HTTP ' + _wod.status + ' ' + await _wod.text());
|
|
1739
|
+
await fetch('/workflow/file');
|
|
1740
|
+
} catch (e) {
|
|
1741
|
+
alert('Open failed: ' + (e.message || e));
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
window.addEventListener('load', async () => {
|
|
1746
|
+
try {
|
|
1747
|
+
const g = await fetch('/generation').then(r => r.json());
|
|
1748
|
+
myGeneration = g.generation || 0;
|
|
1749
|
+
} catch {}
|
|
1750
|
+
|
|
1751
|
+
// Init channels UI helpers
|
|
1752
|
+
initTagInputs();
|
|
1753
|
+
initSliders();
|
|
1754
|
+
|
|
1755
|
+
// Load all configs in parallel
|
|
1756
|
+
const results = await Promise.allSettled([
|
|
1757
|
+
loadChannelsConfig(),
|
|
1758
|
+
loadChannelsCliCheck(),
|
|
1759
|
+
loadAgentData(),
|
|
1760
|
+
loadMemoryData(),
|
|
1761
|
+
loadSearchConfig(),
|
|
1762
|
+
genLoadConfig(),
|
|
1763
|
+
loadModulesConfig(),
|
|
1764
|
+
loadSecurityConfig(),
|
|
1765
|
+
wfLoadConfig(),
|
|
1766
|
+
]);
|
|
1767
|
+
const LOADER_NAMES = ['channels', 'cli-check', 'agent', 'memory', 'search', 'general', 'modules', 'security', 'workflow'];
|
|
1768
|
+
let criticalFailed = false;
|
|
1769
|
+
results.forEach((r, i) => {
|
|
1770
|
+
if (r.status === 'rejected') {
|
|
1771
|
+
console.error('[mixdog] loader', LOADER_NAMES[i] || i, 'failed:', r.reason);
|
|
1772
|
+
if (i === 0 || i === 2) criticalFailed = true; // channels(0) or agent(2) — channels now throws on non-2xx
|
|
1773
|
+
}
|
|
1774
|
+
});
|
|
1775
|
+
if (criticalFailed) {
|
|
1776
|
+
const banner = document.createElement('div');
|
|
1777
|
+
banner.id = 'load-error-banner';
|
|
1778
|
+
banner.style.cssText = 'position:fixed;top:0;left:0;right:0;z-index:9999;background:#c0392b;color:#fff;font-size:13px;font-weight:600;padding:10px 16px;display:flex;align-items:center;gap:12px;';
|
|
1779
|
+
banner.innerHTML = '<span style="font-size:16px;">⚠</span><span>Failed to load configuration from server. Some settings may be missing or incorrect \u2014 check that setup-server is running and reload.</span><button onclick="location.reload()" style="margin-left:auto;padding:4px 12px;background:rgba(255,255,255,0.2);border:1px solid rgba(255,255,255,0.4);border-radius:4px;color:#fff;cursor:pointer;font-size:12px;">Reload</button><button onclick="this.closest(\'#load-error-banner\').remove()" style="padding:4px 10px;background:transparent;border:1px solid rgba(255,255,255,0.3);border-radius:4px;color:#fff;cursor:pointer;font-size:12px;">Dismiss</button>';
|
|
1780
|
+
document.body.prepend(banner);
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
// Post-load init — populate preset dropdowns after agConfig is ready
|
|
1784
|
+
populatePresetDropdowns();
|
|
1785
|
+
renderAdvanced();
|
|
1786
|
+
isDirty = false;
|
|
1787
|
+
|
|
1788
|
+
// Reveal the main area now that every loader has resolved (or rejected
|
|
1789
|
+
// with a visible banner). Until this point `.main` was opacity:0 so the
|
|
1790
|
+
// user never sees uninitialized HTML defaults (empty inputs, toggles
|
|
1791
|
+
// flipping on after config arrives, etc).
|
|
1792
|
+
document.querySelector('.main')?.classList.add('ready');
|
|
1793
|
+
document.getElementById('boot-spinner')?.classList.add('hidden');
|
|
1794
|
+
setTimeout(() => document.getElementById('boot-spinner')?.remove(), 240);
|
|
1795
|
+
|
|
1796
|
+
// Poll for newer window
|
|
1797
|
+
setInterval(async () => {
|
|
1798
|
+
try {
|
|
1799
|
+
const g = await fetch('/generation').then(r => r.json());
|
|
1800
|
+
if (g.generation > myGeneration) { isDirty = false; window.close(); }
|
|
1801
|
+
} catch {}
|
|
1802
|
+
}, 2000);
|
|
1803
|
+
});
|
|
1804
|
+
|
|
1805
|
+
// ============================================================
|
|
1806
|
+
// CHANNELS MODULE
|
|
1807
|
+
// ============================================================
|
|
1808
|
+
|
|
1809
|
+
// -- Tag input system --
|
|
1810
|
+
function initTagInputs() {
|
|
1811
|
+
document.querySelectorAll('.tag-container').forEach(container => {
|
|
1812
|
+
if (container.querySelector('.tag-input')) return;
|
|
1813
|
+
const inp = document.createElement('input');
|
|
1814
|
+
inp.className = 'tag-input';
|
|
1815
|
+
inp.placeholder = 'Type and press Enter';
|
|
1816
|
+
inp.addEventListener('keydown', e => {
|
|
1817
|
+
if (e.key === 'Enter' && inp.value.trim()) {
|
|
1818
|
+
e.preventDefault();
|
|
1819
|
+
addTag(container, inp.value.trim());
|
|
1820
|
+
inp.value = '';
|
|
1821
|
+
} else if (e.key === 'Backspace' && !inp.value) {
|
|
1822
|
+
const last = container.querySelector('.tag-item:last-of-type');
|
|
1823
|
+
if (last) last.remove();
|
|
1824
|
+
}
|
|
1825
|
+
});
|
|
1826
|
+
container.appendChild(inp);
|
|
1827
|
+
container.addEventListener('click', () => inp.focus());
|
|
1828
|
+
});
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
function addTag(container, text) {
|
|
1832
|
+
const tag = document.createElement('span');
|
|
1833
|
+
tag.className = 'tag-item';
|
|
1834
|
+
tag.dataset.value = text;
|
|
1835
|
+
const textNode = document.createTextNode(text);
|
|
1836
|
+
const x = document.createElement('span');
|
|
1837
|
+
x.className = 'tag-x';
|
|
1838
|
+
x.textContent = '\u00d7';
|
|
1839
|
+
x.addEventListener('click', function() { this.parentElement.remove(); });
|
|
1840
|
+
tag.appendChild(textNode);
|
|
1841
|
+
tag.appendChild(x);
|
|
1842
|
+
const inp = container.querySelector('.tag-input');
|
|
1843
|
+
container.insertBefore(tag, inp);
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
function getTagValues(containerId) {
|
|
1847
|
+
return Array.from(document.getElementById(containerId).querySelectorAll('.tag-item'))
|
|
1848
|
+
.map(t => t.dataset.value);
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
function setTagValues(containerId, values) {
|
|
1852
|
+
const container = document.getElementById(containerId);
|
|
1853
|
+
container.querySelectorAll('.tag-item').forEach(t => t.remove());
|
|
1854
|
+
(values || []).forEach(v => addTag(container, v));
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
// -- Slider init --
|
|
1858
|
+
function initSliders() {
|
|
1859
|
+
document.querySelectorAll('.r-slider').forEach(slider => {
|
|
1860
|
+
const valEl = document.getElementById(slider.id + '-val');
|
|
1861
|
+
if (valEl) {
|
|
1862
|
+
slider.addEventListener('input', () => { valEl.textContent = slider.value; });
|
|
1863
|
+
}
|
|
1864
|
+
});
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
// -- Quiet hours (DND tab) --
|
|
1868
|
+
function onQuietPresetChange() {
|
|
1869
|
+
const sel = document.getElementById('dnd-quiet-preset');
|
|
1870
|
+
const custom = document.getElementById('dnd-quiet-custom');
|
|
1871
|
+
if (sel.value === 'custom') custom.classList.remove('hidden');
|
|
1872
|
+
else custom.classList.add('hidden');
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
|
|
1876
|
+
// -- Channel list --
|
|
1877
|
+
function getChannelOptions() {
|
|
1878
|
+
const opts = [];
|
|
1879
|
+
document.querySelectorAll('#ch-channel-list .dyn-item').forEach(item => {
|
|
1880
|
+
const label = item.querySelector('[data-f="ch-label"]')?.value.trim();
|
|
1881
|
+
if (label) opts.push(label);
|
|
1882
|
+
});
|
|
1883
|
+
return opts;
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
function makeChannelSelect(selected, cfg) {
|
|
1887
|
+
const c = cfg || {};
|
|
1888
|
+
const opts = getChannelOptions();
|
|
1889
|
+
let html = '<select class="r-select" data-f="channel" style="width:200px;flex:none;"' + (c.onchange ? ' onchange="' + c.onchange + '"' : '') + '>';
|
|
1890
|
+
if (c.placeholder) {
|
|
1891
|
+
const ph = document.createElement('option');
|
|
1892
|
+
ph.value = ''; ph.textContent = c.placeholder; if (!selected) ph.selected = true;
|
|
1893
|
+
html += ph.outerHTML;
|
|
1894
|
+
}
|
|
1895
|
+
opts.forEach(o => {
|
|
1896
|
+
const opt = document.createElement('option');
|
|
1897
|
+
opt.value = o; opt.textContent = o; if (o === selected) opt.selected = true;
|
|
1898
|
+
html += opt.outerHTML;
|
|
1899
|
+
});
|
|
1900
|
+
if (!opts.length && !c.placeholder) html += '<option value="">--</option>';
|
|
1901
|
+
html += '</select>';
|
|
1902
|
+
return html;
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
function addChannel(label, channelId, mode, isMain) {
|
|
1906
|
+
const list = document.getElementById('ch-channel-list');
|
|
1907
|
+
const item = document.createElement('div');
|
|
1908
|
+
item.className = 'dyn-item';
|
|
1909
|
+
|
|
1910
|
+
const radio = document.createElement('div');
|
|
1911
|
+
radio.className = 'r-radio' + (isMain ? ' on' : '');
|
|
1912
|
+
radio.title = 'Main channel';
|
|
1913
|
+
radio.addEventListener('click', function() { setMainChannel(this); });
|
|
1914
|
+
|
|
1915
|
+
const labelInp = document.createElement('input');
|
|
1916
|
+
labelInp.className = 'r-input';
|
|
1917
|
+
labelInp.type = 'text';
|
|
1918
|
+
labelInp.placeholder = 'label';
|
|
1919
|
+
labelInp.value = label || '';
|
|
1920
|
+
labelInp.style.cssText = 'width:90px;flex:none;';
|
|
1921
|
+
labelInp.dataset.f = 'ch-label';
|
|
1922
|
+
|
|
1923
|
+
const idInp = document.createElement('input');
|
|
1924
|
+
idInp.className = 'r-input';
|
|
1925
|
+
idInp.type = 'text';
|
|
1926
|
+
idInp.placeholder = 'Channel ID';
|
|
1927
|
+
idInp.value = channelId || '';
|
|
1928
|
+
idInp.style.cssText = 'min-width:140px;';
|
|
1929
|
+
idInp.dataset.f = 'ch-id';
|
|
1930
|
+
|
|
1931
|
+
const sel = document.createElement('select');
|
|
1932
|
+
sel.className = 'r-select';
|
|
1933
|
+
sel.style.cssText = 'width:110px;flex:none;';
|
|
1934
|
+
sel.dataset.f = 'ch-mode';
|
|
1935
|
+
['interactive', 'monitor'].forEach(val => {
|
|
1936
|
+
const opt = document.createElement('option');
|
|
1937
|
+
opt.value = val;
|
|
1938
|
+
opt.textContent = val;
|
|
1939
|
+
if (mode === val) opt.selected = true;
|
|
1940
|
+
sel.appendChild(opt);
|
|
1941
|
+
});
|
|
1942
|
+
|
|
1943
|
+
const tip = document.createElement('span');
|
|
1944
|
+
tip.className = 'tip';
|
|
1945
|
+
tip.style.cssText = 'margin:0 2px;';
|
|
1946
|
+
tip.textContent = '?';
|
|
1947
|
+
const tipText = document.createElement('span');
|
|
1948
|
+
tipText.className = 'tip-text';
|
|
1949
|
+
tipText.textContent = 'interactive: listen and reply / monitor: listen only and report to main';
|
|
1950
|
+
tip.appendChild(tipText);
|
|
1951
|
+
|
|
1952
|
+
const remove = document.createElement('span');
|
|
1953
|
+
remove.className = 'dyn-remove';
|
|
1954
|
+
remove.textContent = '\u00d7';
|
|
1955
|
+
remove.addEventListener('click', function() { this.parentElement.remove(); });
|
|
1956
|
+
|
|
1957
|
+
item.appendChild(radio);
|
|
1958
|
+
item.appendChild(labelInp);
|
|
1959
|
+
item.appendChild(idInp);
|
|
1960
|
+
item.appendChild(sel);
|
|
1961
|
+
item.appendChild(tip);
|
|
1962
|
+
item.appendChild(remove);
|
|
1963
|
+
list.appendChild(item);
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
function setMainChannel(radio) {
|
|
1967
|
+
document.querySelectorAll('#ch-channel-list .r-radio').forEach(r => r.classList.remove('on'));
|
|
1968
|
+
radio.classList.add('on');
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
|
|
1972
|
+
// -- Schedule cards --
|
|
1973
|
+
// Stored `time` is a cron expression (the server converts legacy HH:MM /
|
|
1974
|
+
// every<N>m / hourly / daily on save). Map known cron shapes back to the
|
|
1975
|
+
// card's Frequency/Every/At vocabulary; an unrecognized cron stays verbatim
|
|
1976
|
+
// in the At field (and round-trips through save untouched).
|
|
1977
|
+
function schedTimeToUi(time) {
|
|
1978
|
+
const t = String(time || '').trim();
|
|
1979
|
+
if (!t) return { freq: 'interval', interval: '01:00' };
|
|
1980
|
+
let m = t.match(/^\*\/(\d{1,2}) \* \* \* \*$/);
|
|
1981
|
+
if (m) { const n = parseInt(m[1], 10); return { freq: 'interval', interval: String(Math.floor(n / 60)).padStart(2, '0') + ':' + String(n % 60).padStart(2, '0') }; }
|
|
1982
|
+
if (/^0 \* \* \* \*$/.test(t)) return { freq: 'interval', interval: '01:00' };
|
|
1983
|
+
m = t.match(/^(\d{1,2}) (\d{1,2}) \* \* (\*|1-5|0,6)$/);
|
|
1984
|
+
if (m) {
|
|
1985
|
+
const at = String(parseInt(m[2], 10)).padStart(2, '0') + ':' + String(parseInt(m[1], 10)).padStart(2, '0');
|
|
1986
|
+
return { freq: m[3] === '1-5' ? 'weekday' : (m[3] === '0,6' ? 'weekly' : 'daily'), at };
|
|
1987
|
+
}
|
|
1988
|
+
// Legacy tokens (pre-conversion configs) — keep recognizing them on load.
|
|
1989
|
+
// Mirror the server's clamp (setup-server.mjs every<N>m -> */N with N in 1..59)
|
|
1990
|
+
// so the displayed interval equals the converted cadence.
|
|
1991
|
+
const lm = t.match(/^every(\d+)m$/);
|
|
1992
|
+
if (lm) { const n = Math.min(Math.max(parseInt(lm[1], 10), 1), 59); return { freq: 'interval', interval: '00:' + String(n).padStart(2, '0') }; }
|
|
1993
|
+
if (t === 'hourly') return { freq: 'interval', interval: '01:00' };
|
|
1994
|
+
if (t === 'daily') return { freq: 'daily', at: '00:00' };
|
|
1995
|
+
if (/^([01]?\d|2[0-3]):[0-5]\d$/.test(t)) return { freq: 'daily', at: t };
|
|
1996
|
+
// Unrecognized cron: show raw in At, pass through verbatim on save.
|
|
1997
|
+
return { freq: 'daily', at: t, raw: true };
|
|
1998
|
+
}
|
|
1999
|
+
function addScheduleCard(data) {
|
|
2000
|
+
const list = document.getElementById('sched-tasks');
|
|
2001
|
+
const emptyEl = document.getElementById('sched-empty');
|
|
2002
|
+
if (emptyEl) emptyEl.style.display = 'none';
|
|
2003
|
+
const card = document.createElement('div');
|
|
2004
|
+
card.className = 'card';
|
|
2005
|
+
const d = data || {};
|
|
2006
|
+
const daysVal = d.days || 'daily';
|
|
2007
|
+
const nameVal = d.name || 'Untitled';
|
|
2008
|
+
const timeUi = schedTimeToUi(d.time);
|
|
2009
|
+
// Legacy HH:MM entries carried day-of-week in the separate `days` field;
|
|
2010
|
+
// fold it into one effective frequency so exactly one option is selected.
|
|
2011
|
+
const effFreq = timeUi.freq === 'daily' && daysVal === 'weekday' ? 'weekday'
|
|
2012
|
+
: (timeUi.freq === 'daily' && daysVal === 'weekend' ? 'weekly' : timeUi.freq);
|
|
2013
|
+
card.innerHTML =
|
|
2014
|
+
'<div class="card-hdr" onclick="this.parentElement.classList.toggle(\'collapsed\')">' +
|
|
2015
|
+
'<span class="card-title" data-f="sc-title"></span>' +
|
|
2016
|
+
'<div class="card-hdr-right">' +
|
|
2017
|
+
'<div class="r-toggle' + (d.enabled !== false ? ' on' : '') + '" onclick="event.stopPropagation();this.classList.toggle(\'on\')" data-f="sc-enabled"></div>' +
|
|
2018
|
+
'<span class="card-remove" onclick="event.stopPropagation();this.closest(\'.card\').remove()">×</span>' +
|
|
2019
|
+
'</div>' +
|
|
2020
|
+
'</div>' +
|
|
2021
|
+
'<div class="card-content">' +
|
|
2022
|
+
'<div class="r"><span class="r-name-wide">Name</span><input class="r-input" type="text" placeholder="Schedule name" value="" data-f="sc-name" style="flex:1;"></div>' +
|
|
2023
|
+
'<div class="r"><span class="r-name-wide">Frequency</span>' +
|
|
2024
|
+
'<select class="r-select" data-f="sc-freq" onchange="onSchedFreqChange(this)">' +
|
|
2025
|
+
'<option value="interval"' + (effFreq === 'interval' ? ' selected' : '') + '>Interval</option>' +
|
|
2026
|
+
'<option value="daily"' + (effFreq === 'daily' ? ' selected' : '') + '>Daily</option>' +
|
|
2027
|
+
'<option value="weekday"' + (effFreq === 'weekday' ? ' selected' : '') + '>Weekdays</option>' +
|
|
2028
|
+
'<option value="weekly"' + (effFreq === 'weekly' ? ' selected' : '') + '>Weekly</option>' +
|
|
2029
|
+
'</select></div>' +
|
|
2030
|
+
'<div class="r" data-interval-row><span class="r-name-wide">Every</span>' +
|
|
2031
|
+
'<input class="r-input" type="text" placeholder="01:30" value="" data-f="sc-interval" style="width:80px">' +
|
|
2032
|
+
'<span style="font-size:11px;color:var(--text-4);margin-left:8px">HH:MM</span></div>' +
|
|
2033
|
+
'<div class="r" data-at-row style="display:none"><span class="r-name-wide">At</span>' +
|
|
2034
|
+
'<input class="r-input" type="text" placeholder="09:00" value="" data-f="sc-at" style="width:80px"></div>' +
|
|
2035
|
+
'<div class="r"><span class="r-name-wide">Channel</span>' + makeChannelSelect(d.channel, { placeholder: '(here \u00b7 current session)', onchange: 'onSchedChannelChange(this)' }) + '</div>' +
|
|
2036
|
+
'<div class="r" data-model-row style="' + (d.channel ? '' : 'display:none') + '"><span class="r-name-wide">Model</span><select class="r-select" data-f="sc-model" data-initial=""></select></div>' +
|
|
2037
|
+
'<div style="padding:12px 14px;border-top:1px solid var(--border)">' +
|
|
2038
|
+
'<div style="display:flex;align-items:center;margin-bottom:6px">' +
|
|
2039
|
+
'<span style="font-size:11px;color:var(--text-3)">Instructions</span>' +
|
|
2040
|
+
'<span class="file-open" data-f="sc-open">Open</span>' +
|
|
2041
|
+
'</div>' +
|
|
2042
|
+
'<textarea class="r-input" data-f="sc-prompt" placeholder="Describe what the bot should do." style="width:100%;height:150px;resize:vertical;font-family:inherit;padding:10px;line-height:1.5"></textarea>' +
|
|
2043
|
+
'</div>' +
|
|
2044
|
+
'</div>';
|
|
2045
|
+
list.appendChild(card);
|
|
2046
|
+
const nameInput = card.querySelector('[data-f="sc-name"]');
|
|
2047
|
+
if (nameInput) nameInput.value = d.name || '';
|
|
2048
|
+
const titleEl = card.querySelector('[data-f="sc-title"]');
|
|
2049
|
+
if (titleEl) titleEl.textContent = nameVal;
|
|
2050
|
+
nameInput?.addEventListener('input', () => { titleEl.textContent = nameInput.value || 'Untitled'; });
|
|
2051
|
+
const scOpenBtn = card.querySelector('[data-f="sc-open"]');
|
|
2052
|
+
if (scOpenBtn) scOpenBtn.addEventListener('click', () => schedOpenPrompt(encodeURIComponent(d.name || '')));
|
|
2053
|
+
const promptTA = card.querySelector('[data-f="sc-prompt"]');
|
|
2054
|
+
if (promptTA) promptTA.value = d.prompt || '';
|
|
2055
|
+
if (d.name) card.classList.add('collapsed');
|
|
2056
|
+
const freqSel = card.querySelector('[data-f="sc-freq"]');
|
|
2057
|
+
if (freqSel) onSchedFreqChange(freqSel);
|
|
2058
|
+
const intervalInput = card.querySelector('[data-f="sc-interval"]');
|
|
2059
|
+
if (intervalInput) intervalInput.value = timeUi.interval || '01:00';
|
|
2060
|
+
const atInput = card.querySelector('[data-f="sc-at"]');
|
|
2061
|
+
if (atInput) atInput.value = timeUi.at || '09:00';
|
|
2062
|
+
// Fill model dropdown immediately
|
|
2063
|
+
const modelSel = card.querySelector('[data-f="sc-model"]');
|
|
2064
|
+
if (modelSel) {
|
|
2065
|
+
modelSel.setAttribute('data-initial', d.model || 'sonnet-mid');
|
|
2066
|
+
const presets = agPresets.length ? agPresets : (agConfig?.presets || []);
|
|
2067
|
+
const initial = d.model || 'sonnet-mid';
|
|
2068
|
+
modelSel.innerHTML = '';
|
|
2069
|
+
presets.forEach(p => { const opt = document.createElement('option'); opt.value = p.id || p.name; opt.textContent = p.name || p.id; if (initial === opt.value) opt.selected = true; modelSel.appendChild(opt); });
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
function webhookOpenInstructions(encodedName) {
|
|
2074
|
+
const name = decodeURIComponent(encodedName);
|
|
2075
|
+
if (!name) { alert('Save the webhook first.'); return; }
|
|
2076
|
+
fetch('/webhooks/file/' + encodedName);
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
function schedOpenPrompt(encodedName) {
|
|
2080
|
+
const name = decodeURIComponent(encodedName);
|
|
2081
|
+
if (!name) { alert('Save the schedule first.'); return; }
|
|
2082
|
+
fetch('/schedules/file/' + encodedName);
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
function onSchedFreqChange(sel) {
|
|
2086
|
+
const card = sel.closest('.card');
|
|
2087
|
+
const intervalRow = card.querySelector('[data-interval-row]');
|
|
2088
|
+
const atRow = card.querySelector('[data-at-row]');
|
|
2089
|
+
if (sel.value === 'interval') { intervalRow.style.display = ''; atRow.style.display = 'none'; }
|
|
2090
|
+
else { intervalRow.style.display = 'none'; atRow.style.display = ''; }
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
function onSchedChannelChange(sel) {
|
|
2094
|
+
const card = sel.closest('.card');
|
|
2095
|
+
const modelRow = card.querySelector('[data-model-row]');
|
|
2096
|
+
if (modelRow) modelRow.style.display = sel.value ? '' : 'none';
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
// -- Event cards --
|
|
2100
|
+
function addEventCard(data) {
|
|
2101
|
+
const list = document.getElementById('event-rules');
|
|
2102
|
+
const emptyEl = document.getElementById('webhook-empty');
|
|
2103
|
+
if (emptyEl) emptyEl.style.display = 'none';
|
|
2104
|
+
const card = document.createElement('div');
|
|
2105
|
+
card.className = 'card';
|
|
2106
|
+
const d = data || {};
|
|
2107
|
+
const nameVal = d.name || 'Untitled';
|
|
2108
|
+
const isExisting = !!d.name;
|
|
2109
|
+
card.dataset.originalName = d.name || '';
|
|
2110
|
+
card.innerHTML =
|
|
2111
|
+
'<div class="card-hdr" onclick="this.parentElement.classList.toggle(\'collapsed\')">' +
|
|
2112
|
+
'<span class="card-title" data-f="ev-title"></span>' +
|
|
2113
|
+
'<div class="card-hdr-right">' +
|
|
2114
|
+
'<span class="card-remove" onclick="event.stopPropagation();deleteWebhook(this.closest(\'.card\'))">×</span>' +
|
|
2115
|
+
'</div>' +
|
|
2116
|
+
'</div>' +
|
|
2117
|
+
'<div class="card-content">' +
|
|
2118
|
+
'<div class="r"><span class="r-name-wide">Name</span><input class="r-input" type="text" placeholder="Endpoint name (e.g. github-issues)" value="" data-f="ev-name" style="flex:1;" oninput="this.closest(\'.card\').querySelector(\'[data-f=ev-title]\').textContent=this.value||\'Untitled\'">' + '</div>' +
|
|
2119
|
+
'<div class="r"><span class="r-name-wide">Channel</span><input class="r-input" type="text" placeholder="main" value="" data-f="ev-channel" style="flex:1;"></div>' +
|
|
2120
|
+
'<div class="r" data-f="ev-model-row"><span class="r-name-wide">Model</span>' +
|
|
2121
|
+
'<select class="r-select" data-f="ev-model" data-initial=""></select></div>' +
|
|
2122
|
+
'<div class="r"><span class="r-name-wide">Secret</span><input class="r-input" type="password" value="" placeholder="Optional" data-f="ev-secret" autocomplete="new-password"></div>' +
|
|
2123
|
+
'<div style="padding:12px 14px;border-top:1px solid var(--border)">' +
|
|
2124
|
+
'<div style="display:flex;align-items:center;margin-bottom:6px">' +
|
|
2125
|
+
'<span style="font-size:11px;color:var(--text-3)">Instructions</span>' +
|
|
2126
|
+
'<span class="file-open" data-f="ev-open">Open</span>' +
|
|
2127
|
+
'</div>' +
|
|
2128
|
+
'<textarea class="r-input" data-f="ev-instructions" placeholder="Describe how to handle this webhook payload." style="width:100%;height:150px;resize:vertical;font-family:inherit;padding:10px;line-height:1.5"></textarea>' +
|
|
2129
|
+
'</div>' +
|
|
2130
|
+
'</div>';
|
|
2131
|
+
list.appendChild(card);
|
|
2132
|
+
const evNameInput = card.querySelector('[data-f="ev-name"]');
|
|
2133
|
+
const evTitleEl = card.querySelector('[data-f="ev-title"]');
|
|
2134
|
+
if (evTitleEl) evTitleEl.textContent = nameVal;
|
|
2135
|
+
if (evNameInput) evNameInput.value = d.name || '';
|
|
2136
|
+
const evChannelInput = card.querySelector('[data-f="ev-channel"]');
|
|
2137
|
+
if (evChannelInput) evChannelInput.value = d.channel || 'main';
|
|
2138
|
+
const evSecretInput = card.querySelector('[data-f="ev-secret"]');
|
|
2139
|
+
if (evSecretInput) evSecretInput.value = d.secret || '';
|
|
2140
|
+
const evInstrTA = card.querySelector('[data-f="ev-instructions"]');
|
|
2141
|
+
if (evInstrTA) evInstrTA.value = d.instructions || '';
|
|
2142
|
+
evNameInput?.addEventListener('input', () => { evTitleEl.textContent = evNameInput.value || 'Untitled'; });
|
|
2143
|
+
if (isExisting) card.classList.add('collapsed');
|
|
2144
|
+
// Fill model dropdown from agent presets. Defer when presets not yet
|
|
2145
|
+
// loaded — populatePresetDropdowns() will refill once they arrive.
|
|
2146
|
+
const evModelSel = card.querySelector('[data-f="ev-model"]');
|
|
2147
|
+
if (evModelSel) {
|
|
2148
|
+
evModelSel.setAttribute('data-initial', d.model || '');
|
|
2149
|
+
const presets = agPresets.length ? agPresets : (agConfig?.presets || []);
|
|
2150
|
+
presets.forEach(p => {
|
|
2151
|
+
const opt = document.createElement('option');
|
|
2152
|
+
opt.value = p.id || p.name;
|
|
2153
|
+
opt.textContent = p.name || p.id;
|
|
2154
|
+
if (d.model && (p.id === d.model || p.name === d.model)) opt.selected = true;
|
|
2155
|
+
evModelSel.appendChild(opt);
|
|
2156
|
+
});
|
|
2157
|
+
}
|
|
2158
|
+
const evOpenBtn = card.querySelector('[data-f="ev-open"]');
|
|
2159
|
+
if (evOpenBtn) evOpenBtn.addEventListener('click', () => webhookOpenInstructions(encodeURIComponent(d.name || '')));
|
|
2160
|
+
// Role is no longer user-selectable — POST /webhooks pins it to the
|
|
2161
|
+
// `webhook-handler` hidden role on the server side.
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
async function deleteWebhook(card) {
|
|
2165
|
+
const name = card.querySelector('[data-f="ev-name"]')?.value;
|
|
2166
|
+
if (name) { await fetch('/webhooks?name=' + encodeURIComponent(name), { method: 'DELETE' }); }
|
|
2167
|
+
card.remove();
|
|
2168
|
+
const list = document.getElementById('event-rules');
|
|
2169
|
+
const emptyEl = document.getElementById('webhook-empty');
|
|
2170
|
+
if (emptyEl && !list.querySelector('.card')) emptyEl.style.display = '';
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
// -- Advanced --
|
|
2174
|
+
async function loadChannelsCliCheck() {
|
|
2175
|
+
try {
|
|
2176
|
+
const data = await fetch('/cli-check').then(r => r.json());
|
|
2177
|
+
cliStatus = data;
|
|
2178
|
+
// Attach voice config paths for the installed-state display.
|
|
2179
|
+
if (data.whisper?.installed && data.voice) {
|
|
2180
|
+
cliStatus._tribCfgVoice = data.voice;
|
|
2181
|
+
}
|
|
2182
|
+
} catch {}
|
|
2183
|
+
}
|
|
2184
|
+
|
|
2185
|
+
function populatePresetDropdowns() {
|
|
2186
|
+
const presets = agPresets.length ? agPresets : (agConfig?.presets || []);
|
|
2187
|
+
console.log('[mixdog] populatePresetDropdowns:', presets.length, 'presets');
|
|
2188
|
+
if (!presets.length) return;
|
|
2189
|
+
// Schedule cards — fill model dropdowns
|
|
2190
|
+
document.querySelectorAll('#sched-tasks .card [data-f="sc-model"]').forEach(sel => {
|
|
2191
|
+
const current = sel.value || sel.dataset.initial || '';
|
|
2192
|
+
sel.innerHTML = ''; presets.forEach(p => { const opt = document.createElement('option'); opt.value = p.id || p.name; opt.textContent = p.name || p.id; if (current === opt.value) opt.selected = true; sel.appendChild(opt); });
|
|
2193
|
+
});
|
|
2194
|
+
// Webhook cards — fill model dropdowns (required for delegate mode)
|
|
2195
|
+
document.querySelectorAll('#event-rules .card [data-f="ev-model"]').forEach(sel => {
|
|
2196
|
+
const current = sel.value || sel.dataset.initial || '';
|
|
2197
|
+
sel.innerHTML = ''; presets.forEach(p => { const opt = document.createElement('option'); opt.value = p.id || p.name; opt.textContent = p.name || p.id; if (current === opt.value) opt.selected = true; sel.appendChild(opt); });
|
|
2198
|
+
});
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
function renderAdvanced() {
|
|
2202
|
+
renderVoiceSection();
|
|
2203
|
+
renderWebhookSection();
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
function renderVoiceSection() {
|
|
2207
|
+
const el = document.getElementById('voice-section');
|
|
2208
|
+
if (!cliStatus.whisper?.installed) {
|
|
2209
|
+
const missing = [];
|
|
2210
|
+
if (!cliStatus.whisper?.binary) missing.push('binary');
|
|
2211
|
+
if (!cliStatus.whisper?.model) missing.push('model');
|
|
2212
|
+
if (!cliStatus.whisper?.ffmpeg) missing.push('ffmpeg');
|
|
2213
|
+
const partial = missing.length > 0 && missing.length < 3;
|
|
2214
|
+
const action = partial ? 'Repair' : 'Install';
|
|
2215
|
+
const runtimeLabel = cliStatus.voice?.label || 'whisper.cpp';
|
|
2216
|
+
const modelName = cliStatus.voice?.modelName || 'large-v3-turbo';
|
|
2217
|
+
const detail = missing.length
|
|
2218
|
+
? 'Missing: ' + missing.join(', ') + ' · ' + runtimeLabel + ' · ' + modelName
|
|
2219
|
+
: runtimeLabel + ' · ' + modelName;
|
|
2220
|
+
el.innerHTML =
|
|
2221
|
+
'<div class="r">' +
|
|
2222
|
+
'<span class="r-name-wide">Speech-to-text</span>' +
|
|
2223
|
+
'<span class="r-tag no">Not installed</span>' +
|
|
2224
|
+
'<span style="flex:1;font-size:11px;color:var(--text-3);margin-left:12px">' + escapeHtml(detail) + '</span>' +
|
|
2225
|
+
'<button class="save" id="btn-install-voice" data-action-label="' + action + '" onclick="installVoice()" style="width:auto;height:28px;padding:0 14px;font-size:11px;flex-shrink:0">' + action + '</button>' +
|
|
2226
|
+
'</div>' +
|
|
2227
|
+
'<div id="voice-install-status" style="font-size:11px;color:var(--text-3);display:none;padding:6px 14px;">' +
|
|
2228
|
+
'<div id="voice-install-progress-track" style="height:4px;background:var(--border);border-radius:2px;overflow:hidden;margin-bottom:6px;display:none">' +
|
|
2229
|
+
'<div id="voice-install-progress-bar" style="height:100%;width:0%;background:var(--accent);transition:width .15s"></div>' +
|
|
2230
|
+
'</div>' +
|
|
2231
|
+
'<div id="voice-install-progress-text"></div>' +
|
|
2232
|
+
'</div>';
|
|
2233
|
+
} else {
|
|
2234
|
+
const runtimeLabel = cliStatus.voice?.label || 'whisper.cpp';
|
|
2235
|
+
const modelName = cliStatus.voice?.modelName || 'large-v3-turbo';
|
|
2236
|
+
const action = 'Reinstall';
|
|
2237
|
+
el.innerHTML =
|
|
2238
|
+
'<div class="r">' +
|
|
2239
|
+
'<span class="r-name-wide">Speech-to-text</span>' +
|
|
2240
|
+
'<span class="r-tag ok">Installed</span>' +
|
|
2241
|
+
'<span style="flex:1;font-size:11px;color:var(--text-3);margin-left:12px">' + escapeHtml(runtimeLabel + ' · ' + modelName) + '</span>' +
|
|
2242
|
+
'<a href="#" id="btn-install-voice" data-action-label="' + action + '" onclick="installVoice();return false;" class="r-link" style="font-size:11px;">' + action + '</a>' +
|
|
2243
|
+
'</div>' +
|
|
2244
|
+
'<div id="voice-install-status" style="font-size:11px;color:var(--text-3);display:none;padding:6px 14px;">' +
|
|
2245
|
+
'<div id="voice-install-progress-track" style="height:4px;background:var(--border);border-radius:2px;overflow:hidden;margin-bottom:6px;display:none">' +
|
|
2246
|
+
'<div id="voice-install-progress-bar" style="height:100%;width:0%;background:var(--accent);transition:width .15s"></div>' +
|
|
2247
|
+
'</div>' +
|
|
2248
|
+
'<div id="voice-install-progress-text"></div>' +
|
|
2249
|
+
'</div>';
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
function fmtMB(bytes) {
|
|
2254
|
+
const n = Number(bytes) || 0;
|
|
2255
|
+
return (n / (1024 * 1024)).toFixed(1) + ' MB';
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
function fmtGB(bytes) {
|
|
2259
|
+
const n = Number(bytes) || 0;
|
|
2260
|
+
return (n / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
function installVoice() {
|
|
2264
|
+
const trigger = document.getElementById('btn-install-voice');
|
|
2265
|
+
const statusEl = document.getElementById('voice-install-status');
|
|
2266
|
+
const textEl = document.getElementById('voice-install-progress-text');
|
|
2267
|
+
const trackEl = document.getElementById('voice-install-progress-track');
|
|
2268
|
+
const barEl = document.getElementById('voice-install-progress-bar');
|
|
2269
|
+
const idleLabel = trigger?.dataset?.actionLabel || 'Install';
|
|
2270
|
+
const isButton = trigger?.tagName === 'BUTTON';
|
|
2271
|
+
|
|
2272
|
+
const restoreTrigger = () => {
|
|
2273
|
+
if (!trigger) return;
|
|
2274
|
+
trigger.textContent = idleLabel;
|
|
2275
|
+
if (isButton) trigger.disabled = false;
|
|
2276
|
+
else trigger.style.pointerEvents = '';
|
|
2277
|
+
trigger.style.opacity = '';
|
|
2278
|
+
};
|
|
2279
|
+
|
|
2280
|
+
const setInstalling = () => {
|
|
2281
|
+
if (!trigger) return;
|
|
2282
|
+
trigger.textContent = 'Installing\u2026';
|
|
2283
|
+
if (isButton) trigger.disabled = true;
|
|
2284
|
+
else { trigger.style.pointerEvents = 'none'; trigger.style.opacity = '0.6'; }
|
|
2285
|
+
};
|
|
2286
|
+
|
|
2287
|
+
const setStatus = (msg, color, { barPct = null, indeterminate = false } = {}) => {
|
|
2288
|
+
if (statusEl) {
|
|
2289
|
+
statusEl.style.display = 'block';
|
|
2290
|
+
statusEl.style.color = color || 'var(--text-4)';
|
|
2291
|
+
}
|
|
2292
|
+
if (textEl) textEl.textContent = msg || '';
|
|
2293
|
+
if (trackEl && barEl) {
|
|
2294
|
+
if (barPct == null && !indeterminate) {
|
|
2295
|
+
trackEl.style.display = 'none';
|
|
2296
|
+
barEl.style.width = '0%';
|
|
2297
|
+
} else {
|
|
2298
|
+
trackEl.style.display = 'block';
|
|
2299
|
+
if (indeterminate) {
|
|
2300
|
+
barEl.style.width = '30%';
|
|
2301
|
+
} else {
|
|
2302
|
+
barEl.style.width = Math.min(100, Math.max(0, barPct)) + '%';
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
};
|
|
2307
|
+
|
|
2308
|
+
setInstalling();
|
|
2309
|
+
setStatus('Starting install\u2026', 'var(--text-3)', { indeterminate: true });
|
|
2310
|
+
|
|
2311
|
+
(async () => {
|
|
2312
|
+
try {
|
|
2313
|
+
const resp = await fetch('/install/voice-runtime', { method: 'POST' });
|
|
2314
|
+
if (!resp.ok || !resp.body) throw new Error('install request failed (' + resp.status + ')');
|
|
2315
|
+
const reader = resp.body.getReader();
|
|
2316
|
+
const decoder = new TextDecoder();
|
|
2317
|
+
let buf = '';
|
|
2318
|
+
|
|
2319
|
+
const handleLine = (line) => {
|
|
2320
|
+
if (!line) return;
|
|
2321
|
+
let d;
|
|
2322
|
+
try { d = JSON.parse(line); } catch { return; }
|
|
2323
|
+
if (d.type === 'progress') {
|
|
2324
|
+
const phase = d.phase || 'download';
|
|
2325
|
+
const downloaded = Number(d.downloaded) || 0;
|
|
2326
|
+
const total = Number(d.total) || 0;
|
|
2327
|
+
if (total > 0) {
|
|
2328
|
+
const pct = Math.floor((downloaded / total) * 100);
|
|
2329
|
+
setStatus(
|
|
2330
|
+
phase + ' \u00b7 ' + fmtMB(downloaded) + ' / ' + fmtMB(total) + ' (' + pct + '%)',
|
|
2331
|
+
'var(--text-3)',
|
|
2332
|
+
{ barPct: pct },
|
|
2333
|
+
);
|
|
2334
|
+
} else {
|
|
2335
|
+
setStatus(
|
|
2336
|
+
phase + ' \u00b7 received ' + fmtMB(downloaded),
|
|
2337
|
+
'var(--text-3)',
|
|
2338
|
+
{ indeterminate: true },
|
|
2339
|
+
);
|
|
2340
|
+
}
|
|
2341
|
+
} else if (d.type === 'done' && d.ok) {
|
|
2342
|
+
setStatus('\u2713 Done', 'var(--green)', { barPct: 100 });
|
|
2343
|
+
restoreTrigger();
|
|
2344
|
+
Promise.resolve(loadChannelsCliCheck()).then(() => renderAdvanced()).catch(() => {});
|
|
2345
|
+
} else if (d.type === 'error' || (d.type === 'done' && !d.ok)) {
|
|
2346
|
+
restoreTrigger();
|
|
2347
|
+
setStatus('\u2717 ' + (d.error || 'install failed'), 'var(--red)', { barPct: null });
|
|
2348
|
+
}
|
|
2349
|
+
};
|
|
2350
|
+
|
|
2351
|
+
while (true) {
|
|
2352
|
+
const { value, done } = await reader.read();
|
|
2353
|
+
if (done) break;
|
|
2354
|
+
buf += decoder.decode(value, { stream: true });
|
|
2355
|
+
const parts = buf.split('\n');
|
|
2356
|
+
buf = parts.pop() || '';
|
|
2357
|
+
for (const line of parts) handleLine(line.trim());
|
|
2358
|
+
}
|
|
2359
|
+
if (buf.trim()) handleLine(buf.trim());
|
|
2360
|
+
} catch (err) {
|
|
2361
|
+
restoreTrigger();
|
|
2362
|
+
setStatus('\u2717 ' + (err && err.message || String(err)), 'var(--red)', { barPct: null });
|
|
2363
|
+
}
|
|
2364
|
+
})();
|
|
2365
|
+
return;
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
function toggleCollapsible(toggleEl) {
|
|
2369
|
+
toggleEl.classList.toggle('open');
|
|
2370
|
+
const body = toggleEl.nextElementSibling;
|
|
2371
|
+
body.classList.toggle('open');
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2374
|
+
function renderWebhookSection() {
|
|
2375
|
+
const el = document.getElementById('webhook-section');
|
|
2376
|
+
if (!cliStatus.ngrok?.installed) {
|
|
2377
|
+
el.innerHTML =
|
|
2378
|
+
'<div class="cli-status warn">' +
|
|
2379
|
+
'<span style="font-size:16px;">⚠</span>' +
|
|
2380
|
+
'<div>ngrok not found<br>' +
|
|
2381
|
+
'<span style="font-size:11px;color:var(--text-4)">Receives external events (GitHub, Sentry, etc.)</span></div>' +
|
|
2382
|
+
'<button class="save" id="btn-install-ngrok" onclick="installTool(\'ngrok\')" style="width:auto;height:30px;padding:0 14px;margin-left:auto;font-size:11px">Install</button>' +
|
|
2383
|
+
'</div>';
|
|
2384
|
+
} else {
|
|
2385
|
+
const w = chConfig?.webhook || {};
|
|
2386
|
+
// Layout: Enabled toggle → Domain (primary) → Auth Token → links row.
|
|
2387
|
+
// The two "Get" links sit beneath the inputs on their own row so they
|
|
2388
|
+
// do not crowd the input fields. Custom domains require an authtoken;
|
|
2389
|
+
// the layout makes that ordering obvious.
|
|
2390
|
+
el.innerHTML =
|
|
2391
|
+
'<div class="r"><span class="r-name-wide">Enabled</span><div class="r-toggle' + (w.enabled ? ' on' : '') + '" onclick="this.classList.toggle(\'on\')" id="ch-webhook-enabled"></div></div>' +
|
|
2392
|
+
'<div class="r"><span class="r-name-wide">Domain</span><input class="r-input" id="ch-webhook-domain" type="text" placeholder="your-name.ngrok-free.dev" autocomplete="off"></div>' +
|
|
2393
|
+
'<div class="r"><span class="r-name-wide">Auth Token</span><div class="input-group" style="flex:1;width:auto;"><input class="r-input" id="ch-webhook-authtoken" type="password" placeholder="2x..." autocomplete="new-password"><button class="eye-btn" onclick="toggleVis(this)" type="button">👁</button></div></div>' +
|
|
2394
|
+
'<div class="r" style="min-height:auto;padding:8px 14px;gap:14px;justify-content:flex-end;">' +
|
|
2395
|
+
'<a href="https://dashboard.ngrok.com/domains" target="_blank" style="color:var(--accent);text-decoration:none;font-size:11px;">Get Domain ↗</a>' +
|
|
2396
|
+
'<a href="https://dashboard.ngrok.com/get-started/your-authtoken" target="_blank" style="color:var(--accent);text-decoration:none;font-size:11px;">Get Auth Token ↗</a>' +
|
|
2397
|
+
'</div>';
|
|
2398
|
+
document.getElementById('ch-webhook-authtoken').value = w.authtoken || '';
|
|
2399
|
+
document.getElementById('ch-webhook-domain').value = w.domain || '';
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
async function installTool(tool) {
|
|
2404
|
+
const btn = document.getElementById('btn-install-' + tool);
|
|
2405
|
+
if (!btn) return;
|
|
2406
|
+
btn.textContent = 'Installing...'; btn.disabled = true;
|
|
2407
|
+
try {
|
|
2408
|
+
const r = await fetch('/install', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tool }) }).then(r => r.json());
|
|
2409
|
+
if (r.ok) { btn.textContent = '\u2713 Done'; btn.classList.add('done'); await loadChannelsCliCheck(); renderAdvanced(); }
|
|
2410
|
+
else { btn.textContent = 'Failed'; setTimeout(() => { btn.textContent = 'Install'; btn.disabled = false; }, 3000); }
|
|
2411
|
+
} catch { btn.textContent = 'Failed'; setTimeout(() => { btn.textContent = 'Install'; btn.disabled = false; }, 3000); }
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
// -- Channels load / populate --
|
|
2415
|
+
async function loadChannelsConfig() {
|
|
2416
|
+
const cfgRes = await fetch('/config').then(r => { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); });
|
|
2417
|
+
const invalidDiscordToken = cfgRes?._secretDiagnostics?.discordToken;
|
|
2418
|
+
if (invalidDiscordToken?.invalid) {
|
|
2419
|
+
const warn = document.getElementById('warn-connection');
|
|
2420
|
+
if (warn) warn.textContent = invalidDiscordToken.problem || 'Saved Bot token is invalid and was cleared.';
|
|
2421
|
+
}
|
|
2422
|
+
chConfig = cfgRes;
|
|
2423
|
+
if (cfgRes._bot) { chExtraData.bot = cfgRes._bot; delete cfgRes._bot; }
|
|
2424
|
+
if (cfgRes._secretDiagnostics) delete cfgRes._secretDiagnostics;
|
|
2425
|
+
await populateChannels(cfgRes);
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2428
|
+
async function populateChannels(cfg) {
|
|
2429
|
+
// Connection
|
|
2430
|
+
const discord = cfg?.discord || {};
|
|
2431
|
+
if (discord.token) document.getElementById('ch-discord-token').value = discord.token;
|
|
2432
|
+
if (discord.applicationId) document.getElementById('ch-discord-appid').value = discord.applicationId;
|
|
2433
|
+
|
|
2434
|
+
// Channels
|
|
2435
|
+
const chCfg = cfg?.channelsConfig || {};
|
|
2436
|
+
const mainCh = cfg?.mainChannel;
|
|
2437
|
+
let first = true;
|
|
2438
|
+
for (const [name, ch] of Object.entries(chCfg)) {
|
|
2439
|
+
const isMain = mainCh ? (name === mainCh) : first;
|
|
2440
|
+
addChannel(name, ch.channelId, ch.mode || 'interactive', isMain);
|
|
2441
|
+
first = false;
|
|
2442
|
+
}
|
|
2443
|
+
if (Object.keys(chCfg).length === 0) addChannel('main', '', 'interactive', true);
|
|
2444
|
+
|
|
2445
|
+
// Access
|
|
2446
|
+
const access = cfg?.access || {};
|
|
2447
|
+
if (access.dmPolicy) document.getElementById('ch-dm-policy').value = access.dmPolicy;
|
|
2448
|
+
setTagValues('ch-allowfrom', access.allowFrom);
|
|
2449
|
+
setTagValues('ch-mentionpatterns', access.mentionPatterns);
|
|
2450
|
+
if (access.ackReaction) document.getElementById('ch-ackreaction').classList.add('on');
|
|
2451
|
+
|
|
2452
|
+
// DND: top-level cfg.quiet (server always populates default via applyChannelsDefaults)
|
|
2453
|
+
const quiet = cfg?.quiet || {};
|
|
2454
|
+
populateQuietHours(quiet.schedule);
|
|
2455
|
+
if (quiet.holidays) document.getElementById('dnd-holidays').classList.add('on');
|
|
2456
|
+
const setToggle = (id, on) => { const el = document.getElementById(id); if (!el) return; if (on) el.classList.add('on'); else el.classList.remove('on'); };
|
|
2457
|
+
setToggle('dnd-respect-webhook', cfg?.webhook?.respectQuiet ?? false);
|
|
2458
|
+
setToggle('dnd-respect-schedule', cfg?.schedules?.respectQuiet ?? true);
|
|
2459
|
+
|
|
2460
|
+
// Schedules (folder-based) — same invariant as webhooks: `${CLAUDE_PLUGIN_DATA}/schedules/<name>/`
|
|
2461
|
+
// is the single source of truth. `cfg.schedules.items` / `interactive` / `nonInteractive` were a
|
|
2462
|
+
// parallel store that never absorbed entries registered via the `schedule-add` skill, so cards
|
|
2463
|
+
// disappeared from the setup UI even though the scheduler was firing them.
|
|
2464
|
+
try { const scs = await fetch('/schedules').then(r => r.json()); scs.forEach(s => addScheduleCard(s)); } catch {}
|
|
2465
|
+
|
|
2466
|
+
// Webhooks (folder-based)
|
|
2467
|
+
try { const whs = await fetch('/webhooks').then(r => r.json()); whs.forEach(w => addEventCard(w)); } catch {}
|
|
2468
|
+
}
|
|
2469
|
+
|
|
2470
|
+
function populateQuietHours(schedule) {
|
|
2471
|
+
if (!schedule) return;
|
|
2472
|
+
const presets = ['23:00-09:00', '23:00-07:00', '22:00-08:00', '00:00-09:00'];
|
|
2473
|
+
const sel = document.getElementById('dnd-quiet-preset');
|
|
2474
|
+
if (!sel) return;
|
|
2475
|
+
if (presets.includes(schedule)) { sel.value = schedule; }
|
|
2476
|
+
else { sel.value = 'custom'; document.getElementById('dnd-quiet-custom').classList.remove('hidden'); const parts = schedule.split('-'); if (parts.length === 2) { document.getElementById('dnd-quiet-start').value = parts[0]; document.getElementById('dnd-quiet-end').value = parts[1]; } }
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
// -- Channels build data --
|
|
2480
|
+
function buildChannelsData() {
|
|
2481
|
+
const channelsConfig = {};
|
|
2482
|
+
let mainChannel = null;
|
|
2483
|
+
document.querySelectorAll('#ch-channel-list .dyn-item').forEach(item => {
|
|
2484
|
+
const label = item.querySelector('[data-f="ch-label"]')?.value.trim() || 'main';
|
|
2485
|
+
const chId = item.querySelector('[data-f="ch-id"]')?.value.trim();
|
|
2486
|
+
const mode = item.querySelector('[data-f="ch-mode"]')?.value || 'interactive';
|
|
2487
|
+
const isMain = item.querySelector('.r-radio')?.classList.contains('on');
|
|
2488
|
+
if (chId) { channelsConfig[label] = { channelId: chId, mode }; if (isMain) mainChannel = label; }
|
|
2489
|
+
});
|
|
2490
|
+
|
|
2491
|
+
const quietPreset = document.getElementById('dnd-quiet-preset').value;
|
|
2492
|
+
let quietSchedule = '';
|
|
2493
|
+
if (quietPreset === 'custom') { const s = document.getElementById('dnd-quiet-start').value.trim(); const e = document.getElementById('dnd-quiet-end').value.trim(); if (s && e) quietSchedule = s + '-' + e; }
|
|
2494
|
+
else if (quietPreset !== 'none') { quietSchedule = quietPreset; }
|
|
2495
|
+
const quietHolidays = document.getElementById('dnd-holidays').classList.contains('on') ? DEFAULT_HOLIDAY_COUNTRY : false;
|
|
2496
|
+
const respectWebhook = document.getElementById('dnd-respect-webhook').classList.contains('on');
|
|
2497
|
+
const respectSchedule = document.getElementById('dnd-respect-schedule').classList.contains('on');
|
|
2498
|
+
|
|
2499
|
+
// Schedule cards are persisted exclusively via POST /schedules (one
|
|
2500
|
+
// request per card → `${CLAUDE_PLUGIN_DATA}/schedules/<name>/`). POST
|
|
2501
|
+
// /config no longer carries a `schedules.items` payload; only the
|
|
2502
|
+
// `respectQuiet` flag travels with the channels section.
|
|
2503
|
+
|
|
2504
|
+
const voice = {};
|
|
2505
|
+
const voiceCmd = document.getElementById('voice-command');
|
|
2506
|
+
if (voiceCmd) { voice.command = voiceCmd.value; voice.model = document.getElementById('voice-model')?.value || ''; voice.language = document.getElementById('voice-language')?.value || 'auto'; voice.pythonModel = document.getElementById('voice-python-model')?.value || ''; }
|
|
2507
|
+
|
|
2508
|
+
const webhook = { respectQuiet: respectWebhook };
|
|
2509
|
+
const webhookAuthtoken = document.getElementById('ch-webhook-authtoken');
|
|
2510
|
+
if (webhookAuthtoken) { webhook.enabled = document.getElementById('ch-webhook-enabled')?.classList.contains('on') || false; webhook.authtoken = webhookAuthtoken.value; webhook.domain = document.getElementById('ch-webhook-domain')?.value || undefined; webhook.port = parseInt(document.getElementById('ch-webhook-port')?.value) || undefined; webhook.batchInterval = parseInt(document.getElementById('ch-webhook-batch-interval')?.value) || undefined; }
|
|
2511
|
+
|
|
2512
|
+
return {
|
|
2513
|
+
discord: { token: document.getElementById('ch-discord-token').value, applicationId: document.getElementById('ch-discord-appid').value },
|
|
2514
|
+
channelsConfig, mainChannel,
|
|
2515
|
+
access: { dmPolicy: document.getElementById('ch-dm-policy').value, allowFrom: getTagValues('ch-allowfrom'), mentionPatterns: getTagValues('ch-mentionpatterns'), ackReaction: document.getElementById('ch-ackreaction').classList.contains('on') },
|
|
2516
|
+
voice, schedules: { respectQuiet: respectSchedule },
|
|
2517
|
+
webhook,
|
|
2518
|
+
quiet: { schedule: quietSchedule, holidays: quietHolidays },
|
|
2519
|
+
};
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2522
|
+
// -- Channels save --
|
|
2523
|
+
async function savePanel() {
|
|
2524
|
+
const activePanel = document.querySelector('.panel.active');
|
|
2525
|
+
const warnEl = activePanel?.querySelector('.warn-msg') || document.createElement('div');
|
|
2526
|
+
const btn = activePanel?.querySelector('.save');
|
|
2527
|
+
warnEl.textContent = '';
|
|
2528
|
+
// DND custom quiet-hours validation. Runs regardless of which panel is
|
|
2529
|
+
// active since buildChannelsData() reads the DND fields unconditionally.
|
|
2530
|
+
const quietPresetEl = document.getElementById('dnd-quiet-preset');
|
|
2531
|
+
if (quietPresetEl && quietPresetEl.value === 'custom') {
|
|
2532
|
+
const hhmm = /^(?:[01]\d|2[0-3]):[0-5]\d$/;
|
|
2533
|
+
const s = (document.getElementById('dnd-quiet-start')?.value || '').trim();
|
|
2534
|
+
const e = (document.getElementById('dnd-quiet-end')?.value || '').trim();
|
|
2535
|
+
if (!hhmm.test(s) || !hhmm.test(e)) {
|
|
2536
|
+
const dndWarn = document.getElementById('warn-dnd') || warnEl;
|
|
2537
|
+
dndWarn.textContent = 'Invalid quiet hours. Use HH:MM (00:00 - 23:59) for both start and end.';
|
|
2538
|
+
if (btn) { btn.textContent = 'Save'; btn.disabled = false; }
|
|
2539
|
+
return;
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
if (btn) { btn.textContent = 'Saving...'; btn.disabled = true; }
|
|
2543
|
+
try {
|
|
2544
|
+
// Save webhooks individually
|
|
2545
|
+
const whCards = document.querySelectorAll('#event-rules .card');
|
|
2546
|
+
for (const card of whCards) {
|
|
2547
|
+
const evChannel = (card.querySelector('[data-f="ev-channel"]')?.value || '').trim();
|
|
2548
|
+
const wh = {
|
|
2549
|
+
name: card.querySelector('[data-f="ev-name"]')?.value.trim() || '',
|
|
2550
|
+
secret: card.querySelector('[data-f="ev-secret"]')?.value || undefined,
|
|
2551
|
+
instructions: card.querySelector('[data-f="ev-instructions"]')?.value || '',
|
|
2552
|
+
model: card.querySelector('[data-f="ev-model"]')?.value || ''
|
|
2553
|
+
};
|
|
2554
|
+
if (evChannel && evChannel !== 'here') wh.channel = evChannel;
|
|
2555
|
+
if (wh.name) { const _wr = await fetch('/webhooks', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(wh) }); if (!_wr.ok) throw new Error('HTTP ' + _wr.status + ' ' + await _wr.text()); card.dataset.originalName = wh.name; }
|
|
2556
|
+
}
|
|
2557
|
+
// Save schedules individually
|
|
2558
|
+
const scCards = document.querySelectorAll('#sched-tasks .card');
|
|
2559
|
+
for (const card of scCards) {
|
|
2560
|
+
const name = card.querySelector('[data-f="sc-name"]')?.value.trim() || '';
|
|
2561
|
+
if (name) {
|
|
2562
|
+
const scChannel = (card.querySelector('[data-f="channel"]')?.value || '').trim();
|
|
2563
|
+
const sc = { name, time: '', days: 'daily', model: card.querySelector('[data-f="sc-model"]')?.value || '', enabled: card.querySelector('[data-f="sc-enabled"]')?.classList.contains('on') ?? true, prompt: card.querySelector('[data-f="sc-prompt"]')?.value || '' };
|
|
2564
|
+
if (scChannel && scChannel !== 'here') sc.channel = scChannel;
|
|
2565
|
+
const freq = card.querySelector('[data-f="sc-freq"]')?.value || 'interval';
|
|
2566
|
+
if (freq === 'interval') { const iv = card.querySelector('[data-f="sc-interval"]')?.value.trim() || '01:00'; const m = iv.match(/^(\d+):(\d+)$/); const mins = m ? parseInt(m[1]) * 60 + parseInt(m[2]) : 60; sc.time = (mins >= 1 && mins < 60) ? ('every' + mins + 'm') : 'hourly'; }
|
|
2567
|
+
else {
|
|
2568
|
+
const at = card.querySelector('[data-f="sc-at"]')?.value.trim() || '09:00';
|
|
2569
|
+
sc.days = freq === 'weekday' ? 'weekday' : freq === 'weekly' ? 'weekend' : 'daily';
|
|
2570
|
+
// A raw cron in the At field (>=5 fields) passes through verbatim —
|
|
2571
|
+
// the server validates it; days is encoded in the expression itself.
|
|
2572
|
+
sc.time = at;
|
|
2573
|
+
}
|
|
2574
|
+
const _sr = await fetch('/schedules', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(sc) }); if (!_sr.ok) throw new Error('HTTP ' + _sr.status + ' ' + await _sr.text());
|
|
2575
|
+
}
|
|
2576
|
+
}
|
|
2577
|
+
// Save config
|
|
2578
|
+
const data = buildChannelsData();
|
|
2579
|
+
const cfgRes = await fetch('/config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) });
|
|
2580
|
+
let cfgJson = {};
|
|
2581
|
+
if (!cfgRes.ok) { let msg = 'HTTP ' + cfgRes.status; try { const j = await cfgRes.json(); msg = j.error || j.message || msg; } catch { try { msg = await cfgRes.text() || msg; } catch {} } throw new Error(msg); }
|
|
2582
|
+
else { try { cfgJson = await cfgRes.json(); } catch {} }
|
|
2583
|
+
isDirty = false;
|
|
2584
|
+
const discordTokenStatus = cfgJson.secretStatus?.channels?.discordToken || {};
|
|
2585
|
+
const discordTokenStored = !!(discordTokenStatus.keychain || discordTokenStatus.env) && !discordTokenStatus.invalid;
|
|
2586
|
+
if (discordTokenStatus.invalid) {
|
|
2587
|
+
warnEl.textContent = discordTokenStatus.problem || 'Saved Bot token is invalid.';
|
|
2588
|
+
} else if ((data.discord?.applicationId || Object.keys(data.channelsConfig || {}).length) && !discordTokenStored) {
|
|
2589
|
+
warnEl.textContent = 'Saved. Bot token is empty, so the Discord bot is disconnected; channel worker continues in headless mode.';
|
|
2590
|
+
}
|
|
2591
|
+
if (btn) { btn.textContent = '\u2713 Saved'; btn.classList.add('done'); setTimeout(() => { btn.textContent = 'Save'; btn.classList.remove('done'); btn.disabled = false; }, 1500); }
|
|
2592
|
+
} catch (e) { console.error('[mixdog] save error:', e); warnEl.textContent = 'Save failed: ' + (e?.message || e); if (btn) { btn.textContent = 'Save'; btn.disabled = false; } }
|
|
2593
|
+
}
|
|
2594
|
+
|
|
2595
|
+
async function saveConfig() { await savePanel(); }
|
|
2596
|
+
|
|
2597
|
+
// ============================================================
|
|
2598
|
+
// AGENT MODULE
|
|
2599
|
+
// ============================================================
|
|
2600
|
+
|
|
2601
|
+
const AG_API_PROVIDERS = [
|
|
2602
|
+
{id:'openai',name:'OpenAI',env:'OPENAI_API_KEY',url:'https://platform.openai.com/api-keys'},
|
|
2603
|
+
{id:'anthropic',name:'Anthropic',env:'ANTHROPIC_API_KEY',url:'https://console.anthropic.com/settings/keys'},
|
|
2604
|
+
{id:'gemini',name:'Gemini',env:'GEMINI_API_KEY',url:'https://aistudio.google.com/apikey'},
|
|
2605
|
+
{id:'deepseek',name:'DeepSeek',env:'DEEPSEEK_API_KEY',url:'https://platform.deepseek.com/api_keys'},
|
|
2606
|
+
{id:'xai',name:'xAI',env:'XAI_API_KEY',url:'https://console.x.ai'},
|
|
2607
|
+
{id:'nvidia',name:'NVIDIA',env:'NVIDIA_API_KEY',url:'https://build.nvidia.com'},
|
|
2608
|
+
];
|
|
2609
|
+
const AG_OAUTH_PROVIDERS = [
|
|
2610
|
+
{id:'openai-oauth',name:'Codex',desc:'~/.codex/auth.json'},
|
|
2611
|
+
{id:'anthropic-oauth',name:'Claude Code',desc:'~/.claude/.credentials.json'},
|
|
2612
|
+
{id:'grok-oauth',name:'Grok',desc:'~/.grok/auth.json — or browser OAuth (consent: "Grok Build")',login:true},
|
|
2613
|
+
];
|
|
2614
|
+
const AG_LOCAL_PROVIDERS = [
|
|
2615
|
+
{id:'ollama',name:'Ollama',url:'http://localhost:11434/v1'},
|
|
2616
|
+
{id:'lmstudio',name:'LM Studio',url:'http://localhost:1234/v1'},
|
|
2617
|
+
];
|
|
2618
|
+
|
|
2619
|
+
const AG_EFFORT_OPTIONS = {
|
|
2620
|
+
openai: [{v:'none',label:'None'},{v:'low',label:'Low'},{v:'medium',label:'Medium'},{v:'high',label:'High'},{v:'xhigh',label:'Extra High'}],
|
|
2621
|
+
'openai-oauth': [{v:'none',label:'None'},{v:'low',label:'Low'},{v:'medium',label:'Medium'},{v:'high',label:'High'},{v:'xhigh',label:'Extra High'}],
|
|
2622
|
+
anthropic: [{v:'low',label:'Low'},{v:'medium',label:'Medium'},{v:'high',label:'High'},{v:'max',label:'Max'}],
|
|
2623
|
+
'anthropic-oauth': [{v:'low',label:'Low'},{v:'medium',label:'Medium'},{v:'high',label:'High'},{v:'max',label:'Max'}],
|
|
2624
|
+
};
|
|
2625
|
+
// Per-family effort profiles. Lookup order: model.reasoningLevels (provider-
|
|
2626
|
+
// declared) → AG_FAMILY_EFFORT[family] → AG_EFFORT_OPTIONS[provider].
|
|
2627
|
+
const AG_FAMILY_EFFORT = {
|
|
2628
|
+
opus: ['low', 'medium', 'high', 'xhigh', 'max'],
|
|
2629
|
+
sonnet: ['low', 'medium', 'high'],
|
|
2630
|
+
haiku: [],
|
|
2631
|
+
'gpt-5.5': ['none', 'low', 'medium', 'high', 'xhigh'],
|
|
2632
|
+
'gpt-5.4': ['none', 'low', 'medium', 'high', 'xhigh'],
|
|
2633
|
+
'gpt-5.2': ['none', 'low', 'medium', 'high', 'xhigh'],
|
|
2634
|
+
'gpt-5': ['none', 'low', 'medium', 'high', 'xhigh'],
|
|
2635
|
+
'gpt-mini': ['none', 'low', 'medium', 'high', 'xhigh'],
|
|
2636
|
+
'gpt-nano': ['none', 'low', 'medium', 'high'],
|
|
2637
|
+
'gpt-codex': ['none', 'low', 'medium', 'high'],
|
|
2638
|
+
};
|
|
2639
|
+
const AG_EFFORT_LABEL = { none: 'None', low: 'Low', medium: 'Medium', high: 'High', xhigh: 'Extra High', max: 'Max' };
|
|
2640
|
+
// Families that don't support fast mode even when the provider does.
|
|
2641
|
+
const AG_FAMILY_NO_FAST = new Set(['haiku', 'gpt-nano', 'gpt-codex']);
|
|
2642
|
+
const AG_FAST_PROVIDERS = new Set(['anthropic', 'anthropic-oauth', 'openai', 'openai-oauth']);
|
|
2643
|
+
let agModelList = [];
|
|
2644
|
+
const AG_ACCESS_LABELS = { full: 'Read & Write', readonly: 'Read Only', mcp: 'None' };
|
|
2645
|
+
|
|
2646
|
+
async function loadAgentData() {
|
|
2647
|
+
const r = await fetch('/agent/config').then(r => r.json()).catch(() => ({}));
|
|
2648
|
+
agConfig = r.config || {};
|
|
2649
|
+
agAuth = r.auth || {};
|
|
2650
|
+
agRenderAll();
|
|
2651
|
+
await agLoadPresets();
|
|
2652
|
+
await loadAgMaintenance();
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2655
|
+
function agRenderAll() {
|
|
2656
|
+
const c = agConfig.providers || {};
|
|
2657
|
+
const apiSec = document.getElementById('ag-api-sec');
|
|
2658
|
+
apiSec.innerHTML = '';
|
|
2659
|
+
for (const p of AG_API_PROVIDERS) {
|
|
2660
|
+
const stored = !!agAuth.keyStored?.[p.id] || !!c[p.id]?.apiKey;
|
|
2661
|
+
const hasEnv = agAuth.envKeys?.[p.id];
|
|
2662
|
+
let tag, link;
|
|
2663
|
+
if (stored) { tag = '<span class="r-tag ok">Set</span>'; link = ''; }
|
|
2664
|
+
else if (hasEnv) { tag = '<span class="r-tag env">Env</span>'; link = ''; }
|
|
2665
|
+
else { tag = ''; link = `<a class="r-link" href="${p.url}" target="_blank">Get Key ↗</a>`; }
|
|
2666
|
+
// The stored key value is never sent to the browser; the field starts empty
|
|
2667
|
+
// and a non-empty placeholder signals a saved key. Typing replaces it.
|
|
2668
|
+
const ph = stored ? '••••••••••••' : p.env;
|
|
2669
|
+
apiSec.innerHTML += `<div class="r"><span class="r-name">${p.name}</span><div class="input-group"><input class="r-input" id="ag-key-${p.id}" type="password" value="" placeholder="${ph}" autocomplete="new-password"><button class="eye-btn" onclick="toggleVis(this)" type="button">👁</button></div>${tag}${link}</div>`;
|
|
2670
|
+
}
|
|
2671
|
+
const oauthSec = document.getElementById('ag-oauth-sec');
|
|
2672
|
+
oauthSec.innerHTML = '';
|
|
2673
|
+
for (const p of AG_OAUTH_PROVIDERS) {
|
|
2674
|
+
const detected = agOAuthDetected(p.id);
|
|
2675
|
+
const tag = detected ? '<span class="r-tag ok">Set</span>' : '<span class="r-tag no">Not Set</span>';
|
|
2676
|
+
// Login / Re-login button removed per request — OAuth state is shown via the Set / Not Set tag only.
|
|
2677
|
+
const btn = '';
|
|
2678
|
+
oauthSec.innerHTML += `<div class="r"><span class="r-name">${p.name}</span><span style="flex:1;font-size:12px;color:var(--text-4)">${p.desc}</span>${tag}${btn}</div>`;
|
|
2679
|
+
}
|
|
2680
|
+
const localSec = document.getElementById('ag-local-sec');
|
|
2681
|
+
localSec.innerHTML = '';
|
|
2682
|
+
for (const p of AG_LOCAL_PROVIDERS) {
|
|
2683
|
+
const url = c[p.id]?.baseURL || p.url;
|
|
2684
|
+
const detected = agAuth[p.id];
|
|
2685
|
+
const tag = detected ? '<span class="r-tag ok">Set</span>' : '<span class="r-tag no">Not Set</span>';
|
|
2686
|
+
localSec.innerHTML += `<div class="r"><span class="r-name">${p.name}</span><input class="r-input" id="ag-url-${p.id}" value="${escapeAttr(url)}" placeholder="${p.url}" style="min-width:0;flex:1;margin-right:12px">${tag}</div>`;
|
|
2687
|
+
}
|
|
2688
|
+
agRenderStatus();
|
|
2689
|
+
}
|
|
2690
|
+
|
|
2691
|
+
function agRenderStatus() {
|
|
2692
|
+
// Status panel was removed from the sidebar — guard so missing container
|
|
2693
|
+
// does not crash loadAgentData (the red "Failed to load configuration"
|
|
2694
|
+
// banner was a symptom of that crash; loadAgMaintenance was also
|
|
2695
|
+
// unreachable, leaving the Task Presets list empty).
|
|
2696
|
+
const sec = document.getElementById('ag-status-sec');
|
|
2697
|
+
if (!sec) return;
|
|
2698
|
+
const c = agConfig.providers || {};
|
|
2699
|
+
let h = '';
|
|
2700
|
+
for (const p of AG_API_PROVIDERS) { const hasKey = !!agAuth.keyStored?.[p.id] || !!c[p.id]?.apiKey; const hasEnv = agAuth.envKeys?.[p.id]; const tag = hasKey ? '<span class="r-tag ok">API Key</span>' : hasEnv ? '<span class="r-tag env">Env Var</span>' : '<span class="r-tag no">Off</span>'; h += `<div class="r"><span class="r-name">${p.name}</span>${tag}</div>`; }
|
|
2701
|
+
for (const p of AG_OAUTH_PROVIDERS) { const detected = agOAuthDetected(p.id); const tag = detected ? '<span class="r-tag oauth">OAuth</span>' : '<span class="r-tag no">Off</span>'; h += `<div class="r"><span class="r-name">${p.name}</span>${tag}</div>`; }
|
|
2702
|
+
for (const p of AG_LOCAL_PROVIDERS) { const enabled = c[p.id]?.enabled === true; const detected = agAuth[p.id]; let tag; if (enabled && detected) tag = '<span class="r-tag ok">Enabled</span>'; else if (enabled) tag = '<span class="r-tag oauth">Enabled</span>'; else if (detected) tag = '<span class="r-tag not-running">Disabled</span>'; else tag = '<span class="r-tag no">Off</span>'; h += `<div class="r"><span class="r-name">${p.name}</span>${tag}</div>`; }
|
|
2703
|
+
sec.innerHTML = h;
|
|
2704
|
+
}
|
|
2705
|
+
|
|
2706
|
+
async function agSaveProviders() {
|
|
2707
|
+
const providers = {};
|
|
2708
|
+
for (const p of AG_API_PROVIDERS) { const key = document.getElementById('ag-key-'+p.id)?.value.trim(); if (key) providers[p.id] = { apiKey: key, enabled: true }; }
|
|
2709
|
+
for (const p of AG_LOCAL_PROVIDERS) { const url = document.getElementById('ag-url-'+p.id)?.value.trim(); const detected = agAuth[p.id]; providers[p.id] = { enabled: detected, baseURL: url || p.url }; }
|
|
2710
|
+
await agDoSave({ providers }, 'agent-providers');
|
|
2711
|
+
}
|
|
2712
|
+
|
|
2713
|
+
async function agDoSave(data, panelId) {
|
|
2714
|
+
const panel = document.getElementById('panel-' + panelId);
|
|
2715
|
+
const btn = panel?.querySelector('.save');
|
|
2716
|
+
const warn = panel?.querySelector('.warn-msg');
|
|
2717
|
+
if (warn) warn.textContent = '';
|
|
2718
|
+
if (btn) { btn.textContent = 'Saving...'; btn.disabled = true; }
|
|
2719
|
+
try {
|
|
2720
|
+
const agCfgRes = await fetch('/agent/config', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(data) });
|
|
2721
|
+
if (!agCfgRes.ok) { let msg = 'HTTP ' + agCfgRes.status; try { const j = await agCfgRes.json(); msg = j.error || j.message || msg; } catch { try { msg = await agCfgRes.text() || msg; } catch {} } throw new Error(msg); }
|
|
2722
|
+
isDirty = false;
|
|
2723
|
+
if (btn) { btn.textContent = '\u2713 Saved'; btn.classList.add('done'); setTimeout(()=>{btn.textContent='Save';btn.classList.remove('done');btn.disabled=false},1500); }
|
|
2724
|
+
await loadAgentData();
|
|
2725
|
+
} catch { if (warn) warn.textContent = 'Save failed'; if (btn) { btn.textContent = 'Save'; btn.disabled = false; } }
|
|
2726
|
+
}
|
|
2727
|
+
|
|
2728
|
+
function agOAuthDetected(providerId, auth = agAuth) {
|
|
2729
|
+
if (providerId === 'openai-oauth') return !!auth?.codexOAuth;
|
|
2730
|
+
if (providerId === 'anthropic-oauth') return !!auth?.anthropicOAuth;
|
|
2731
|
+
if (providerId === 'grok-oauth') return !!auth?.grokOAuth;
|
|
2732
|
+
if (providerId === 'copilot') return !!auth?.copilot;
|
|
2733
|
+
return false;
|
|
2734
|
+
}
|
|
2735
|
+
|
|
2736
|
+
// Grok CLI OAuth login. An existing `grok` CLI login (~/.grok/auth.json) shows
|
|
2737
|
+
// as "Set" automatically; this button signs in from the browser when no CLI
|
|
2738
|
+
// login exists. The request blocks until consent completes, then we re-fetch
|
|
2739
|
+
// auth so the row flips to "Set".
|
|
2740
|
+
async function grokOAuthLogin(btn) {
|
|
2741
|
+
if (btn) { btn.disabled = true; btn.textContent = 'Waiting for browser…'; }
|
|
2742
|
+
try {
|
|
2743
|
+
const r = await fetch('/agent/grok-oauth/login', { method: 'POST' }).then(r => r.json());
|
|
2744
|
+
if (r && r.ok) { await loadAgentData(); }
|
|
2745
|
+
else { alert('Grok login failed: ' + ((r && r.error) || 'unknown error')); if (btn) btn.disabled = false; }
|
|
2746
|
+
} catch (e) {
|
|
2747
|
+
alert('Grok login failed: ' + (e?.message || e));
|
|
2748
|
+
if (btn) btn.disabled = false;
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
|
|
2752
|
+
function agGetEnabledProviders() {
|
|
2753
|
+
const out = [];
|
|
2754
|
+
const c = agConfig.providers || {};
|
|
2755
|
+
for (const p of AG_API_PROVIDERS) { if (agAuth.keyStored?.[p.id] || c[p.id]?.apiKey || agAuth.envKeys?.[p.id]) out.push(p.id); }
|
|
2756
|
+
for (const p of AG_OAUTH_PROVIDERS) { if (agOAuthDetected(p.id)) out.push(p.id); }
|
|
2757
|
+
for (const p of AG_LOCAL_PROVIDERS) { if (c[p.id]?.enabled === true) out.push(p.id); }
|
|
2758
|
+
return out;
|
|
2759
|
+
}
|
|
2760
|
+
|
|
2761
|
+
async function agLoadPresets() {
|
|
2762
|
+
try { const r = await fetch('/agent/presets').then(r => r.json()); agPresets = r.presets || []; } catch { agPresets = []; }
|
|
2763
|
+
agRenderPresetList();
|
|
2764
|
+
}
|
|
2765
|
+
|
|
2766
|
+
function agRenderPresetList() {
|
|
2767
|
+
const list = document.getElementById('ag-preset-list');
|
|
2768
|
+
if (agPresets.length === 0) { list.innerHTML = '<div class="empty">No presets yet. Click <strong>+ New Preset</strong> to create one.</div>'; return; }
|
|
2769
|
+
list.innerHTML = agPresets.map(p => {
|
|
2770
|
+
const accessLabel = AG_ACCESS_LABELS[p.tools] || 'Read & Write';
|
|
2771
|
+
const provModel = `${p.provider}/${p.model}`;
|
|
2772
|
+
const meta = [provModel, p.tools && p.tools !== 'full' ? accessLabel : null, p.effort ? `effort=${p.effort}` : null, p.fast ? 'Fast' : null].filter(Boolean).join(' · ');
|
|
2773
|
+
return `<div class="preset-card"><div class="preset-info"><div class="preset-id">${escapeHtml(p.name || p.id)}</div><div class="preset-meta">${escapeHtml(meta)}</div></div><div class="preset-actions"><button class="icon-btn edit-btn" onclick="agOpenPresetForm('${escapeAttr(p.id)}')">Edit</button><button class="icon-btn danger" onclick="agDeletePreset('${escapeAttr(p.id)}')">Delete</button></div></div>`;
|
|
2774
|
+
}).join('');
|
|
2775
|
+
}
|
|
2776
|
+
|
|
2777
|
+
function agOpenPresetForm(id) {
|
|
2778
|
+
agEditingPresetId = id;
|
|
2779
|
+
const sec = document.getElementById('ag-preset-form-sec');
|
|
2780
|
+
const title = document.getElementById('ag-preset-form-title');
|
|
2781
|
+
sec.style.display = 'block';
|
|
2782
|
+
const advToggle = sec.querySelector('.advanced-toggle');
|
|
2783
|
+
const advContent = document.getElementById('ag-pf-access-section');
|
|
2784
|
+
if (advToggle) advToggle.querySelector('.arrow').classList.remove('open');
|
|
2785
|
+
if (advContent) advContent.classList.remove('open');
|
|
2786
|
+
const providerSel = document.getElementById('ag-pf-provider');
|
|
2787
|
+
const enabled = agGetEnabledProviders();
|
|
2788
|
+
if (enabled.length === 0) { providerSel.innerHTML = '<option value="">No enabled providers</option>'; providerSel.disabled = true; }
|
|
2789
|
+
else { providerSel.disabled = false; providerSel.innerHTML = enabled.map(p => `<option value="${p}">${p}</option>`).join(''); }
|
|
2790
|
+
if (id) {
|
|
2791
|
+
title.textContent = 'Edit Preset';
|
|
2792
|
+
const p = agPresets.find(x => x.id === id);
|
|
2793
|
+
if (p) { document.getElementById('ag-pf-name').value = p.name || p.id; const providerVal = p.provider || ''; providerSel.value = providerVal; agOnProviderChange().then(() => { document.getElementById('ag-pf-model').value = p.model; agUpdateEffortAndFast(providerVal); document.getElementById('ag-pf-effort').value = p.effort || ''; document.getElementById('ag-pf-fast').classList.toggle('on', !!p.fast); document.querySelectorAll('#ag-pf-tools .radio-btn').forEach(b => b.classList.toggle('on', b.dataset.v === (p.tools || 'full'))); if (p.tools && p.tools !== 'full') { if (advToggle) advToggle.querySelector('.arrow').classList.add('open'); if (advContent) advContent.classList.add('open'); } }); return; }
|
|
2794
|
+
}
|
|
2795
|
+
title.textContent = 'New Preset';
|
|
2796
|
+
document.getElementById('ag-pf-name').value = '';
|
|
2797
|
+
document.getElementById('ag-pf-fast').classList.remove('on');
|
|
2798
|
+
document.querySelectorAll('#ag-pf-tools .radio-btn').forEach(b => b.classList.toggle('on', b.dataset.v === 'full'));
|
|
2799
|
+
if (enabled.length > 0) { providerSel.value = enabled[0]; agOnProviderChange(); }
|
|
2800
|
+
}
|
|
2801
|
+
|
|
2802
|
+
function agClosePresetForm() { document.getElementById('ag-preset-form-sec').style.display = 'none'; agEditingPresetId = null; document.getElementById('warn-agent-presets').textContent = ''; }
|
|
2803
|
+
|
|
2804
|
+
async function agOnProviderChange() {
|
|
2805
|
+
const provider = document.getElementById('ag-pf-provider').value;
|
|
2806
|
+
let modelSel = document.getElementById('ag-pf-model');
|
|
2807
|
+
if (modelSel.tagName === 'INPUT') {
|
|
2808
|
+
const sel = document.createElement('select');
|
|
2809
|
+
sel.id = 'ag-pf-model';
|
|
2810
|
+
sel.className = 'r-select';
|
|
2811
|
+
modelSel.replaceWith(sel);
|
|
2812
|
+
modelSel = sel;
|
|
2813
|
+
}
|
|
2814
|
+
|
|
2815
|
+
let models;
|
|
2816
|
+
modelSel.disabled = true;
|
|
2817
|
+
modelSel.innerHTML = '<option value="">Loading...</option>';
|
|
2818
|
+
modelSel.onchange = null;
|
|
2819
|
+
try {
|
|
2820
|
+
const r = await fetch(`/agent/models?provider=${encodeURIComponent(provider)}`).then(r => r.json());
|
|
2821
|
+
if (!r.ok) {
|
|
2822
|
+
const msg = r.error || 'Model catalog unavailable';
|
|
2823
|
+
modelSel.innerHTML = '<option value="">' + escapeHtml(msg) + '</option>';
|
|
2824
|
+
document.getElementById('warn-agent-presets').textContent = msg;
|
|
2825
|
+
agReplaceModelWithInput('');
|
|
2826
|
+
return;
|
|
2827
|
+
}
|
|
2828
|
+
models = (r.models || []).map(m => typeof m === 'string' ? { id: m } : m).filter(m => m && m.id);
|
|
2829
|
+
} catch {
|
|
2830
|
+
modelSel.innerHTML = '<option value="">Failed to fetch</option>';
|
|
2831
|
+
document.getElementById('warn-agent-presets').textContent = 'Failed to fetch model catalog';
|
|
2832
|
+
agReplaceModelWithInput('');
|
|
2833
|
+
return;
|
|
2834
|
+
}
|
|
2835
|
+
if (!Array.isArray(models) || models.length === 0) {
|
|
2836
|
+
modelSel.innerHTML = '<option value="">No models found</option>';
|
|
2837
|
+
document.getElementById('warn-agent-presets').textContent = 'No models found for this provider';
|
|
2838
|
+
agReplaceModelWithInput('');
|
|
2839
|
+
return;
|
|
2840
|
+
}
|
|
2841
|
+
|
|
2842
|
+
document.getElementById('warn-agent-presets').textContent = '';
|
|
2843
|
+
agModelList = models;
|
|
2844
|
+
agRenderModelOptions(modelSel, models, provider);
|
|
2845
|
+
modelSel.disabled = false;
|
|
2846
|
+
modelSel.onchange = () => agUpdateEffortAndFast(provider);
|
|
2847
|
+
agUpdateEffortAndFast(provider);
|
|
2848
|
+
}
|
|
2849
|
+
|
|
2850
|
+
// Shared filter for any model dropdown that surfaces coding-agent picks
|
|
2851
|
+
// (agent presets, search-provider model presets). Three-stage filter:
|
|
2852
|
+
// 1. Chat-capable only (drop embedding/audio/image/etc).
|
|
2853
|
+
// 2. If catalog carries `created` timestamps → strict 6-month cutoff
|
|
2854
|
+
// from today. Older releases drop out.
|
|
2855
|
+
// 3. Curated catalogs without timestamps → fall back to either the
|
|
2856
|
+
// `latest:true` flag (anthropic-oauth marks family-max this way) or
|
|
2857
|
+
// a client-side family-version comparison that keeps the highest
|
|
2858
|
+
// `major.minor` per stem. Drops older generations of openai-oauth /
|
|
2859
|
+
// gemini whose providers never set `latest`.
|
|
2860
|
+
function filterCodingModels(models) {
|
|
2861
|
+
const NON_CHAT_PREFIX = /^(dall-e|tts|whisper|text-embedding|text-moderation|omni-moderation|babbage|davinci|ada-|sora|chatgpt-image|codex-auto|computer-use|imagen|veo|gemini-embedding|aqa-|grok-imagine)/i;
|
|
2862
|
+
const NON_CHAT_TOKEN = /-(image|audio|realtime|moderation|embedding|instruct|search|transcribe|tts|live|preview-tts|computer-use|gemma|robotics|vision-only|imagine|video)(-|$)/i;
|
|
2863
|
+
const DATED_RE = /-\d{4}(-\d{2}-\d{2})?$/;
|
|
2864
|
+
let visible = (models || []).filter(m => {
|
|
2865
|
+
if (!m || !m.id) return false;
|
|
2866
|
+
if (m.mode && m.mode !== 'chat') return false;
|
|
2867
|
+
if (NON_CHAT_PREFIX.test(m.id) || NON_CHAT_TOKEN.test(m.id)) return false;
|
|
2868
|
+
// Drop short-form dated snapshots (`-2024-05-13` / `-2024`) ONLY when
|
|
2869
|
+
// the provider didn't tag a tier. Tier='dated' alone is no longer
|
|
2870
|
+
// blocked — for catalogs whose only entry for a family is dated
|
|
2871
|
+
// (e.g. anthropic-oauth's haiku), losing them removes the family
|
|
2872
|
+
// entirely. The family-max pass below dedupes alongside any alias.
|
|
2873
|
+
if (!m.tier && DATED_RE.test(m.id)) return false;
|
|
2874
|
+
return true;
|
|
2875
|
+
});
|
|
2876
|
+
|
|
2877
|
+
const hasCreated = visible.some(m => typeof m.created === 'number' && m.created > 0);
|
|
2878
|
+
if (hasCreated) {
|
|
2879
|
+
// Strict 6-month cutoff from today. Mixed-date catalogs treat undated
|
|
2880
|
+
// entries as current so curated aliases ride alongside live entries.
|
|
2881
|
+
visible.sort((a, b) => (b.created || 0) - (a.created || 0));
|
|
2882
|
+
const cutoff = Math.floor(Date.now() / 1000) - 180 * 24 * 60 * 60;
|
|
2883
|
+
const within = visible.filter(m => !m.created || m.created >= cutoff);
|
|
2884
|
+
return within.length ? within : visible.slice(0, 1);
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2887
|
+
// No timestamps anywhere — curated catalog. Group by the provider-
|
|
2888
|
+
// declared `family` when available (preserves codex/mini/nano splits
|
|
2889
|
+
// that pure id-stem parsing collapses), fall back to a regex stem.
|
|
2890
|
+
// Keep entries whose version equals the per-family max.
|
|
2891
|
+
//
|
|
2892
|
+
// Date-stripping: chunks >= 1000 are YYYY / YYYYMMDD snapshots, not
|
|
2893
|
+
// minor versions. Without this, `claude-opus-4-20250514` would compare
|
|
2894
|
+
// as [4, 20250514] and beat the actual alias `claude-opus-4-8` ([4, 8]).
|
|
2895
|
+
// Minor versions stay safely under 1000.
|
|
2896
|
+
const versionRe = /^([a-z][a-z0-9-]*?)-(\d+(?:[.-]\d+)*)(?:[-_]|$)/i;
|
|
2897
|
+
// Strip a trailing `.\d+` from the family token so generation siblings
|
|
2898
|
+
// (gpt-5.5 / gpt-5.4 / gpt-5.2 / gpt-5) collapse into one `gpt-5` group
|
|
2899
|
+
// and version-max keeps only the latest minor — an invariant-based stand-in
|
|
2900
|
+
// for the strict 6-month cutoff that timestamped catalogs already enforce.
|
|
2901
|
+
// Non-versioned family names (gpt-mini, gpt-codex, gemini-flash) are
|
|
2902
|
+
// unaffected — they have no trailing `.N` to strip.
|
|
2903
|
+
const parsed = visible.map(m => {
|
|
2904
|
+
const match = m.id.match(versionRe);
|
|
2905
|
+
const rawStem = m.family || (match ? match[1] : m.id);
|
|
2906
|
+
const stem = rawStem.replace(/\.\d+$/, '');
|
|
2907
|
+
const rawVersion = match ? match[2].split(/[.-]/).map(Number) : [];
|
|
2908
|
+
const version = rawVersion.filter(n => n < 1000);
|
|
2909
|
+
return { m, stem, version };
|
|
2910
|
+
});
|
|
2911
|
+
const cmp = (a, b) => {
|
|
2912
|
+
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
2913
|
+
const x = a[i] || 0, y = b[i] || 0;
|
|
2914
|
+
if (x !== y) return x - y;
|
|
2915
|
+
}
|
|
2916
|
+
return 0;
|
|
2917
|
+
};
|
|
2918
|
+
const maxByStem = new Map();
|
|
2919
|
+
for (const p of parsed) {
|
|
2920
|
+
const cur = maxByStem.get(p.stem);
|
|
2921
|
+
if (!cur || cmp(p.version, cur) > 0) maxByStem.set(p.stem, p.version);
|
|
2922
|
+
}
|
|
2923
|
+
return parsed
|
|
2924
|
+
.filter(p => cmp(p.version, maxByStem.get(p.stem) || []) === 0)
|
|
2925
|
+
.map(p => p.m);
|
|
2926
|
+
}
|
|
2927
|
+
|
|
2928
|
+
function agRenderModelOptions(sel, models, provider) {
|
|
2929
|
+
const visible = filterCodingModels(models);
|
|
2930
|
+
sel.innerHTML = visible.map(m => `<option value="${escapeAttr(m.id)}">${escapeHtml(m.display || m.id)}</option>`).join('');
|
|
2931
|
+
}
|
|
2932
|
+
|
|
2933
|
+
function agEffortOptionsFor(provider, model) {
|
|
2934
|
+
if (Array.isArray(model?.reasoningLevels) && model.reasoningLevels.length > 0) {
|
|
2935
|
+
return [...model.reasoningLevels];
|
|
2936
|
+
}
|
|
2937
|
+
if (model?.family && Object.prototype.hasOwnProperty.call(AG_FAMILY_EFFORT, model.family)) {
|
|
2938
|
+
return AG_FAMILY_EFFORT[model.family];
|
|
2939
|
+
}
|
|
2940
|
+
const opts = AG_EFFORT_OPTIONS[provider];
|
|
2941
|
+
if (opts) return opts.map(o => o.v);
|
|
2942
|
+
return null;
|
|
2943
|
+
}
|
|
2944
|
+
|
|
2945
|
+
function agUpdateEffortAndFast(provider) {
|
|
2946
|
+
const modelEl = document.getElementById('ag-pf-model');
|
|
2947
|
+
const effortRow = document.getElementById('ag-pf-effort-row');
|
|
2948
|
+
const effortSel = document.getElementById('ag-pf-effort');
|
|
2949
|
+
const fastRow = document.getElementById('ag-pf-fast-row');
|
|
2950
|
+
const modelResolved = modelEl && modelEl.tagName !== 'INPUT';
|
|
2951
|
+
const modelId = modelResolved ? modelEl.value : '';
|
|
2952
|
+
const model = modelResolved ? (agModelList.find(m => m.id === modelId) || null) : null;
|
|
2953
|
+
const allowed = modelResolved ? agEffortOptionsFor(provider, model) : null;
|
|
2954
|
+
|
|
2955
|
+
if (!allowed || allowed.length === 0) {
|
|
2956
|
+
effortRow.style.display = 'none';
|
|
2957
|
+
effortSel.innerHTML = '';
|
|
2958
|
+
} else {
|
|
2959
|
+
effortRow.style.display = 'flex';
|
|
2960
|
+
effortSel.innerHTML = allowed.map(v => `<option value="${v}">${AG_EFFORT_LABEL[v] || v}</option>`).join('');
|
|
2961
|
+
}
|
|
2962
|
+
|
|
2963
|
+
const providerFast = AG_FAST_PROVIDERS.has(provider);
|
|
2964
|
+
const familyNoFast = model?.family && AG_FAMILY_NO_FAST.has(model.family);
|
|
2965
|
+
const fastAllowed = modelResolved && providerFast && !familyNoFast;
|
|
2966
|
+
fastRow.style.display = fastAllowed ? 'flex' : 'none';
|
|
2967
|
+
if (!fastAllowed) document.getElementById('ag-pf-fast').classList.remove('on');
|
|
2968
|
+
}
|
|
2969
|
+
|
|
2970
|
+
function agReplaceModelWithInput(value) { const old = document.getElementById('ag-pf-model'); if (old.tagName === 'INPUT') return; const inp = document.createElement('input'); inp.id = 'ag-pf-model'; inp.className = 'r-input'; inp.placeholder = 'Enter model ID manually'; inp.value = value || ''; old.replaceWith(inp); }
|
|
2971
|
+
|
|
2972
|
+
function agSelectTools(el) { el.parentElement.querySelectorAll('.radio-btn').forEach(b => b.classList.remove('on')); el.classList.add('on'); }
|
|
2973
|
+
|
|
2974
|
+
async function agSavePreset() {
|
|
2975
|
+
const warn = document.getElementById('warn-agent-presets');
|
|
2976
|
+
warn.textContent = '';
|
|
2977
|
+
const name = document.getElementById('ag-pf-name').value.trim();
|
|
2978
|
+
const provider = document.getElementById('ag-pf-provider').value;
|
|
2979
|
+
const modelEl = document.getElementById('ag-pf-model');
|
|
2980
|
+
const model = (modelEl.tagName === 'INPUT' ? modelEl.value : modelEl.value).trim();
|
|
2981
|
+
const effort = document.getElementById('ag-pf-effort').value;
|
|
2982
|
+
const fast = document.getElementById('ag-pf-fast').classList.contains('on');
|
|
2983
|
+
const tools = document.querySelector('#ag-pf-tools .radio-btn.on')?.dataset.v || 'full';
|
|
2984
|
+
if (!name) { warn.textContent = 'Name is required'; return; }
|
|
2985
|
+
if (!provider) { warn.textContent = 'Provider is required'; return; }
|
|
2986
|
+
if (!model) { warn.textContent = 'Model is required'; return; }
|
|
2987
|
+
const id = agEditingPresetId || name.toLowerCase().replace(/[^a-z0-9._-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
|
2988
|
+
if (!id) { warn.textContent = 'Name must contain valid characters'; return; }
|
|
2989
|
+
const body = { id, name, model, tools, provider, type: 'bridge' };
|
|
2990
|
+
if (effort) body.effort = effort;
|
|
2991
|
+
const fastRowVisible = document.getElementById('ag-pf-fast-row').style.display !== 'none';
|
|
2992
|
+
if (fast && fastRowVisible) body.fast = true;
|
|
2993
|
+
try {
|
|
2994
|
+
const r = await fetch('/agent/presets', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }).then(r => r.json());
|
|
2995
|
+
if (!r.ok) { warn.textContent = r.error || 'Save failed'; return; }
|
|
2996
|
+
isDirty = false;
|
|
2997
|
+
agClosePresetForm();
|
|
2998
|
+
await agLoadPresets();
|
|
2999
|
+
refreshAllPresetDropdowns();
|
|
3000
|
+
} catch (err) { warn.textContent = 'Save failed: ' + (err?.message || 'network error'); }
|
|
3001
|
+
}
|
|
3002
|
+
|
|
3003
|
+
async function agDeletePreset(id) {
|
|
3004
|
+
if (!confirm(`Delete preset "${id}"?`)) return;
|
|
3005
|
+
try {
|
|
3006
|
+
await fetch(`/agent/presets?id=${encodeURIComponent(id)}`, { method: 'DELETE' });
|
|
3007
|
+
await agLoadPresets();
|
|
3008
|
+
refreshAllPresetDropdowns();
|
|
3009
|
+
} catch { document.getElementById('warn-agent-presets').textContent = 'Delete failed'; }
|
|
3010
|
+
}
|
|
3011
|
+
|
|
3012
|
+
// Single fan-out that re-renders every UI surface that materializes a
|
|
3013
|
+
// preset dropdown from agPresets. Called after any preset CRUD so a
|
|
3014
|
+
// freshly saved / renamed / deleted preset surfaces everywhere without a
|
|
3015
|
+
// page reload. Covers: Schedule cards
|
|
3016
|
+
// ([data-f="sc-model"]), Webhook delegate cards ([data-f="ev-model"]),
|
|
3017
|
+
// Maintenance task selects, and Custom Workflow role rows.
|
|
3018
|
+
function refreshAllPresetDropdowns() {
|
|
3019
|
+
try { populatePresetDropdowns(); } catch (e) { console.warn('[mixdog] populatePresetDropdowns failed:', e); }
|
|
3020
|
+
try { renderAgMaintenance(); } catch (e) { console.warn('[mixdog] renderAgMaintenance failed:', e); }
|
|
3021
|
+
try { wfRenderRoles(); } catch (e) { console.warn('[mixdog] wfRenderRoles failed:', e); }
|
|
3022
|
+
}
|
|
3023
|
+
|
|
3024
|
+
// ============================================================
|
|
3025
|
+
// MEMORY MODULE
|
|
3026
|
+
// ============================================================
|
|
3027
|
+
|
|
3028
|
+
const MEM_API_PROVIDERS = AG_API_PROVIDERS;
|
|
3029
|
+
const MEM_OAUTH_PROVIDERS = AG_OAUTH_PROVIDERS;
|
|
3030
|
+
const MEM_LOCAL_PROVIDERS = AG_LOCAL_PROVIDERS;
|
|
3031
|
+
|
|
3032
|
+
async function loadMemoryData() {
|
|
3033
|
+
memConfig = await fetch('/memory/config').then(r => r.json()).catch(() => ({}));
|
|
3034
|
+
memAuth = await fetch('/memory/auth').then(r => r.json()).catch(() => ({}));
|
|
3035
|
+
memPopulateConfig();
|
|
3036
|
+
memRenderAll();
|
|
3037
|
+
await Promise.all([memLoadFiles(), memLoadCoreMemory()]);
|
|
3038
|
+
}
|
|
3039
|
+
|
|
3040
|
+
function memPopulateConfig() {
|
|
3041
|
+
const titleEl = document.getElementById('mem-set-user-title');
|
|
3042
|
+
if (titleEl) titleEl.value = memConfig.user?.title || '';
|
|
3043
|
+
const retEl = document.getElementById('mem-set-retention');
|
|
3044
|
+
if (retEl && memConfig.cycle1?.interval) retEl.value = memConfig.cycle1.interval;
|
|
3045
|
+
const intEl = document.getElementById('mem-set-interval');
|
|
3046
|
+
if (intEl && memConfig.cycle2?.interval) intEl.value = memConfig.cycle2.interval;
|
|
3047
|
+
}
|
|
3048
|
+
|
|
3049
|
+
function memRenderAll() {
|
|
3050
|
+
const c = memConfig.providers || {};
|
|
3051
|
+
const apiSec = document.getElementById('mem-api-sec');
|
|
3052
|
+
if (!apiSec) return;
|
|
3053
|
+
apiSec.innerHTML = '';
|
|
3054
|
+
for (const p of MEM_API_PROVIDERS) {
|
|
3055
|
+
const key = c[p.id]?.apiKey || '';
|
|
3056
|
+
const hasStored = !!memAuth.keyStored?.[p.id];
|
|
3057
|
+
const hasEnv = memAuth.envKeys?.[p.id];
|
|
3058
|
+
let tag;
|
|
3059
|
+
if (key || hasStored) tag = '<span class="r-tag ok">Set</span>';
|
|
3060
|
+
else if (hasEnv) tag = '<span class="r-tag env">Env</span>';
|
|
3061
|
+
else tag = `<a class="r-link" href="${p.url}" target="_blank">Get Key ↗</a>`;
|
|
3062
|
+
apiSec.innerHTML += `<div class="r" data-provider="${p.id}"><span class="r-name">${p.name}</span><div class="input-group"><input class="r-input" id="mem-key-${p.id}" type="password" value="${escapeAttr(key)}" placeholder="${p.env}" onblur="memValidateKeyOnBlur('${p.id}')" autocomplete="new-password"><button class="eye-btn" onclick="toggleVis(this)" type="button">👁</button></div><span id="mem-tag-${p.id}" style="margin-left:auto">${tag}</span><span class="r-validation" id="mem-val-${p.id}"></span></div>`;
|
|
3063
|
+
}
|
|
3064
|
+
const oauthSec = document.getElementById('mem-oauth-sec');
|
|
3065
|
+
oauthSec.innerHTML = '';
|
|
3066
|
+
for (const p of MEM_OAUTH_PROVIDERS) { const detected = agOAuthDetected(p.id, memAuth); const tag = detected ? '<span class="r-tag ok">Set</span>' : '<span class="r-tag no">Not Set</span>'; oauthSec.innerHTML += `<div class="r"><span class="r-name">${p.name}</span><span style="flex:1;font-size:12px;color:var(--text-4)">${p.desc}</span>${tag}</div>`; }
|
|
3067
|
+
const localSec = document.getElementById('mem-local-sec');
|
|
3068
|
+
localSec.innerHTML = '';
|
|
3069
|
+
for (const p of MEM_LOCAL_PROVIDERS) { const url = c[p.id]?.baseURL || p.url; const detected = memAuth[p.id]; const tag = detected ? '<span class="r-tag running">Running</span>' : '<span class="r-tag not-running">Not Running</span>'; localSec.innerHTML += `<div class="r"><span class="r-name">${p.name}</span><input class="r-input" id="mem-url-${p.id}" value="${escapeAttr(url)}" placeholder="${p.url}">${tag}</div>`; }
|
|
3070
|
+
}
|
|
3071
|
+
|
|
3072
|
+
const _memOrigTags = {};
|
|
3073
|
+
async function memValidateKeyOnBlur(providerId) {
|
|
3074
|
+
const input = document.getElementById('mem-key-' + providerId);
|
|
3075
|
+
const valEl = document.getElementById('mem-val-' + providerId);
|
|
3076
|
+
const tagEl = document.getElementById('mem-tag-' + providerId);
|
|
3077
|
+
if (!input || !valEl || !tagEl) return;
|
|
3078
|
+
const key = input.value.trim();
|
|
3079
|
+
if (!_memOrigTags[providerId]) _memOrigTags[providerId] = tagEl.innerHTML;
|
|
3080
|
+
if (!key) { input.classList.remove('warn','valid'); valEl.className='r-validation'; valEl.innerHTML=''; tagEl.innerHTML=_memOrigTags[providerId]||''; return; }
|
|
3081
|
+
input.classList.remove('warn','valid'); valEl.className='r-validation checking'; valEl.innerHTML='<span class="spinner"></span>Checking...';
|
|
3082
|
+
try {
|
|
3083
|
+
const res = await fetch('/memory/validate', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({keys:{[providerId]:key}}) });
|
|
3084
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} ${await res.text()}`);
|
|
3085
|
+
const data = await res.json();
|
|
3086
|
+
const result = data.validation?.[providerId];
|
|
3087
|
+
if (input.value.trim() !== key) return;
|
|
3088
|
+
if (result === 'valid') { input.classList.remove('warn'); input.classList.add('valid'); valEl.className='r-validation ok'; valEl.textContent=''; tagEl.innerHTML='<span class="r-tag ok">Active</span>'; }
|
|
3089
|
+
else { input.classList.remove('valid'); input.classList.add('warn'); valEl.className='r-validation fail'; valEl.textContent='Invalid key'; tagEl.innerHTML=_memOrigTags[providerId]||''; }
|
|
3090
|
+
} catch (e) { if (input.value.trim() !== key) return; input.classList.remove('valid'); input.classList.add('warn'); valEl.className='r-validation fail'; valEl.textContent='Validation failed: '+(e?.message||e); tagEl.innerHTML=_memOrigTags[providerId]||''; }
|
|
3091
|
+
}
|
|
3092
|
+
|
|
3093
|
+
async function memSaveProviders() {
|
|
3094
|
+
const providers = {};
|
|
3095
|
+
for (const p of MEM_API_PROVIDERS) { const key = document.getElementById('mem-key-'+p.id)?.value.trim(); if (key) providers[p.id] = { apiKey: key }; }
|
|
3096
|
+
for (const p of MEM_LOCAL_PROVIDERS) { const url = document.getElementById('mem-url-'+p.id)?.value.trim(); providers[p.id] = { baseURL: url || p.url }; }
|
|
3097
|
+
const btn = document.querySelector('#panel-mem-providers .save'); const warn = document.getElementById('warn-mem-providers');
|
|
3098
|
+
warn.textContent = ''; btn.textContent = 'Saving...'; btn.disabled = true;
|
|
3099
|
+
try { const memProvRes = await fetch('/memory/config', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({providers}) }); if (!memProvRes.ok) { let msg = 'HTTP ' + memProvRes.status; try { const j = await memProvRes.json(); msg = j.error || j.message || msg; } catch { try { msg = await memProvRes.text() || msg; } catch {} } throw new Error(msg); } btn.textContent='\u2713 Saved'; btn.classList.add('done'); setTimeout(()=>{btn.textContent='Save';btn.classList.remove('done');btn.disabled=false},1500); await loadMemoryData(); } catch (e) { warn.textContent='Save failed: '+(e?.message||e); btn.textContent='Save'; btn.disabled=false; }
|
|
3100
|
+
}
|
|
3101
|
+
|
|
3102
|
+
function memBuildConfigData() {
|
|
3103
|
+
return {
|
|
3104
|
+
user: {
|
|
3105
|
+
title: document.getElementById('mem-set-user-title')?.value.trim() || '',
|
|
3106
|
+
},
|
|
3107
|
+
cycle1: { interval: document.getElementById('mem-set-retention')?.value || '10m' },
|
|
3108
|
+
cycle2: { interval: document.getElementById('mem-set-interval')?.value.trim() || '1h' },
|
|
3109
|
+
};
|
|
3110
|
+
}
|
|
3111
|
+
|
|
3112
|
+
async function memSaveConfig() {
|
|
3113
|
+
const data = memBuildConfigData(); const btn = document.querySelector('#panel-mem-settings .save'); const warn = document.getElementById('warn-mem-settings');
|
|
3114
|
+
warn.textContent = ''; btn.textContent = 'Saving...'; btn.disabled = true;
|
|
3115
|
+
try { const memCfgRes = await fetch('/memory/config', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(data) }); if (!memCfgRes.ok) { let msg = 'HTTP ' + memCfgRes.status; try { const j = await memCfgRes.json(); msg = j.error || j.message || msg; } catch { try { msg = await memCfgRes.text() || msg; } catch {} } throw new Error(msg); } btn.textContent='\u2713 Saved'; btn.classList.add('done'); setTimeout(()=>{btn.textContent='Save';btn.classList.remove('done');btn.disabled=false},1500); } catch (e) { warn.textContent='Save failed: '+(e?.message||e); btn.textContent='Save'; btn.disabled=false; }
|
|
3116
|
+
}
|
|
3117
|
+
|
|
3118
|
+
async function memRunBackfill() {
|
|
3119
|
+
const btn = document.getElementById('mem-btn-backfill'); const window_ = document.getElementById('mem-set-backfill-window').value;
|
|
3120
|
+
btn.textContent = 'Running...'; btn.disabled = true;
|
|
3121
|
+
try { await fetch('/memory/backfill', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({window:window_}) }); btn.textContent='\u2713 Done'; setTimeout(()=>{btn.textContent='Run';btn.disabled=false},2000); } catch { btn.textContent='Failed'; setTimeout(()=>{btn.textContent='Run';btn.disabled=false},2000); }
|
|
3122
|
+
}
|
|
3123
|
+
|
|
3124
|
+
function memDeleteConfirmCheck() {
|
|
3125
|
+
const input = document.getElementById('mem-del-confirm'); const btn = document.getElementById('mem-btn-delete');
|
|
3126
|
+
const ready = (input.value === 'DELETE ALL MEMORY');
|
|
3127
|
+
btn.disabled = !ready;
|
|
3128
|
+
btn.style.opacity = ready ? '1' : '0.4';
|
|
3129
|
+
btn.style.cursor = ready ? 'pointer' : 'not-allowed';
|
|
3130
|
+
}
|
|
3131
|
+
|
|
3132
|
+
async function memDeleteAll() {
|
|
3133
|
+
const input = document.getElementById('mem-del-confirm'); const btn = document.getElementById('mem-btn-delete'); const warn = document.getElementById('warn-mem-delete');
|
|
3134
|
+
if (input.value !== 'DELETE ALL MEMORY') return;
|
|
3135
|
+
if (!window.confirm('Delete ALL memory entries? This cannot be undone.')) return;
|
|
3136
|
+
warn.textContent = ''; btn.textContent = 'Deleting...'; btn.disabled = true;
|
|
3137
|
+
try {
|
|
3138
|
+
const r = await fetch('/memory/delete', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({ confirm:'DELETE ALL MEMORY' }) });
|
|
3139
|
+
const j = await r.json();
|
|
3140
|
+
if (!j.ok) throw new Error(j.error || 'unknown error');
|
|
3141
|
+
warn.style.color = 'var(--text-3)'; warn.textContent = 'Deleted ' + j.deleted + ' generated entries. Preserved ' + (j.core_preserved || 0) + ' core entries.';
|
|
3142
|
+
btn.textContent = '✓ Deleted'; input.value = '';
|
|
3143
|
+
setTimeout(() => { btn.textContent = 'Delete all'; memDeleteConfirmCheck(); }, 2500);
|
|
3144
|
+
} catch (e) {
|
|
3145
|
+
warn.style.color = '#e5534b'; warn.textContent = 'Failed: ' + (e.message || e);
|
|
3146
|
+
btn.textContent = 'Delete all'; btn.disabled = false;
|
|
3147
|
+
}
|
|
3148
|
+
}
|
|
3149
|
+
|
|
3150
|
+
async function memLoadFiles() { try{const data=await fetch('/memory/files').then(r=>r.json());document.getElementById('mem-file-bot').value=data['bot.md']||'';document.getElementById('mem-file-user-profile').value=data['user.md']||'';}catch{} }
|
|
3151
|
+
|
|
3152
|
+
function memOpenFile(name) {
|
|
3153
|
+
fetch('/memory/file/' + encodeURIComponent(name));
|
|
3154
|
+
}
|
|
3155
|
+
|
|
3156
|
+
async function memSaveFiles() {
|
|
3157
|
+
const btn=document.querySelector('#panel-mem-files .save'); const warn=document.getElementById('warn-mem-files');
|
|
3158
|
+
warn.textContent=''; btn.textContent='Saving...'; btn.disabled=true;
|
|
3159
|
+
try{const _mfr=await fetch('/memory/files',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({'bot.md':document.getElementById('mem-file-bot').value,'user.md':document.getElementById('mem-file-user-profile').value})});if(!_mfr.ok)throw new Error('HTTP '+_mfr.status+' '+await _mfr.text());const _mcr=await fetch('/memory/config',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({user:{title:document.getElementById('mem-set-user-title')?.value.trim()||''}})});if(!_mcr.ok)throw new Error('HTTP '+_mcr.status+' '+await _mcr.text());btn.textContent='\u2713 Saved';btn.classList.add('done');setTimeout(()=>{btn.textContent='Save';btn.classList.remove('done');btn.disabled=false},1500);}catch(e){warn.style.color='#e5534b';warn.textContent='Save failed: '+(e.message||e);btn.textContent='Save';btn.disabled=false;}
|
|
3160
|
+
}
|
|
3161
|
+
|
|
3162
|
+
async function memLoadCoreMemory() {
|
|
3163
|
+
const target = document.getElementById('mem-core-memory-list');
|
|
3164
|
+
if (!target) return;
|
|
3165
|
+
try {
|
|
3166
|
+
const r = await fetch('/api/memory/core').then(r => r.json());
|
|
3167
|
+
if (r && r.ok === false) {
|
|
3168
|
+
// Server reachable but memory backend down (e.g. mixdog process not
|
|
3169
|
+
// running → active-instance.json missing). Distinguish from a genuine
|
|
3170
|
+
// "no entries" state so the user knows to start the service.
|
|
3171
|
+
target.innerHTML = '<div class="empty">Memory service unavailable — entries cannot be loaded. Start the mixdog process and reopen.</div>';
|
|
3172
|
+
return;
|
|
3173
|
+
}
|
|
3174
|
+
memRenderCoreMemory(r.items || []);
|
|
3175
|
+
} catch {
|
|
3176
|
+
target.innerHTML = '<div class="empty">Failed to load core memory.</div>';
|
|
3177
|
+
}
|
|
3178
|
+
}
|
|
3179
|
+
|
|
3180
|
+
function memRenderCoreMemory(items) {
|
|
3181
|
+
const list=document.getElementById('mem-core-memory-list');
|
|
3182
|
+
if(items.length===0){list.innerHTML='<div class="empty" id="mem-core-empty">No core memory entries yet — click + Add to create one</div>';return;}
|
|
3183
|
+
list.innerHTML=items.map(item=>`<div class="r" style="flex-direction:column;align-items:stretch;padding:10px 14px;gap:6px">
|
|
3184
|
+
<div style="display:flex;align-items:center;gap:8px">
|
|
3185
|
+
<span style="font-weight:600">${escapeHtml(item.element||'')}</span>
|
|
3186
|
+
<span style="font-size:10px;padding:2px 6px;border-radius:3px;background:rgba(100,100,200,0.15);color:var(--text-3);font-weight:600;text-transform:uppercase">${escapeHtml(item.category||'')}</span>
|
|
3187
|
+
<span style="margin-left:auto;font-size:10px;color:var(--text-4)">${escapeHtml(item.project_id || 'COMMON')}</span>
|
|
3188
|
+
<span style="cursor:pointer;color:#e5534b;font-size:13px;font-weight:600;padding:4px 8px;border-radius:4px;background:rgba(229,83,75,0.1);white-space:nowrap" onclick="memDeleteCoreMemory(${item.id})">Delete</span>
|
|
3189
|
+
</div>
|
|
3190
|
+
<div style="font-size:11px;line-height:1.5;color:var(--text-2);padding:4px 2px">${escapeHtml(item.summary||'')}</div>
|
|
3191
|
+
</div>`).join('');
|
|
3192
|
+
}
|
|
3193
|
+
|
|
3194
|
+
async function memDeleteCoreMemory(id) { try{const r=await fetch('/api/memory/core/'+id+'/delete',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({})});if(!r.ok)throw new Error('HTTP '+r.status+' '+await r.text());await memLoadCoreMemory();}catch(e){alert('Failed to delete: '+(e.message||e));} }
|
|
3195
|
+
|
|
3196
|
+
function memAddCoreMemory() {
|
|
3197
|
+
const list=document.getElementById('mem-core-memory-list');
|
|
3198
|
+
const empty=document.getElementById('mem-core-empty');
|
|
3199
|
+
if(empty)empty.remove();
|
|
3200
|
+
const div=document.createElement('div');
|
|
3201
|
+
div.className='r';
|
|
3202
|
+
div.style.cssText='flex-direction:column;align-items:stretch;padding:10px 14px;gap:6px';
|
|
3203
|
+
div.innerHTML=`
|
|
3204
|
+
<div style="display:flex;align-items:center;gap:8px">
|
|
3205
|
+
<select class="r-input" style="width:110px;font-size:11px" data-cm-new="1" data-cm-f="category">
|
|
3206
|
+
<option value="rule">rule</option>
|
|
3207
|
+
<option value="constraint">constraint</option>
|
|
3208
|
+
<option value="decision">decision</option>
|
|
3209
|
+
<option value="fact" selected>fact</option>
|
|
3210
|
+
<option value="goal">goal</option>
|
|
3211
|
+
<option value="preference">preference</option>
|
|
3212
|
+
<option value="task">task</option>
|
|
3213
|
+
<option value="issue">issue</option>
|
|
3214
|
+
</select>
|
|
3215
|
+
<input class="r-input" placeholder="Project (common)" style="width:150px;font-size:11px" data-cm-new="1" data-cm-f="project_id">
|
|
3216
|
+
<input class="r-input" placeholder="Element (5-10 words)" style="flex:1;font-weight:600" data-cm-new="1" data-cm-f="element">
|
|
3217
|
+
<span style="cursor:pointer;color:var(--text-3);font-size:12px;font-weight:600;padding:4px 10px;border-radius:4px;background:rgba(100,200,100,0.15)" onclick="memSaveNewEntry(this)">Save</span>
|
|
3218
|
+
<span style="cursor:pointer;color:#e5534b;font-size:13px;font-weight:600;padding:4px 8px;border-radius:4px;background:rgba(229,83,75,0.1);white-space:nowrap" onclick="this.closest('.r').remove()">Cancel</span>
|
|
3219
|
+
</div>
|
|
3220
|
+
<textarea class="r-input" style="height:80px;resize:vertical;font-size:11px;line-height:1.5" placeholder="Summary (max 120 chars; one durable fact/rule/preference)..." data-cm-new="1" data-cm-f="summary"></textarea>
|
|
3221
|
+
`;
|
|
3222
|
+
list.appendChild(div);
|
|
3223
|
+
}
|
|
3224
|
+
|
|
3225
|
+
async function memSaveNewEntry(btn) {
|
|
3226
|
+
const row=btn.closest('.r');
|
|
3227
|
+
const category=row.querySelector('[data-cm-f="category"]').value;
|
|
3228
|
+
const project_id=row.querySelector('[data-cm-f="project_id"]').value.trim()||'common';
|
|
3229
|
+
const element=row.querySelector('[data-cm-f="element"]').value.trim();
|
|
3230
|
+
const summary=row.querySelector('[data-cm-f="summary"]').value.trim();
|
|
3231
|
+
if(!element||!summary){alert('Element and summary required');return;}
|
|
3232
|
+
try{
|
|
3233
|
+
const r=await fetch('/api/memory/core',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({element,summary,category,project_id})});
|
|
3234
|
+
if(!r.ok) throw new Error(`HTTP ${r.status} ${await r.text()}`);
|
|
3235
|
+
const j=await r.json();
|
|
3236
|
+
if(!j.ok){alert('Save failed: '+(j.error||'unknown'));return;}
|
|
3237
|
+
await memLoadCoreMemory();
|
|
3238
|
+
}catch(e){alert('Save failed: '+e.message);}
|
|
3239
|
+
}
|
|
3240
|
+
|
|
3241
|
+
async function memSetCoreMemoryStatus(id, status) { try{const _r=await fetch('/api/memory/entries/'+id+'/status',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({status})});if(!_r.ok)throw new Error('HTTP '+_r.status+' '+await _r.text());await memLoadCoreMemory();}catch(e){alert('Failed to update status: '+(e.message||e));} }
|
|
3242
|
+
|
|
3243
|
+
// ============================================================
|
|
3244
|
+
// SEARCH MODULE
|
|
3245
|
+
// ============================================================
|
|
3246
|
+
|
|
3247
|
+
// API-key-bearing providers — the 3 whose credentials live in search.credentials
|
|
3248
|
+
// (managed by the keyring). The other 4 providers (anthropic-oauth /
|
|
3249
|
+
// openai-oauth / openai-api / xai-api) reuse credentials
|
|
3250
|
+
// registered in the Agent panel and do not need a row here.
|
|
3251
|
+
const SR_KEY_PROVIDERS = [
|
|
3252
|
+
{ id: 'firecrawl', name: 'Firecrawl', url: 'https://firecrawl.dev' },
|
|
3253
|
+
{ id: 'tavily', name: 'Tavily', url: 'https://tavily.com' },
|
|
3254
|
+
{ id: 'exa', name: 'Exa', url: 'https://exa.ai' },
|
|
3255
|
+
];
|
|
3256
|
+
|
|
3257
|
+
// Full provider catalog with capability summary. Drives the active-provider
|
|
3258
|
+
// dropdown label so the user sees what each choice means before saving.
|
|
3259
|
+
// Provider list matches src/search/lib/backends/index.mjs PROVIDER_CAPS.
|
|
3260
|
+
const SR_PROVIDERS = [
|
|
3261
|
+
{ id: 'anthropic-oauth', label: 'Anthropic (OAuth)' },
|
|
3262
|
+
{ id: 'openai-oauth', label: 'OpenAI (OAuth)' },
|
|
3263
|
+
{ id: 'openai-api', label: 'OpenAI (API key)' },
|
|
3264
|
+
{ id: 'gemini-api', label: 'Gemini (API key)' },
|
|
3265
|
+
{ id: 'xai-api', label: 'xAI (API key)' },
|
|
3266
|
+
{ id: 'grok-oauth', label: 'Grok (OAuth)' },
|
|
3267
|
+
{ id: 'tavily', label: 'Tavily' },
|
|
3268
|
+
{ id: 'firecrawl', label: 'Firecrawl' },
|
|
3269
|
+
{ id: 'exa', label: 'Exa' },
|
|
3270
|
+
];
|
|
3271
|
+
const SR_PROVIDER_BY_ID = Object.fromEntries(SR_PROVIDERS.map(p => [p.id, p]));
|
|
3272
|
+
|
|
3273
|
+
function srProviderTag(_p) {
|
|
3274
|
+
return 'search';
|
|
3275
|
+
}
|
|
3276
|
+
|
|
3277
|
+
function srMakeKeyRow(p) {
|
|
3278
|
+
const d = document.createElement('div');
|
|
3279
|
+
d.className = 'r'; d.dataset.id = p.id;
|
|
3280
|
+
const nm = document.createElement('span'); nm.className = 'r-name'; nm.style.width = '90px'; nm.textContent = p.name; d.appendChild(nm);
|
|
3281
|
+
const group = document.createElement('div'); group.className = 'input-group'; group.style.flex = '1'; group.style.width = 'auto';
|
|
3282
|
+
const inp = document.createElement('input'); inp.className = 'r-input'; inp.type = 'password'; inp.placeholder = 'API KEY'; inp.dataset.k = 'sr-' + p.id;
|
|
3283
|
+
inp.addEventListener('input', () => { inp.classList.remove('warn', 'valid'); isDirty = true; });
|
|
3284
|
+
group.appendChild(inp);
|
|
3285
|
+
const eyeBtn = document.createElement('button'); eyeBtn.className = 'eye-btn'; eyeBtn.type = 'button'; eyeBtn.innerHTML = '👁';
|
|
3286
|
+
eyeBtn.onclick = function () { toggleVis(this); };
|
|
3287
|
+
group.appendChild(eyeBtn);
|
|
3288
|
+
d.appendChild(group);
|
|
3289
|
+
if (p.url) {
|
|
3290
|
+
const a = document.createElement('a'); a.className = 'r-link'; a.href = p.url; a.target = '_blank'; a.textContent = 'Get ↗';
|
|
3291
|
+
d.appendChild(a);
|
|
3292
|
+
}
|
|
3293
|
+
return d;
|
|
3294
|
+
}
|
|
3295
|
+
SR_KEY_PROVIDERS.forEach(p => document.getElementById('sr-search-sec').appendChild(srMakeKeyRow(p)));
|
|
3296
|
+
|
|
3297
|
+
function srRenderProviderOptions(available) {
|
|
3298
|
+
const sel = document.getElementById('sr-provider');
|
|
3299
|
+
if (!sel) return;
|
|
3300
|
+
sel.innerHTML = '';
|
|
3301
|
+
if (!available || !available.length) {
|
|
3302
|
+
const opt = document.createElement('option');
|
|
3303
|
+
opt.value = ''; opt.disabled = true; opt.selected = true;
|
|
3304
|
+
opt.textContent = '(no usable provider — register a credential below or in the Agent panel)';
|
|
3305
|
+
sel.appendChild(opt);
|
|
3306
|
+
sel.disabled = true;
|
|
3307
|
+
return;
|
|
3308
|
+
}
|
|
3309
|
+
sel.disabled = false;
|
|
3310
|
+
SR_PROVIDERS.filter(p => available.includes(p.id)).forEach(p => {
|
|
3311
|
+
const opt = document.createElement('option');
|
|
3312
|
+
opt.value = p.id;
|
|
3313
|
+
opt.textContent = p.label;
|
|
3314
|
+
sel.appendChild(opt);
|
|
3315
|
+
});
|
|
3316
|
+
}
|
|
3317
|
+
|
|
3318
|
+
function srUpdateProviderHint() {
|
|
3319
|
+
const hint = document.getElementById('sr-provider-hint');
|
|
3320
|
+
const sel = document.getElementById('sr-provider');
|
|
3321
|
+
if (!hint || !sel) return;
|
|
3322
|
+
const p = SR_PROVIDER_BY_ID[sel.value];
|
|
3323
|
+
if (!p) { hint.textContent = ''; return; }
|
|
3324
|
+
const parts = [];
|
|
3325
|
+
parts.push('Two-step model: `search` returns snippets + URLs; follow up with `web_fetch` for raw page bodies.');
|
|
3326
|
+
parts.push('`web_fetch` uses local readability+puppeteer extractors (provider-independent).');
|
|
3327
|
+
hint.textContent = parts.join(' ');
|
|
3328
|
+
}
|
|
3329
|
+
|
|
3330
|
+
// Model-preset families exposed in Search Provider. Each family fetches its
|
|
3331
|
+
// catalog from the bare provider ID (openai-api uses openai-api directly,
|
|
3332
|
+
// xai-api → xai-api). Anthropic is intentionally
|
|
3333
|
+
// omitted: search.anthropic-oauth pins claude-haiku-4-5 server-side.
|
|
3334
|
+
// Probe order matters when the user has multiple credentials for the same
|
|
3335
|
+
// family — the first available provider drives the displayed catalog.
|
|
3336
|
+
// openai-oauth probes first so the dropdown matches the Codex catalog the
|
|
3337
|
+
// user sees under Agent → Providers → GPT (OAuth); the live /v1/models
|
|
3338
|
+
// catalog from openai-api is the fallback when only the API key is set.
|
|
3339
|
+
const SR_MODEL_FAMILIES = [
|
|
3340
|
+
{ family: 'openai', label: 'OpenAI', probeProviders: ['openai-oauth', 'openai-api'] },
|
|
3341
|
+
{ family: 'gemini', label: 'Gemini', probeProviders: ['gemini-api'] },
|
|
3342
|
+
{ family: 'xai', label: 'xAI', probeProviders: ['xai-api', 'grok-oauth'] },
|
|
3343
|
+
];
|
|
3344
|
+
|
|
3345
|
+
document.getElementById('sr-provider')?.addEventListener('change', () => {
|
|
3346
|
+
isDirty = true;
|
|
3347
|
+
srUpdateProviderHint();
|
|
3348
|
+
});
|
|
3349
|
+
|
|
3350
|
+
// Render one row per family whose backing provider is in availableProviders.
|
|
3351
|
+
// Each row fetches its model catalog live from /agent/models — no static
|
|
3352
|
+
// lists. Empty result silently hides the row (invariant: only show models
|
|
3353
|
+
// the user can actually use). Selection persists under search.models[family].
|
|
3354
|
+
// Per-family cache of the filtered model objects + the provider id we
|
|
3355
|
+
// probed for each family. srUpdateOpenAIEffortFast reads these to derive
|
|
3356
|
+
// AG_FAMILY_EFFORT / AG_FAST_PROVIDERS membership when the user changes
|
|
3357
|
+
// the selected model.
|
|
3358
|
+
let srModelListByFam = {};
|
|
3359
|
+
let srProviderByFam = {};
|
|
3360
|
+
|
|
3361
|
+
// Build a family model <select> that renders immediately — disabled and
|
|
3362
|
+
// flagged data-pending — showing the saved model id as the selected option
|
|
3363
|
+
// so the row appears fully populated before its live catalog round-trip
|
|
3364
|
+
// finishes. The pending flag keeps srBuildData from persisting this family
|
|
3365
|
+
// (disabled selects are skipped), so a Save mid-fetch never clobbers the
|
|
3366
|
+
// stored selection. srRenderModelPresets clears data-pending + disabled once
|
|
3367
|
+
// the catalog arrives and replaces these placeholder options.
|
|
3368
|
+
function buildFamSelect(family) {
|
|
3369
|
+
const stored = srConfig?.models?.[family] || '';
|
|
3370
|
+
const opt = stored
|
|
3371
|
+
? '<option value="' + escapeAttr(stored) + '" selected>' + escapeHtml(stored) + '</option>'
|
|
3372
|
+
: '<option value="">Loading…</option>';
|
|
3373
|
+
return '<select class="r-select" data-fam="' + family + '" data-pending="1" style="flex:0 0 260px;width:260px" disabled>' + opt + '</select>';
|
|
3374
|
+
}
|
|
3375
|
+
|
|
3376
|
+
// Terminal settle for a family select when no usable catalog arrived. The
|
|
3377
|
+
// stored selection (if any) stays visible and selected so a Save round-trips
|
|
3378
|
+
// the identical value; otherwise the reason renders as an empty-value option
|
|
3379
|
+
// (empty values are skipped by srBuildData). Always clears data-pending and
|
|
3380
|
+
// re-enables so a row can never stay locked after its fetch settles.
|
|
3381
|
+
function srSettleFamSelect(sel, reason) {
|
|
3382
|
+
if (!sel.value) sel.innerHTML = '<option value="">' + escapeHtml(reason) + '</option>';
|
|
3383
|
+
sel.removeAttribute('data-pending');
|
|
3384
|
+
sel.disabled = false;
|
|
3385
|
+
}
|
|
3386
|
+
|
|
3387
|
+
async function srRenderModelPresets() {
|
|
3388
|
+
const sec = document.getElementById('sr-model-presets-sec');
|
|
3389
|
+
const body = document.getElementById('sr-model-presets-body');
|
|
3390
|
+
if (!sec || !body) return;
|
|
3391
|
+
const available = new Set(srConfig?.availableProviders || []);
|
|
3392
|
+
const visible = SR_MODEL_FAMILIES.filter(f => f.probeProviders.some(p => available.has(p)));
|
|
3393
|
+
if (visible.length === 0) {
|
|
3394
|
+
sec.style.display = 'none';
|
|
3395
|
+
body.innerHTML = '';
|
|
3396
|
+
srModelListByFam = {};
|
|
3397
|
+
srProviderByFam = {};
|
|
3398
|
+
return;
|
|
3399
|
+
}
|
|
3400
|
+
sec.style.display = '';
|
|
3401
|
+
body.innerHTML = '';
|
|
3402
|
+
srModelListByFam = {};
|
|
3403
|
+
srProviderByFam = {};
|
|
3404
|
+
for (const fam of visible) {
|
|
3405
|
+
const row = document.createElement('div');
|
|
3406
|
+
row.className = 'r';
|
|
3407
|
+
// openai row carries an extra hidden effort dropdown + fast toggle.
|
|
3408
|
+
// They reveal themselves in srUpdateOpenAIEffortFast() once the model
|
|
3409
|
+
// list resolves and we can read the selected model's family. Gemini /
|
|
3410
|
+
// xAI rows stay model-only — reasoning_effort + service_tier are not
|
|
3411
|
+
// exposed by their search backends in this revision.
|
|
3412
|
+
// OpenAI row carries the extra effort dropdown + labeled Fast Mode
|
|
3413
|
+
// toggle. The model select is narrowed (max-width:280px) on every
|
|
3414
|
+
// family row so the OpenAI extras fit on one line, and Gemini / xAI
|
|
3415
|
+
// share the same width for visual alignment.
|
|
3416
|
+
const extra = fam.family === 'openai'
|
|
3417
|
+
? '<select class="r-select" data-fam-effort="openai" style="width:auto;flex:0 0 auto;margin-left:8px;display:none"></select>' +
|
|
3418
|
+
'<span data-fam-fast-label="openai" style="margin-left:10px;font-size:11px;color:var(--text-3);display:none">Fast Mode</span>' +
|
|
3419
|
+
'<div class="r-toggle" data-fam-fast="openai" style="margin-left:8px;display:none" title="Speed optimized output (premium)"></div>'
|
|
3420
|
+
: '';
|
|
3421
|
+
row.innerHTML =
|
|
3422
|
+
'<span class="r-name" style="width:90px">' + fam.label + '</span>' +
|
|
3423
|
+
buildFamSelect(fam.family) +
|
|
3424
|
+
extra;
|
|
3425
|
+
body.appendChild(row);
|
|
3426
|
+
if (fam.family === 'openai') {
|
|
3427
|
+
const fastEl = row.querySelector('[data-fam-fast="openai"]');
|
|
3428
|
+
fastEl?.addEventListener('click', () => { fastEl.classList.toggle('on'); isDirty = true; });
|
|
3429
|
+
const effortEl = row.querySelector('select[data-fam-effort="openai"]');
|
|
3430
|
+
effortEl?.addEventListener('change', () => { isDirty = true; });
|
|
3431
|
+
}
|
|
3432
|
+
}
|
|
3433
|
+
await Promise.all(visible.map(async fam => {
|
|
3434
|
+
const sel = body.querySelector('select[data-fam="' + fam.family + '"]');
|
|
3435
|
+
if (!sel) return;
|
|
3436
|
+
const probe = fam.probeProviders.find(p => available.has(p)) || fam.probeProviders[0];
|
|
3437
|
+
srProviderByFam[fam.family] = probe;
|
|
3438
|
+
try {
|
|
3439
|
+
const r = await fetch('/agent/models?provider=' + encodeURIComponent(probe)).then(r => r.json());
|
|
3440
|
+
const rawModels = (r?.models || []).map(m => typeof m === 'string' ? { id: m } : m).filter(m => m && m.id);
|
|
3441
|
+
const visibleModels = filterCodingModels(rawModels);
|
|
3442
|
+
srModelListByFam[fam.family] = visibleModels;
|
|
3443
|
+
if (!visibleModels.length) {
|
|
3444
|
+
srSettleFamSelect(sel, 'No models available');
|
|
3445
|
+
return;
|
|
3446
|
+
}
|
|
3447
|
+
const stored = srConfig?.models?.[fam.family] || '';
|
|
3448
|
+
sel.innerHTML = '';
|
|
3449
|
+
visibleModels.forEach(m => {
|
|
3450
|
+
const opt = document.createElement('option');
|
|
3451
|
+
opt.value = m.id;
|
|
3452
|
+
opt.textContent = m.display || m.id;
|
|
3453
|
+
if (m.id === stored) opt.selected = true;
|
|
3454
|
+
sel.appendChild(opt);
|
|
3455
|
+
});
|
|
3456
|
+
// Catalog has arrived — clear the pending flag and enable the row.
|
|
3457
|
+
sel.removeAttribute('data-pending');
|
|
3458
|
+
sel.disabled = false;
|
|
3459
|
+
sel.addEventListener('change', () => { isDirty = true; if (fam.family === 'openai') srUpdateOpenAIEffortFast(); });
|
|
3460
|
+
if (fam.family === 'openai') srUpdateOpenAIEffortFast();
|
|
3461
|
+
} catch {
|
|
3462
|
+
srSettleFamSelect(sel, 'Failed to fetch models');
|
|
3463
|
+
}
|
|
3464
|
+
}));
|
|
3465
|
+
}
|
|
3466
|
+
|
|
3467
|
+
// Reveal/hide effort + fast based on the currently selected openai model.
|
|
3468
|
+
// Mirrors agUpdateEffortAndFast() so the rules are identical: model family
|
|
3469
|
+
// drives effort options, AG_FAST_PROVIDERS / AG_FAMILY_NO_FAST drive fast.
|
|
3470
|
+
function srUpdateOpenAIEffortFast() {
|
|
3471
|
+
const body = document.getElementById('sr-model-presets-body');
|
|
3472
|
+
if (!body) return;
|
|
3473
|
+
const modelSel = body.querySelector('select[data-fam="openai"]');
|
|
3474
|
+
const effortSel = body.querySelector('select[data-fam-effort="openai"]');
|
|
3475
|
+
const fastEl = body.querySelector('[data-fam-fast="openai"]');
|
|
3476
|
+
if (!modelSel || !effortSel || !fastEl) return;
|
|
3477
|
+
const modelId = modelSel.value || '';
|
|
3478
|
+
const list = srModelListByFam.openai || [];
|
|
3479
|
+
const model = list.find(m => m.id === modelId) || null;
|
|
3480
|
+
const provider = srProviderByFam.openai || '';
|
|
3481
|
+
// Normalize search-side provider keys to the agent-side keys used by
|
|
3482
|
+
// AG_FAST_PROVIDERS / AG_EFFORT_OPTIONS. 'openai-api' is the same backend
|
|
3483
|
+
// as 'openai' for capability membership; 'openai-oauth' is already shared.
|
|
3484
|
+
const normalizedProvider = provider === 'openai-api' ? 'openai' : provider;
|
|
3485
|
+
const allowed = model ? agEffortOptionsFor(normalizedProvider, model) : null;
|
|
3486
|
+
const storedEffort = srConfig?.modelOptions?.openai?.effort || '';
|
|
3487
|
+
if (!allowed || allowed.length === 0) {
|
|
3488
|
+
effortSel.style.display = 'none';
|
|
3489
|
+
effortSel.innerHTML = '';
|
|
3490
|
+
} else {
|
|
3491
|
+
effortSel.style.display = '';
|
|
3492
|
+
effortSel.innerHTML = allowed.map(v => '<option value="' + v + '"' + (v === storedEffort ? ' selected' : '') + '>' + (AG_EFFORT_LABEL[v] || v) + '</option>').join('');
|
|
3493
|
+
}
|
|
3494
|
+
const providerFast = AG_FAST_PROVIDERS.has(normalizedProvider);
|
|
3495
|
+
const familyNoFast = model?.family && AG_FAMILY_NO_FAST.has(model.family);
|
|
3496
|
+
const fastAllowed = !!model && providerFast && !familyNoFast;
|
|
3497
|
+
const fastLabel = body.querySelector('[data-fam-fast-label="openai"]');
|
|
3498
|
+
fastEl.style.display = fastAllowed ? '' : 'none';
|
|
3499
|
+
if (fastLabel) fastLabel.style.display = fastAllowed ? '' : 'none';
|
|
3500
|
+
if (!fastAllowed) fastEl.classList.remove('on');
|
|
3501
|
+
else if (srConfig?.modelOptions?.openai?.fast) fastEl.classList.add('on');
|
|
3502
|
+
else fastEl.classList.remove('on');
|
|
3503
|
+
}
|
|
3504
|
+
|
|
3505
|
+
async function loadSearchConfig() {
|
|
3506
|
+
const cfg = await fetch('/search/config').then(r => r.json()).catch(() => ({}));
|
|
3507
|
+
srConfig = cfg;
|
|
3508
|
+
srPopulateSearch(cfg);
|
|
3509
|
+
}
|
|
3510
|
+
|
|
3511
|
+
function srPopulateSearch(cfg) {
|
|
3512
|
+
srRenderProviderOptions(cfg?.availableProviders || []);
|
|
3513
|
+
const sel = document.getElementById('sr-provider');
|
|
3514
|
+
if (sel && !sel.disabled) {
|
|
3515
|
+
const active = cfg?.provider;
|
|
3516
|
+
if (active && Array.from(sel.options).some(o => o.value === active)) sel.value = active;
|
|
3517
|
+
}
|
|
3518
|
+
srUpdateProviderHint();
|
|
3519
|
+
srRenderModelPresets();
|
|
3520
|
+
|
|
3521
|
+
const creds = cfg?.rawSearch?.credentials || {};
|
|
3522
|
+
SR_KEY_PROVIDERS.forEach(p => {
|
|
3523
|
+
const inp = document.querySelector('[data-k="sr-' + p.id + '"]');
|
|
3524
|
+
const key = creds[p.id]?.apiKey || '';
|
|
3525
|
+
if (inp) inp.value = key;
|
|
3526
|
+
});
|
|
3527
|
+
}
|
|
3528
|
+
|
|
3529
|
+
function srBuildData() {
|
|
3530
|
+
const data = {};
|
|
3531
|
+
const sel = document.getElementById('sr-provider');
|
|
3532
|
+
if (sel && sel.value) data.provider = sel.value;
|
|
3533
|
+
// Collect per-family model selections from the Model presets section.
|
|
3534
|
+
const models = {};
|
|
3535
|
+
document.querySelectorAll('#sr-model-presets-body select[data-fam]').forEach(s => {
|
|
3536
|
+
if (s.disabled) return;
|
|
3537
|
+
const fam = s.dataset.fam;
|
|
3538
|
+
if (fam && s.value) models[fam] = s.value;
|
|
3539
|
+
});
|
|
3540
|
+
if (Object.keys(models).length) data.models = models;
|
|
3541
|
+
// Per-family reasoning effort + service-tier toggles. Only openai exposes
|
|
3542
|
+
// these in this revision; absent/hidden inputs are simply skipped.
|
|
3543
|
+
const modelOptions = {};
|
|
3544
|
+
const openaiOpts = {};
|
|
3545
|
+
const effortSel = document.querySelector('#sr-model-presets-body select[data-fam-effort="openai"]');
|
|
3546
|
+
const fastEl = document.querySelector('#sr-model-presets-body [data-fam-fast="openai"]');
|
|
3547
|
+
if (effortSel && effortSel.style.display !== 'none' && effortSel.value) openaiOpts.effort = effortSel.value;
|
|
3548
|
+
if (fastEl && fastEl.style.display !== 'none' && fastEl.classList.contains('on')) openaiOpts.fast = true;
|
|
3549
|
+
if (Object.keys(openaiOpts).length) modelOptions.openai = openaiOpts;
|
|
3550
|
+
data.modelOptions = modelOptions;
|
|
3551
|
+
const searchProviders = {};
|
|
3552
|
+
SR_KEY_PROVIDERS.forEach(p => {
|
|
3553
|
+
const inp = document.querySelector('[data-k="sr-' + p.id + '"]');
|
|
3554
|
+
if (inp) searchProviders[p.id] = inp.value;
|
|
3555
|
+
});
|
|
3556
|
+
data.searchProviders = searchProviders;
|
|
3557
|
+
return data;
|
|
3558
|
+
}
|
|
3559
|
+
|
|
3560
|
+
async function srSavePanel() {
|
|
3561
|
+
const activePanel = document.querySelector('.panel.active');
|
|
3562
|
+
const warnEl = activePanel?.querySelector('.warn-msg') || document.createElement('div');
|
|
3563
|
+
const btn = activePanel?.querySelector('.save');
|
|
3564
|
+
warnEl.textContent = '';
|
|
3565
|
+
const data = srBuildData();
|
|
3566
|
+
|
|
3567
|
+
if (btn) { btn.textContent = 'Saving...'; btn.disabled = true; }
|
|
3568
|
+
try {
|
|
3569
|
+
const res = await fetch('/search/config', {
|
|
3570
|
+
method: 'POST',
|
|
3571
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3572
|
+
body: JSON.stringify(data),
|
|
3573
|
+
});
|
|
3574
|
+
if (!res.ok) {
|
|
3575
|
+
let msg = 'HTTP ' + res.status;
|
|
3576
|
+
try { const j = await res.json(); msg = j.error || j.message || msg; }
|
|
3577
|
+
catch { try { msg = await res.text() || msg; } catch {} }
|
|
3578
|
+
throw new Error(msg);
|
|
3579
|
+
}
|
|
3580
|
+
isDirty = false;
|
|
3581
|
+
// Newly-registered keys must immediately flip the corresponding provider
|
|
3582
|
+
// into the dropdown without a UI reload — re-fetch the config and repopulate.
|
|
3583
|
+
await loadSearchConfig();
|
|
3584
|
+
if (btn) { btn.textContent = '✓ Saved'; btn.classList.add('done'); setTimeout(() => { btn.textContent = 'Save'; btn.classList.remove('done'); btn.disabled = false; }, 1500); }
|
|
3585
|
+
} catch (e) {
|
|
3586
|
+
console.error('[mixdog] save error:', e);
|
|
3587
|
+
warnEl.textContent = 'Save failed: ' + (e?.message || e);
|
|
3588
|
+
if (btn) { btn.textContent = 'Save'; btn.disabled = false; }
|
|
3589
|
+
}
|
|
3590
|
+
}
|
|
3591
|
+
|
|
3592
|
+
// ============================================================
|
|
3593
|
+
// -- Agent Maintenance --
|
|
3594
|
+
const AG_MAINT_TASKS = [
|
|
3595
|
+
{ id: 'explore', label: 'Explore', desc: 'Filesystem exploration agent (explore tool)' },
|
|
3596
|
+
{ id: 'cycle1', label: 'Memory Cycle 1', desc: 'Chunker / classifier (memory ingestion)' },
|
|
3597
|
+
{ id: 'cycle2', label: 'Memory Cycle 2', desc: 'Root re-scorer (core memory promotion)' },
|
|
3598
|
+
{ id: 'cycle3', label: 'Memory Cycle 3', desc: 'Core memory reviewer' },
|
|
3599
|
+
];
|
|
3600
|
+
let agMaintenance = {};
|
|
3601
|
+
let agMaintenanceDefaults = {};
|
|
3602
|
+
|
|
3603
|
+
async function loadAgMaintenance() {
|
|
3604
|
+
try {
|
|
3605
|
+
const [r] = await Promise.all([
|
|
3606
|
+
fetch('/agent/maintenance').then(r => r.json()),
|
|
3607
|
+
agPresets.length ? Promise.resolve() : agLoadPresets(),
|
|
3608
|
+
]);
|
|
3609
|
+
agMaintenance = r.maintenance || {};
|
|
3610
|
+
agMaintenanceDefaults = r.defaults || {};
|
|
3611
|
+
} catch { agMaintenance = {}; agMaintenanceDefaults = {}; }
|
|
3612
|
+
renderAgMaintenance();
|
|
3613
|
+
}
|
|
3614
|
+
|
|
3615
|
+
function renderAgMaintenance() {
|
|
3616
|
+
const container = document.getElementById('ag-maint-tasks');
|
|
3617
|
+
if (!container) return;
|
|
3618
|
+
container.innerHTML = '';
|
|
3619
|
+
for (const task of AG_MAINT_TASKS) {
|
|
3620
|
+
// Only an EXPLICIT slot value selects a preset; an unset non-default slot
|
|
3621
|
+
// renders as "Inherit default" (truly unset) so Save never persists a
|
|
3622
|
+
// phantom override. Stored values may be a preset id or a display name.
|
|
3623
|
+
const explicit = agMaintenance[task.id] || '';
|
|
3624
|
+
const matched = agPresets.find(p => p.id === explicit) || agPresets.find(p => (p.name || '') === explicit);
|
|
3625
|
+
const currentId = matched ? matched.id : '';
|
|
3626
|
+
const row = document.createElement('div');
|
|
3627
|
+
row.className = 'r';
|
|
3628
|
+
const nameSpan = document.createElement('span');
|
|
3629
|
+
nameSpan.className = 'r-name';
|
|
3630
|
+
nameSpan.textContent = task.label;
|
|
3631
|
+
const sel = document.createElement('select');
|
|
3632
|
+
sel.className = 'r-select';
|
|
3633
|
+
sel.id = 'ag-maint-' + task.id;
|
|
3634
|
+
sel.style.cssText = 'width:200px;flex:0 0 200px';
|
|
3635
|
+
const descSpan = document.createElement('span');
|
|
3636
|
+
descSpan.style.cssText = 'flex:1;font-size:11px;color:var(--text-4);margin-left:10px';
|
|
3637
|
+
descSpan.textContent = task.desc;
|
|
3638
|
+
row.appendChild(nameSpan);
|
|
3639
|
+
row.appendChild(sel);
|
|
3640
|
+
row.appendChild(descSpan);
|
|
3641
|
+
container.appendChild(row);
|
|
3642
|
+
// "Inherit default" option removed per request — each slot lists explicit
|
|
3643
|
+
// presets only. An unset slot simply shows the first preset; Save persists
|
|
3644
|
+
// whatever is selected.
|
|
3645
|
+
agPresets.forEach(p => {
|
|
3646
|
+
const opt = document.createElement('option');
|
|
3647
|
+
opt.value = p.id;
|
|
3648
|
+
opt.textContent = p.name || p.id;
|
|
3649
|
+
if (p.id === currentId) opt.selected = true;
|
|
3650
|
+
sel.appendChild(opt);
|
|
3651
|
+
});
|
|
3652
|
+
}
|
|
3653
|
+
}
|
|
3654
|
+
|
|
3655
|
+
async function saveAgMaintenance() {
|
|
3656
|
+
const warn = document.getElementById('warn-ag-maint');
|
|
3657
|
+
warn.textContent = '';
|
|
3658
|
+
const data = {};
|
|
3659
|
+
for (const task of AG_MAINT_TASKS) {
|
|
3660
|
+
const v = document.getElementById('ag-maint-' + task.id)?.value;
|
|
3661
|
+
if (v) data[task.id] = v;
|
|
3662
|
+
else data[task.id] = null; // inherit → clear any override
|
|
3663
|
+
}
|
|
3664
|
+
try {
|
|
3665
|
+
const r = await fetch('/agent/maintenance', {
|
|
3666
|
+
method: 'POST',
|
|
3667
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3668
|
+
body: JSON.stringify(data),
|
|
3669
|
+
}).then(r => r.json());
|
|
3670
|
+
if (!r.ok) { warn.textContent = r.error || 'Save failed'; return; }
|
|
3671
|
+
const btn = document.querySelector('#panel-agent-maintenance .save');
|
|
3672
|
+
btn.classList.add('done'); btn.textContent = 'Saved';
|
|
3673
|
+
setTimeout(() => { btn.classList.remove('done'); btn.textContent = 'Save'; }, 1500);
|
|
3674
|
+
} catch {
|
|
3675
|
+
warn.textContent = 'Save failed';
|
|
3676
|
+
}
|
|
3677
|
+
}
|
|
3678
|
+
</script>
|
|
3679
|
+
|
|
3680
|
+
<div class="modal-overlay" id="close-modal">
|
|
3681
|
+
<div class="modal-box">
|
|
3682
|
+
<div class="modal-title">Unsaved Changes</div>
|
|
3683
|
+
<div class="modal-msg">You have unsaved changes. What would you like to do?</div>
|
|
3684
|
+
<div class="modal-actions">
|
|
3685
|
+
<button class="mbtn primary" onclick="saveAndClose()">Save & Close</button>
|
|
3686
|
+
<button class="mbtn danger-outline" onclick="discardAndClose()">Close Without Saving</button>
|
|
3687
|
+
<button class="mbtn secondary" onclick="cancelClose()">Cancel</button>
|
|
3688
|
+
</div>
|
|
3689
|
+
</div>
|
|
3690
|
+
</div>
|
|
3691
|
+
|
|
3692
|
+
</body>
|
|
3693
|
+
</html>
|