mixdog 0.7.18 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (844) hide show
  1. package/README.md +37 -331
  2. package/package.json +67 -99
  3. package/scripts/boot-smoke.mjs +94 -0
  4. package/scripts/build-tui.mjs +52 -0
  5. package/scripts/compact-smoke.mjs +199 -0
  6. package/scripts/lead-workflow-smoke.mjs +598 -0
  7. package/scripts/live-worker-smoke.mjs +239 -0
  8. package/scripts/output-style-smoke.mjs +101 -0
  9. package/scripts/smoke-loop-report.mjs +221 -0
  10. package/scripts/smoke-loop.mjs +201 -0
  11. package/scripts/smoke.mjs +113 -0
  12. package/scripts/tool-failures.mjs +143 -0
  13. package/scripts/tool-smoke.mjs +456 -0
  14. package/src/agents/debugger/AGENT.md +3 -0
  15. package/src/agents/debugger/agent.json +6 -0
  16. package/src/agents/explore/AGENT.md +4 -0
  17. package/src/agents/explore/agent.json +6 -0
  18. package/src/agents/heavy-worker/AGENT.md +3 -0
  19. package/src/agents/heavy-worker/agent.json +6 -0
  20. package/src/agents/maintainer/AGENT.md +3 -0
  21. package/src/agents/maintainer/agent.json +6 -0
  22. package/src/agents/reviewer/AGENT.md +3 -0
  23. package/src/agents/reviewer/agent.json +6 -0
  24. package/src/agents/scheduler-task.md +3 -0
  25. package/src/agents/web-researcher/AGENT.md +3 -0
  26. package/src/agents/web-researcher/agent.json +6 -0
  27. package/src/agents/webhook-handler.md +3 -0
  28. package/src/agents/worker/AGENT.md +3 -0
  29. package/src/agents/worker/agent.json +6 -0
  30. package/src/app.mjs +90 -0
  31. package/src/cli.mjs +11 -0
  32. package/src/defaults/hidden-roles.json +72 -0
  33. package/src/defaults/mixdog-config.template.json +15 -0
  34. package/src/hooks/lib/permission-evaluator.cjs +488 -0
  35. package/src/hooks/lib/settings-loader.cjs +112 -0
  36. package/src/lib/keychain-cjs.cjs +332 -0
  37. package/src/lib/plugin-paths.cjs +28 -0
  38. package/src/lib/rules-builder.cjs +315 -0
  39. package/src/mixdog-session-runtime.mjs +3704 -0
  40. package/src/output-styles/default.md +38 -0
  41. package/src/output-styles/extreme-simple.md +17 -0
  42. package/src/output-styles/simple.md +17 -0
  43. package/src/repl.mjs +322 -0
  44. package/src/rules/bridge/00-common.md +5 -0
  45. package/src/rules/bridge/20-skip-protocol.md +11 -0
  46. package/src/rules/bridge/30-explorer.md +4 -0
  47. package/src/rules/bridge/40-cycle1-agent.md +28 -0
  48. package/src/rules/bridge/41-cycle2-agent.md +59 -0
  49. package/src/rules/lead/00-tool-lead.md +5 -0
  50. package/src/rules/lead/01-general.md +5 -0
  51. package/src/rules/lead/02-channels.md +3 -0
  52. package/src/rules/lead/04-workflow.md +12 -0
  53. package/src/rules/shared/00-language.md +3 -0
  54. package/src/rules/shared/01-tool.md +3 -0
  55. package/src/runtime/agent/orchestrator/bridge-trace.mjs +814 -0
  56. package/src/runtime/agent/orchestrator/cache-mtime.mjs +60 -0
  57. package/src/runtime/agent/orchestrator/config.mjs +446 -0
  58. package/src/runtime/agent/orchestrator/context/collect.mjs +796 -0
  59. package/src/runtime/agent/orchestrator/dispatch-persist.mjs +417 -0
  60. package/src/runtime/agent/orchestrator/internal-roles.mjs +188 -0
  61. package/src/runtime/agent/orchestrator/internal-tools.mjs +88 -0
  62. package/src/runtime/agent/orchestrator/mcp/client.mjs +345 -0
  63. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +2104 -0
  64. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +784 -0
  65. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +341 -0
  66. package/src/runtime/agent/orchestrator/providers/gemini.mjs +1679 -0
  67. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +959 -0
  68. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +213 -0
  69. package/src/runtime/agent/orchestrator/providers/model-cache.mjs +38 -0
  70. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +471 -0
  71. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +615 -0
  72. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +808 -0
  73. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +1719 -0
  74. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +2587 -0
  75. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +1953 -0
  76. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +136 -0
  77. package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +317 -0
  78. package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +109 -0
  79. package/src/runtime/agent/orchestrator/providers/registry.mjs +247 -0
  80. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +332 -0
  81. package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +11 -0
  82. package/src/runtime/agent/orchestrator/providers/trace-utils.mjs +50 -0
  83. package/src/runtime/agent/orchestrator/session/cache/prefetch-cache.mjs +142 -0
  84. package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +318 -0
  85. package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +367 -0
  86. package/src/runtime/agent/orchestrator/session/compact.mjs +882 -0
  87. package/src/runtime/agent/orchestrator/session/context-utils.mjs +233 -0
  88. package/src/runtime/agent/orchestrator/session/loop.mjs +2320 -0
  89. package/src/runtime/agent/orchestrator/session/manager.mjs +2960 -0
  90. package/src/runtime/agent/orchestrator/session/result-classification.mjs +65 -0
  91. package/src/runtime/agent/orchestrator/session/store.mjs +663 -0
  92. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +166 -0
  93. package/src/runtime/agent/orchestrator/smart-bridge/bridge-llm.mjs +339 -0
  94. package/src/runtime/agent/orchestrator/smart-bridge/cache-strategy.mjs +419 -0
  95. package/src/runtime/agent/orchestrator/stall-policy.mjs +227 -0
  96. package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +235 -0
  97. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +723 -0
  98. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +389 -0
  99. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +637 -0
  100. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +165 -0
  101. package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +104 -0
  102. package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +194 -0
  103. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +596 -0
  104. package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +110 -0
  105. package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +153 -0
  106. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +118 -0
  107. package/src/runtime/agent/orchestrator/tools/builtin/read-open.mjs +189 -0
  108. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +731 -0
  109. package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +168 -0
  110. package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +602 -0
  111. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +465 -0
  112. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +160 -0
  113. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +982 -0
  114. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +1087 -0
  115. package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +231 -0
  116. package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +223 -0
  117. package/src/runtime/agent/orchestrator/tools/builtin.mjs +478 -0
  118. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +24 -0
  119. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4102 -0
  120. package/src/runtime/agent/orchestrator/tools/destructive-warning.mjs +323 -0
  121. package/src/runtime/agent/orchestrator/tools/graph-binary-fetcher.mjs +154 -0
  122. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +26 -0
  123. package/src/runtime/agent/orchestrator/tools/patch-binary-fetcher.mjs +143 -0
  124. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +18 -0
  125. package/src/runtime/agent/orchestrator/tools/patch.mjs +2772 -0
  126. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +114 -0
  127. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +880 -0
  128. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +312 -0
  129. package/src/runtime/channels/backends/discord.mjs +781 -0
  130. package/src/runtime/channels/data/voice-runtime-manifest.json +138 -0
  131. package/src/runtime/channels/index.mjs +3309 -0
  132. package/src/runtime/channels/lib/config.mjs +285 -0
  133. package/src/runtime/channels/lib/drop-trace.mjs +71 -0
  134. package/src/runtime/channels/lib/event-pipeline.mjs +81 -0
  135. package/src/runtime/channels/lib/holidays.mjs +138 -0
  136. package/src/runtime/channels/lib/hook-pipe-server.mjs +671 -0
  137. package/src/runtime/channels/lib/output-forwarder.mjs +765 -0
  138. package/src/runtime/channels/lib/runtime-paths.mjs +497 -0
  139. package/src/runtime/channels/lib/scheduler.mjs +710 -0
  140. package/src/runtime/channels/lib/session-discovery.mjs +102 -0
  141. package/src/runtime/channels/lib/state-file.mjs +68 -0
  142. package/src/runtime/channels/lib/status-snapshot.mjs +224 -0
  143. package/src/runtime/channels/lib/tool-format.mjs +124 -0
  144. package/src/runtime/channels/lib/transcript-discovery.mjs +195 -0
  145. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +734 -0
  146. package/src/runtime/channels/lib/webhook.mjs +1288 -0
  147. package/src/runtime/channels/tool-defs.mjs +177 -0
  148. package/src/runtime/lib/keychain-cjs.cjs +289 -0
  149. package/src/runtime/memory/data/runtime-manifest.json +40 -0
  150. package/src/runtime/memory/index.mjs +3600 -0
  151. package/src/runtime/memory/lib/core-memory-store.mjs +336 -0
  152. package/src/runtime/memory/lib/embedding-provider.mjs +275 -0
  153. package/src/runtime/memory/lib/embedding-worker.mjs +331 -0
  154. package/src/runtime/memory/lib/memory-cycle-requests.mjs +276 -0
  155. package/src/runtime/memory/lib/memory-cycle1.mjs +783 -0
  156. package/src/runtime/memory/lib/memory-cycle2.mjs +1389 -0
  157. package/src/runtime/memory/lib/memory-cycle3.mjs +646 -0
  158. package/src/runtime/memory/lib/memory-embed.mjs +300 -0
  159. package/src/runtime/memory/lib/memory-ops-policy.mjs +149 -0
  160. package/src/runtime/memory/lib/memory-recall-store.mjs +644 -0
  161. package/src/runtime/memory/lib/memory.mjs +418 -0
  162. package/src/runtime/memory/lib/pg/adapter.mjs +314 -0
  163. package/src/runtime/memory/lib/pg/process.mjs +366 -0
  164. package/src/runtime/memory/lib/pg/supervisor.mjs +495 -0
  165. package/src/runtime/memory/lib/runtime-fetcher.mjs +464 -0
  166. package/src/runtime/memory/lib/trace-store.mjs +734 -0
  167. package/src/runtime/memory/tool-defs.mjs +79 -0
  168. package/src/runtime/search/index.mjs +925 -0
  169. package/src/runtime/search/lib/config.mjs +61 -0
  170. package/src/runtime/search/lib/web-tools.mjs +1278 -0
  171. package/src/runtime/search/tool-defs.mjs +64 -0
  172. package/src/runtime/shared/atomic-file.mjs +435 -0
  173. package/src/runtime/shared/background-tasks.mjs +376 -0
  174. package/src/runtime/shared/child-guardian.mjs +98 -0
  175. package/src/runtime/shared/config.mjs +393 -0
  176. package/src/runtime/shared/err-text.mjs +121 -0
  177. package/src/runtime/shared/launcher-control.mjs +259 -0
  178. package/src/runtime/shared/llm/http-agent.mjs +129 -0
  179. package/src/runtime/shared/open-url.mjs +37 -0
  180. package/src/runtime/shared/plugin-paths.mjs +25 -0
  181. package/src/runtime/shared/process-shutdown.mjs +147 -0
  182. package/src/runtime/shared/schedules-store.mjs +70 -0
  183. package/src/runtime/shared/tool-execution-contract.mjs +104 -0
  184. package/src/runtime/shared/tool-surface.mjs +950 -0
  185. package/src/runtime/shared/user-cwd.mjs +221 -0
  186. package/src/runtime/shared/user-data-guard.mjs +232 -0
  187. package/src/runtime/shared/workspace-router.mjs +259 -0
  188. package/src/standalone/bridge-tool.mjs +1414 -0
  189. package/src/standalone/channel-admin.mjs +366 -0
  190. package/src/standalone/channel-worker-preload.cjs +3 -0
  191. package/src/standalone/channel-worker.mjs +353 -0
  192. package/src/standalone/explore-tool.mjs +233 -0
  193. package/src/standalone/hook-bus.mjs +246 -0
  194. package/src/standalone/plugin-admin.mjs +247 -0
  195. package/src/standalone/provider-admin.mjs +338 -0
  196. package/src/standalone/seeds.mjs +94 -0
  197. package/src/standalone/usage-dashboard.mjs +510 -0
  198. package/src/tui/App.jsx +5438 -0
  199. package/src/tui/components/AnsiText.jsx +199 -0
  200. package/src/tui/components/ContextPanel.jsx +217 -0
  201. package/src/tui/components/Markdown.jsx +205 -0
  202. package/src/tui/components/MarkdownTable.jsx +204 -0
  203. package/src/tui/components/Message.jsx +103 -0
  204. package/src/tui/components/Picker.jsx +317 -0
  205. package/src/tui/components/PromptInput.jsx +584 -0
  206. package/src/tui/components/QueuedCommands.jsx +47 -0
  207. package/src/tui/components/SlashCommandPalette.jsx +114 -0
  208. package/src/tui/components/Spinner.jsx +317 -0
  209. package/src/tui/components/StatusLine.jsx +87 -0
  210. package/src/tui/components/TextEntryPanel.jsx +323 -0
  211. package/src/tui/components/ToolExecution.jsx +772 -0
  212. package/src/tui/components/TurnDone.jsx +78 -0
  213. package/src/tui/components/UsagePanel.jsx +331 -0
  214. package/src/tui/dist/index.mjs +12359 -0
  215. package/src/tui/engine.mjs +2410 -0
  216. package/src/tui/figures.mjs +50 -0
  217. package/src/tui/hooks/useEngine.mjs +16 -0
  218. package/src/tui/index.jsx +254 -0
  219. package/src/tui/input-editing.mjs +242 -0
  220. package/src/tui/markdown/format-token.mjs +194 -0
  221. package/src/tui/paste-attachments.mjs +198 -0
  222. package/src/tui/runtime/shared/process-shutdown.mjs +1 -0
  223. package/src/tui/spinner-verbs.mjs +45 -0
  224. package/src/tui/theme.mjs +67 -0
  225. package/src/tui/time-format.mjs +53 -0
  226. package/src/ui/ansi.mjs +115 -0
  227. package/src/ui/markdown.mjs +195 -0
  228. package/src/ui/statusline.mjs +730 -0
  229. package/src/ui/tool-card.mjs +101 -0
  230. package/src/vendor/statusline/bin/statusline-lib.mjs +805 -0
  231. package/src/vendor/statusline/bin/statusline-route.mjs +596 -0
  232. package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +285 -0
  233. package/src/vendor/statusline/src/gateway/claude-current.mjs +320 -0
  234. package/src/vendor/statusline/src/gateway/route-meta.mjs +753 -0
  235. package/src/vendor/statusline/src/gateway/session-routes.mjs +244 -0
  236. package/src/workflows/default/WORKFLOW.md +7 -0
  237. package/src/workflows/default/workflow.json +14 -0
  238. package/vendor/ink/build/ansi-tokenizer.d.ts +38 -0
  239. package/vendor/ink/build/ansi-tokenizer.js +316 -0
  240. package/vendor/ink/build/ansi-tokenizer.js.map +1 -0
  241. package/vendor/ink/build/colorize.d.ts +3 -0
  242. package/vendor/ink/build/colorize.js +48 -0
  243. package/vendor/ink/build/colorize.js.map +1 -0
  244. package/vendor/ink/build/components/AccessibilityContext.d.ts +3 -0
  245. package/vendor/ink/build/components/AccessibilityContext.js +5 -0
  246. package/vendor/ink/build/components/AccessibilityContext.js.map +1 -0
  247. package/vendor/ink/build/components/AnimationContext.d.ts +9 -0
  248. package/vendor/ink/build/components/AnimationContext.js +13 -0
  249. package/vendor/ink/build/components/AnimationContext.js.map +1 -0
  250. package/vendor/ink/build/components/App.d.ts +24 -0
  251. package/vendor/ink/build/components/App.js +554 -0
  252. package/vendor/ink/build/components/App.js.map +1 -0
  253. package/vendor/ink/build/components/AppContext.d.ts +80 -0
  254. package/vendor/ink/build/components/AppContext.js +25 -0
  255. package/vendor/ink/build/components/AppContext.js.map +1 -0
  256. package/vendor/ink/build/components/BackgroundContext.d.ts +4 -0
  257. package/vendor/ink/build/components/BackgroundContext.js +3 -0
  258. package/vendor/ink/build/components/BackgroundContext.js.map +1 -0
  259. package/vendor/ink/build/components/Box.d.ts +130 -0
  260. package/vendor/ink/build/components/Box.js +34 -0
  261. package/vendor/ink/build/components/Box.js.map +1 -0
  262. package/vendor/ink/build/components/CursorContext.d.ts +11 -0
  263. package/vendor/ink/build/components/CursorContext.js +8 -0
  264. package/vendor/ink/build/components/CursorContext.js.map +1 -0
  265. package/vendor/ink/build/components/ErrorBoundary.d.ts +18 -0
  266. package/vendor/ink/build/components/ErrorBoundary.js +23 -0
  267. package/vendor/ink/build/components/ErrorBoundary.js.map +1 -0
  268. package/vendor/ink/build/components/ErrorOverview.d.ts +6 -0
  269. package/vendor/ink/build/components/ErrorOverview.js +90 -0
  270. package/vendor/ink/build/components/ErrorOverview.js.map +1 -0
  271. package/vendor/ink/build/components/FocusContext.d.ts +16 -0
  272. package/vendor/ink/build/components/FocusContext.js +17 -0
  273. package/vendor/ink/build/components/FocusContext.js.map +1 -0
  274. package/vendor/ink/build/components/Newline.d.ts +13 -0
  275. package/vendor/ink/build/components/Newline.js +8 -0
  276. package/vendor/ink/build/components/Newline.js.map +1 -0
  277. package/vendor/ink/build/components/Spacer.d.ts +7 -0
  278. package/vendor/ink/build/components/Spacer.js +11 -0
  279. package/vendor/ink/build/components/Spacer.js.map +1 -0
  280. package/vendor/ink/build/components/Static.d.ts +24 -0
  281. package/vendor/ink/build/components/Static.js +28 -0
  282. package/vendor/ink/build/components/Static.js.map +1 -0
  283. package/vendor/ink/build/components/StderrContext.d.ts +15 -0
  284. package/vendor/ink/build/components/StderrContext.js +13 -0
  285. package/vendor/ink/build/components/StderrContext.js.map +1 -0
  286. package/vendor/ink/build/components/StdinContext.d.ts +28 -0
  287. package/vendor/ink/build/components/StdinContext.js +20 -0
  288. package/vendor/ink/build/components/StdinContext.js.map +1 -0
  289. package/vendor/ink/build/components/StdoutContext.d.ts +15 -0
  290. package/vendor/ink/build/components/StdoutContext.js +13 -0
  291. package/vendor/ink/build/components/StdoutContext.js.map +1 -0
  292. package/vendor/ink/build/components/Text.d.ts +55 -0
  293. package/vendor/ink/build/components/Text.js +50 -0
  294. package/vendor/ink/build/components/Text.js.map +1 -0
  295. package/vendor/ink/build/components/Transform.d.ts +16 -0
  296. package/vendor/ink/build/components/Transform.js +15 -0
  297. package/vendor/ink/build/components/Transform.js.map +1 -0
  298. package/vendor/ink/build/cursor-helpers.d.ts +39 -0
  299. package/vendor/ink/build/cursor-helpers.js +62 -0
  300. package/vendor/ink/build/cursor-helpers.js.map +1 -0
  301. package/vendor/ink/build/devtools-window-polyfill.d.ts +1 -0
  302. package/vendor/ink/build/devtools-window-polyfill.js +68 -0
  303. package/vendor/ink/build/devtools-window-polyfill.js.map +1 -0
  304. package/vendor/ink/build/devtools.d.ts +1 -0
  305. package/vendor/ink/build/devtools.js +36 -0
  306. package/vendor/ink/build/devtools.js.map +1 -0
  307. package/vendor/ink/build/dom.d.ts +62 -0
  308. package/vendor/ink/build/dom.js +143 -0
  309. package/vendor/ink/build/dom.js.map +1 -0
  310. package/vendor/ink/build/get-max-width.d.ts +3 -0
  311. package/vendor/ink/build/get-max-width.js +10 -0
  312. package/vendor/ink/build/get-max-width.js.map +1 -0
  313. package/vendor/ink/build/hooks/use-animation.d.ts +49 -0
  314. package/vendor/ink/build/hooks/use-animation.js +87 -0
  315. package/vendor/ink/build/hooks/use-animation.js.map +1 -0
  316. package/vendor/ink/build/hooks/use-app.d.ts +5 -0
  317. package/vendor/ink/build/hooks/use-app.js +8 -0
  318. package/vendor/ink/build/hooks/use-app.js.map +1 -0
  319. package/vendor/ink/build/hooks/use-box-metrics.d.ts +59 -0
  320. package/vendor/ink/build/hooks/use-box-metrics.js +81 -0
  321. package/vendor/ink/build/hooks/use-box-metrics.js.map +1 -0
  322. package/vendor/ink/build/hooks/use-cursor.d.ts +12 -0
  323. package/vendor/ink/build/hooks/use-cursor.js +29 -0
  324. package/vendor/ink/build/hooks/use-cursor.js.map +1 -0
  325. package/vendor/ink/build/hooks/use-focus-manager.d.ts +43 -0
  326. package/vendor/ink/build/hooks/use-focus-manager.js +18 -0
  327. package/vendor/ink/build/hooks/use-focus-manager.js.map +1 -0
  328. package/vendor/ink/build/hooks/use-focus.d.ts +30 -0
  329. package/vendor/ink/build/hooks/use-focus.js +43 -0
  330. package/vendor/ink/build/hooks/use-focus.js.map +1 -0
  331. package/vendor/ink/build/hooks/use-input.d.ts +132 -0
  332. package/vendor/ink/build/hooks/use-input.js +126 -0
  333. package/vendor/ink/build/hooks/use-input.js.map +1 -0
  334. package/vendor/ink/build/hooks/use-is-screen-reader-enabled.d.ts +6 -0
  335. package/vendor/ink/build/hooks/use-is-screen-reader-enabled.js +12 -0
  336. package/vendor/ink/build/hooks/use-is-screen-reader-enabled.js.map +1 -0
  337. package/vendor/ink/build/hooks/use-paste.d.ts +35 -0
  338. package/vendor/ink/build/hooks/use-paste.js +62 -0
  339. package/vendor/ink/build/hooks/use-paste.js.map +1 -0
  340. package/vendor/ink/build/hooks/use-stderr.d.ts +5 -0
  341. package/vendor/ink/build/hooks/use-stderr.js +8 -0
  342. package/vendor/ink/build/hooks/use-stderr.js.map +1 -0
  343. package/vendor/ink/build/hooks/use-stdin.d.ts +7 -0
  344. package/vendor/ink/build/hooks/use-stdin.js +9 -0
  345. package/vendor/ink/build/hooks/use-stdin.js.map +1 -0
  346. package/vendor/ink/build/hooks/use-stdout.d.ts +5 -0
  347. package/vendor/ink/build/hooks/use-stdout.js +8 -0
  348. package/vendor/ink/build/hooks/use-stdout.js.map +1 -0
  349. package/vendor/ink/build/hooks/use-window-size.d.ts +18 -0
  350. package/vendor/ink/build/hooks/use-window-size.js +22 -0
  351. package/vendor/ink/build/hooks/use-window-size.js.map +1 -0
  352. package/vendor/ink/build/index.d.ts +42 -0
  353. package/vendor/ink/build/index.js +24 -0
  354. package/vendor/ink/build/index.js.map +1 -0
  355. package/vendor/ink/build/ink.d.ts +146 -0
  356. package/vendor/ink/build/ink.js +1022 -0
  357. package/vendor/ink/build/ink.js.map +1 -0
  358. package/vendor/ink/build/input-parser.d.ts +10 -0
  359. package/vendor/ink/build/input-parser.js +194 -0
  360. package/vendor/ink/build/input-parser.js.map +1 -0
  361. package/vendor/ink/build/instances.d.ts +3 -0
  362. package/vendor/ink/build/instances.js +8 -0
  363. package/vendor/ink/build/instances.js.map +1 -0
  364. package/vendor/ink/build/kitty-keyboard.d.ts +23 -0
  365. package/vendor/ink/build/kitty-keyboard.js +32 -0
  366. package/vendor/ink/build/kitty-keyboard.js.map +1 -0
  367. package/vendor/ink/build/log-update.d.ts +20 -0
  368. package/vendor/ink/build/log-update.js +261 -0
  369. package/vendor/ink/build/log-update.js.map +1 -0
  370. package/vendor/ink/build/measure-element.d.ts +20 -0
  371. package/vendor/ink/build/measure-element.js +13 -0
  372. package/vendor/ink/build/measure-element.js.map +1 -0
  373. package/vendor/ink/build/measure-text.d.ts +6 -0
  374. package/vendor/ink/build/measure-text.js +21 -0
  375. package/vendor/ink/build/measure-text.js.map +1 -0
  376. package/vendor/ink/build/output.d.ts +35 -0
  377. package/vendor/ink/build/output.js +328 -0
  378. package/vendor/ink/build/output.js.map +1 -0
  379. package/vendor/ink/build/parse-keypress.d.ts +20 -0
  380. package/vendor/ink/build/parse-keypress.js +495 -0
  381. package/vendor/ink/build/parse-keypress.js.map +1 -0
  382. package/vendor/ink/build/reconciler.d.ts +4 -0
  383. package/vendor/ink/build/reconciler.js +306 -0
  384. package/vendor/ink/build/reconciler.js.map +1 -0
  385. package/vendor/ink/build/render-background.d.ts +4 -0
  386. package/vendor/ink/build/render-background.js +25 -0
  387. package/vendor/ink/build/render-background.js.map +1 -0
  388. package/vendor/ink/build/render-border.d.ts +4 -0
  389. package/vendor/ink/build/render-border.js +84 -0
  390. package/vendor/ink/build/render-border.js.map +1 -0
  391. package/vendor/ink/build/render-node-to-output.d.ts +14 -0
  392. package/vendor/ink/build/render-node-to-output.js +162 -0
  393. package/vendor/ink/build/render-node-to-output.js.map +1 -0
  394. package/vendor/ink/build/render-to-string.d.ts +38 -0
  395. package/vendor/ink/build/render-to-string.js +116 -0
  396. package/vendor/ink/build/render-to-string.js.map +1 -0
  397. package/vendor/ink/build/render.d.ts +176 -0
  398. package/vendor/ink/build/render.js +71 -0
  399. package/vendor/ink/build/render.js.map +1 -0
  400. package/vendor/ink/build/renderer.d.ts +8 -0
  401. package/vendor/ink/build/renderer.js +64 -0
  402. package/vendor/ink/build/renderer.js.map +1 -0
  403. package/vendor/ink/build/sanitize-ansi.d.ts +2 -0
  404. package/vendor/ink/build/sanitize-ansi.js +27 -0
  405. package/vendor/ink/build/sanitize-ansi.js.map +1 -0
  406. package/vendor/ink/build/squash-text-nodes.d.ts +3 -0
  407. package/vendor/ink/build/squash-text-nodes.js +36 -0
  408. package/vendor/ink/build/squash-text-nodes.js.map +1 -0
  409. package/vendor/ink/build/styles.d.ts +302 -0
  410. package/vendor/ink/build/styles.js +303 -0
  411. package/vendor/ink/build/styles.js.map +1 -0
  412. package/vendor/ink/build/utils.d.ts +9 -0
  413. package/vendor/ink/build/utils.js +19 -0
  414. package/vendor/ink/build/utils.js.map +1 -0
  415. package/vendor/ink/build/wrap-text.d.ts +3 -0
  416. package/vendor/ink/build/wrap-text.js +38 -0
  417. package/vendor/ink/build/wrap-text.js.map +1 -0
  418. package/vendor/ink/build/write-synchronized.d.ts +4 -0
  419. package/vendor/ink/build/write-synchronized.js +9 -0
  420. package/vendor/ink/build/write-synchronized.js.map +1 -0
  421. package/vendor/ink/license +10 -0
  422. package/vendor/ink/package.json +137 -0
  423. package/.claude-plugin/marketplace.json +0 -34
  424. package/.claude-plugin/plugin.json +0 -20
  425. package/.gitattributes +0 -34
  426. package/.mcp.json +0 -14
  427. package/ARCHITECTURE.md +0 -77
  428. package/CHANGELOG.md +0 -30
  429. package/CONTRIBUTING.md +0 -45
  430. package/DATA-FLOW.md +0 -79
  431. package/LICENSE +0 -21
  432. package/SECURITY.md +0 -138
  433. package/UNINSTALL.md +0 -115
  434. package/agents/maintenance.md +0 -5
  435. package/agents/memory-classification.md +0 -30
  436. package/agents/scheduler-task.md +0 -18
  437. package/agents/webhook-handler.md +0 -27
  438. package/agents/worker.md +0 -24
  439. package/bin/bridge +0 -133
  440. package/bin/statusline-launcher.mjs +0 -82
  441. package/bin/statusline-lib.mjs +0 -581
  442. package/bin/statusline-route.mjs +0 -273
  443. package/bin/statusline.mjs +0 -638
  444. package/bun.lock +0 -927
  445. package/commands/config.md +0 -16
  446. package/commands/doctor.md +0 -13
  447. package/commands/model.md +0 -61
  448. package/commands/setup.md +0 -17
  449. package/defaults/hidden-roles.json +0 -68
  450. package/defaults/memory-chunk-prompt.md +0 -63
  451. package/defaults/mixdog-config.template.json +0 -27
  452. package/defaults/user-workflow.json +0 -8
  453. package/defaults/user-workflow.md +0 -17
  454. package/hooks/hooks.json +0 -73
  455. package/hooks/lib/active-instance.cjs +0 -77
  456. package/hooks/lib/permission-evaluator.cjs +0 -411
  457. package/hooks/lib/permission-route.cjs +0 -63
  458. package/hooks/lib/settings-loader.cjs +0 -117
  459. package/hooks/post-tool-use.cjs +0 -84
  460. package/hooks/pre-mcp-sandbox.cjs +0 -158
  461. package/hooks/pre-tool-subagent.cjs +0 -258
  462. package/hooks/session-start.cjs +0 -1493
  463. package/hooks/shim-launcher.cjs +0 -65
  464. package/hooks/turn-timer.cjs +0 -82
  465. package/lib/claude-md-writer.cjs +0 -386
  466. package/lib/keychain-cjs.cjs +0 -290
  467. package/lib/plugin-paths.cjs +0 -69
  468. package/lib/rules-builder.cjs +0 -241
  469. package/native/README.md +0 -117
  470. package/native/prebuilt/linux-aarch64/mixdog-shim +0 -0
  471. package/native/prebuilt/linux-x86_64/mixdog-shim +0 -0
  472. package/native/prebuilt/macos-aarch64/mixdog-shim +0 -0
  473. package/native/prebuilt/macos-x86_64/mixdog-shim +0 -0
  474. package/native/prebuilt/windows-x86_64/mixdog-shim.exe +0 -0
  475. package/prompts/code-review.txt +0 -16
  476. package/prompts/security-audit.txt +0 -17
  477. package/rules/bridge/00-common.md +0 -39
  478. package/rules/bridge/20-skip-protocol.md +0 -18
  479. package/rules/bridge/30-explorer.md +0 -33
  480. package/rules/bridge/40-cycle1-agent.md +0 -52
  481. package/rules/bridge/41-cycle2-agent.md +0 -62
  482. package/rules/lead/00-tool-lead.md +0 -61
  483. package/rules/lead/01-general.md +0 -26
  484. package/rules/lead/02-channels.md +0 -49
  485. package/rules/lead/03-team.md +0 -27
  486. package/rules/lead/04-workflow.md +0 -20
  487. package/rules/shared/00-language.md +0 -14
  488. package/rules/shared/01-tool.md +0 -138
  489. package/scripts/bootstrap.mjs +0 -130
  490. package/scripts/bridge-unify-smoke.mjs +0 -308
  491. package/scripts/build-runtime-linux.sh +0 -348
  492. package/scripts/build-runtime-macos.sh +0 -217
  493. package/scripts/build-runtime-windows.ps1 +0 -242
  494. package/scripts/builtin-utils-smoke.mjs +0 -398
  495. package/scripts/bump.mjs +0 -80
  496. package/scripts/check-json.mjs +0 -45
  497. package/scripts/check-syntax-changed.mjs +0 -102
  498. package/scripts/check-syntax.mjs +0 -58
  499. package/scripts/code-graph-batch.test.mjs +0 -33
  500. package/scripts/config-preserve-smoke.mjs +0 -180
  501. package/scripts/doctor.mjs +0 -489
  502. package/scripts/edit-normalize-fuzz.mjs +0 -130
  503. package/scripts/edit-normalize-smoke.mjs +0 -401
  504. package/scripts/edit-operation-smoke.mjs +0 -369
  505. package/scripts/edit2-smoke.mjs +0 -63
  506. package/scripts/ensure-deps.mjs +0 -259
  507. package/scripts/fuzzy-e2e.mjs +0 -28
  508. package/scripts/fuzzy-smoke.mjs +0 -26
  509. package/scripts/gateway-model.mjs +0 -596
  510. package/scripts/generate-runtime-manifest.mjs +0 -166
  511. package/scripts/guard-smoke.mjs +0 -66
  512. package/scripts/hidden-role-schema-smoke.mjs +0 -162
  513. package/scripts/hook-routing-smoke.mjs +0 -29
  514. package/scripts/inject-input.ps1 +0 -204
  515. package/scripts/io-complex-smoke.mjs +0 -667
  516. package/scripts/io-explore-bench.mjs +0 -424
  517. package/scripts/io-guardrails-smoke.mjs +0 -205
  518. package/scripts/io-mini-bench-baseline.json +0 -11
  519. package/scripts/io-mini-bench.mjs +0 -216
  520. package/scripts/io-route-harness.mjs +0 -933
  521. package/scripts/io-telemetry-report.mjs +0 -691
  522. package/scripts/lib/gateway-inventory.mjs +0 -178
  523. package/scripts/lib/gateway-settings.mjs +0 -78
  524. package/scripts/mutation-bench.mjs +0 -564
  525. package/scripts/mutation-io-smoke.mjs +0 -1097
  526. package/scripts/native-patch-bridge-smoke.mjs +0 -288
  527. package/scripts/native-patch-smoke.mjs +0 -304
  528. package/scripts/openai-oauth-catalog-smoke.mjs +0 -53
  529. package/scripts/patch-interior-context-smoke.mjs +0 -49
  530. package/scripts/patch-newline-utf8-smoke.mjs +0 -157
  531. package/scripts/perf-hook-smoke.mjs +0 -71
  532. package/scripts/permission-eval-smoke.mjs +0 -443
  533. package/scripts/prep-patch.mjs +0 -53
  534. package/scripts/prep-shim.mjs +0 -96
  535. package/scripts/provider-cache-smoke.mjs +0 -687
  536. package/scripts/report-runtime-health.mjs +0 -132
  537. package/scripts/resolve-bun.mjs +0 -60
  538. package/scripts/run-mcp.mjs +0 -1473
  539. package/scripts/salvage-v4a-shatter.test.mjs +0 -58
  540. package/scripts/scoped-cache-io-smoke.mjs +0 -103
  541. package/scripts/shell-policy-round3-smoke.mjs +0 -46
  542. package/scripts/smoke-runtime-negative.ps1 +0 -100
  543. package/scripts/smoke-runtime-negative.sh +0 -95
  544. package/scripts/stall-policy-smoke.mjs +0 -50
  545. package/scripts/start-memory-worker.mjs +0 -23
  546. package/scripts/statusline-launcher-smoke.mjs +0 -235
  547. package/scripts/stress-atomic-write.mjs +0 -1028
  548. package/scripts/test-fault-inject.mjs +0 -164
  549. package/scripts/test-large-file.mjs +0 -174
  550. package/scripts/tool-edge-smoke.mjs +0 -209
  551. package/scripts/uninstall.mjs +0 -238
  552. package/scripts/webhook-selfheal-smoke.mjs +0 -27
  553. package/scripts/write-overwrite-guard-smoke.mjs +0 -56
  554. package/server-main.mjs +0 -3350
  555. package/server.mjs +0 -468
  556. package/setup/config-merge.mjs +0 -246
  557. package/setup/install.mjs +0 -574
  558. package/setup/launch-core.mjs +0 -617
  559. package/setup/launch.mjs +0 -101
  560. package/setup/locate-claude.mjs +0 -56
  561. package/setup/mixdog-cli.mjs +0 -122
  562. package/setup/setup-server.mjs +0 -3305
  563. package/setup/setup.html +0 -3740
  564. package/setup/tui.mjs +0 -325
  565. package/skills/retro-skill-proposer/SKILL.md +0 -92
  566. package/skills/schedule-add/SKILL.md +0 -77
  567. package/skills/setup/SKILL.md +0 -356
  568. package/skills/webhook-add/SKILL.md +0 -81
  569. package/src/agent/bridge-stall-watchdog.mjs +0 -337
  570. package/src/agent/index.mjs +0 -2229
  571. package/src/agent/orchestrator/ai-wrapped-dispatch.mjs +0 -1010
  572. package/src/agent/orchestrator/bridge-retry.mjs +0 -220
  573. package/src/agent/orchestrator/bridge-trace.mjs +0 -601
  574. package/src/agent/orchestrator/cache-mtime.mjs +0 -58
  575. package/src/agent/orchestrator/config.mjs +0 -405
  576. package/src/agent/orchestrator/context/collect.mjs +0 -651
  577. package/src/agent/orchestrator/dispatch-persist.mjs +0 -549
  578. package/src/agent/orchestrator/drain-registry.mjs +0 -50
  579. package/src/agent/orchestrator/explore-validator.mjs +0 -8
  580. package/src/agent/orchestrator/internal-roles.mjs +0 -118
  581. package/src/agent/orchestrator/internal-tools.mjs +0 -88
  582. package/src/agent/orchestrator/jobs.mjs +0 -116
  583. package/src/agent/orchestrator/mcp/client.mjs +0 -364
  584. package/src/agent/orchestrator/providers/anthropic-oauth.mjs +0 -1884
  585. package/src/agent/orchestrator/providers/anthropic.mjs +0 -598
  586. package/src/agent/orchestrator/providers/gemini.mjs +0 -1530
  587. package/src/agent/orchestrator/providers/grok-oauth.mjs +0 -779
  588. package/src/agent/orchestrator/providers/model-catalog.mjs +0 -374
  589. package/src/agent/orchestrator/providers/openai-compat-stream.mjs +0 -366
  590. package/src/agent/orchestrator/providers/openai-compat.mjs +0 -1511
  591. package/src/agent/orchestrator/providers/openai-oauth-ws.mjs +0 -1891
  592. package/src/agent/orchestrator/providers/openai-oauth.mjs +0 -1456
  593. package/src/agent/orchestrator/providers/openai-ws.mjs +0 -127
  594. package/src/agent/orchestrator/providers/registry.mjs +0 -192
  595. package/src/agent/orchestrator/providers/retry-classifier.mjs +0 -325
  596. package/src/agent/orchestrator/session/cache/prefetch-cache.mjs +0 -142
  597. package/src/agent/orchestrator/session/cache/read-cache.mjs +0 -319
  598. package/src/agent/orchestrator/session/cache/scoped-cache.mjs +0 -361
  599. package/src/agent/orchestrator/session/loop.mjs +0 -1619
  600. package/src/agent/orchestrator/session/manager.mjs +0 -1991
  601. package/src/agent/orchestrator/session/result-classification.mjs +0 -65
  602. package/src/agent/orchestrator/session/store.mjs +0 -632
  603. package/src/agent/orchestrator/session/stream-watchdog.mjs +0 -130
  604. package/src/agent/orchestrator/session/tool-result-offload.mjs +0 -166
  605. package/src/agent/orchestrator/session/trim.mjs +0 -491
  606. package/src/agent/orchestrator/smart-bridge/CACHE-SHARD.md +0 -115
  607. package/src/agent/orchestrator/smart-bridge/bridge-llm.mjs +0 -331
  608. package/src/agent/orchestrator/smart-bridge/cache-obs.mjs +0 -150
  609. package/src/agent/orchestrator/smart-bridge/cache-strategy.mjs +0 -228
  610. package/src/agent/orchestrator/smart-bridge/index.mjs +0 -215
  611. package/src/agent/orchestrator/smart-bridge/profiles.mjs +0 -37
  612. package/src/agent/orchestrator/smart-bridge/registry.mjs +0 -348
  613. package/src/agent/orchestrator/stall-policy.mjs +0 -201
  614. package/src/agent/orchestrator/tool-loop-guard.mjs +0 -75
  615. package/src/agent/orchestrator/tools/bash-session.mjs +0 -722
  616. package/src/agent/orchestrator/tools/builtin/arg-guard.mjs +0 -511
  617. package/src/agent/orchestrator/tools/builtin/bash-tool.mjs +0 -480
  618. package/src/agent/orchestrator/tools/builtin/builtin-tools.mjs +0 -256
  619. package/src/agent/orchestrator/tools/builtin/edit-base-guard.mjs +0 -58
  620. package/src/agent/orchestrator/tools/builtin/edit-byte-plan.mjs +0 -240
  621. package/src/agent/orchestrator/tools/builtin/edit-byte-utils.mjs +0 -113
  622. package/src/agent/orchestrator/tools/builtin/edit-commit.mjs +0 -74
  623. package/src/agent/orchestrator/tools/builtin/edit-context-utils.mjs +0 -242
  624. package/src/agent/orchestrator/tools/builtin/edit-diagnostics.mjs +0 -211
  625. package/src/agent/orchestrator/tools/builtin/edit-engine.mjs +0 -1364
  626. package/src/agent/orchestrator/tools/builtin/edit-failure-context.mjs +0 -126
  627. package/src/agent/orchestrator/tools/builtin/edit-hint.mjs +0 -141
  628. package/src/agent/orchestrator/tools/builtin/edit-match-utils.mjs +0 -194
  629. package/src/agent/orchestrator/tools/builtin/edit-partial-write.mjs +0 -60
  630. package/src/agent/orchestrator/tools/builtin/edit-stale-refresh.mjs +0 -168
  631. package/src/agent/orchestrator/tools/builtin/edit-tool.mjs +0 -173
  632. package/src/agent/orchestrator/tools/builtin/edit-utf8-guard.mjs +0 -48
  633. package/src/agent/orchestrator/tools/builtin/fuzzy-match.mjs +0 -99
  634. package/src/agent/orchestrator/tools/builtin/glob-walk.mjs +0 -193
  635. package/src/agent/orchestrator/tools/builtin/list-tool.mjs +0 -597
  636. package/src/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
  637. package/src/agent/orchestrator/tools/builtin/notebook-edit-tool.mjs +0 -300
  638. package/src/agent/orchestrator/tools/builtin/path-diagnostics.mjs +0 -152
  639. package/src/agent/orchestrator/tools/builtin/read-formatting.mjs +0 -118
  640. package/src/agent/orchestrator/tools/builtin/read-open.mjs +0 -190
  641. package/src/agent/orchestrator/tools/builtin/read-single-tool.mjs +0 -728
  642. package/src/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +0 -173
  643. package/src/agent/orchestrator/tools/builtin/read-streaming.mjs +0 -602
  644. package/src/agent/orchestrator/tools/builtin/rename-tool.mjs +0 -196
  645. package/src/agent/orchestrator/tools/builtin/rg-runner.mjs +0 -422
  646. package/src/agent/orchestrator/tools/builtin/search-builders.mjs +0 -158
  647. package/src/agent/orchestrator/tools/builtin/search-tool.mjs +0 -869
  648. package/src/agent/orchestrator/tools/builtin/shell-jobs.mjs +0 -962
  649. package/src/agent/orchestrator/tools/builtin/shell-runtime.mjs +0 -223
  650. package/src/agent/orchestrator/tools/builtin/snapshot-store.mjs +0 -206
  651. package/src/agent/orchestrator/tools/builtin/write-tool.mjs +0 -401
  652. package/src/agent/orchestrator/tools/builtin.mjs +0 -503
  653. package/src/agent/orchestrator/tools/code-graph-tool-defs.mjs +0 -24
  654. package/src/agent/orchestrator/tools/code-graph.mjs +0 -4095
  655. package/src/agent/orchestrator/tools/cwd-tool.mjs +0 -298
  656. package/src/agent/orchestrator/tools/destructive-warning.mjs +0 -323
  657. package/src/agent/orchestrator/tools/edit-normalize.mjs +0 -603
  658. package/src/agent/orchestrator/tools/graph-binary-fetcher.mjs +0 -154
  659. package/src/agent/orchestrator/tools/graph-manifest.json +0 -26
  660. package/src/agent/orchestrator/tools/host-input.mjs +0 -204
  661. package/src/agent/orchestrator/tools/patch-binary-fetcher.mjs +0 -143
  662. package/src/agent/orchestrator/tools/patch-manifest.json +0 -26
  663. package/src/agent/orchestrator/tools/patch-tool-defs.mjs +0 -20
  664. package/src/agent/orchestrator/tools/patch.mjs +0 -2754
  665. package/src/agent/orchestrator/tools/progress-message.mjs +0 -118
  666. package/src/agent/orchestrator/tools/shell-command.mjs +0 -865
  667. package/src/agent/orchestrator/tools/shell-policy-imports.mjs +0 -7
  668. package/src/agent/orchestrator/tools/shell-snapshot.mjs +0 -313
  669. package/src/agent/orchestrator/workflow-store.mjs +0 -93
  670. package/src/agent/tool-defs.mjs +0 -110
  671. package/src/channels/backends/discord.mjs +0 -784
  672. package/src/channels/data/voice-runtime-manifest.json +0 -138
  673. package/src/channels/index.mjs +0 -3268
  674. package/src/channels/lib/config.mjs +0 -292
  675. package/src/channels/lib/drop-trace.mjs +0 -71
  676. package/src/channels/lib/event-pipeline.mjs +0 -81
  677. package/src/channels/lib/holidays.mjs +0 -138
  678. package/src/channels/lib/hook-pipe-server.mjs +0 -822
  679. package/src/channels/lib/output-forwarder.mjs +0 -765
  680. package/src/channels/lib/runtime-paths.mjs +0 -552
  681. package/src/channels/lib/scheduler.mjs +0 -723
  682. package/src/channels/lib/session-discovery.mjs +0 -103
  683. package/src/channels/lib/state-file.mjs +0 -68
  684. package/src/channels/lib/status-snapshot.mjs +0 -219
  685. package/src/channels/lib/tool-format.mjs +0 -140
  686. package/src/channels/lib/transcript-discovery.mjs +0 -195
  687. package/src/channels/lib/voice-runtime-fetcher.mjs +0 -734
  688. package/src/channels/lib/webhook.mjs +0 -1318
  689. package/src/channels/tool-defs.mjs +0 -170
  690. package/src/daemon/host.mjs +0 -118
  691. package/src/daemon/mcp-transport.mjs +0 -47
  692. package/src/daemon/session.mjs +0 -100
  693. package/src/daemon/thin-client.mjs +0 -71
  694. package/src/daemon/transport.mjs +0 -163
  695. package/src/gateway/claude-current.mjs +0 -255
  696. package/src/gateway/oauth-usage.mjs +0 -598
  697. package/src/gateway/route-meta.mjs +0 -629
  698. package/src/gateway/server.mjs +0 -713
  699. package/src/memory/data/runtime-manifest.json +0 -40
  700. package/src/memory/index.mjs +0 -3332
  701. package/src/memory/lib/core-memory-store.mjs +0 -330
  702. package/src/memory/lib/embedding-provider.mjs +0 -269
  703. package/src/memory/lib/embedding-worker.mjs +0 -323
  704. package/src/memory/lib/memory-cycle1.mjs +0 -645
  705. package/src/memory/lib/memory-cycle2.mjs +0 -1284
  706. package/src/memory/lib/memory-cycle3.mjs +0 -540
  707. package/src/memory/lib/memory-embed.mjs +0 -299
  708. package/src/memory/lib/memory-ops-policy.mjs +0 -190
  709. package/src/memory/lib/memory-recall-store.mjs +0 -638
  710. package/src/memory/lib/memory.mjs +0 -412
  711. package/src/memory/lib/pg/adapter.mjs +0 -308
  712. package/src/memory/lib/pg/process.mjs +0 -360
  713. package/src/memory/lib/pg/supervisor.mjs +0 -396
  714. package/src/memory/lib/runtime-fetcher.mjs +0 -458
  715. package/src/memory/lib/trace-store.mjs +0 -728
  716. package/src/memory/tool-defs.mjs +0 -79
  717. package/src/search/index.mjs +0 -1173
  718. package/src/search/lib/backends/anthropic-oauth.mjs +0 -98
  719. package/src/search/lib/backends/exa.mjs +0 -50
  720. package/src/search/lib/backends/firecrawl.mjs +0 -61
  721. package/src/search/lib/backends/gemini-api.mjs +0 -83
  722. package/src/search/lib/backends/grok-oauth.mjs +0 -86
  723. package/src/search/lib/backends/index.mjs +0 -150
  724. package/src/search/lib/backends/openai-api.mjs +0 -144
  725. package/src/search/lib/backends/openai-oauth.mjs +0 -102
  726. package/src/search/lib/backends/openai-web-search.mjs +0 -76
  727. package/src/search/lib/backends/tavily.mjs +0 -55
  728. package/src/search/lib/backends/xai-api.mjs +0 -113
  729. package/src/search/lib/config.mjs +0 -192
  730. package/src/search/lib/provider-usage.mjs +0 -67
  731. package/src/search/lib/providers.mjs +0 -47
  732. package/src/search/lib/search-intent.mjs +0 -109
  733. package/src/search/lib/setup-handler.mjs +0 -261
  734. package/src/search/lib/web-tools.mjs +0 -1219
  735. package/src/search/tool-defs.mjs +0 -83
  736. package/src/setup/defender-exclusion.mjs +0 -183
  737. package/src/shared/atomic-file.mjs +0 -436
  738. package/src/shared/config.mjs +0 -372
  739. package/src/shared/daemon-recycle.mjs +0 -108
  740. package/src/shared/disable-claude-builtins.mjs +0 -91
  741. package/src/shared/err-text.mjs +0 -12
  742. package/src/shared/llm/http-agent.mjs +0 -123
  743. package/src/shared/open-url.mjs +0 -62
  744. package/src/shared/plugin-paths.mjs +0 -58
  745. package/src/shared/schedules-store.mjs +0 -70
  746. package/src/shared/seed.mjs +0 -161
  747. package/src/shared/user-cwd.mjs +0 -225
  748. package/src/shared/user-data-guard.mjs +0 -244
  749. package/src/status/aggregator.mjs +0 -584
  750. package/src/status/server.mjs +0 -413
  751. package/tools.json +0 -1671
  752. /package/{defaults → src/defaults}/cycle3-review-prompt.md +0 -0
  753. /package/{defaults → src/defaults}/memory-promote-prompt.md +0 -0
  754. /package/{hooks → src/hooks}/lib/permission-rules.cjs +0 -0
  755. /package/{lib → src/lib}/config-cjs.cjs +0 -0
  756. /package/{lib → src/lib}/hook-pipe-path.cjs +0 -0
  757. /package/{lib → src/lib}/mixdog-debug.cjs +0 -0
  758. /package/{lib → src/lib}/text-utils.cjs +0 -0
  759. /package/{rules → src/rules}/bridge/42-cycle3-agent.md +0 -0
  760. /package/src/{agent → runtime/agent}/orchestrator/activity-bus.mjs +0 -0
  761. /package/src/{agent → runtime/agent}/orchestrator/providers/anthropic-betas.mjs +0 -0
  762. /package/src/{agent → runtime/agent}/orchestrator/session/abort-lookup.mjs +0 -0
  763. /package/src/{agent → runtime/agent}/orchestrator/session/cache/post-edit-marks.mjs +0 -0
  764. /package/src/{agent → runtime/agent}/orchestrator/session/cache/scoped-cache-outcome.mjs +0 -0
  765. /package/src/{agent → runtime/agent}/orchestrator/session/cache/util.mjs +0 -0
  766. /package/src/{agent → runtime/agent}/orchestrator/session/read-dedup.mjs +0 -0
  767. /package/src/{agent → runtime/agent}/orchestrator/session/save-session-worker.mjs +0 -0
  768. /package/src/{agent → runtime/agent}/orchestrator/smart-bridge/session-builder.mjs +0 -0
  769. /package/src/{agent → runtime/agent}/orchestrator/tools/bash-policy-scan.mjs +0 -0
  770. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/advisory-lock.mjs +0 -0
  771. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/atomic-write.mjs +0 -0
  772. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/binary-file.mjs +0 -0
  773. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/cache-layers.mjs +0 -0
  774. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/cwd-utils.mjs +0 -0
  775. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/device-paths.mjs +0 -0
  776. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -0
  777. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/diff-utils.mjs +0 -0
  778. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/fs-reachability.mjs +0 -0
  779. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/grep-formatting.mjs +0 -0
  780. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/hash-utils.mjs +0 -0
  781. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/list-formatting.mjs +0 -0
  782. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/open-config-tool.mjs +0 -0
  783. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/path-locks.mjs +0 -0
  784. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/path-utils.mjs +0 -0
  785. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-args.mjs +0 -0
  786. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-batch.mjs +0 -0
  787. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-constants.mjs +0 -0
  788. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-image-resize.mjs +0 -0
  789. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-image.mjs +0 -0
  790. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-lines.mjs +0 -0
  791. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-mode-tool.mjs +0 -0
  792. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-range-index.mjs +0 -0
  793. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-ranges.mjs +0 -0
  794. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-special-files.mjs +0 -0
  795. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-tool.mjs +0 -0
  796. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-windows.mjs +0 -0
  797. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/shell-analysis.mjs +0 -0
  798. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/shell-output.mjs +0 -0
  799. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/snapshot-helpers.mjs +0 -0
  800. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/snapshot-validation.mjs +0 -0
  801. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/text-stats.mjs +0 -0
  802. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/windows-roots.mjs +0 -0
  803. /package/src/{agent → runtime/agent}/orchestrator/tools/code-graph-prewarm-worker.mjs +0 -0
  804. /package/src/{agent → runtime/agent}/orchestrator/tools/env-scrub.mjs +0 -0
  805. /package/src/{agent → runtime/agent}/orchestrator/tools/mutation-content-cache.mjs +0 -0
  806. /package/src/{agent → runtime/agent}/orchestrator/tools/mutation-planner.mjs +0 -0
  807. /package/src/{agent → runtime/agent}/orchestrator/tools/next-call-utils.mjs +0 -0
  808. /package/src/{agent → runtime/agent}/orchestrator/tools/result-compression.mjs +0 -0
  809. /package/src/{agent → runtime/agent}/orchestrator/tools/shell-exec-policy.mjs +0 -0
  810. /package/src/{agent → runtime/agent}/orchestrator/tools/shell-policy-danger-target.mjs +0 -0
  811. /package/src/{agent → runtime/agent}/orchestrator/tools/shell-policy.mjs +0 -0
  812. /package/src/{channels → runtime/channels}/lib/cli-worker-host.mjs +0 -0
  813. /package/src/{channels → runtime/channels}/lib/config-lock.mjs +0 -0
  814. /package/src/{channels → runtime/channels}/lib/event-queue.mjs +0 -0
  815. /package/src/{channels → runtime/channels}/lib/executor.mjs +0 -0
  816. /package/src/{channels → runtime/channels}/lib/format.mjs +0 -0
  817. /package/src/{channels → runtime/channels}/lib/interaction-workflows.mjs +0 -0
  818. /package/src/{channels → runtime/channels}/lib/memory-client.mjs +0 -0
  819. /package/src/{channels → runtime/channels}/lib/session-control.mjs +0 -0
  820. /package/src/{channels → runtime/channels}/lib/settings.mjs +0 -0
  821. /package/src/{channels → runtime/channels}/lib/whisper-server.mjs +0 -0
  822. /package/src/{memory → runtime/memory}/lib/agent-ipc.mjs +0 -0
  823. /package/src/{memory → runtime/memory}/lib/bridge-trace-queries.mjs +0 -0
  824. /package/src/{memory → runtime/memory}/lib/llm-worker-host.mjs +0 -0
  825. /package/src/{memory → runtime/memory}/lib/memory-cycle.mjs +0 -0
  826. /package/src/{memory → runtime/memory}/lib/memory-extraction.mjs +0 -0
  827. /package/src/{memory → runtime/memory}/lib/memory-maintenance-store.mjs +0 -0
  828. /package/src/{memory → runtime/memory}/lib/memory-recall-id-patch.mjs +0 -0
  829. /package/src/{memory → runtime/memory}/lib/memory-recall-read-query.mjs +0 -0
  830. /package/src/{memory → runtime/memory}/lib/memory-recall-scope-filter.mjs +0 -0
  831. /package/src/{memory → runtime/memory}/lib/memory-retrievers.mjs +0 -0
  832. /package/src/{memory → runtime/memory}/lib/memory-score.mjs +0 -0
  833. /package/src/{memory → runtime/memory}/lib/memory-text-utils.mjs +0 -0
  834. /package/src/{memory → runtime/memory}/lib/model-profile.mjs +0 -0
  835. /package/src/{memory → runtime/memory}/lib/project-id-resolver.mjs +0 -0
  836. /package/src/{search → runtime/search}/lib/cache.mjs +0 -0
  837. /package/src/{search → runtime/search}/lib/formatter.mjs +0 -0
  838. /package/src/{search → runtime/search}/lib/state.mjs +0 -0
  839. /package/src/{shared → runtime/shared}/abort-controller.mjs +0 -0
  840. /package/src/{shared → runtime/shared}/llm/cost.mjs +0 -0
  841. /package/src/{shared → runtime/shared}/llm/index.mjs +0 -0
  842. /package/src/{shared → runtime/shared}/llm/pid-cleanup.mjs +0 -0
  843. /package/src/{shared → runtime/shared}/llm/usage-log.mjs +0 -0
  844. /package/src/{shared → runtime/shared}/wsl.mjs +0 -0
package/setup/setup.html DELETED
@@ -1,3740 +0,0 @@
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)">&#128065;</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 &amp; 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">&#9654;</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:330px;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 = '&#128065;&#8205;&#128488;'; }
1412
- else { inp.type = 'password'; btn.innerHTML = '&#128065;'; }
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,'&amp;').replace(/"/g,'&quot;').replace(/</g,'&lt;');
1424
- }
1425
-
1426
- function escapeHtml(v) {
1427
- return String(v ?? '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
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;">&#9888;</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()">&times;</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\'))">&times;</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;">&#9888;</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">&#128065;</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 &#8599;</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 &#8599;</a>' +
2397
- '</div>';
2398
- document.getElementById('ch-webhook-authtoken').value = w.authtoken || '';
2399
- document.getElementById('ch-webhook-domain').value = w.ngrokDomain || 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.ngrokDomain = 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:'opencode-go',name:'OpenCode Go',env:'OPENCODE_API_KEY',url:'https://opencode.ai'},
2608
- ];
2609
- const AG_OAUTH_PROVIDERS = [
2610
- {id:'openai-oauth',name:'Codex',desc:'~/.codex/auth.json',login:true},
2611
- {id:'anthropic-oauth',name:'Claude Code',desc:'~/.claude/.credentials.json',login:true},
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
- const AG_OPENAI_DIRECT_FAST_MODEL_RE = [
2644
- /^gpt-5\.5(?:-\d{4}|$)/,
2645
- /^gpt-5\.4(?:-\d{4}|$)/,
2646
- /^gpt-5\.4-mini(?:-\d{4}|$)/,
2647
- ];
2648
- let agModelList = [];
2649
- const AG_ACCESS_LABELS = { full: 'Read & Write', readonly: 'Read Only', mcp: 'None' };
2650
-
2651
- function agNormalizeFastProvider(provider) {
2652
- return provider === 'openai-api' ? 'openai' : provider;
2653
- }
2654
-
2655
- function agOpenAIDirectSupportsFast(modelId) {
2656
- const id = String(modelId || '').trim();
2657
- return AG_OPENAI_DIRECT_FAST_MODEL_RE.some(re => re.test(id));
2658
- }
2659
-
2660
- function agExplicitFastSupport(model) {
2661
- const hasServiceTiers = Array.isArray(model?.serviceTiers);
2662
- const hasSpeedTiers = Array.isArray(model?.additionalSpeedTiers);
2663
- if (!hasServiceTiers && !hasSpeedTiers) return null;
2664
- const serviceTiers = hasServiceTiers ? model.serviceTiers : [];
2665
- const speedTiers = hasSpeedTiers ? model.additionalSpeedTiers : [];
2666
- const serviceFast = serviceTiers.some(t => {
2667
- const id = typeof t === 'string' ? t : t?.id;
2668
- return id === 'priority' || id === 'fast';
2669
- });
2670
- const speedFast = speedTiers.some(t => t === 'fast' || t === 'priority');
2671
- return serviceFast || speedFast;
2672
- }
2673
-
2674
- function agModelSupportsFast(provider, model) {
2675
- const normalizedProvider = agNormalizeFastProvider(provider);
2676
- if (!model) return false;
2677
- const explicit = agExplicitFastSupport(model);
2678
- if (explicit !== null) return explicit;
2679
- if (normalizedProvider === 'openai') return agOpenAIDirectSupportsFast(model.id);
2680
- const providerFast = AG_FAST_PROVIDERS.has(normalizedProvider);
2681
- const familyNoFast = model?.family && AG_FAMILY_NO_FAST.has(model.family);
2682
- return providerFast && !familyNoFast;
2683
- }
2684
-
2685
- async function loadAgentData() {
2686
- const r = await fetch('/agent/config').then(r => r.json()).catch(() => ({}));
2687
- agConfig = r.config || {};
2688
- agAuth = r.auth || {};
2689
- agRenderAll();
2690
- await agLoadPresets();
2691
- await loadAgMaintenance();
2692
- }
2693
-
2694
- function agRenderAll() {
2695
- const c = agConfig.providers || {};
2696
- const apiSec = document.getElementById('ag-api-sec');
2697
- apiSec.innerHTML = '';
2698
- for (const p of AG_API_PROVIDERS) {
2699
- const stored = !!agAuth.keyStored?.[p.id] || !!c[p.id]?.apiKey;
2700
- const hasEnv = agAuth.envKeys?.[p.id];
2701
- let tag, link;
2702
- if (stored) { tag = '<span class="r-tag ok">Set</span>'; link = ''; }
2703
- else if (hasEnv) { tag = '<span class="r-tag env">Env</span>'; link = ''; }
2704
- else { tag = ''; link = `<a class="r-link" href="${p.url}" target="_blank">Get Key &#8599;</a>`; }
2705
- // The stored key value is never sent to the browser; the field starts empty
2706
- // and a non-empty placeholder signals a saved key. Typing replaces it.
2707
- const ph = stored ? '••••••••••••' : p.env;
2708
- 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">&#128065;</button></div>${tag}${link}</div>`;
2709
- }
2710
- const oauthSec = document.getElementById('ag-oauth-sec');
2711
- oauthSec.innerHTML = '';
2712
- for (const p of AG_OAUTH_PROVIDERS) {
2713
- const detected = agOAuthDetected(p.id);
2714
- const tag = detected ? '<span class="r-tag ok">Set</span>' : '<span class="r-tag no">Not Set</span>';
2715
- const btn = p.login
2716
- ? `<button class="save" style="width:auto;height:28px;padding:0 12px;font-size:11px;flex-shrink:0;margin-left:8px" type="button" onclick="oauthProviderLogin('${escapeAttr(p.id)}', this)">${detected ? 'Re-login' : 'Sign in'}</button>`
2717
- : '';
2718
- 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>`;
2719
- }
2720
- const localSec = document.getElementById('ag-local-sec');
2721
- localSec.innerHTML = '';
2722
- for (const p of AG_LOCAL_PROVIDERS) {
2723
- const url = c[p.id]?.baseURL || p.url;
2724
- const detected = agAuth[p.id];
2725
- const tag = detected ? '<span class="r-tag ok">Set</span>' : '<span class="r-tag no">Not Set</span>';
2726
- 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>`;
2727
- }
2728
- agRenderStatus();
2729
- }
2730
-
2731
- function agRenderStatus() {
2732
- // Status panel was removed from the sidebar — guard so missing container
2733
- // does not crash loadAgentData (the red "Failed to load configuration"
2734
- // banner was a symptom of that crash; loadAgMaintenance was also
2735
- // unreachable, leaving the Task Presets list empty).
2736
- const sec = document.getElementById('ag-status-sec');
2737
- if (!sec) return;
2738
- const c = agConfig.providers || {};
2739
- let h = '';
2740
- 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>`; }
2741
- 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>`; }
2742
- 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>`; }
2743
- sec.innerHTML = h;
2744
- }
2745
-
2746
- async function agSaveProviders() {
2747
- const providers = {};
2748
- 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 }; }
2749
- 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 }; }
2750
- await agDoSave({ providers }, 'agent-providers');
2751
- }
2752
-
2753
- async function agDoSave(data, panelId) {
2754
- const panel = document.getElementById('panel-' + panelId);
2755
- const btn = panel?.querySelector('.save');
2756
- const warn = panel?.querySelector('.warn-msg');
2757
- if (warn) warn.textContent = '';
2758
- if (btn) { btn.textContent = 'Saving...'; btn.disabled = true; }
2759
- try {
2760
- const agCfgRes = await fetch('/agent/config', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(data) });
2761
- 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); }
2762
- isDirty = false;
2763
- if (btn) { btn.textContent = '\u2713 Saved'; btn.classList.add('done'); setTimeout(()=>{btn.textContent='Save';btn.classList.remove('done');btn.disabled=false},1500); }
2764
- await loadAgentData();
2765
- } catch { if (warn) warn.textContent = 'Save failed'; if (btn) { btn.textContent = 'Save'; btn.disabled = false; } }
2766
- }
2767
-
2768
- function agOAuthDetected(providerId, auth = agAuth) {
2769
- if (providerId === 'openai-oauth') return !!auth?.codexOAuth;
2770
- if (providerId === 'anthropic-oauth') return !!auth?.anthropicOAuth;
2771
- if (providerId === 'grok-oauth') return !!auth?.grokOAuth;
2772
- if (providerId === 'copilot') return !!auth?.copilot;
2773
- return false;
2774
- }
2775
-
2776
- const OAUTH_LOGIN_ROUTES = {
2777
- 'openai-oauth': '/agent/openai-oauth/login',
2778
- 'anthropic-oauth': '/agent/anthropic-oauth/login',
2779
- 'grok-oauth': '/agent/grok-oauth/login',
2780
- };
2781
- const OAUTH_LOGIN_LABELS = {
2782
- 'openai-oauth': 'Codex',
2783
- 'anthropic-oauth': 'Claude',
2784
- 'grok-oauth': 'Grok',
2785
- };
2786
-
2787
- // Browser OAuth login for providers with login:true. CLI cred files (~/.codex,
2788
- // ~/.claude, ~/.grok) show as "Set" automatically; Sign in runs when none exist.
2789
- // Blocks until consent completes, then re-fetches auth so the row flips to Set.
2790
- async function oauthProviderLogin(providerId, btn) {
2791
- const route = OAUTH_LOGIN_ROUTES[providerId];
2792
- if (!route) return;
2793
- const label = OAUTH_LOGIN_LABELS[providerId] || providerId;
2794
- if (btn) { btn.disabled = true; btn.textContent = 'Waiting for browser…'; }
2795
- try {
2796
- const r = await fetch(route, { method: 'POST' }).then(r => r.json());
2797
- if (r && r.ok) { await loadAgentData(); }
2798
- else { alert(label + ' login failed: ' + ((r && r.error) || 'unknown error')); if (btn) btn.disabled = false; }
2799
- } catch (e) {
2800
- alert(label + ' login failed: ' + (e?.message || e));
2801
- if (btn) btn.disabled = false;
2802
- }
2803
- }
2804
-
2805
- function agGetEnabledProviders() {
2806
- const out = [];
2807
- const c = agConfig.providers || {};
2808
- for (const p of AG_API_PROVIDERS) { if (agAuth.keyStored?.[p.id] || c[p.id]?.apiKey || agAuth.envKeys?.[p.id]) out.push(p.id); }
2809
- for (const p of AG_OAUTH_PROVIDERS) { if (agOAuthDetected(p.id)) out.push(p.id); }
2810
- for (const p of AG_LOCAL_PROVIDERS) { if (c[p.id]?.enabled === true) out.push(p.id); }
2811
- return out;
2812
- }
2813
-
2814
- async function agLoadPresets() {
2815
- try { const r = await fetch('/agent/presets').then(r => r.json()); agPresets = r.presets || []; } catch { agPresets = []; }
2816
- agRenderPresetList();
2817
- }
2818
-
2819
- function agRenderPresetList() {
2820
- const list = document.getElementById('ag-preset-list');
2821
- if (agPresets.length === 0) { list.innerHTML = '<div class="empty">No presets yet. Click <strong>+ New Preset</strong> to create one.</div>'; return; }
2822
- list.innerHTML = agPresets.map(p => {
2823
- const accessLabel = AG_ACCESS_LABELS[p.tools] || 'Read & Write';
2824
- const provModel = `${p.provider}/${p.model}`;
2825
- const meta = [provModel, p.tools && p.tools !== 'full' ? accessLabel : null, p.effort ? `effort=${p.effort}` : null, p.fast ? 'Fast' : null].filter(Boolean).join(' · ');
2826
- 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>`;
2827
- }).join('');
2828
- }
2829
-
2830
- function agOpenPresetForm(id) {
2831
- agEditingPresetId = id;
2832
- const sec = document.getElementById('ag-preset-form-sec');
2833
- const title = document.getElementById('ag-preset-form-title');
2834
- sec.style.display = 'block';
2835
- const advToggle = sec.querySelector('.advanced-toggle');
2836
- const advContent = document.getElementById('ag-pf-access-section');
2837
- if (advToggle) advToggle.querySelector('.arrow').classList.remove('open');
2838
- if (advContent) advContent.classList.remove('open');
2839
- const providerSel = document.getElementById('ag-pf-provider');
2840
- const enabled = agGetEnabledProviders();
2841
- if (enabled.length === 0) { providerSel.innerHTML = '<option value="">No enabled providers</option>'; providerSel.disabled = true; }
2842
- else { providerSel.disabled = false; providerSel.innerHTML = enabled.map(p => `<option value="${p}">${p}</option>`).join(''); }
2843
- if (id) {
2844
- title.textContent = 'Edit Preset';
2845
- const p = agPresets.find(x => x.id === id);
2846
- 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; }
2847
- }
2848
- title.textContent = 'New Preset';
2849
- document.getElementById('ag-pf-name').value = '';
2850
- document.getElementById('ag-pf-fast').classList.remove('on');
2851
- document.querySelectorAll('#ag-pf-tools .radio-btn').forEach(b => b.classList.toggle('on', b.dataset.v === 'full'));
2852
- if (enabled.length > 0) { providerSel.value = enabled[0]; agOnProviderChange(); }
2853
- }
2854
-
2855
- function agClosePresetForm() { document.getElementById('ag-preset-form-sec').style.display = 'none'; agEditingPresetId = null; document.getElementById('warn-agent-presets').textContent = ''; }
2856
-
2857
- async function agOnProviderChange() {
2858
- const provider = document.getElementById('ag-pf-provider').value;
2859
- let modelSel = document.getElementById('ag-pf-model');
2860
- if (modelSel.tagName === 'INPUT') {
2861
- const sel = document.createElement('select');
2862
- sel.id = 'ag-pf-model';
2863
- sel.className = 'r-select';
2864
- modelSel.replaceWith(sel);
2865
- modelSel = sel;
2866
- }
2867
-
2868
- let models;
2869
- modelSel.disabled = true;
2870
- modelSel.innerHTML = '<option value="">Loading...</option>';
2871
- modelSel.onchange = null;
2872
- try {
2873
- const r = await fetch(`/agent/models?provider=${encodeURIComponent(provider)}`).then(r => r.json());
2874
- if (!r.ok) {
2875
- const msg = r.error || 'Model catalog unavailable';
2876
- modelSel.innerHTML = '<option value="">' + escapeHtml(msg) + '</option>';
2877
- document.getElementById('warn-agent-presets').textContent = msg;
2878
- agReplaceModelWithInput('');
2879
- return;
2880
- }
2881
- models = (r.models || []).map(m => typeof m === 'string' ? { id: m } : m).filter(m => m && m.id);
2882
- } catch {
2883
- modelSel.innerHTML = '<option value="">Failed to fetch</option>';
2884
- document.getElementById('warn-agent-presets').textContent = 'Failed to fetch model catalog';
2885
- agReplaceModelWithInput('');
2886
- return;
2887
- }
2888
- if (!Array.isArray(models) || models.length === 0) {
2889
- modelSel.innerHTML = '<option value="">No models found</option>';
2890
- document.getElementById('warn-agent-presets').textContent = 'No models found for this provider';
2891
- agReplaceModelWithInput('');
2892
- return;
2893
- }
2894
-
2895
- document.getElementById('warn-agent-presets').textContent = '';
2896
- agModelList = models;
2897
- agRenderModelOptions(modelSel, models, provider);
2898
- modelSel.disabled = false;
2899
- modelSel.onchange = () => agUpdateEffortAndFast(provider);
2900
- agUpdateEffortAndFast(provider);
2901
- }
2902
-
2903
- // Shared filter for any model dropdown that surfaces coding-agent picks
2904
- // (agent presets, search-provider model presets). Three-stage filter:
2905
- // 1. Chat-capable only (drop embedding/audio/image/etc).
2906
- // 2. If catalog carries `created` timestamps → strict 6-month cutoff
2907
- // from today. Older releases drop out.
2908
- // 3. Curated catalogs without timestamps → fall back to either the
2909
- // `latest:true` flag (anthropic-oauth marks family-max this way) or
2910
- // a client-side family-version comparison that keeps the highest
2911
- // `major.minor` per stem. Drops older generations of openai-oauth /
2912
- // gemini whose providers never set `latest`.
2913
- function filterCodingModels(models) {
2914
- 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;
2915
- 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;
2916
- const DATED_RE = /-\d{4}(-\d{2}-\d{2})?$/;
2917
- let visible = (models || []).filter(m => {
2918
- if (!m || !m.id) return false;
2919
- if (m.mode && m.mode !== 'chat') return false;
2920
- if (NON_CHAT_PREFIX.test(m.id) || NON_CHAT_TOKEN.test(m.id)) return false;
2921
- // Drop short-form dated snapshots (`-2024-05-13` / `-2024`) ONLY when
2922
- // the provider didn't tag a tier. Tier='dated' alone is no longer
2923
- // blocked — for catalogs whose only entry for a family is dated
2924
- // (e.g. anthropic-oauth's haiku), losing them removes the family
2925
- // entirely. The family-max pass below dedupes alongside any alias.
2926
- if (!m.tier && DATED_RE.test(m.id)) return false;
2927
- return true;
2928
- });
2929
-
2930
- const hasCreated = visible.some(m => typeof m.created === 'number' && m.created > 0);
2931
- if (hasCreated) {
2932
- // Strict 6-month cutoff from today. Mixed-date catalogs treat undated
2933
- // entries as current so curated aliases ride alongside live entries.
2934
- visible.sort((a, b) => (b.created || 0) - (a.created || 0));
2935
- const cutoff = Math.floor(Date.now() / 1000) - 180 * 24 * 60 * 60;
2936
- const within = visible.filter(m => !m.created || m.created >= cutoff);
2937
- return within.length ? within : visible.slice(0, 1);
2938
- }
2939
-
2940
- // No timestamps anywhere — curated catalog. Group by the provider-
2941
- // declared `family` when available (preserves codex/mini/nano splits
2942
- // that pure id-stem parsing collapses), fall back to a regex stem.
2943
- // Keep entries whose version equals the per-family max.
2944
- //
2945
- // Date-stripping: chunks >= 1000 are YYYY / YYYYMMDD snapshots, not
2946
- // minor versions. Without this, `claude-opus-4-20250514` would compare
2947
- // as [4, 20250514] and beat the actual alias `claude-opus-4-8` ([4, 8]).
2948
- // Minor versions stay safely under 1000.
2949
- const versionRe = /^([a-z][a-z0-9-]*?)-(\d+(?:[.-]\d+)*)(?:[-_]|$)/i;
2950
- // Strip a trailing `.\d+` from the family token so generation siblings
2951
- // (gpt-5.5 / gpt-5.4 / gpt-5.2 / gpt-5) collapse into one `gpt-5` group
2952
- // and version-max keeps only the latest minor — an invariant-based stand-in
2953
- // for the strict 6-month cutoff that timestamped catalogs already enforce.
2954
- // Non-versioned family names (gpt-mini, gpt-codex, gemini-flash) are
2955
- // unaffected — they have no trailing `.N` to strip.
2956
- const parsed = visible.map(m => {
2957
- const match = m.id.match(versionRe);
2958
- const rawStem = m.family || (match ? match[1] : m.id);
2959
- const stem = rawStem.replace(/\.\d+$/, '');
2960
- const rawVersion = match ? match[2].split(/[.-]/).map(Number) : [];
2961
- const version = rawVersion.filter(n => n < 1000);
2962
- return { m, stem, version };
2963
- });
2964
- const cmp = (a, b) => {
2965
- for (let i = 0; i < Math.max(a.length, b.length); i++) {
2966
- const x = a[i] || 0, y = b[i] || 0;
2967
- if (x !== y) return x - y;
2968
- }
2969
- return 0;
2970
- };
2971
- const maxByStem = new Map();
2972
- for (const p of parsed) {
2973
- const cur = maxByStem.get(p.stem);
2974
- if (!cur || cmp(p.version, cur) > 0) maxByStem.set(p.stem, p.version);
2975
- }
2976
- return parsed
2977
- .filter(p => cmp(p.version, maxByStem.get(p.stem) || []) === 0)
2978
- .map(p => p.m);
2979
- }
2980
-
2981
- function agRenderModelOptions(sel, models, provider) {
2982
- const visible = filterCodingModels(models);
2983
- sel.innerHTML = visible.map(m => `<option value="${escapeAttr(m.id)}">${escapeHtml(m.display || m.id)}</option>`).join('');
2984
- }
2985
-
2986
- function agEffortOptionsFor(provider, model) {
2987
- if (Array.isArray(model?.reasoningLevels) && model.reasoningLevels.length > 0) {
2988
- return [...model.reasoningLevels];
2989
- }
2990
- if (model?.family && Object.prototype.hasOwnProperty.call(AG_FAMILY_EFFORT, model.family)) {
2991
- return AG_FAMILY_EFFORT[model.family];
2992
- }
2993
- const opts = AG_EFFORT_OPTIONS[provider];
2994
- if (opts) return opts.map(o => o.v);
2995
- return null;
2996
- }
2997
-
2998
- function agUpdateEffortAndFast(provider) {
2999
- const modelEl = document.getElementById('ag-pf-model');
3000
- const effortRow = document.getElementById('ag-pf-effort-row');
3001
- const effortSel = document.getElementById('ag-pf-effort');
3002
- const fastRow = document.getElementById('ag-pf-fast-row');
3003
- const modelResolved = modelEl && modelEl.tagName !== 'INPUT';
3004
- const modelId = modelResolved ? modelEl.value : '';
3005
- const model = modelResolved ? (agModelList.find(m => m.id === modelId) || null) : null;
3006
- const allowed = modelResolved ? agEffortOptionsFor(provider, model) : null;
3007
-
3008
- if (!allowed || allowed.length === 0) {
3009
- effortRow.style.display = 'none';
3010
- effortSel.innerHTML = '';
3011
- } else {
3012
- effortRow.style.display = 'flex';
3013
- effortSel.innerHTML = allowed.map(v => `<option value="${v}">${AG_EFFORT_LABEL[v] || v}</option>`).join('');
3014
- }
3015
-
3016
- const fastAllowed = modelResolved && agModelSupportsFast(provider, model);
3017
- fastRow.style.display = fastAllowed ? 'flex' : 'none';
3018
- if (!fastAllowed) document.getElementById('ag-pf-fast').classList.remove('on');
3019
- }
3020
-
3021
- 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); }
3022
-
3023
- function agSelectTools(el) { el.parentElement.querySelectorAll('.radio-btn').forEach(b => b.classList.remove('on')); el.classList.add('on'); }
3024
-
3025
- async function agSavePreset() {
3026
- const warn = document.getElementById('warn-agent-presets');
3027
- warn.textContent = '';
3028
- const name = document.getElementById('ag-pf-name').value.trim();
3029
- const provider = document.getElementById('ag-pf-provider').value;
3030
- const modelEl = document.getElementById('ag-pf-model');
3031
- const model = (modelEl.tagName === 'INPUT' ? modelEl.value : modelEl.value).trim();
3032
- const effort = document.getElementById('ag-pf-effort').value;
3033
- const fast = document.getElementById('ag-pf-fast').classList.contains('on');
3034
- const tools = document.querySelector('#ag-pf-tools .radio-btn.on')?.dataset.v || 'full';
3035
- if (!name) { warn.textContent = 'Name is required'; return; }
3036
- if (!provider) { warn.textContent = 'Provider is required'; return; }
3037
- if (!model) { warn.textContent = 'Model is required'; return; }
3038
- const id = agEditingPresetId || name.toLowerCase().replace(/[^a-z0-9._-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
3039
- if (!id) { warn.textContent = 'Name must contain valid characters'; return; }
3040
- const body = { id, name, model, tools, provider, type: 'bridge' };
3041
- if (effort) body.effort = effort;
3042
- const fastRowVisible = document.getElementById('ag-pf-fast-row').style.display !== 'none';
3043
- if (fast && fastRowVisible) body.fast = true;
3044
- try {
3045
- const r = await fetch('/agent/presets', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }).then(r => r.json());
3046
- if (!r.ok) { warn.textContent = r.error || 'Save failed'; return; }
3047
- isDirty = false;
3048
- agClosePresetForm();
3049
- await agLoadPresets();
3050
- refreshAllPresetDropdowns();
3051
- } catch (err) { warn.textContent = 'Save failed: ' + (err?.message || 'network error'); }
3052
- }
3053
-
3054
- async function agDeletePreset(id) {
3055
- if (!confirm(`Delete preset "${id}"?`)) return;
3056
- try {
3057
- await fetch(`/agent/presets?id=${encodeURIComponent(id)}`, { method: 'DELETE' });
3058
- await agLoadPresets();
3059
- refreshAllPresetDropdowns();
3060
- } catch { document.getElementById('warn-agent-presets').textContent = 'Delete failed'; }
3061
- }
3062
-
3063
- // Single fan-out that re-renders every UI surface that materializes a
3064
- // preset dropdown from agPresets. Called after any preset CRUD so a
3065
- // freshly saved / renamed / deleted preset surfaces everywhere without a
3066
- // page reload. Covers: Schedule cards
3067
- // ([data-f="sc-model"]), Webhook delegate cards ([data-f="ev-model"]),
3068
- // Maintenance task selects, and Custom Workflow role rows.
3069
- function refreshAllPresetDropdowns() {
3070
- try { populatePresetDropdowns(); } catch (e) { console.warn('[mixdog] populatePresetDropdowns failed:', e); }
3071
- try { renderAgMaintenance(); } catch (e) { console.warn('[mixdog] renderAgMaintenance failed:', e); }
3072
- try { wfRenderRoles(); } catch (e) { console.warn('[mixdog] wfRenderRoles failed:', e); }
3073
- }
3074
-
3075
- // ============================================================
3076
- // MEMORY MODULE
3077
- // ============================================================
3078
-
3079
- const MEM_API_PROVIDERS = AG_API_PROVIDERS;
3080
- const MEM_OAUTH_PROVIDERS = AG_OAUTH_PROVIDERS;
3081
- const MEM_LOCAL_PROVIDERS = AG_LOCAL_PROVIDERS;
3082
-
3083
- async function loadMemoryData() {
3084
- memConfig = await fetch('/memory/config').then(r => r.json()).catch(() => ({}));
3085
- memAuth = await fetch('/memory/auth').then(r => r.json()).catch(() => ({}));
3086
- memPopulateConfig();
3087
- memRenderAll();
3088
- await Promise.all([memLoadFiles(), memLoadCoreMemory()]);
3089
- }
3090
-
3091
- function memPopulateConfig() {
3092
- const titleEl = document.getElementById('mem-set-user-title');
3093
- if (titleEl) titleEl.value = memConfig.user?.title || '';
3094
- const retEl = document.getElementById('mem-set-retention');
3095
- if (retEl && memConfig.cycle1?.interval) retEl.value = memConfig.cycle1.interval;
3096
- const intEl = document.getElementById('mem-set-interval');
3097
- if (intEl && memConfig.cycle2?.interval) intEl.value = memConfig.cycle2.interval;
3098
- }
3099
-
3100
- function memRenderAll() {
3101
- const c = memConfig.providers || {};
3102
- const apiSec = document.getElementById('mem-api-sec');
3103
- if (!apiSec) return;
3104
- apiSec.innerHTML = '';
3105
- for (const p of MEM_API_PROVIDERS) {
3106
- const key = c[p.id]?.apiKey || '';
3107
- const hasStored = !!memAuth.keyStored?.[p.id];
3108
- const hasEnv = memAuth.envKeys?.[p.id];
3109
- let tag;
3110
- if (key || hasStored) tag = '<span class="r-tag ok">Set</span>';
3111
- else if (hasEnv) tag = '<span class="r-tag env">Env</span>';
3112
- else tag = `<a class="r-link" href="${p.url}" target="_blank">Get Key &#8599;</a>`;
3113
- 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">&#128065;</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>`;
3114
- }
3115
- const oauthSec = document.getElementById('mem-oauth-sec');
3116
- oauthSec.innerHTML = '';
3117
- 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>`; }
3118
- const localSec = document.getElementById('mem-local-sec');
3119
- localSec.innerHTML = '';
3120
- 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>`; }
3121
- }
3122
-
3123
- const _memOrigTags = {};
3124
- async function memValidateKeyOnBlur(providerId) {
3125
- const input = document.getElementById('mem-key-' + providerId);
3126
- const valEl = document.getElementById('mem-val-' + providerId);
3127
- const tagEl = document.getElementById('mem-tag-' + providerId);
3128
- if (!input || !valEl || !tagEl) return;
3129
- const key = input.value.trim();
3130
- if (!_memOrigTags[providerId]) _memOrigTags[providerId] = tagEl.innerHTML;
3131
- if (!key) { input.classList.remove('warn','valid'); valEl.className='r-validation'; valEl.innerHTML=''; tagEl.innerHTML=_memOrigTags[providerId]||''; return; }
3132
- input.classList.remove('warn','valid'); valEl.className='r-validation checking'; valEl.innerHTML='<span class="spinner"></span>Checking...';
3133
- try {
3134
- const res = await fetch('/memory/validate', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({keys:{[providerId]:key}}) });
3135
- if (!res.ok) throw new Error(`HTTP ${res.status} ${await res.text()}`);
3136
- const data = await res.json();
3137
- const result = data.validation?.[providerId];
3138
- if (input.value.trim() !== key) return;
3139
- 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>'; }
3140
- else { input.classList.remove('valid'); input.classList.add('warn'); valEl.className='r-validation fail'; valEl.textContent='Invalid key'; tagEl.innerHTML=_memOrigTags[providerId]||''; }
3141
- } 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]||''; }
3142
- }
3143
-
3144
- async function memSaveProviders() {
3145
- const providers = {};
3146
- for (const p of MEM_API_PROVIDERS) { const key = document.getElementById('mem-key-'+p.id)?.value.trim(); if (key) providers[p.id] = { apiKey: key }; }
3147
- for (const p of MEM_LOCAL_PROVIDERS) { const url = document.getElementById('mem-url-'+p.id)?.value.trim(); providers[p.id] = { baseURL: url || p.url }; }
3148
- const btn = document.querySelector('#panel-mem-providers .save'); const warn = document.getElementById('warn-mem-providers');
3149
- warn.textContent = ''; btn.textContent = 'Saving...'; btn.disabled = true;
3150
- 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; }
3151
- }
3152
-
3153
- function memBuildConfigData() {
3154
- return {
3155
- user: {
3156
- title: document.getElementById('mem-set-user-title')?.value.trim() || '',
3157
- },
3158
- cycle1: { interval: document.getElementById('mem-set-retention')?.value || '10m' },
3159
- cycle2: { interval: document.getElementById('mem-set-interval')?.value.trim() || '1h' },
3160
- };
3161
- }
3162
-
3163
- async function memSaveConfig() {
3164
- const data = memBuildConfigData(); const btn = document.querySelector('#panel-mem-settings .save'); const warn = document.getElementById('warn-mem-settings');
3165
- warn.textContent = ''; btn.textContent = 'Saving...'; btn.disabled = true;
3166
- 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; }
3167
- }
3168
-
3169
- async function memRunBackfill() {
3170
- const btn = document.getElementById('mem-btn-backfill'); const window_ = document.getElementById('mem-set-backfill-window').value;
3171
- btn.textContent = 'Running...'; btn.disabled = true;
3172
- 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); }
3173
- }
3174
-
3175
- function memDeleteConfirmCheck() {
3176
- const input = document.getElementById('mem-del-confirm'); const btn = document.getElementById('mem-btn-delete');
3177
- const ready = (input.value === 'DELETE ALL MEMORY');
3178
- btn.disabled = !ready;
3179
- btn.style.opacity = ready ? '1' : '0.4';
3180
- btn.style.cursor = ready ? 'pointer' : 'not-allowed';
3181
- }
3182
-
3183
- async function memDeleteAll() {
3184
- const input = document.getElementById('mem-del-confirm'); const btn = document.getElementById('mem-btn-delete'); const warn = document.getElementById('warn-mem-delete');
3185
- if (input.value !== 'DELETE ALL MEMORY') return;
3186
- if (!window.confirm('Delete ALL memory entries? This cannot be undone.')) return;
3187
- warn.textContent = ''; btn.textContent = 'Deleting...'; btn.disabled = true;
3188
- try {
3189
- const r = await fetch('/memory/delete', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({ confirm:'DELETE ALL MEMORY' }) });
3190
- const j = await r.json();
3191
- if (!j.ok) throw new Error(j.error || 'unknown error');
3192
- warn.style.color = 'var(--text-3)'; warn.textContent = 'Deleted ' + j.deleted + ' generated entries. Preserved ' + (j.core_preserved || 0) + ' core entries.';
3193
- btn.textContent = '✓ Deleted'; input.value = '';
3194
- setTimeout(() => { btn.textContent = 'Delete all'; memDeleteConfirmCheck(); }, 2500);
3195
- } catch (e) {
3196
- warn.style.color = '#e5534b'; warn.textContent = 'Failed: ' + (e.message || e);
3197
- btn.textContent = 'Delete all'; btn.disabled = false;
3198
- }
3199
- }
3200
-
3201
- 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{} }
3202
-
3203
- function memOpenFile(name) {
3204
- fetch('/memory/file/' + encodeURIComponent(name));
3205
- }
3206
-
3207
- async function memSaveFiles() {
3208
- const btn=document.querySelector('#panel-mem-files .save'); const warn=document.getElementById('warn-mem-files');
3209
- warn.textContent=''; btn.textContent='Saving...'; btn.disabled=true;
3210
- 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;}
3211
- }
3212
-
3213
- async function memLoadCoreMemory() {
3214
- const target = document.getElementById('mem-core-memory-list');
3215
- if (!target) return;
3216
- try {
3217
- const r = await fetch('/api/memory/core').then(r => r.json());
3218
- if (r && r.ok === false) {
3219
- // Server reachable but memory backend down (e.g. mixdog process not
3220
- // running → active-instance.json missing). Distinguish from a genuine
3221
- // "no entries" state so the user knows to start the service.
3222
- target.innerHTML = '<div class="empty">Memory service unavailable — entries cannot be loaded. Start the mixdog process and reopen.</div>';
3223
- return;
3224
- }
3225
- memRenderCoreMemory(r.items || []);
3226
- } catch {
3227
- target.innerHTML = '<div class="empty">Failed to load core memory.</div>';
3228
- }
3229
- }
3230
-
3231
- function memRenderCoreMemory(items) {
3232
- const list=document.getElementById('mem-core-memory-list');
3233
- 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;}
3234
- list.innerHTML=items.map(item=>`<div class="r" style="flex-direction:column;align-items:stretch;padding:10px 14px;gap:6px">
3235
- <div style="display:flex;align-items:center;gap:8px">
3236
- <span style="font-weight:600">${escapeHtml(item.element||'')}</span>
3237
- <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>
3238
- <span style="margin-left:auto;font-size:10px;color:var(--text-4)">${escapeHtml(item.project_id || 'COMMON')}</span>
3239
- <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>
3240
- </div>
3241
- <div style="font-size:11px;line-height:1.5;color:var(--text-2);padding:4px 2px">${escapeHtml(item.summary||'')}</div>
3242
- </div>`).join('');
3243
- }
3244
-
3245
- 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));} }
3246
-
3247
- function memAddCoreMemory() {
3248
- const list=document.getElementById('mem-core-memory-list');
3249
- const empty=document.getElementById('mem-core-empty');
3250
- if(empty)empty.remove();
3251
- const div=document.createElement('div');
3252
- div.className='r';
3253
- div.style.cssText='flex-direction:column;align-items:stretch;padding:10px 14px;gap:6px';
3254
- div.innerHTML=`
3255
- <div style="display:flex;align-items:center;gap:8px">
3256
- <select class="r-input" style="width:110px;font-size:11px" data-cm-new="1" data-cm-f="category">
3257
- <option value="rule">rule</option>
3258
- <option value="constraint">constraint</option>
3259
- <option value="decision">decision</option>
3260
- <option value="fact" selected>fact</option>
3261
- <option value="goal">goal</option>
3262
- <option value="preference">preference</option>
3263
- <option value="task">task</option>
3264
- <option value="issue">issue</option>
3265
- </select>
3266
- <input class="r-input" placeholder="Project (common)" style="width:150px;font-size:11px" data-cm-new="1" data-cm-f="project_id">
3267
- <input class="r-input" placeholder="Element (5-10 words)" style="flex:1;font-weight:600" data-cm-new="1" data-cm-f="element">
3268
- <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>
3269
- <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>
3270
- </div>
3271
- <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>
3272
- `;
3273
- list.appendChild(div);
3274
- }
3275
-
3276
- async function memSaveNewEntry(btn) {
3277
- const row=btn.closest('.r');
3278
- const category=row.querySelector('[data-cm-f="category"]').value;
3279
- const project_id=row.querySelector('[data-cm-f="project_id"]').value.trim()||'common';
3280
- const element=row.querySelector('[data-cm-f="element"]').value.trim();
3281
- const summary=row.querySelector('[data-cm-f="summary"]').value.trim();
3282
- if(!element||!summary){alert('Element and summary required');return;}
3283
- try{
3284
- const r=await fetch('/api/memory/core',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({element,summary,category,project_id})});
3285
- if(!r.ok) throw new Error(`HTTP ${r.status} ${await r.text()}`);
3286
- const j=await r.json();
3287
- if(!j.ok){alert('Save failed: '+(j.error||'unknown'));return;}
3288
- await memLoadCoreMemory();
3289
- }catch(e){alert('Save failed: '+e.message);}
3290
- }
3291
-
3292
- 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));} }
3293
-
3294
- // ============================================================
3295
- // SEARCH MODULE
3296
- // ============================================================
3297
-
3298
- // API-key-bearing providers — the 3 whose credentials live in search.credentials
3299
- // (managed by the keyring). The other 4 providers (anthropic-oauth /
3300
- // openai-oauth / openai-api / xai-api) reuse credentials
3301
- // registered in the Agent panel and do not need a row here.
3302
- const SR_KEY_PROVIDERS = [
3303
- { id: 'firecrawl', name: 'Firecrawl', url: 'https://firecrawl.dev' },
3304
- { id: 'tavily', name: 'Tavily', url: 'https://tavily.com' },
3305
- { id: 'exa', name: 'Exa', url: 'https://exa.ai' },
3306
- ];
3307
-
3308
- // Full provider catalog with capability summary. Drives the active-provider
3309
- // dropdown label so the user sees what each choice means before saving.
3310
- // Provider list matches src/search/lib/backends/index.mjs PROVIDER_CAPS.
3311
- const SR_PROVIDERS = [
3312
- { id: 'anthropic-oauth', label: 'Anthropic (OAuth)' },
3313
- { id: 'openai-oauth', label: 'OpenAI (OAuth)' },
3314
- { id: 'openai-api', label: 'OpenAI (API key)' },
3315
- { id: 'gemini-api', label: 'Gemini (API key)' },
3316
- { id: 'xai-api', label: 'xAI (API key)' },
3317
- { id: 'grok-oauth', label: 'Grok (OAuth)' },
3318
- { id: 'tavily', label: 'Tavily' },
3319
- { id: 'firecrawl', label: 'Firecrawl' },
3320
- { id: 'exa', label: 'Exa' },
3321
- ];
3322
- const SR_PROVIDER_BY_ID = Object.fromEntries(SR_PROVIDERS.map(p => [p.id, p]));
3323
-
3324
- function srProviderTag(_p) {
3325
- return 'search';
3326
- }
3327
-
3328
- function srMakeKeyRow(p) {
3329
- const d = document.createElement('div');
3330
- d.className = 'r'; d.dataset.id = p.id;
3331
- const nm = document.createElement('span'); nm.className = 'r-name'; nm.style.width = '90px'; nm.textContent = p.name; d.appendChild(nm);
3332
- const group = document.createElement('div'); group.className = 'input-group'; group.style.flex = '1'; group.style.width = 'auto';
3333
- const inp = document.createElement('input'); inp.className = 'r-input'; inp.type = 'password'; inp.placeholder = 'API KEY'; inp.dataset.k = 'sr-' + p.id;
3334
- inp.addEventListener('input', () => { inp.classList.remove('warn', 'valid'); isDirty = true; });
3335
- group.appendChild(inp);
3336
- const eyeBtn = document.createElement('button'); eyeBtn.className = 'eye-btn'; eyeBtn.type = 'button'; eyeBtn.innerHTML = '&#128065;';
3337
- eyeBtn.onclick = function () { toggleVis(this); };
3338
- group.appendChild(eyeBtn);
3339
- d.appendChild(group);
3340
- if (p.url) {
3341
- const a = document.createElement('a'); a.className = 'r-link'; a.href = p.url; a.target = '_blank'; a.textContent = 'Get ↗';
3342
- d.appendChild(a);
3343
- }
3344
- return d;
3345
- }
3346
- SR_KEY_PROVIDERS.forEach(p => document.getElementById('sr-search-sec').appendChild(srMakeKeyRow(p)));
3347
-
3348
- function srRenderProviderOptions(available) {
3349
- const sel = document.getElementById('sr-provider');
3350
- if (!sel) return;
3351
- sel.innerHTML = '';
3352
- if (!available || !available.length) {
3353
- const opt = document.createElement('option');
3354
- opt.value = ''; opt.disabled = true; opt.selected = true;
3355
- opt.textContent = '(no usable provider — register a credential below or in the Agent panel)';
3356
- sel.appendChild(opt);
3357
- sel.disabled = true;
3358
- return;
3359
- }
3360
- sel.disabled = false;
3361
- SR_PROVIDERS.filter(p => available.includes(p.id)).forEach(p => {
3362
- const opt = document.createElement('option');
3363
- opt.value = p.id;
3364
- opt.textContent = p.label;
3365
- sel.appendChild(opt);
3366
- });
3367
- }
3368
-
3369
- function srUpdateProviderHint() {
3370
- const hint = document.getElementById('sr-provider-hint');
3371
- const sel = document.getElementById('sr-provider');
3372
- if (!hint || !sel) return;
3373
- const p = SR_PROVIDER_BY_ID[sel.value];
3374
- if (!p) { hint.textContent = ''; return; }
3375
- const parts = [];
3376
- parts.push('Two-step model: `search` returns snippets + URLs; follow up with `web_fetch` for raw page bodies.');
3377
- parts.push('`web_fetch` uses local readability+puppeteer extractors (provider-independent).');
3378
- hint.textContent = parts.join(' ');
3379
- }
3380
-
3381
- // Model-preset families exposed in Search Provider. Each family fetches its
3382
- // catalog from the bare provider ID (openai-api uses openai-api directly,
3383
- // xai-api → xai-api). Anthropic is intentionally
3384
- // omitted: search.anthropic-oauth pins claude-haiku-4-5 server-side.
3385
- // Probe order matters when the user has multiple credentials for the same
3386
- // family — the first available provider drives the displayed catalog.
3387
- // openai-oauth probes first so the dropdown matches the Codex catalog the
3388
- // user sees under Agent → Providers → GPT (OAuth); the live /v1/models
3389
- // catalog from openai-api is the fallback when only the API key is set.
3390
- const SR_MODEL_FAMILIES = [
3391
- { family: 'openai', label: 'OpenAI', probeProviders: ['openai-oauth', 'openai-api'] },
3392
- { family: 'gemini', label: 'Gemini', probeProviders: ['gemini-api'] },
3393
- { family: 'xai', label: 'xAI', probeProviders: ['xai-api', 'grok-oauth'] },
3394
- ];
3395
-
3396
- document.getElementById('sr-provider')?.addEventListener('change', () => {
3397
- isDirty = true;
3398
- srUpdateProviderHint();
3399
- });
3400
-
3401
- // Render one row per family whose backing provider is in availableProviders.
3402
- // Each row fetches its model catalog live from /agent/models — no static
3403
- // lists. Empty result silently hides the row (invariant: only show models
3404
- // the user can actually use). Selection persists under search.models[family].
3405
- // Per-family cache of the filtered model objects + the provider id we
3406
- // probed for each family. srUpdateOpenAIEffortFast reads these to derive
3407
- // AG_FAMILY_EFFORT / AG_FAST_PROVIDERS membership when the user changes
3408
- // the selected model.
3409
- let srModelListByFam = {};
3410
- let srProviderByFam = {};
3411
-
3412
- // Build a family model <select> that renders immediately — disabled and
3413
- // flagged data-pending — showing the saved model id as the selected option
3414
- // so the row appears fully populated before its live catalog round-trip
3415
- // finishes. The pending flag keeps srBuildData from persisting this family
3416
- // (disabled selects are skipped), so a Save mid-fetch never clobbers the
3417
- // stored selection. srRenderModelPresets clears data-pending + disabled once
3418
- // the catalog arrives and replaces these placeholder options.
3419
- function buildFamSelect(family) {
3420
- const stored = srConfig?.models?.[family] || '';
3421
- const opt = stored
3422
- ? '<option value="' + escapeAttr(stored) + '" selected>' + escapeHtml(stored) + '</option>'
3423
- : '<option value="">Loading…</option>';
3424
- return '<select class="r-select" data-fam="' + family + '" data-pending="1" style="flex:0 0 260px;width:260px" disabled>' + opt + '</select>';
3425
- }
3426
-
3427
- // Terminal settle for a family select when no usable catalog arrived. The
3428
- // stored selection (if any) stays visible and selected so a Save round-trips
3429
- // the identical value; otherwise the reason renders as an empty-value option
3430
- // (empty values are skipped by srBuildData). Always clears data-pending and
3431
- // re-enables so a row can never stay locked after its fetch settles.
3432
- function srSettleFamSelect(sel, reason) {
3433
- if (!sel.value) sel.innerHTML = '<option value="">' + escapeHtml(reason) + '</option>';
3434
- sel.removeAttribute('data-pending');
3435
- sel.disabled = false;
3436
- }
3437
-
3438
- async function srRenderModelPresets() {
3439
- const sec = document.getElementById('sr-model-presets-sec');
3440
- const body = document.getElementById('sr-model-presets-body');
3441
- if (!sec || !body) return;
3442
- const available = new Set(srConfig?.availableProviders || []);
3443
- const visible = SR_MODEL_FAMILIES.filter(f => f.probeProviders.some(p => available.has(p)));
3444
- if (visible.length === 0) {
3445
- sec.style.display = 'none';
3446
- body.innerHTML = '';
3447
- srModelListByFam = {};
3448
- srProviderByFam = {};
3449
- return;
3450
- }
3451
- sec.style.display = '';
3452
- body.innerHTML = '';
3453
- srModelListByFam = {};
3454
- srProviderByFam = {};
3455
- for (const fam of visible) {
3456
- const row = document.createElement('div');
3457
- row.className = 'r';
3458
- // openai row carries an extra hidden effort dropdown + fast toggle.
3459
- // They reveal themselves in srUpdateOpenAIEffortFast() once the model
3460
- // list resolves and we can read the selected model's family. Gemini /
3461
- // xAI rows stay model-only — reasoning_effort + service_tier are not
3462
- // exposed by their search backends in this revision.
3463
- // OpenAI row carries the extra effort dropdown + labeled Fast Mode
3464
- // toggle. The model select is narrowed (max-width:280px) on every
3465
- // family row so the OpenAI extras fit on one line, and Gemini / xAI
3466
- // share the same width for visual alignment.
3467
- const extra = fam.family === 'openai'
3468
- ? '<select class="r-select" data-fam-effort="openai" style="width:auto;flex:0 0 auto;margin-left:8px;display:none"></select>' +
3469
- '<span data-fam-fast-label="openai" style="margin-left:10px;font-size:11px;color:var(--text-3);display:none">Fast Mode</span>' +
3470
- '<div class="r-toggle" data-fam-fast="openai" style="margin-left:8px;display:none" title="Speed optimized output (premium)"></div>'
3471
- : '';
3472
- row.innerHTML =
3473
- '<span class="r-name" style="width:90px">' + fam.label + '</span>' +
3474
- buildFamSelect(fam.family) +
3475
- extra;
3476
- body.appendChild(row);
3477
- if (fam.family === 'openai') {
3478
- const fastEl = row.querySelector('[data-fam-fast="openai"]');
3479
- fastEl?.addEventListener('click', () => { fastEl.classList.toggle('on'); isDirty = true; });
3480
- const effortEl = row.querySelector('select[data-fam-effort="openai"]');
3481
- effortEl?.addEventListener('change', () => { isDirty = true; });
3482
- }
3483
- }
3484
- await Promise.all(visible.map(async fam => {
3485
- const sel = body.querySelector('select[data-fam="' + fam.family + '"]');
3486
- if (!sel) return;
3487
- const probe = fam.probeProviders.find(p => available.has(p)) || fam.probeProviders[0];
3488
- srProviderByFam[fam.family] = probe;
3489
- try {
3490
- const r = await fetch('/agent/models?provider=' + encodeURIComponent(probe)).then(r => r.json());
3491
- const rawModels = (r?.models || []).map(m => typeof m === 'string' ? { id: m } : m).filter(m => m && m.id);
3492
- const visibleModels = filterCodingModels(rawModels);
3493
- srModelListByFam[fam.family] = visibleModels;
3494
- if (!visibleModels.length) {
3495
- srSettleFamSelect(sel, 'No models available');
3496
- return;
3497
- }
3498
- const stored = srConfig?.models?.[fam.family] || '';
3499
- sel.innerHTML = '';
3500
- visibleModels.forEach(m => {
3501
- const opt = document.createElement('option');
3502
- opt.value = m.id;
3503
- opt.textContent = m.display || m.id;
3504
- if (m.id === stored) opt.selected = true;
3505
- sel.appendChild(opt);
3506
- });
3507
- // Catalog has arrived — clear the pending flag and enable the row.
3508
- sel.removeAttribute('data-pending');
3509
- sel.disabled = false;
3510
- sel.addEventListener('change', () => { isDirty = true; if (fam.family === 'openai') srUpdateOpenAIEffortFast(); });
3511
- if (fam.family === 'openai') srUpdateOpenAIEffortFast();
3512
- } catch {
3513
- srSettleFamSelect(sel, 'Failed to fetch models');
3514
- }
3515
- }));
3516
- }
3517
-
3518
- // Reveal/hide effort + fast based on the currently selected openai model.
3519
- // Mirrors agUpdateEffortAndFast() so the rules are identical: provider/model
3520
- // metadata drives effort options and Fast Mode availability.
3521
- function srUpdateOpenAIEffortFast() {
3522
- const body = document.getElementById('sr-model-presets-body');
3523
- if (!body) return;
3524
- const modelSel = body.querySelector('select[data-fam="openai"]');
3525
- const effortSel = body.querySelector('select[data-fam-effort="openai"]');
3526
- const fastEl = body.querySelector('[data-fam-fast="openai"]');
3527
- if (!modelSel || !effortSel || !fastEl) return;
3528
- const modelId = modelSel.value || '';
3529
- const list = srModelListByFam.openai || [];
3530
- const model = list.find(m => m.id === modelId) || null;
3531
- const provider = srProviderByFam.openai || '';
3532
- // Normalize search-side provider keys to the agent-side keys used by
3533
- // AG_FAST_PROVIDERS / AG_EFFORT_OPTIONS. 'openai-api' is the same backend
3534
- // as 'openai' for capability membership; 'openai-oauth' is already shared.
3535
- const normalizedProvider = provider === 'openai-api' ? 'openai' : provider;
3536
- const allowed = model ? agEffortOptionsFor(normalizedProvider, model) : null;
3537
- const storedEffort = srConfig?.modelOptions?.openai?.effort || '';
3538
- if (!allowed || allowed.length === 0) {
3539
- effortSel.style.display = 'none';
3540
- effortSel.innerHTML = '';
3541
- } else {
3542
- effortSel.style.display = '';
3543
- effortSel.innerHTML = allowed.map(v => '<option value="' + v + '"' + (v === storedEffort ? ' selected' : '') + '>' + (AG_EFFORT_LABEL[v] || v) + '</option>').join('');
3544
- }
3545
- const fastAllowed = !!model && agModelSupportsFast(normalizedProvider, model);
3546
- const fastLabel = body.querySelector('[data-fam-fast-label="openai"]');
3547
- fastEl.style.display = fastAllowed ? '' : 'none';
3548
- if (fastLabel) fastLabel.style.display = fastAllowed ? '' : 'none';
3549
- if (!fastAllowed) fastEl.classList.remove('on');
3550
- else if (srConfig?.modelOptions?.openai?.fast) fastEl.classList.add('on');
3551
- else fastEl.classList.remove('on');
3552
- }
3553
-
3554
- async function loadSearchConfig() {
3555
- const cfg = await fetch('/search/config').then(r => r.json()).catch(() => ({}));
3556
- srConfig = cfg;
3557
- srPopulateSearch(cfg);
3558
- }
3559
-
3560
- function srPopulateSearch(cfg) {
3561
- srRenderProviderOptions(cfg?.availableProviders || []);
3562
- const sel = document.getElementById('sr-provider');
3563
- if (sel && !sel.disabled) {
3564
- const active = cfg?.provider;
3565
- if (active && Array.from(sel.options).some(o => o.value === active)) sel.value = active;
3566
- }
3567
- srUpdateProviderHint();
3568
- srRenderModelPresets();
3569
-
3570
- const creds = cfg?.rawSearch?.credentials || {};
3571
- SR_KEY_PROVIDERS.forEach(p => {
3572
- const inp = document.querySelector('[data-k="sr-' + p.id + '"]');
3573
- const key = creds[p.id]?.apiKey || '';
3574
- if (inp) inp.value = key;
3575
- });
3576
- }
3577
-
3578
- function srBuildData() {
3579
- const data = {};
3580
- const sel = document.getElementById('sr-provider');
3581
- if (sel && sel.value) data.provider = sel.value;
3582
- // Collect per-family model selections from the Model presets section.
3583
- const models = {};
3584
- document.querySelectorAll('#sr-model-presets-body select[data-fam]').forEach(s => {
3585
- if (s.disabled) return;
3586
- const fam = s.dataset.fam;
3587
- if (fam && s.value) models[fam] = s.value;
3588
- });
3589
- if (Object.keys(models).length) data.models = models;
3590
- // Per-family reasoning effort + service-tier toggles. Only openai exposes
3591
- // these in this revision; absent/hidden inputs are simply skipped.
3592
- const modelOptions = {};
3593
- const openaiOpts = {};
3594
- const effortSel = document.querySelector('#sr-model-presets-body select[data-fam-effort="openai"]');
3595
- const fastEl = document.querySelector('#sr-model-presets-body [data-fam-fast="openai"]');
3596
- if (effortSel && effortSel.style.display !== 'none' && effortSel.value) openaiOpts.effort = effortSel.value;
3597
- if (fastEl && fastEl.style.display !== 'none' && fastEl.classList.contains('on')) openaiOpts.fast = true;
3598
- if (Object.keys(openaiOpts).length) modelOptions.openai = openaiOpts;
3599
- data.modelOptions = modelOptions;
3600
- const searchProviders = {};
3601
- SR_KEY_PROVIDERS.forEach(p => {
3602
- const inp = document.querySelector('[data-k="sr-' + p.id + '"]');
3603
- if (inp) searchProviders[p.id] = inp.value;
3604
- });
3605
- data.searchProviders = searchProviders;
3606
- return data;
3607
- }
3608
-
3609
- async function srSavePanel() {
3610
- const activePanel = document.querySelector('.panel.active');
3611
- const warnEl = activePanel?.querySelector('.warn-msg') || document.createElement('div');
3612
- const btn = activePanel?.querySelector('.save');
3613
- warnEl.textContent = '';
3614
- const data = srBuildData();
3615
-
3616
- if (btn) { btn.textContent = 'Saving...'; btn.disabled = true; }
3617
- try {
3618
- const res = await fetch('/search/config', {
3619
- method: 'POST',
3620
- headers: { 'Content-Type': 'application/json' },
3621
- body: JSON.stringify(data),
3622
- });
3623
- if (!res.ok) {
3624
- let msg = 'HTTP ' + res.status;
3625
- try { const j = await res.json(); msg = j.error || j.message || msg; }
3626
- catch { try { msg = await res.text() || msg; } catch {} }
3627
- throw new Error(msg);
3628
- }
3629
- isDirty = false;
3630
- // Newly-registered keys must immediately flip the corresponding provider
3631
- // into the dropdown without a UI reload — re-fetch the config and repopulate.
3632
- await loadSearchConfig();
3633
- if (btn) { btn.textContent = '✓ Saved'; btn.classList.add('done'); setTimeout(() => { btn.textContent = 'Save'; btn.classList.remove('done'); btn.disabled = false; }, 1500); }
3634
- } catch (e) {
3635
- console.error('[mixdog] save error:', e);
3636
- warnEl.textContent = 'Save failed: ' + (e?.message || e);
3637
- if (btn) { btn.textContent = 'Save'; btn.disabled = false; }
3638
- }
3639
- }
3640
-
3641
- // ============================================================
3642
- // -- Agent Maintenance --
3643
- const AG_MAINT_TASKS = [
3644
- { id: 'explore', label: 'Explore', desc: 'Filesystem exploration agent (explore tool)' },
3645
- { id: 'memory', label: 'Memory Cycles', desc: 'Chunker / re-scorer / core reviewer (cycles 1-3)' },
3646
- ];
3647
- let agMaintenance = {};
3648
- let agMaintenanceDefaults = {};
3649
-
3650
- async function loadAgMaintenance() {
3651
- try {
3652
- const [r] = await Promise.all([
3653
- fetch('/agent/maintenance').then(r => r.json()),
3654
- agPresets.length ? Promise.resolve() : agLoadPresets(),
3655
- ]);
3656
- agMaintenance = r.maintenance || {};
3657
- agMaintenanceDefaults = r.defaults || {};
3658
- } catch { agMaintenance = {}; agMaintenanceDefaults = {}; }
3659
- renderAgMaintenance();
3660
- }
3661
-
3662
- function renderAgMaintenance() {
3663
- const container = document.getElementById('ag-maint-tasks');
3664
- if (!container) return;
3665
- container.innerHTML = '';
3666
- for (const task of AG_MAINT_TASKS) {
3667
- // Only an EXPLICIT slot value selects a preset; an unset non-default slot
3668
- // renders as "Inherit default" (truly unset) so Save never persists a
3669
- // phantom override. Stored values may be a preset id or a display name.
3670
- const explicit = agMaintenance[task.id] || '';
3671
- const matched = agPresets.find(p => p.id === explicit) || agPresets.find(p => (p.name || '') === explicit);
3672
- const currentId = matched ? matched.id : '';
3673
- const row = document.createElement('div');
3674
- row.className = 'r';
3675
- const nameSpan = document.createElement('span');
3676
- nameSpan.className = 'r-name';
3677
- nameSpan.textContent = task.label;
3678
- const sel = document.createElement('select');
3679
- sel.className = 'r-select';
3680
- sel.id = 'ag-maint-' + task.id;
3681
- sel.style.cssText = 'width:200px;flex:0 0 200px';
3682
- const descSpan = document.createElement('span');
3683
- descSpan.style.cssText = 'flex:1;font-size:11px;color:var(--text-4);margin-left:10px';
3684
- descSpan.textContent = task.desc;
3685
- row.appendChild(nameSpan);
3686
- row.appendChild(sel);
3687
- row.appendChild(descSpan);
3688
- container.appendChild(row);
3689
- // "Inherit default" option removed per request — each slot lists explicit
3690
- // presets only. An unset slot simply shows the first preset; Save persists
3691
- // whatever is selected.
3692
- agPresets.forEach(p => {
3693
- const opt = document.createElement('option');
3694
- opt.value = p.id;
3695
- opt.textContent = p.name || p.id;
3696
- if (p.id === currentId) opt.selected = true;
3697
- sel.appendChild(opt);
3698
- });
3699
- }
3700
- }
3701
-
3702
- async function saveAgMaintenance() {
3703
- const warn = document.getElementById('warn-ag-maint');
3704
- warn.textContent = '';
3705
- const data = {};
3706
- for (const task of AG_MAINT_TASKS) {
3707
- const v = document.getElementById('ag-maint-' + task.id)?.value;
3708
- if (v) data[task.id] = v;
3709
- else data[task.id] = null; // inherit → clear any override
3710
- }
3711
- try {
3712
- const r = await fetch('/agent/maintenance', {
3713
- method: 'POST',
3714
- headers: { 'Content-Type': 'application/json' },
3715
- body: JSON.stringify(data),
3716
- }).then(r => r.json());
3717
- if (!r.ok) { warn.textContent = r.error || 'Save failed'; return; }
3718
- const btn = document.querySelector('#panel-agent-maintenance .save');
3719
- btn.classList.add('done'); btn.textContent = 'Saved';
3720
- setTimeout(() => { btn.classList.remove('done'); btn.textContent = 'Save'; }, 1500);
3721
- } catch {
3722
- warn.textContent = 'Save failed';
3723
- }
3724
- }
3725
- </script>
3726
-
3727
- <div class="modal-overlay" id="close-modal">
3728
- <div class="modal-box">
3729
- <div class="modal-title">Unsaved Changes</div>
3730
- <div class="modal-msg">You have unsaved changes. What would you like to do?</div>
3731
- <div class="modal-actions">
3732
- <button class="mbtn primary" onclick="saveAndClose()">Save & Close</button>
3733
- <button class="mbtn danger-outline" onclick="discardAndClose()">Close Without Saving</button>
3734
- <button class="mbtn secondary" onclick="cancelClose()">Cancel</button>
3735
- </div>
3736
- </div>
3737
- </div>
3738
-
3739
- </body>
3740
- </html>