@geminixiang/mikan 1.0.0-beta.76 → 1.0.0-beta.78

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 (535) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/adapters/commands/login.d.ts.map +1 -1
  3. package/dist/adapters/commands/login.js +0 -9
  4. package/dist/adapters/commands/login.js.map +1 -1
  5. package/dist/adapters/commands/manifest.d.ts +0 -19
  6. package/dist/adapters/commands/manifest.d.ts.map +1 -1
  7. package/dist/adapters/commands/manifest.js +0 -34
  8. package/dist/adapters/commands/manifest.js.map +1 -1
  9. package/dist/adapters/commands/model.d.ts +0 -5
  10. package/dist/adapters/commands/model.d.ts.map +1 -1
  11. package/dist/adapters/commands/model.js +0 -5
  12. package/dist/adapters/commands/model.js.map +1 -1
  13. package/dist/adapters/commands/new.d.ts.map +1 -1
  14. package/dist/adapters/commands/new.js +0 -2
  15. package/dist/adapters/commands/new.js.map +1 -1
  16. package/dist/adapters/commands/registry.d.ts +0 -1
  17. package/dist/adapters/commands/registry.d.ts.map +1 -1
  18. package/dist/adapters/commands/registry.js +0 -8
  19. package/dist/adapters/commands/registry.js.map +1 -1
  20. package/dist/adapters/commands/session-view.d.ts.map +1 -1
  21. package/dist/adapters/commands/session-view.js +0 -2
  22. package/dist/adapters/commands/session-view.js.map +1 -1
  23. package/dist/adapters/commands/types.d.ts +0 -29
  24. package/dist/adapters/commands/types.d.ts.map +1 -1
  25. package/dist/adapters/commands/types.js.map +1 -1
  26. package/dist/adapters/commands/utils.d.ts +0 -3
  27. package/dist/adapters/commands/utils.d.ts.map +1 -1
  28. package/dist/adapters/commands/utils.js +0 -3
  29. package/dist/adapters/commands/utils.js.map +1 -1
  30. package/dist/adapters/discord/bot.d.ts +0 -4
  31. package/dist/adapters/discord/bot.d.ts.map +1 -1
  32. package/dist/adapters/discord/bot.js +5 -41
  33. package/dist/adapters/discord/bot.js.map +1 -1
  34. package/dist/adapters/discord/components.d.ts +0 -23
  35. package/dist/adapters/discord/components.d.ts.map +1 -1
  36. package/dist/adapters/discord/components.js +0 -23
  37. package/dist/adapters/discord/components.js.map +1 -1
  38. package/dist/adapters/discord/context.d.ts.map +1 -1
  39. package/dist/adapters/discord/context.js +1 -11
  40. package/dist/adapters/discord/context.js.map +1 -1
  41. package/dist/adapters/discord/format.d.ts +0 -1
  42. package/dist/adapters/discord/format.d.ts.map +1 -1
  43. package/dist/adapters/discord/format.js +0 -37
  44. package/dist/adapters/discord/format.js.map +1 -1
  45. package/dist/adapters/github/bot.d.ts +0 -44
  46. package/dist/adapters/github/bot.d.ts.map +1 -1
  47. package/dist/adapters/github/bot.js +1 -120
  48. package/dist/adapters/github/bot.js.map +1 -1
  49. package/dist/adapters/github/client.d.ts +0 -57
  50. package/dist/adapters/github/client.d.ts.map +1 -1
  51. package/dist/adapters/github/client.js +0 -59
  52. package/dist/adapters/github/client.js.map +1 -1
  53. package/dist/adapters/github/context.d.ts.map +1 -1
  54. package/dist/adapters/github/context.js +1 -8
  55. package/dist/adapters/github/context.js.map +1 -1
  56. package/dist/adapters/github/github-ops.d.ts +0 -55
  57. package/dist/adapters/github/github-ops.d.ts.map +1 -1
  58. package/dist/adapters/github/github-ops.js +0 -59
  59. package/dist/adapters/github/github-ops.js.map +1 -1
  60. package/dist/adapters/github/ids.d.ts +0 -27
  61. package/dist/adapters/github/ids.d.ts.map +1 -1
  62. package/dist/adapters/github/ids.js +0 -29
  63. package/dist/adapters/github/ids.js.map +1 -1
  64. package/dist/adapters/github/repo.d.ts +0 -25
  65. package/dist/adapters/github/repo.d.ts.map +1 -1
  66. package/dist/adapters/github/repo.js +0 -49
  67. package/dist/adapters/github/repo.js.map +1 -1
  68. package/dist/adapters/github/tool-pack.d.ts +0 -10
  69. package/dist/adapters/github/tool-pack.d.ts.map +1 -1
  70. package/dist/adapters/github/tool-pack.js +0 -10
  71. package/dist/adapters/github/tool-pack.js.map +1 -1
  72. package/dist/adapters/github/tools/checks.d.ts +0 -6
  73. package/dist/adapters/github/tools/checks.d.ts.map +1 -1
  74. package/dist/adapters/github/tools/checks.js +0 -7
  75. package/dist/adapters/github/tools/checks.js.map +1 -1
  76. package/dist/adapters/github/tools/issue.d.ts +0 -5
  77. package/dist/adapters/github/tools/issue.d.ts.map +1 -1
  78. package/dist/adapters/github/tools/issue.js +0 -5
  79. package/dist/adapters/github/tools/issue.js.map +1 -1
  80. package/dist/adapters/github/tools/pr.d.ts +0 -7
  81. package/dist/adapters/github/tools/pr.d.ts.map +1 -1
  82. package/dist/adapters/github/tools/pr.js +0 -7
  83. package/dist/adapters/github/tools/pr.js.map +1 -1
  84. package/dist/adapters/github/tools/read.d.ts +0 -6
  85. package/dist/adapters/github/tools/read.d.ts.map +1 -1
  86. package/dist/adapters/github/tools/read.js +0 -8
  87. package/dist/adapters/github/tools/read.js.map +1 -1
  88. package/dist/adapters/github/tools/review-reply.d.ts +0 -5
  89. package/dist/adapters/github/tools/review-reply.d.ts.map +1 -1
  90. package/dist/adapters/github/tools/review-reply.js +0 -5
  91. package/dist/adapters/github/tools/review-reply.js.map +1 -1
  92. package/dist/adapters/github/tools/sync.d.ts +0 -6
  93. package/dist/adapters/github/tools/sync.d.ts.map +1 -1
  94. package/dist/adapters/github/tools/sync.js +0 -6
  95. package/dist/adapters/github/tools/sync.js.map +1 -1
  96. package/dist/adapters/github/types.d.ts +0 -88
  97. package/dist/adapters/github/types.d.ts.map +1 -1
  98. package/dist/adapters/github/types.js.map +1 -1
  99. package/dist/adapters/github/webhook.d.ts +0 -13
  100. package/dist/adapters/github/webhook.d.ts.map +1 -1
  101. package/dist/adapters/github/webhook.js +0 -16
  102. package/dist/adapters/github/webhook.js.map +1 -1
  103. package/dist/adapters/index.d.ts +3 -4
  104. package/dist/adapters/index.d.ts.map +1 -1
  105. package/dist/adapters/index.js +4 -4
  106. package/dist/adapters/index.js.map +1 -1
  107. package/dist/adapters/intake.d.ts +0 -17
  108. package/dist/adapters/intake.d.ts.map +1 -1
  109. package/dist/adapters/intake.js +6 -32
  110. package/dist/adapters/intake.js.map +1 -1
  111. package/dist/adapters/markdown-tables.d.ts +0 -10
  112. package/dist/adapters/markdown-tables.d.ts.map +1 -1
  113. package/dist/adapters/markdown-tables.js +1 -42
  114. package/dist/adapters/markdown-tables.js.map +1 -1
  115. package/dist/adapters/progressive-renderer.d.ts.map +1 -1
  116. package/dist/adapters/progressive-renderer.js +3 -25
  117. package/dist/adapters/progressive-renderer.js.map +1 -1
  118. package/dist/adapters/shared.d.ts +0 -57
  119. package/dist/adapters/shared.d.ts.map +1 -1
  120. package/dist/adapters/shared.js +0 -71
  121. package/dist/adapters/shared.js.map +1 -1
  122. package/dist/adapters/slack/assistant.d.ts +0 -49
  123. package/dist/adapters/slack/assistant.d.ts.map +1 -1
  124. package/dist/adapters/slack/assistant.js +0 -97
  125. package/dist/adapters/slack/assistant.js.map +1 -1
  126. package/dist/adapters/slack/auto-reply-context.d.ts +0 -7
  127. package/dist/adapters/slack/auto-reply-context.d.ts.map +1 -1
  128. package/dist/adapters/slack/auto-reply-context.js +0 -7
  129. package/dist/adapters/slack/auto-reply-context.js.map +1 -1
  130. package/dist/adapters/slack/blocks.d.ts +0 -9
  131. package/dist/adapters/slack/blocks.d.ts.map +1 -1
  132. package/dist/adapters/slack/blocks.js +0 -20
  133. package/dist/adapters/slack/blocks.js.map +1 -1
  134. package/dist/adapters/slack/bot.d.ts +0 -116
  135. package/dist/adapters/slack/bot.d.ts.map +1 -1
  136. package/dist/adapters/slack/bot.js +7 -231
  137. package/dist/adapters/slack/bot.js.map +1 -1
  138. package/dist/adapters/slack/context.d.ts.map +1 -1
  139. package/dist/adapters/slack/context.js +1 -3
  140. package/dist/adapters/slack/context.js.map +1 -1
  141. package/dist/adapters/slack/jev-context.d.ts +0 -13
  142. package/dist/adapters/slack/jev-context.d.ts.map +1 -1
  143. package/dist/adapters/slack/jev-context.js +0 -12
  144. package/dist/adapters/slack/jev-context.js.map +1 -1
  145. package/dist/adapters/slack/markdown.d.ts +0 -1
  146. package/dist/adapters/slack/markdown.d.ts.map +1 -1
  147. package/dist/adapters/slack/markdown.js +0 -5
  148. package/dist/adapters/slack/markdown.js.map +1 -1
  149. package/dist/adapters/slack/persona.d.ts +0 -9
  150. package/dist/adapters/slack/persona.d.ts.map +1 -1
  151. package/dist/adapters/slack/persona.js +0 -9
  152. package/dist/adapters/slack/persona.js.map +1 -1
  153. package/dist/adapters/slack/response-lifecycle.d.ts.map +1 -1
  154. package/dist/adapters/slack/response-lifecycle.js +1 -45
  155. package/dist/adapters/slack/response-lifecycle.js.map +1 -1
  156. package/dist/adapters/slack/session.d.ts +2 -1
  157. package/dist/adapters/slack/session.d.ts.map +1 -1
  158. package/dist/adapters/slack/session.js +2 -2
  159. package/dist/adapters/slack/session.js.map +1 -1
  160. package/dist/adapters/slack/stream-limits.d.ts +0 -25
  161. package/dist/adapters/slack/stream-limits.d.ts.map +1 -1
  162. package/dist/adapters/slack/stream-limits.js +0 -34
  163. package/dist/adapters/slack/stream-limits.js.map +1 -1
  164. package/dist/adapters/slack/task-intent.d.ts +0 -13
  165. package/dist/adapters/slack/task-intent.d.ts.map +1 -1
  166. package/dist/adapters/slack/task-intent.js +0 -7
  167. package/dist/adapters/slack/task-intent.js.map +1 -1
  168. package/dist/adapters/slack/task-status.d.ts +0 -1
  169. package/dist/adapters/slack/task-status.d.ts.map +1 -1
  170. package/dist/adapters/slack/task-status.js +1 -9
  171. package/dist/adapters/slack/task-status.js.map +1 -1
  172. package/dist/adapters/slack/tool-pack.d.ts +0 -6
  173. package/dist/adapters/slack/tool-pack.d.ts.map +1 -1
  174. package/dist/adapters/slack/tool-pack.js +0 -6
  175. package/dist/adapters/slack/tool-pack.js.map +1 -1
  176. package/dist/adapters/slack/tools/blockkit.d.ts.map +1 -1
  177. package/dist/adapters/slack/tools/blockkit.js +0 -11
  178. package/dist/adapters/slack/tools/blockkit.js.map +1 -1
  179. package/dist/adapters/slack/types.d.ts +0 -19
  180. package/dist/adapters/slack/types.d.ts.map +1 -1
  181. package/dist/adapters/slack/types.js.map +1 -1
  182. package/dist/adapters/slack/update-diagnostics.d.ts +0 -1
  183. package/dist/adapters/slack/update-diagnostics.d.ts.map +1 -1
  184. package/dist/adapters/slack/update-diagnostics.js +1 -7
  185. package/dist/adapters/slack/update-diagnostics.js.map +1 -1
  186. package/dist/adapters/telegram/bot.d.ts +0 -6
  187. package/dist/adapters/telegram/bot.d.ts.map +1 -1
  188. package/dist/adapters/telegram/bot.js +1 -50
  189. package/dist/adapters/telegram/bot.js.map +1 -1
  190. package/dist/adapters/telegram/context.d.ts.map +1 -1
  191. package/dist/adapters/telegram/context.js +1 -8
  192. package/dist/adapters/telegram/context.js.map +1 -1
  193. package/dist/adapters/types.d.ts +0 -57
  194. package/dist/adapters/types.d.ts.map +1 -1
  195. package/dist/adapters/types.js.map +1 -1
  196. package/dist/adapters/web/admin/client-assets.d.ts +4 -0
  197. package/dist/adapters/web/admin/client-assets.d.ts.map +1 -0
  198. package/dist/adapters/web/admin/client-assets.js +1765 -0
  199. package/dist/adapters/web/admin/client-assets.js.map +1 -0
  200. package/dist/adapters/web/admin/portal.d.ts +0 -10
  201. package/dist/adapters/web/admin/portal.d.ts.map +1 -1
  202. package/dist/adapters/web/admin/portal.js +6 -1909
  203. package/dist/adapters/web/admin/portal.js.map +1 -1
  204. package/dist/adapters/web/admin/types.d.ts +0 -3
  205. package/dist/adapters/web/admin/types.d.ts.map +1 -1
  206. package/dist/adapters/web/admin/types.js.map +1 -1
  207. package/dist/adapters/web/login/oauth.d.ts.map +1 -1
  208. package/dist/adapters/web/login/oauth.js +3 -7
  209. package/dist/adapters/web/login/oauth.js.map +1 -1
  210. package/dist/adapters/web/login/portal.d.ts +0 -10
  211. package/dist/adapters/web/login/portal.d.ts.map +1 -1
  212. package/dist/adapters/web/login/portal.js +4 -37
  213. package/dist/adapters/web/login/portal.js.map +1 -1
  214. package/dist/adapters/web/login/types.d.ts +0 -5
  215. package/dist/adapters/web/login/types.d.ts.map +1 -1
  216. package/dist/adapters/web/login/types.js.map +1 -1
  217. package/dist/adapters/web/portal-shell.d.ts +0 -12
  218. package/dist/adapters/web/portal-shell.d.ts.map +1 -1
  219. package/dist/adapters/web/portal-shell.js +8 -25
  220. package/dist/adapters/web/portal-shell.js.map +1 -1
  221. package/dist/adapters/web/server.d.ts +0 -1
  222. package/dist/adapters/web/server.d.ts.map +1 -1
  223. package/dist/adapters/web/server.js +0 -2
  224. package/dist/adapters/web/server.js.map +1 -1
  225. package/dist/adapters/web/session-view/portal.d.ts.map +1 -1
  226. package/dist/adapters/web/session-view/portal.js +13 -24
  227. package/dist/adapters/web/session-view/portal.js.map +1 -1
  228. package/dist/adapters/web/session-view/types.d.ts.map +1 -1
  229. package/dist/adapters/web/session-view/types.js.map +1 -1
  230. package/dist/adapters/web/token-store.d.ts +0 -2
  231. package/dist/adapters/web/token-store.d.ts.map +1 -1
  232. package/dist/adapters/web/token-store.js +0 -2
  233. package/dist/adapters/web/token-store.js.map +1 -1
  234. package/dist/cli/arg-grammar.d.ts +0 -3
  235. package/dist/cli/arg-grammar.d.ts.map +1 -1
  236. package/dist/cli/arg-grammar.js +0 -3
  237. package/dist/cli/arg-grammar.js.map +1 -1
  238. package/dist/cli/boot.d.ts.map +1 -1
  239. package/dist/cli/boot.js +3 -12
  240. package/dist/cli/boot.js.map +1 -1
  241. package/dist/cli/download.d.ts.map +1 -1
  242. package/dist/cli/download.js +2 -6
  243. package/dist/cli/download.js.map +1 -1
  244. package/dist/cli/office.d.ts.map +1 -1
  245. package/dist/cli/office.js +0 -31
  246. package/dist/cli/office.js.map +1 -1
  247. package/dist/cli/onboard.d.ts +0 -2
  248. package/dist/cli/onboard.d.ts.map +1 -1
  249. package/dist/cli/onboard.js +0 -30
  250. package/dist/cli/onboard.js.map +1 -1
  251. package/dist/cli/process-lifecycle.d.ts +0 -5
  252. package/dist/cli/process-lifecycle.d.ts.map +1 -1
  253. package/dist/cli/process-lifecycle.js +0 -6
  254. package/dist/cli/process-lifecycle.js.map +1 -1
  255. package/dist/cli/sandbox.d.ts +3 -0
  256. package/dist/cli/sandbox.d.ts.map +1 -0
  257. package/dist/cli/sandbox.js +79 -0
  258. package/dist/cli/sandbox.js.map +1 -0
  259. package/dist/cli/sessions.d.ts.map +1 -1
  260. package/dist/cli/sessions.js +0 -11
  261. package/dist/cli/sessions.js.map +1 -1
  262. package/dist/cli/types.d.ts +2 -6
  263. package/dist/cli/types.d.ts.map +1 -1
  264. package/dist/cli/types.js.map +1 -1
  265. package/dist/env-manifest.d.ts +0 -21
  266. package/dist/env-manifest.d.ts.map +1 -1
  267. package/dist/env-manifest.js +0 -11
  268. package/dist/env-manifest.js.map +1 -1
  269. package/dist/events/index.d.ts +0 -51
  270. package/dist/events/index.d.ts.map +1 -1
  271. package/dist/events/index.js +0 -42
  272. package/dist/events/index.js.map +1 -1
  273. package/dist/events/scheduler.d.ts +0 -8
  274. package/dist/events/scheduler.d.ts.map +1 -1
  275. package/dist/events/scheduler.js +0 -8
  276. package/dist/events/scheduler.js.map +1 -1
  277. package/dist/file-guards.d.ts +0 -15
  278. package/dist/file-guards.d.ts.map +1 -1
  279. package/dist/file-guards.js +2 -26
  280. package/dist/file-guards.js.map +1 -1
  281. package/dist/harness/execution-env.d.ts +0 -15
  282. package/dist/harness/execution-env.d.ts.map +1 -1
  283. package/dist/harness/execution-env.js +4 -55
  284. package/dist/harness/execution-env.js.map +1 -1
  285. package/dist/harness/http.d.ts +0 -12
  286. package/dist/harness/http.d.ts.map +1 -1
  287. package/dist/harness/http.js +0 -34
  288. package/dist/harness/http.js.map +1 -1
  289. package/dist/harness/index.d.ts +4 -7
  290. package/dist/harness/index.d.ts.map +1 -1
  291. package/dist/harness/index.js +4 -7
  292. package/dist/harness/index.js.map +1 -1
  293. package/dist/harness/jev.d.ts +0 -28
  294. package/dist/harness/jev.d.ts.map +1 -1
  295. package/dist/harness/jev.js +0 -45
  296. package/dist/harness/jev.js.map +1 -1
  297. package/dist/harness/mcp-result.d.ts +20 -0
  298. package/dist/harness/mcp-result.d.ts.map +1 -0
  299. package/dist/harness/mcp-result.js +171 -0
  300. package/dist/harness/mcp-result.js.map +1 -0
  301. package/dist/harness/mcp.d.ts +0 -19
  302. package/dist/harness/mcp.d.ts.map +1 -1
  303. package/dist/harness/mcp.js +7 -66
  304. package/dist/harness/mcp.js.map +1 -1
  305. package/dist/harness/models.d.ts +0 -19
  306. package/dist/harness/models.d.ts.map +1 -1
  307. package/dist/harness/models.js +0 -52
  308. package/dist/harness/models.js.map +1 -1
  309. package/dist/harness/open-connector.d.ts +0 -12
  310. package/dist/harness/open-connector.d.ts.map +1 -1
  311. package/dist/harness/open-connector.js +0 -22
  312. package/dist/harness/open-connector.js.map +1 -1
  313. package/dist/harness/presenter.d.ts +1 -0
  314. package/dist/harness/presenter.d.ts.map +1 -1
  315. package/dist/harness/presenter.js +16 -66
  316. package/dist/harness/presenter.js.map +1 -1
  317. package/dist/harness/prompt.d.ts +0 -11
  318. package/dist/harness/prompt.d.ts.map +1 -1
  319. package/dist/harness/prompt.js +1 -18
  320. package/dist/harness/prompt.js.map +1 -1
  321. package/dist/harness/runner.d.ts.map +1 -1
  322. package/dist/harness/runner.js +8 -28
  323. package/dist/harness/runner.js.map +1 -1
  324. package/dist/harness/session.d.ts +11 -28
  325. package/dist/harness/session.d.ts.map +1 -1
  326. package/dist/harness/session.js +100 -38
  327. package/dist/harness/session.js.map +1 -1
  328. package/dist/harness/skills.d.ts +0 -22
  329. package/dist/harness/skills.d.ts.map +1 -1
  330. package/dist/harness/skills.js +0 -20
  331. package/dist/harness/skills.js.map +1 -1
  332. package/dist/harness/subagent-profiles.d.ts +0 -4
  333. package/dist/harness/subagent-profiles.d.ts.map +1 -1
  334. package/dist/harness/subagent-profiles.js +0 -29
  335. package/dist/harness/subagent-profiles.js.map +1 -1
  336. package/dist/harness/subagent.d.ts +0 -25
  337. package/dist/harness/subagent.d.ts.map +1 -1
  338. package/dist/harness/subagent.js +1 -70
  339. package/dist/harness/subagent.js.map +1 -1
  340. package/dist/harness/tools/event.d.ts.map +1 -1
  341. package/dist/harness/tools/event.js +0 -9
  342. package/dist/harness/tools/event.js.map +1 -1
  343. package/dist/harness/tools/generate-image.d.ts +0 -6
  344. package/dist/harness/tools/generate-image.d.ts.map +1 -1
  345. package/dist/harness/tools/generate-image.js.map +1 -1
  346. package/dist/harness/tools/host-fn-tool.d.ts +0 -17
  347. package/dist/harness/tools/host-fn-tool.d.ts.map +1 -1
  348. package/dist/harness/tools/host-fn-tool.js +0 -16
  349. package/dist/harness/tools/host-fn-tool.js.map +1 -1
  350. package/dist/harness/tools/index.d.ts +1 -7
  351. package/dist/harness/tools/index.d.ts.map +1 -1
  352. package/dist/harness/tools/index.js +3 -14
  353. package/dist/harness/tools/index.js.map +1 -1
  354. package/dist/harness/tools/jev-browser.d.ts +4 -94
  355. package/dist/harness/tools/jev-browser.d.ts.map +1 -1
  356. package/dist/harness/tools/jev-browser.js +203 -92
  357. package/dist/harness/tools/jev-browser.js.map +1 -1
  358. package/dist/harness/tools/jev.d.ts +0 -8
  359. package/dist/harness/tools/jev.d.ts.map +1 -1
  360. package/dist/harness/tools/jev.js +0 -11
  361. package/dist/harness/tools/jev.js.map +1 -1
  362. package/dist/harness/tools/pi-tools.d.ts +1 -11
  363. package/dist/harness/tools/pi-tools.d.ts.map +1 -1
  364. package/dist/harness/tools/pi-tools.js +1 -30
  365. package/dist/harness/tools/pi-tools.js.map +1 -1
  366. package/dist/harness/tools/react.d.ts +0 -5
  367. package/dist/harness/tools/react.d.ts.map +1 -1
  368. package/dist/harness/tools/react.js +0 -5
  369. package/dist/harness/tools/react.js.map +1 -1
  370. package/dist/harness/tools/secret-redaction.d.ts +0 -25
  371. package/dist/harness/tools/secret-redaction.d.ts.map +1 -1
  372. package/dist/harness/tools/secret-redaction.js +3 -36
  373. package/dist/harness/tools/secret-redaction.js.map +1 -1
  374. package/dist/harness/tools/subagent.d.ts +0 -34
  375. package/dist/harness/tools/subagent.d.ts.map +1 -1
  376. package/dist/harness/tools/subagent.js +0 -92
  377. package/dist/harness/tools/subagent.js.map +1 -1
  378. package/dist/harness/tools/task.d.ts +0 -1
  379. package/dist/harness/tools/task.d.ts.map +1 -1
  380. package/dist/harness/tools/task.js +0 -1
  381. package/dist/harness/tools/task.js.map +1 -1
  382. package/dist/harness/tools/types.d.ts +0 -16
  383. package/dist/harness/tools/types.d.ts.map +1 -1
  384. package/dist/harness/tools/types.js.map +1 -1
  385. package/dist/harness/types.d.ts +1 -77
  386. package/dist/harness/types.d.ts.map +1 -1
  387. package/dist/harness/types.js.map +1 -1
  388. package/dist/index.d.ts.map +1 -1
  389. package/dist/index.js +0 -2
  390. package/dist/index.js.map +1 -1
  391. package/dist/log.d.ts.map +1 -1
  392. package/dist/log.js +0 -13
  393. package/dist/log.js.map +1 -1
  394. package/dist/main.js +7 -52
  395. package/dist/main.js.map +1 -1
  396. package/dist/memory-capture/index.d.ts +16 -0
  397. package/dist/memory-capture/index.d.ts.map +1 -0
  398. package/dist/memory-capture/index.js +194 -0
  399. package/dist/memory-capture/index.js.map +1 -0
  400. package/dist/memory-capture/types.d.ts +27 -0
  401. package/dist/memory-capture/types.d.ts.map +1 -0
  402. package/dist/memory-capture/types.js.map +1 -0
  403. package/dist/observability/index.d.ts +0 -9
  404. package/dist/observability/index.d.ts.map +1 -1
  405. package/dist/observability/index.js +0 -9
  406. package/dist/observability/index.js.map +1 -1
  407. package/dist/observability/instrument.js +0 -5
  408. package/dist/observability/instrument.js.map +1 -1
  409. package/dist/observability/otel.js +0 -1
  410. package/dist/observability/otel.js.map +1 -1
  411. package/dist/observability/sentry.d.ts +0 -18
  412. package/dist/observability/sentry.d.ts.map +1 -1
  413. package/dist/observability/sentry.js +0 -36
  414. package/dist/observability/sentry.js.map +1 -1
  415. package/dist/observability/types.d.ts +1 -10
  416. package/dist/observability/types.d.ts.map +1 -1
  417. package/dist/observability/types.js.map +1 -1
  418. package/dist/office/index.d.ts +0 -76
  419. package/dist/office/index.d.ts.map +1 -1
  420. package/dist/office/index.js +4 -154
  421. package/dist/office/index.js.map +1 -1
  422. package/dist/office/projection.d.ts +0 -29
  423. package/dist/office/projection.d.ts.map +1 -1
  424. package/dist/office/projection.js +5 -41
  425. package/dist/office/projection.js.map +1 -1
  426. package/dist/office/types.d.ts +0 -66
  427. package/dist/office/types.d.ts.map +1 -1
  428. package/dist/office/types.js.map +1 -1
  429. package/dist/runtime/conversation-runtime.d.ts.map +1 -1
  430. package/dist/runtime/conversation-runtime.js +22 -83
  431. package/dist/runtime/conversation-runtime.js.map +1 -1
  432. package/dist/runtime/session-lifecycle.d.ts +0 -10
  433. package/dist/runtime/session-lifecycle.d.ts.map +1 -1
  434. package/dist/runtime/session-lifecycle.js +0 -12
  435. package/dist/runtime/session-lifecycle.js.map +1 -1
  436. package/dist/runtime/types.d.ts +2 -29
  437. package/dist/runtime/types.d.ts.map +1 -1
  438. package/dist/runtime/types.js.map +1 -1
  439. package/dist/sandbox/cloudflare.d.ts.map +1 -1
  440. package/dist/sandbox/cloudflare.js +2 -1
  441. package/dist/sandbox/cloudflare.js.map +1 -1
  442. package/dist/sandbox/container.d.ts.map +1 -1
  443. package/dist/sandbox/container.js +21 -15
  444. package/dist/sandbox/container.js.map +1 -1
  445. package/dist/sandbox/host.d.ts.map +1 -1
  446. package/dist/sandbox/host.js +0 -2
  447. package/dist/sandbox/host.js.map +1 -1
  448. package/dist/sandbox/identity.d.ts +0 -28
  449. package/dist/sandbox/identity.d.ts.map +1 -1
  450. package/dist/sandbox/identity.js +0 -28
  451. package/dist/sandbox/identity.js.map +1 -1
  452. package/dist/sandbox/index.d.ts +0 -18
  453. package/dist/sandbox/index.d.ts.map +1 -1
  454. package/dist/sandbox/index.js +2 -19
  455. package/dist/sandbox/index.js.map +1 -1
  456. package/dist/sandbox/layout.d.ts +7 -0
  457. package/dist/sandbox/layout.d.ts.map +1 -0
  458. package/dist/sandbox/layout.js +13 -0
  459. package/dist/sandbox/layout.js.map +1 -0
  460. package/dist/sandbox/provisioner.d.ts +24 -65
  461. package/dist/sandbox/provisioner.d.ts.map +1 -1
  462. package/dist/sandbox/provisioner.js +213 -125
  463. package/dist/sandbox/provisioner.js.map +1 -1
  464. package/dist/sandbox/types.d.ts +0 -39
  465. package/dist/sandbox/types.d.ts.map +1 -1
  466. package/dist/sandbox/types.js.map +1 -1
  467. package/dist/sandbox/utils.d.ts +2 -5
  468. package/dist/sandbox/utils.d.ts.map +1 -1
  469. package/dist/sandbox/utils.js +31 -31
  470. package/dist/sandbox/utils.js.map +1 -1
  471. package/dist/sessions/chat-history-sync.d.ts +0 -6
  472. package/dist/sessions/chat-history-sync.d.ts.map +1 -1
  473. package/dist/sessions/chat-history-sync.js +1 -33
  474. package/dist/sessions/chat-history-sync.js.map +1 -1
  475. package/dist/sessions/history-line.d.ts +0 -20
  476. package/dist/sessions/history-line.d.ts.map +1 -1
  477. package/dist/sessions/history-line.js +0 -20
  478. package/dist/sessions/history-line.js.map +1 -1
  479. package/dist/sessions/migrate-common.d.ts +0 -15
  480. package/dist/sessions/migrate-common.d.ts.map +1 -1
  481. package/dist/sessions/migrate-common.js +0 -19
  482. package/dist/sessions/migrate-common.js.map +1 -1
  483. package/dist/sessions/migrate-pi-084.d.ts.map +1 -1
  484. package/dist/sessions/migrate-pi-084.js +0 -7
  485. package/dist/sessions/migrate-pi-084.js.map +1 -1
  486. package/dist/sessions/migrate-v3.d.ts +0 -7
  487. package/dist/sessions/migrate-v3.d.ts.map +1 -1
  488. package/dist/sessions/migrate-v3.js +1 -67
  489. package/dist/sessions/migrate-v3.js.map +1 -1
  490. package/dist/sessions/session-key.d.ts +3 -17
  491. package/dist/sessions/session-key.d.ts.map +1 -1
  492. package/dist/sessions/session-key.js +8 -35
  493. package/dist/sessions/session-key.js.map +1 -1
  494. package/dist/sessions/session-store.d.ts +1 -7
  495. package/dist/sessions/session-store.d.ts.map +1 -1
  496. package/dist/sessions/session-store.js +8 -50
  497. package/dist/sessions/session-store.js.map +1 -1
  498. package/dist/sessions/store.d.ts +0 -60
  499. package/dist/sessions/store.d.ts.map +1 -1
  500. package/dist/sessions/store.js +0 -113
  501. package/dist/sessions/store.js.map +1 -1
  502. package/dist/sessions/types.d.ts +0 -17
  503. package/dist/sessions/types.d.ts.map +1 -1
  504. package/dist/sessions/types.js.map +1 -1
  505. package/dist/settings/apply.d.ts +0 -6
  506. package/dist/settings/apply.d.ts.map +1 -1
  507. package/dist/settings/apply.js +0 -29
  508. package/dist/settings/apply.js.map +1 -1
  509. package/dist/settings/index.d.ts +0 -53
  510. package/dist/settings/index.d.ts.map +1 -1
  511. package/dist/settings/index.js +0 -77
  512. package/dist/settings/index.js.map +1 -1
  513. package/dist/settings/migrate.d.ts +0 -5
  514. package/dist/settings/migrate.d.ts.map +1 -1
  515. package/dist/settings/migrate.js +0 -17
  516. package/dist/settings/migrate.js.map +1 -1
  517. package/dist/types.d.ts +9 -159
  518. package/dist/types.d.ts.map +1 -1
  519. package/dist/types.js.map +1 -1
  520. package/dist/vault/index.d.ts +2 -36
  521. package/dist/vault/index.d.ts.map +1 -1
  522. package/dist/vault/index.js +135 -65
  523. package/dist/vault/index.js.map +1 -1
  524. package/dist/vault/types.d.ts +0 -15
  525. package/dist/vault/types.d.ts.map +1 -1
  526. package/dist/vault/types.js.map +1 -1
  527. package/package.json +1 -1
  528. package/dist/dream/index.d.ts +0 -20
  529. package/dist/dream/index.d.ts.map +0 -1
  530. package/dist/dream/index.js +0 -264
  531. package/dist/dream/index.js.map +0 -1
  532. package/dist/dream/types.d.ts +0 -30
  533. package/dist/dream/types.d.ts.map +0 -1
  534. package/dist/dream/types.js.map +0 -1
  535. /package/dist/{dream → memory-capture}/types.js +0 -0
@@ -5,6 +5,7 @@ import { atomicWritePrivateFile } from "../../../file-guards.js";
5
5
  import { MikanModels, parseFrontmatter, validateSkill } from "../../../harness/index.js";
6
6
  import { SessionStore } from "../../../sessions/session-store.js";
7
7
  import { InMemoryTokenStore } from "../token-store.js";
8
+ import { adminViewFunctionsScript, adminViewStartupScript, adminViewStyles, } from "./client-assets.js";
8
9
  const ADMIN_TOKEN_TTL_MS = 30 * 60 * 1000;
9
10
  export class InMemoryAdminTokenStore extends InMemoryTokenStore {
10
11
  create(args) {
@@ -30,7 +31,6 @@ import { resolveWorkspaceProjection } from "../../../office/projection.js";
30
31
  import { sharedVaultKey } from "../../../vault/index.js";
31
32
  import { modelKey, resolveAdminModelAccessStatuses } from "./provider-models.js";
32
33
  import { assertPlatformName, createOfficeAddress, listRegisteredOffices, sameOffice, } from "../../../office/index.js";
33
- // ── Handler ────────────────────────────────────────────────────────────────────
34
34
  export async function handleAdminRequest(req, res, url, services) {
35
35
  if (!url.pathname.startsWith("/admin"))
36
36
  return false;
@@ -55,7 +55,6 @@ export async function handleAdminRequest(req, res, url, services) {
55
55
  }
56
56
  return false;
57
57
  }
58
- // ── API routing ────────────────────────────────────────────────────────────────
59
58
  async function routeApiRequest(req, res, url, services) {
60
59
  if (req.method === "GET") {
61
60
  await routeGetApiRequest(res, url, services);
@@ -141,11 +140,6 @@ async function routePostApiRequest(req, res, url, services) {
141
140
  jsonRes(res, 404, { error: "Not found" });
142
141
  }
143
142
  }
144
- /**
145
- * Admin scope is a full office address. The platform defaults to the token's
146
- * (an admin invoked from Slack browses Slack offices); cross-platform targets
147
- * name theirs explicitly. Identity validation is the address factory's.
148
- */
149
143
  export function resolveConversationScope(requestedId, requestedPlatform, token) {
150
144
  const fallback = createOfficeAddress(token.platform, token.conversationId);
151
145
  try {
@@ -181,13 +175,6 @@ function requireConversationWorkspace(res, body, services, token) {
181
175
  const workspace = requireAdminWorkspace(res, services);
182
176
  return workspace ? { scope, workspace } : undefined;
183
177
  }
184
- // ── API handlers ───────────────────────────────────────────────────────────────
185
- /**
186
- * Office directories are office-key named and not reversible to raw ids, so
187
- * enumeration reads the office registry — the durable raw-id ↔ office
188
- * mapping — instead of scanning the workspace. Offices whose directory
189
- * disappeared are filtered out.
190
- */
191
178
  function listAdminOffices(workspace) {
192
179
  return listRegisteredOffices(workspace.stateDir)
193
180
  .filter((office) => existsSync(workspace.office(office).dir))
@@ -220,9 +207,7 @@ function conversationLastActivity(workspace, office) {
220
207
  if (stats.mtimeMs > latest)
221
208
  latest = stats.mtimeMs;
222
209
  }
223
- catch {
224
- // ignore
225
- }
210
+ catch { }
226
211
  }
227
212
  };
228
213
  visit(dir, 0);
@@ -330,7 +315,6 @@ function localDayKey(d) {
330
315
  function emptyBucket(date) {
331
316
  return { date, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0, cost: 0 };
332
317
  }
333
- /** Per-conversation daily token usage over the last N days (N clamped to 1..7). */
334
318
  async function serveConversationUsage(res, url, services) {
335
319
  const workspace = requireAdminWorkspace(res, services);
336
320
  if (!workspace)
@@ -366,9 +350,7 @@ async function serveConversationUsage(res, url, services) {
366
350
  await accumulateSessionUsageByDay(join(sessionDir, entry.name), cutoff, buckets, flags);
367
351
  }
368
352
  }
369
- catch {
370
- // No sessions directory yet — return empty buckets.
371
- }
353
+ catch { }
372
354
  const series = order.map((key) => buckets.get(key) ?? emptyBucket(key));
373
355
  const totals = series.reduce((sum, b) => {
374
356
  sum.input += b.input;
@@ -427,9 +409,7 @@ async function accumulateSessionUsageByDay(sessionFile, cutoff, buckets, flags)
427
409
  bucket.cost += numberOrZero(usage.cost?.total);
428
410
  }
429
411
  }
430
- catch {
431
- // Skip unreadable session files.
432
- }
412
+ catch { }
433
413
  }
434
414
  function serveConversationState(res, url, services, token) {
435
415
  const workspace = requireAdminWorkspace(res, services);
@@ -723,15 +703,10 @@ function respondWithSettingsUpdate(res, update) {
723
703
  jsonRes(res, 500, { error: err instanceof Error ? err.message : String(err) });
724
704
  }
725
705
  }
726
- // ── Workspace ──────────────────────────────────────────────────────────────────
727
706
  const WORKSPACE_TREE_MAX_DEPTH = 4;
728
707
  const WORKSPACE_TREE_MAX_ENTRIES = 800;
729
708
  const PREVIEW_FILE_MAX_BYTES = 256 * 1024;
730
709
  const WORKSPACE_TOP_DIRS = new Set(["scratch"]);
731
- /**
732
- * Limit what the admin UI can browse under a conversation directory.
733
- * Allowed: top-level "scratch/" subtree.
734
- */
735
710
  function isWorkspacePathAllowed(rel) {
736
711
  if (rel === "")
737
712
  return true;
@@ -943,11 +918,6 @@ function serveWorkspaceFile(res, url, services, token) {
943
918
  }
944
919
  servePreviewFile(res, safe.absolute, { path: requestedPath }, "File not found");
945
920
  }
946
- /**
947
- * Read skill metadata via the harness frontmatter parser (the owning module).
948
- * The portal historically accepted frontmatter keys case-insensitively, so
949
- * the lookup — not the parser — preserves that leniency.
950
- */
951
921
  function readSkillMeta(filePath) {
952
922
  let text;
953
923
  try {
@@ -995,11 +965,6 @@ export function readSkillsFromDir(skillsDir, source) {
995
965
  }
996
966
  return out.toSorted((a, b) => a.name.localeCompare(b.name));
997
967
  }
998
- /**
999
- * List MCP servers for both scopes, with env/header VALUES redacted to key
1000
- * names: they carry API keys, and this response renders in a browser. The
1001
- * panel edits full entries but only ever needs to show which keys exist.
1002
- */
1003
968
  function serveMcpServersList(res, url, services, token) {
1004
969
  const scope = resolveConversationFromQuery(url, token);
1005
970
  if (scope.error) {
@@ -1041,16 +1006,6 @@ function mcpStringMap(value) {
1041
1006
  return Object.keys(out).length > 0 ? out : undefined;
1042
1007
  }
1043
1008
  const MCP_VERIFY_TIMEOUT_MS = 20_000;
1044
- /**
1045
- * Connect to the given servers once, the way the runner will, and report per
1046
- * server either the tool count or the server's own error text. This is the
1047
- * difference between "settings.json was written" and "it works": the runner
1048
- * only logs load failures host-side, so without this the portal is the only
1049
- * place an operator can learn that a pasted token is wrong.
1050
- *
1051
- * For stdio entries this runs the configured command on the host now, at save
1052
- * time, rather than at the next message.
1053
- */
1054
1009
  async function verifyMcpServers(servers) {
1055
1010
  const names = Object.keys(servers);
1056
1011
  if (names.length === 0)
@@ -1072,13 +1027,6 @@ async function verifyMcpServers(servers) {
1072
1027
  await loaded.dispose();
1073
1028
  }
1074
1029
  }
1075
- /**
1076
- * Compute the scope's next server map for one mutation. `import` takes the
1077
- * cross-client `mcpServers` JSON verbatim (see `parseStandardMcpServers`) and
1078
- * replaces every named entry outright — a pasted declaration is the whole
1079
- * truth for that server, so no env/header carry-over from the previous entry.
1080
- * `touched` lists the entries to connection-check after the write.
1081
- */
1082
1030
  function planMcpMutation(action, body, current) {
1083
1031
  const next = { ...current };
1084
1032
  if (action === "import") {
@@ -1119,16 +1067,6 @@ function planMcpMutation(action, body, current) {
1119
1067
  next[name] = entry.disabled ? { ...entry, disabled: undefined } : { ...entry, disabled: true };
1120
1068
  return { next, touched: {} };
1121
1069
  }
1122
- /**
1123
- * Import / install / remove / toggle / test MCP servers in one scope. Reads
1124
- * the scope's raw map, applies the change, and writes the full map back
1125
- * wholesale because merging would make removal impossible. Runner
1126
- * caches refresh via applyGlobalSettings/applyConversationSettings.
1127
- *
1128
- * Writes persist even when the follow-up connection check fails: the
1129
- * operator edits the entry, not retypes it. `test` re-checks an existing
1130
- * entry without writing.
1131
- */
1132
1070
  async function serveMcpServerMutation(res, body, services, token) {
1133
1071
  const action = body.action;
1134
1072
  if (action !== "import" &&
@@ -1235,14 +1173,6 @@ function resolveSkillsRoot(workspace, scope, source) {
1235
1173
  source,
1236
1174
  };
1237
1175
  }
1238
- /**
1239
- * Create, overwrite, or delete one skill directory. The directory name is
1240
- * fixed at creation (renaming would leave the old directory as an orphan);
1241
- * editing an existing skill always targets its current directory. Writes go
1242
- * straight to the workspace skills tree — the same location the harness
1243
- * loader reads — so a saved skill is available on the conversation's next
1244
- * turn without a restart.
1245
- */
1246
1176
  async function serveSkillMutation(res, body, services, token) {
1247
1177
  const action = body.action;
1248
1178
  if (action !== "save" && action !== "delete") {
@@ -1315,12 +1245,6 @@ async function serveSkillMutation(res, body, services, token) {
1315
1245
  }
1316
1246
  jsonRes(res, 200, { ok: true, name, directory, source: resolved.source });
1317
1247
  }
1318
- // ── Events ─────────────────────────────────────────────────────────────────────
1319
- /**
1320
- * List events through the owning store, whose payloads are validated by the
1321
- * event-format module (files that fail validation stay visible with a null
1322
- * payload so operators can delete them).
1323
- */
1324
1248
  export async function listOfficeEvents(store) {
1325
1249
  const entries = await store.list();
1326
1250
  return entries.map((entry) => {
@@ -1348,7 +1272,6 @@ function requireAdminEventStore(res, services, office) {
1348
1272
  }
1349
1273
  return services.eventStore(office);
1350
1274
  }
1351
- /** Every registered office's events, each read through its own store. */
1352
1275
  async function serveEventsList(res, services) {
1353
1276
  const workspace = requireAdminWorkspace(res, services);
1354
1277
  if (!workspace)
@@ -1363,7 +1286,6 @@ async function serveEventsList(res, services) {
1363
1286
  }
1364
1287
  jsonRes(res, 200, { events });
1365
1288
  }
1366
- /** Per-conversation listing through that office's confined store. */
1367
1289
  async function serveConversationEventsList(res, url, services, token) {
1368
1290
  const scope = resolveConversationFromQuery(url, token);
1369
1291
  if (scope.error) {
@@ -1381,7 +1303,6 @@ async function serveConversationEventsList(res, url, services, token) {
1381
1303
  events: await listOfficeEvents(store),
1382
1304
  });
1383
1305
  }
1384
- /** Delete a single event file scoped to the caller's conversation. */
1385
1306
  async function serveConversationEventDelete(res, body, services, token) {
1386
1307
  const scope = resolveTargetConversation(body, token);
1387
1308
  if (scope.error) {
@@ -1402,7 +1323,6 @@ async function serveConversationEventDelete(res, body, services, token) {
1402
1323
  const store = requireAdminEventStore(res, services, workspace.office(scope.address));
1403
1324
  if (!store)
1404
1325
  return;
1405
- // The store is confined to this office: another office's filename is "not found".
1406
1326
  try {
1407
1327
  await store.delete(name);
1408
1328
  jsonRes(res, 200, { ok: true });
@@ -1412,7 +1332,6 @@ async function serveConversationEventDelete(res, body, services, token) {
1412
1332
  jsonRes(res, /not found/.test(message) ? 404 : 500, { error: message });
1413
1333
  }
1414
1334
  }
1415
- // ── Utilities ──────────────────────────────────────────────────────────────────
1416
1335
  const esc = escapeHtml;
1417
1336
  const ICONS = {
1418
1337
  settings: `<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33h.01a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82v.01a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>`,
@@ -1425,21 +1344,10 @@ const ICONS = {
1425
1344
  overview: `<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="8" height="8" rx="1.5"/><rect x="13" y="3" width="8" height="5" rx="1.5"/><rect x="13" y="11" width="8" height="10" rx="1.5"/><rect x="3" y="14" width="8" height="7" rx="1.5"/></svg>`,
1426
1345
  usage: `<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M3 21V3"/><path d="M3 21h18"/><path d="M7 17V11M12 17V7M17 17v-4"/></svg>`,
1427
1346
  };
1428
- /** One entry in the settings rail: an icon, a label, and the pane id it activates. */
1429
1347
  function railLink(id, label, icon, active = false) {
1430
1348
  const svg = ICONS[icon];
1431
- // The label span is hidden below 860px, so mirror it into aria-label/title to
1432
- // keep an accessible name and a hover hint once the text disappears.
1433
1349
  return `<button class="rail-link${active ? " active" : ""}" type="button" data-pane="${id}" aria-current="${active ? "page" : "false"}" aria-label="${esc(label)}" title="${esc(label)}">${svg}<span>${esc(label)}</span></button>`;
1434
1350
  }
1435
- /**
1436
- * One settings pane: an eyebrow-free header (icon lives in the rail, not
1437
- * repeated here) with a title, a one-line description, and header actions,
1438
- * followed by body content. Every pane shares this shape so scanning the
1439
- * page feels like one continuous settings surface instead of assembled
1440
- * cards — the visual language `frontend-design` calls for here is a
1441
- * quiet, editorial settings app, not a dashboard of boxes.
1442
- */
1443
1351
  function settingsPane(id, title, description, actions, body) {
1444
1352
  return `<section class="pane" id="pane-${id}" data-pane="${id}">
1445
1353
  <header class="pane-head">
@@ -1570,1284 +1478,13 @@ const adminViewBody = `<div class="settings-shell">
1570
1478
  <button id="skill-dialog-submit" class="primary-action-btn" type="button" onclick="submitSkillDialog(this)">Save</button>
1571
1479
  </div>
1572
1480
  </dialog>`;
1573
- const adminViewScript = ` let activeConversationKey = defaultConversationKey;
1574
- function scopeOf(key) {
1575
- const sep = key.indexOf(':');
1576
- return { platform: key.slice(0, sep), conversationId: key.slice(sep + 1) };
1577
- }
1578
- function scopeQuery() {
1579
- const scope = scopeOf(activeConversationKey);
1580
- return 'conversationId=' + encodeURIComponent(scope.conversationId) +
1581
- '&platform=' + encodeURIComponent(scope.platform);
1582
- }
1583
- function scopeBody() {
1584
- const scope = scopeOf(activeConversationKey);
1585
- return { conversationId: scope.conversationId, platform: scope.platform };
1586
- }
1587
- let availableModels = [];
1588
- let modelsLoaded = false;
1589
- let mcpPresets = [];
1590
- let mcpServersByScope = { conversation: {}, global: {} };
1591
- let pendingMcpInstall = null;
1592
-
1593
- // ── Helpers ──────────────────────────────────────────────────────────────────
1594
-
1595
- function escHtml(str) {
1596
- return String(str).replace(/[&<>"']/g, (c) => (
1597
- {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]
1598
- ));
1599
- }
1600
- const escAttr = escHtml;
1601
- async function copyToClipboard(text) {
1602
- try { await navigator.clipboard.writeText(text); } catch { prompt('Copy this link:', text); }
1603
- }
1604
- document.addEventListener('click', (event) => {
1605
- const target = event.target instanceof Element
1606
- ? event.target.closest('[data-admin-action]')
1607
- : null;
1608
- if (!target) return;
1609
- switch (target.dataset.adminAction) {
1610
- case 'preview-file':
1611
- void previewFile(target.dataset.filePath || '');
1612
- break;
1613
- case 'copy-link':
1614
- void copyToClipboard(target.dataset.copyText || '');
1615
- break;
1616
- case 'delete-event':
1617
- void deleteEvent(target.dataset.eventName || '', target);
1618
- break;
1619
- case 'select-conversation':
1620
- setActiveConversation(target.dataset.conversationId || '');
1621
- switchTab('conversation');
1622
- break;
1623
- case 'toggle-timeline-filter':
1624
- toggleTimelineFilter(target.dataset.filterKey || '');
1625
- break;
1626
- }
1627
- });
1628
- async function apiGet(path) {
1629
- const url = path + (path.includes('?') ? '&' : '?') + 'token=' + encodeURIComponent(adminToken);
1630
- const r = await fetch(url);
1631
- const data = await r.json();
1632
- if (!r.ok) throw new Error(data.error || ('HTTP ' + r.status));
1633
- return data;
1634
- }
1635
- async function apiPost(path, body) {
1636
- const r = await fetch(path, {
1637
- method: 'POST',
1638
- headers: { 'Content-Type': 'application/json' },
1639
- body: JSON.stringify({ token: adminToken, ...body }),
1640
- });
1641
- const data = await r.json();
1642
- if (!r.ok) throw new Error(data.error || ('HTTP ' + r.status));
1643
- return data;
1644
- }
1645
- async function loadModels() {
1646
- try {
1647
- const data = await apiGet('/admin/api/models');
1648
- availableModels = Array.isArray(data.models) ? data.models : [];
1649
- } catch (err) {
1650
- availableModels = [];
1651
- } finally {
1652
- modelsLoaded = true;
1653
- }
1654
- }
1655
- function modelRef(provider, model) {
1656
- return provider && model ? provider + '/' + model : '';
1657
- }
1658
- function parseModelRef(value) {
1659
- const slash = value.indexOf('/');
1660
- if (slash <= 0 || slash === value.length - 1) return { provider: '', model: '' };
1661
- return { provider: value.slice(0, slash), model: value.slice(slash + 1) };
1662
- }
1663
- function renderModelOptions(currentProvider, currentModel) {
1664
- const current = modelRef(currentProvider, currentModel);
1665
- const seen = new Set();
1666
- const groups = {
1667
- available: [],
1668
- unverified: [],
1669
- };
1670
- if (current) {
1671
- seen.add(current);
1672
- groups.available.push('<option value="' + escAttr(current) + '">' + escHtml(current + ' (current)') + '</option>');
1673
- }
1674
- for (const model of availableModels) {
1675
- const ref = modelRef(model.provider, model.id);
1676
- if (!ref || seen.has(ref)) continue;
1677
- seen.add(ref);
1678
- const details = [model.name && model.name !== model.id ? model.name : '', model.reasoning ? 'thinking' : '', Array.isArray(model.input) && model.input.includes('image') ? 'image' : '']
1679
- .filter(Boolean)
1680
- .join(' · ');
1681
- const option = '<option value="' + escAttr(ref) + '">' + escHtml(details ? ref + ' — ' + details : ref) + '</option>';
1682
- if (model.status === 'unverified') groups.unverified.push(option);
1683
- else groups.available.push(option);
1684
- }
1685
- const sections = [];
1686
- if (groups.available.length > 0) sections.push('<optgroup label="Available">' + groups.available.join('') + '</optgroup>');
1687
- if (groups.unverified.length > 0) sections.push('<optgroup label="Configured but unverified">' + groups.unverified.join('') + '</optgroup>');
1688
- if (sections.length === 0) {
1689
- return '<option value="">No available models</option>';
1690
- }
1691
- return sections.join('');
1692
- }
1693
-
1694
- // ── Scope tabs (rail top switch) ────────────────────────────────────────────
1695
-
1696
- const scopeBtns = document.querySelectorAll('.rail-scope-btn');
1697
- const tabPanels = document.querySelectorAll('.tab-panel');
1698
- const railNavConversation = document.getElementById('rail-nav-conversation');
1699
- const railNavGlobal = document.getElementById('rail-nav-global');
1700
-
1701
- function switchTab(tabId) {
1702
- scopeBtns.forEach((btn) => {
1703
- const active = btn.dataset.tab === tabId;
1704
- btn.classList.toggle('active', active);
1705
- btn.setAttribute('aria-selected', active ? 'true' : 'false');
1706
- });
1707
- tabPanels.forEach((panel) => panel.classList.toggle('active', panel.id === 'panel-' + tabId));
1708
- railNavConversation.hidden = tabId !== 'conversation';
1709
- railNavGlobal.hidden = tabId !== 'global';
1710
- if (tabId === 'global') initGlobal();
1711
- const firstPane = (tabId === 'global' ? railNavGlobal : railNavConversation).querySelector('.rail-link');
1712
- if (firstPane) selectPane(tabId, firstPane.dataset.pane);
1713
- }
1714
- scopeBtns.forEach((btn) => btn.addEventListener('click', () => switchTab(btn.dataset.tab)));
1715
-
1716
- // ── Rail navigation (single active pane per scope) ──────────────────────────
1717
-
1718
- // Looked up by name (not collected into an object up front) because the
1719
- // loaders below are const declarations further down this same script;
1720
- // referencing them before their own line runs would hit the temporal
1721
- // dead zone. By the time this function is actually called (after a rail
1722
- // click, always after the whole script has executed), they're all
1723
- // initialized.
1724
- function paneLoader(key) {
1725
- switch (key) {
1726
- case 'settings': return loadSettings;
1727
- case 'workspace': return loadWorkspace;
1728
- case 'skills': return loadSkills;
1729
- case 'mcp': return loadMcpServers;
1730
- case 'events': return loadConversationEvents;
1731
- case 'g-overview': return loadAllConversations;
1732
- case 'g-usage': return loadTokenUsage;
1733
- case 'g-settings': return loadGlobalSettings;
1734
- case 'g-mcp': return loadMcpServers;
1735
- case 'g-skills': return loadGlobalSkills;
1736
- case 'g-events': return loadEvents;
1737
- default: return undefined;
1738
- }
1739
- }
1740
- const paneLoaded = new Set();
1741
-
1742
- function ensurePaneLoaded(key) {
1743
- if (paneLoaded.has(key)) return;
1744
- const loader = paneLoader(key);
1745
- if (!loader) return;
1746
- paneLoaded.add(key);
1747
- loader();
1748
- }
1749
-
1750
- function selectPane(scope, key) {
1751
- const nav = scope === 'global' ? railNavGlobal : railNavConversation;
1752
- const panel = document.getElementById(scope === 'global' ? 'panel-global' : 'panel-conversation');
1753
- nav.querySelectorAll('.rail-link').forEach((btn) => {
1754
- const active = btn.dataset.pane === key;
1755
- btn.classList.toggle('active', active);
1756
- btn.setAttribute('aria-current', active ? 'page' : 'false');
1757
- });
1758
- panel.querySelectorAll('.pane').forEach((pane) => {
1759
- pane.classList.toggle('active', pane.dataset.pane === key);
1760
- });
1761
- ensurePaneLoaded(key);
1762
- }
1763
-
1764
- function initRailNav() {
1765
- document.querySelectorAll('.rail-link').forEach((btn) => {
1766
- btn.addEventListener('click', () => {
1767
- const nav = btn.closest('.rail-nav');
1768
- const scope = nav.dataset.scope;
1769
- selectPane(scope, btn.dataset.pane);
1770
- });
1771
- });
1772
- // The first rail link in each scope starts active in the markup, so its
1773
- // pane needs the matching 'active' class and its data loaded up front.
1774
- selectPane('conversation', 'settings');
1775
- }
1776
-
1777
- // ── Conversation switcher ───────────────────────────────────────────────────
1778
-
1779
- async function initConvSwitcher() {
1780
- const sel = document.getElementById('conv-switcher');
1781
- try {
1782
- const data = await apiGet('/admin/api/conversations');
1783
- sel.innerHTML = data.conversations.map((c) => {
1784
- const key = c.platform + ':' + c.conversationId;
1785
- const label = (c.label || c.conversationId) + (c.running ? ' (running)' : '');
1786
- const selected = key === defaultConversationKey ? ' selected' : '';
1787
- return '<option value="' + escAttr(key) + '"' + selected + '>' + escHtml(label) + '</option>';
1788
- }).join('');
1789
- sel.addEventListener('change', () => setActiveConversation(sel.value));
1790
- } catch (err) {
1791
- // ignore; conversation selector stays empty
1792
- }
1793
- }
1794
-
1795
- function setActiveConversation(key) {
1796
- activeConversationKey = key;
1797
- const sel = document.getElementById('conv-switcher');
1798
- if (sel && sel.value !== key) sel.value = key;
1799
- // Data is scoped to the previous conversation; drop the conversation
1800
- // panes (not the global ones — those aren't conversation-scoped) so
1801
- // the active pane refetches for the new scope.
1802
- const activeLink = railNavConversation.querySelector('.rail-link.active');
1803
- ['settings', 'workspace', 'skills', 'mcp', 'events'].forEach((key) => paneLoaded.delete(key));
1804
- if (activeLink) ensurePaneLoaded(activeLink.dataset.pane);
1805
- openLogin(true);
1806
- openSessionView(true);
1807
- }
1808
-
1809
- // ── Settings ─────────────────────────────────────────────────────────────────
1810
-
1811
- const loadSettings = () => loadSettingsPanel('settings-content', () => '/admin/api/conversation-state?' + scopeQuery(), renderSettings);
1812
- const loadGlobalSettings = () => loadSettingsPanel('global-settings-content', () => '/admin/api/settings/global', renderGlobalSettings);
1813
-
1814
- async function loadSettingsPanel(id, path, render) {
1815
- const container = document.getElementById(id);
1816
- container.innerHTML = '<div class="loading-msg">Loading…</div>';
1817
- if (!modelsLoaded) await loadModels();
1818
- try {
1819
- const data = await apiGet(path());
1820
- container.innerHTML = render(data);
1821
- } catch (err) {
1822
- container.innerHTML = '<div class="err-msg">' + escHtml(err.message) + '</div>';
1823
- }
1824
- }
1825
-
1826
- function renderConfigCard(title, content, action, buttonLabel, resultId) {
1827
- return '<div class="config-block"><h3 class="card-subtitle">' + title + '</h3>' + content +
1828
- '<button class="primary-action-btn" onclick="' + action + '(this)">' + buttonLabel + '</button>' +
1829
- '<div id="' + resultId + '" class="inline-result" style="display:none"></div></div>';
1830
- }
1831
-
1832
- function renderOptions(choices, selected) {
1833
- return choices.map(([value, label]) =>
1834
- '<option value="' + escAttr(value) + '"' + (selected === value ? ' selected' : '') + '>' + escHtml(label) + '</option>'
1835
- ).join('');
1836
- }
1837
- function renderThinkingOptions(value) {
1838
- return renderOptions(['off','minimal','low','medium','high','xhigh','max'].map((t) => [t, t]), value);
1839
- }
1840
- function renderReplyOptions(slack) {
1841
- return renderOptions(['top-level','thread'].map((m) => [m, m]), (slack && slack.replyMode) || 'top-level');
1842
- }
1843
- function renderSettings(data) {
1844
- const thinkingOpts = renderThinkingOptions(data.thinkingLevel);
1845
- const replyModeOpts = renderReplyOptions(data.slack);
1846
- const globalReplyMode = (data.slack && data.slack.globalReplyMode) || 'top-level';
1847
- const globalModel = [data.globalProvider, data.globalModel].filter(Boolean).join('/');
1848
- const globalModelLabel = globalModel + (data.globalThinkingLevel ? ':' + data.globalThinkingLevel : '');
1849
- return [
1850
- '<div class="config-grid">',
1851
- renderConfigCard('Model', [
1852
- '<div class="config-row config-row-stack"><label>Model</label><select id="m-model-ref">' + renderModelOptions(data.provider, data.model) + '</select></div>',
1853
- '<div class="config-row"><label>Thinking</label><select id="m-thinking">' + thinkingOpts + '</select></div>',
1854
- '<p class="muted-note">Global default: ' + escHtml(globalModelLabel) + '</p>',
1855
- ].join(''), 'saveModel', 'Save model', 'model-save-result'),
1856
-
1857
- renderVisibilityCard(data),
1858
- renderConfigCard('Slack', [
1859
- '<div class="config-row"><label>Reply mode</label><select id="m-slack-reply-mode">' + replyModeOpts + '</select></div>',
1860
- '<p class="muted-note">Global default: ' + escHtml(globalReplyMode) + '</p>',
1861
- ].join(''), 'saveSlack', 'Save Slack', 'slack-save-result'),
1862
- '</div>',
1863
- ].join('');
1864
- }
1865
-
1866
- async function saveModel(btn) {
1867
- const selectedModel = parseModelRef(document.getElementById('m-model-ref').value.trim());
1868
- const provider = selectedModel.provider;
1869
- const model = selectedModel.model;
1870
- const thinkingLevel = document.getElementById('m-thinking').value;
1871
- const result = document.getElementById('model-save-result');
1872
- if (!provider || !model) {
1873
- result.style.display = 'block'; result.className = 'inline-result err';
1874
- result.textContent = 'Provider and model are required';
1875
- return;
1876
- }
1877
- await saveConversationSetting(btn, result, 'model', { provider, model, thinkingLevel }, 'Save model');
1878
- }
1879
-
1880
- async function saveSlack(btn) {
1881
- const replyMode = document.getElementById('m-slack-reply-mode').value;
1882
- const result = document.getElementById('slack-save-result');
1883
- await saveConversationSetting(btn, result, 'slack', { replyMode }, 'Save Slack');
1884
- }
1885
-
1886
- // Only a public Slack channel has a choice to make; private channels and
1887
- // DMs are always hidden, so the card just states that without a control.
1888
- function renderVisibilityCard(data) {
1889
- const hidden = data.officeVisibilityOverride === 'private';
1890
- const canChoose = data.officeVisibilitySource === 'platform' ? data.officeVisibility === 'public' : hidden;
1891
- if (!canChoose) {
1892
- const why = data.officeVisibilitySource === 'platform'
1893
- ? 'This is a DM, a private channel, or a conversation on a platform other than Slack, so its files are hidden from every other office. The platform decides this; it cannot be opened up here.'
1894
- : 'Slack has not reported what kind of conversation this is yet, so it is treated as hidden until it does.';
1895
- return '<div class="config-block"><h3 class="card-subtitle">Who can see the files in this office</h3>' +
1896
- '<p><strong>Hidden</strong> — only this office.</p><p class="muted-note">' + why + '</p></div>';
1897
- }
1898
- return renderConfigCard('Who can see the files in this office', [
1899
- '<div class="config-row"><label><input type="checkbox" id="m-visibility"' + (hidden ? ' checked' : '') + '> Hide the files in this channel from other offices</label></div>',
1900
- '<p>' + (hidden
1901
- ? '<strong>Hidden</strong> — other offices cannot read the files in this channel, and it can read shared MEMORY.md and skills but not change them.'
1902
- : '<strong>Shared</strong> — every other office can read the files in this channel (read-only), and this channel may update shared MEMORY.md and skills.') + '</p>',
1903
- '<p class="muted-note">Slack says this is a public channel, which is why sharing is the default. Hiding is the only change allowed; nothing can be made more visible than Slack allows.</p>',
1904
- ].join(''), 'saveVisibility', 'Save', 'mount-save-result');
1905
- }
1906
-
1907
- async function saveVisibility(btn) {
1908
- const visibility = document.getElementById('m-visibility').checked ? 'private' : 'default';
1909
- const result = document.getElementById('mount-save-result');
1910
- await saveConversationSetting(btn, result, 'visibility', { visibility }, 'Save visibility', loadSettings);
1911
- }
1912
-
1913
- function saveConversationSetting(btn, result, setting, values, label, onSaved) {
1914
- return saveSetting(btn, result, 'conversations/' + setting, { ...scopeBody(), ...values }, label, onSaved);
1915
- }
1916
-
1917
- async function saveSetting(btn, result, path, values, label, onSaved) {
1918
- btn.disabled = true; btn.textContent = 'Saving…'; result.style.display = 'none';
1919
- try {
1920
- await apiPost('/admin/api/' + path, values);
1921
- result.style.display = 'block'; result.className = 'inline-result ok'; result.textContent = 'Saved ✓';
1922
- if (onSaved) onSaved();
1923
- } catch (err) {
1924
- result.style.display = 'block'; result.className = 'inline-result err'; result.textContent = err.message;
1925
- } finally {
1926
- btn.disabled = false; btn.textContent = label;
1927
- }
1928
- }
1929
-
1930
- // ── Workspace ────────────────────────────────────────────────────────────────
1931
-
1932
- async function loadWorkspace() {
1933
- const treeEl = document.getElementById('workspace-tree');
1934
- const previewEl = document.getElementById('workspace-preview');
1935
- treeEl.innerHTML = '<div class="loading-msg">Loading…</div>';
1936
- previewEl.innerHTML = '<div class="placeholder-msg">Click a file to preview</div>';
1937
- try {
1938
- const data = await apiGet('/admin/api/workspace/tree?' + scopeQuery());
1939
- if (!data.tree) {
1940
- treeEl.innerHTML = '<div class="empty-state">No files</div>';
1941
- return;
1942
- }
1943
- treeEl.innerHTML = '<ul class="tree-root">' + renderTreeChildren(data.tree) + '</ul>';
1944
- } catch (err) {
1945
- treeEl.innerHTML = '<div class="err-msg">' + escHtml(err.message) + '</div>';
1946
- }
1947
- }
1948
-
1949
- function renderTreeChildren(node) {
1950
- if (node.type === 'file') {
1951
- return '<li><button class="tree-file" data-admin-action="preview-file" data-file-path="' + escAttr(node.path) + '">' + escHtml(node.name) + '</button></li>';
1952
- }
1953
- if (!node.children || node.children.length === 0) {
1954
- return '<li><span class="tree-dir empty">' + escHtml(node.name || '.') + '/</span></li>';
1955
- }
1956
- const inner = node.children.map((c) =>
1957
- c.type === 'file'
1958
- ? '<li><button class="tree-file" data-admin-action="preview-file" data-file-path="' + escAttr(c.path) + '">' + escHtml(c.name) + '</button></li>'
1959
- : '<li><details open><summary class="tree-dir">' + escHtml(c.name) + '/</summary><ul>' + renderTreeChildren(c) + '</ul></details></li>'
1960
- ).join('');
1961
- return inner;
1962
- }
1963
-
1964
- function renderPreviewFileResult(previewEl, label, data) {
1965
- if (data.binary) {
1966
- previewEl.innerHTML = '<div class="preview-meta">' + escHtml(label) + ' · ' + data.size + ' bytes · binary</div><div class="placeholder-msg">Binary file — preview not available</div>';
1967
- return;
1968
- }
1969
- previewEl.innerHTML =
1970
- '<div class="preview-meta">' + escHtml(label) + ' · ' + data.size + ' bytes</div>' +
1971
- '<pre class="preview-body">' + escHtml(data.content || '') + '</pre>';
1972
- }
1973
-
1974
- async function previewFile(path) {
1975
- const previewEl = document.getElementById('workspace-preview');
1976
- previewEl.innerHTML = '<div class="loading-msg">Loading ' + escHtml(path) + '…</div>';
1977
- try {
1978
- const data = await apiGet('/admin/api/workspace/file?' + scopeQuery() + '&path=' + encodeURIComponent(path));
1979
- renderPreviewFileResult(previewEl, path, data);
1980
- } catch (err) {
1981
- previewEl.innerHTML = '<div class="err-msg">' + escHtml(err.message) + '</div>';
1982
- }
1983
- }
1984
-
1985
- // ── Skills ───────────────────────────────────────────────────────────────────
1986
-
1987
- async function loadScopePanels(prefix, endpoint, render) {
1988
- const convEl = document.getElementById(prefix + '-conv-content');
1989
- const globalEl = document.getElementById(prefix + '-global-content');
1990
- if (convEl) convEl.innerHTML = '<div class="loading-msg">Loading…</div>';
1991
- if (globalEl) globalEl.innerHTML = '<div class="loading-msg">Loading…</div>';
1992
- try {
1993
- const data = await apiGet('/admin/api/' + endpoint + '?' + scopeQuery());
1994
- render(data, convEl, globalEl);
1995
- } catch (err) {
1996
- if (convEl) convEl.innerHTML = '<div class="err-msg">' + escHtml(err.message) + '</div>';
1997
- if (globalEl) globalEl.innerHTML = '<div class="err-msg">' + escHtml(err.message) + '</div>';
1998
- }
1999
- }
2000
-
2001
- // ── MCP servers ───────────────────────────────────────────────────────────────────────
2002
-
2003
- function mcpMessage(scope, text, kind) {
2004
- const el = document.getElementById(scope === 'global' ? 'mcp-global-msg' : 'mcp-conv-msg');
2005
- if (!el) return;
2006
- el.style.display = 'block';
2007
- el.className = 'status-msg status-msg-' + kind;
2008
- el.textContent = text;
2009
- }
2010
-
2011
- function mcpTransport(server) {
2012
- return server.command
2013
- ? server.command + (server.args && server.args.length ? ' ' + server.args.join(' ') : '')
2014
- : (server.url || '');
2015
- }
2016
-
2017
- function renderMcpServer(scope, name, server) {
2018
- const transport = (server.command ? 'stdio: ' : 'http: ') + mcpTransport(server);
2019
- const keys = [];
2020
- if (server.envKeys && server.envKeys.length) keys.push('env: ' + server.envKeys.join(', '));
2021
- if (server.headerKeys && server.headerKeys.length) keys.push('headers: ' + server.headerKeys.join(', '));
2022
- return '<article class="mcp-preset mcp-installed-card">' +
2023
- '<div class="mcp-preset-top">' +
2024
- '<span class="mcp-preset-category">' + (server.command ? 'STDIO' : 'HTTP') + '</span>' +
2025
- (server.disabled ? '<span class="mcp-badge mcp-badge-warn">disabled</span>' : '<span class="mcp-badge mcp-badge-ok">enabled</span>') +
2026
- '</div>' +
2027
- '<h3>' + escHtml(name) + '</h3>' +
2028
- '<p class="mcp-preset-meta mcp-installed-transport">' + escHtml(transport) + '</p>' +
2029
- (keys.length ? '<div class="mcp-preset-meta">' + escHtml(keys.join(' · ')) + '</div>' : '') +
2030
- '<div class="mcp-preset-actions">' +
2031
- '<button class="mcp-btn" data-mcp-action="test" data-mcp-scope="' + scope + '" data-mcp-name="' + escAttr(name) + '">Test</button>' +
2032
- '<button class="mcp-btn" data-mcp-action="toggle" data-mcp-scope="' + scope + '" data-mcp-name="' + escAttr(name) + '">' + (server.disabled ? 'Enable' : 'Disable') + '</button>' +
2033
- '<button class="mcp-btn mcp-btn-danger" data-mcp-action="remove" data-mcp-scope="' + scope + '" data-mcp-name="' + escAttr(name) + '">Remove</button>' +
2034
- '</div>' +
2035
- '</article>';
2036
- }
2037
-
2038
- function renderMcpPreset(scope, preset, servers) {
2039
- const installed = Boolean(servers[preset.serverName]);
2040
- const transport = preset.server.command ? 'Runs on host' : 'Remote service';
2041
- return '<article class="mcp-preset">' +
2042
- '<div class="mcp-preset-top">' +
2043
- '<span class="mcp-preset-category">' + escHtml(preset.category) + '</span>' +
2044
- (installed ? '<span class="mcp-badge mcp-badge-ok">Installed here</span>' : '') +
2045
- '</div>' +
2046
- '<h3>' + escHtml(preset.name) + '</h3>' +
2047
- '<p>' + escHtml(preset.description) + '</p>' +
2048
- '<div class="mcp-preset-meta">' + escHtml(transport) + ' · <code>' + escHtml(preset.serverName) + '</code></div>' +
2049
- '<div class="mcp-preset-actions">' +
2050
- '<a href="' + escAttr(preset.sourceUrl) + '" target="_blank" rel="noopener">Source ↗</a>' +
2051
- '<button class="primary-action-btn" data-mcp-action="preset" data-mcp-scope="' + scope + '" data-mcp-preset="' + escAttr(preset.id) + '">' + (installed ? 'Review' : 'Install') + '</button>' +
2052
- '</div>' +
2053
- '</article>';
2054
- }
2055
-
2056
- function renderMcpScope(el, scope, servers) {
2057
- const presets = mcpPresets.map((preset) => renderMcpPreset(scope, preset, servers)).join('');
2058
- const rows = Object.entries(servers).map(([name, server]) => renderMcpServer(scope, name, server)).join('');
2059
- el.innerHTML =
2060
- '<div class="mcp-market-head"><div><h3>Explore presets</h3><p>Reviewed recipes that install into this scope.</p></div><span>' + mcpPresets.length + ' available</span></div>' +
2061
- '<div class="mcp-preset-grid">' + presets + '</div>' +
2062
- '<details class="mcp-installed" open><summary>Installed in this scope</summary>' +
2063
- '<div class="mcp-preset-grid">' +
2064
- rows +
2065
- '<button type="button" class="mcp-add-card" data-mcp-action="open-custom" data-mcp-scope="' + scope + '">' +
2066
- '<span class="mcp-add-card-plus">+</span><span>新增 server</span>' +
2067
- '</button>' +
2068
- '</div>' +
2069
- '</details>';
2070
- }
2071
-
2072
- function renderMcpGuidedForm(scope) {
2073
- return '<div class="mcp-guided-grid">' +
2074
- '<label class="mcp-field"><span>Server 名稱</span><input id="mcp-' + scope + '-g-name" class="form-input mcp-json" placeholder="例如 github" autocomplete="off" /></label>' +
2075
- '<div class="mcp-transport-toggle">' +
2076
- '<label><input type="radio" name="mcp-' + scope + '-g-transport" value="stdio" checked /> 本機指令 (stdio)</label>' +
2077
- '<label><input type="radio" name="mcp-' + scope + '-g-transport" value="http" /> 遠端服務 (HTTP)</label>' +
2078
- '</div>' +
2079
- '<div id="mcp-' + scope + '-g-stdio" class="mcp-transport-fields">' +
2080
- '<label class="mcp-field"><span>指令</span><input id="mcp-' + scope + '-g-command" class="form-input mcp-json" placeholder="npx" autocomplete="off" /></label>' +
2081
- '<label class="mcp-field"><span>參數(以逗號分隔)</span><input id="mcp-' + scope + '-g-args" class="form-input mcp-json" placeholder="-y, @modelcontextprotocol/server-github" autocomplete="off" /></label>' +
2082
- '</div>' +
2083
- '<div id="mcp-' + scope + '-g-http" class="mcp-transport-fields" style="display:none">' +
2084
- '<label class="mcp-field"><span>URL</span><input id="mcp-' + scope + '-g-url" class="form-input mcp-json" placeholder="https://mcp.example.com/mcp" autocomplete="off" /></label>' +
2085
- '</div>' +
2086
- '<div class="mcp-kv-block">' +
2087
- '<div class="mcp-kv-head"><span id="mcp-' + scope + '-g-kv-label">環境變數 (env)</span><button type="button" class="mcp-btn" data-mcp-kv-add="' + scope + '">+ 新增一列</button></div>' +
2088
- '<div id="mcp-' + scope + '-g-kv" class="mcp-kv-rows"></div>' +
2089
- '</div>' +
2090
- '</div>';
2091
- }
2092
-
2093
- function renderMcpKvRow() {
2094
- return '<div class="mcp-kv-row">' +
2095
- '<input class="form-input mcp-json" placeholder="KEY" data-kv-key autocomplete="off" />' +
2096
- '<input class="form-input mcp-json" type="password" placeholder="value" data-kv-value autocomplete="off" />' +
2097
- '<button type="button" class="mcp-btn mcp-btn-danger" data-mcp-kv-remove>移除</button>' +
2098
- '</div>';
2099
- }
2100
-
2101
- function addMcpGuidedRow(scope) {
2102
- const container = document.getElementById('mcp-' + scope + '-g-kv');
2103
- if (container) container.insertAdjacentHTML('beforeend', renderMcpKvRow());
2104
- }
2105
-
2106
- function updateMcpGuidedTransport(scope) {
2107
- const checked = document.querySelector('input[name="mcp-' + scope + '-g-transport"]:checked');
2108
- const transport = checked ? checked.value : 'stdio';
2109
- const stdioEl = document.getElementById('mcp-' + scope + '-g-stdio');
2110
- const httpEl = document.getElementById('mcp-' + scope + '-g-http');
2111
- const kvLabel = document.getElementById('mcp-' + scope + '-g-kv-label');
2112
- if (stdioEl) stdioEl.style.display = transport === 'stdio' ? '' : 'none';
2113
- if (httpEl) httpEl.style.display = transport === 'http' ? '' : 'none';
2114
- if (kvLabel) kvLabel.textContent = transport === 'stdio' ? '環境變數 (env)' : 'HTTP Headers';
2115
- }
2116
-
2117
- let pendingMcpCustomScope = null;
2118
-
2119
- function openMcpCustomDialog(scope) {
2120
- pendingMcpCustomScope = scope;
2121
- const content = document.getElementById('mcp-custom-dialog-content');
2122
- content.innerHTML =
2123
- '<div class="mcp-add-mode">' +
2124
- '<button type="button" class="mcp-mode-btn active" data-mcp-mode="guided">引導式表單</button>' +
2125
- '<button type="button" class="mcp-mode-btn" data-mcp-mode="json">貼上 JSON</button>' +
2126
- '</div>' +
2127
- '<div id="mcp-custom-guided-panel" class="mcp-panel">' + renderMcpGuidedForm('custom') + '</div>' +
2128
- '<div id="mcp-custom-json-panel" class="mcp-panel" style="display:none">' +
2129
- '<p class="mcp-manual-hint">貼上 MCP server 文件給的 <code>mcpServers</code> JSON,原樣保存到這個 scope。remote 走 Streamable HTTP,local 走 stdio。</p>' +
2130
- '<textarea id="mcp-custom-json" class="form-input mcp-json" spellcheck="false" rows="9" placeholder="' + escAttr(MCP_JSON_PLACEHOLDER) + '"></textarea>' +
2131
- '</div>';
2132
- const dialogError = document.getElementById('mcp-custom-dialog-error');
2133
- dialogError.style.display = 'none';
2134
- dialogError.textContent = '';
2135
- addMcpGuidedRow('custom');
2136
- document.getElementById('mcp-custom-dialog').showModal();
2137
- }
2138
-
2139
- function closeMcpCustomDialog() {
2140
- pendingMcpCustomScope = null;
2141
- document.getElementById('mcp-custom-dialog').close();
2142
- }
2143
-
2144
- function switchMcpCustomMode(mode) {
2145
- const guidedEl = document.getElementById('mcp-custom-guided-panel');
2146
- const jsonEl = document.getElementById('mcp-custom-json-panel');
2147
- if (guidedEl) guidedEl.style.display = mode === 'guided' ? '' : 'none';
2148
- if (jsonEl) jsonEl.style.display = mode === 'json' ? '' : 'none';
2149
- document.querySelectorAll('#mcp-custom-dialog-content [data-mcp-mode]').forEach((b) => {
2150
- b.classList.toggle('active', b.dataset.mcpMode === mode);
2151
- });
2152
- }
2153
-
2154
- async function submitMcpCustomDialog(btn) {
2155
- const scope = pendingMcpCustomScope;
2156
- if (!scope) return;
2157
- const dialogError = document.getElementById('mcp-custom-dialog-error');
2158
- dialogError.style.display = 'none';
2159
- dialogError.textContent = '';
2160
- const jsonMode = document.getElementById('mcp-custom-json-panel').style.display !== 'none';
2161
- let json;
2162
- if (jsonMode) {
2163
- const raw = document.getElementById('mcp-custom-json').value.trim();
2164
- if (!raw) { dialogError.textContent = '請貼上 mcpServers JSON'; dialogError.style.display = 'block'; return; }
2165
- json = raw;
2166
- } else {
2167
- const nameEl = document.getElementById('mcp-custom-g-name');
2168
- const name = nameEl ? nameEl.value.trim() : '';
2169
- if (!name) { dialogError.textContent = '請輸入 server 名稱'; dialogError.style.display = 'block'; return; }
2170
- const checked = document.querySelector('input[name="mcp-custom-g-transport"]:checked');
2171
- const transport = checked ? checked.value : 'stdio';
2172
- const kv = {};
2173
- document.querySelectorAll('#mcp-custom-g-kv .mcp-kv-row').forEach((row) => {
2174
- const keyInput = row.querySelector('[data-kv-key]');
2175
- const valueInput = row.querySelector('[data-kv-value]');
2176
- const key = keyInput ? keyInput.value.trim() : '';
2177
- if (key) kv[key] = valueInput ? valueInput.value : '';
2178
- });
2179
- let entry;
2180
- if (transport === 'stdio') {
2181
- const commandEl = document.getElementById('mcp-custom-g-command');
2182
- const command = commandEl ? commandEl.value.trim() : '';
2183
- if (!command) { dialogError.textContent = '請輸入指令'; dialogError.style.display = 'block'; return; }
2184
- const argsEl = document.getElementById('mcp-custom-g-args');
2185
- const argsRaw = argsEl ? argsEl.value.trim() : '';
2186
- const args = argsRaw ? argsRaw.split(',').map((s) => s.trim()).filter(Boolean) : [];
2187
- entry = Object.assign({ command: command }, args.length ? { args: args } : {}, Object.keys(kv).length ? { env: kv } : {});
2188
- } else {
2189
- const urlEl = document.getElementById('mcp-custom-g-url');
2190
- const url = urlEl ? urlEl.value.trim() : '';
2191
- if (!url) { dialogError.textContent = '請輸入 URL'; dialogError.style.display = 'block'; return; }
2192
- entry = Object.assign({ url: url }, Object.keys(kv).length ? { headers: kv } : {});
2193
- }
2194
- json = JSON.stringify({ mcpServers: { [name]: entry } });
2195
- }
2196
- btn.disabled = true;
2197
- btn.textContent = '儲存中…';
2198
- try {
2199
- const data = await apiPost('/admin/api/mcp-servers/mutate', {
2200
- action: 'import', scope: scope, json: json, ...scopeBody(),
2201
- });
2202
- const results = Array.isArray(data.results) ? data.results : [];
2203
- const failed = results.filter((r) => r.error).length;
2204
- closeMcpCustomDialog();
2205
- mcpMessage(scope, failed ? '已儲存,但 ' + failed + ' 個 server 連線失敗,見上方選項維護。' : '已儲存並連線成功。', failed ? 'err' : 'ok');
2206
- await loadMcpServers();
2207
- } catch (err) {
2208
- dialogError.textContent = err.message;
2209
- dialogError.style.display = 'block';
2210
- } finally {
2211
- btn.disabled = false;
2212
- btn.textContent = '新增並測試連線';
2213
- }
2214
- }
2215
-
2216
- const MCP_JSON_PLACEHOLDER = JSON.stringify({
2217
- mcpServers: {
2218
- browserless: {
2219
- type: 'http',
2220
- url: 'https://mcp.browserless.io/mcp',
2221
- headers: { Authorization: 'Bearer YOUR_API_TOKEN' },
2222
- },
2223
- },
2224
- }, null, 2);
2225
-
2226
- const loadMcpServers = () => loadScopePanels('mcp', 'mcp-servers', (data, convEl, globalEl) => {
2227
- mcpPresets = Array.isArray(data.presets) ? data.presets : [];
2228
- mcpServersByScope = { conversation: data.conversation || {}, global: data.global || {} };
2229
- if (convEl) renderMcpScope(convEl, 'conversation', mcpServersByScope.conversation);
2230
- if (globalEl) renderMcpScope(globalEl, 'global', mcpServersByScope.global);
2231
- });
2232
-
2233
- async function mutateMcpServer(scope, action, name, extra) {
2234
- mcpMessage(scope, action === 'test' ? '連線測試中…' : '儲存並測試連線中…', 'busy');
2235
- try {
2236
- const data = await apiPost('/admin/api/mcp-servers/mutate', {
2237
- action: action,
2238
- scope: scope,
2239
- ...(name ? { name: name } : {}),
2240
- ...(extra || {}),
2241
- ...scopeBody(),
2242
- });
2243
- const results = Array.isArray(data.results) ? data.results : [];
2244
- const failed = results.filter((r) => r.error).length;
2245
- if (action === 'test') {
2246
- mcpMessage(scope, failed ? '✗ ' + name + ': ' + results[0].error : '✓ ' + name + ': ' + results[0].tools + ' tool(s)', failed ? 'err' : 'ok');
2247
- return;
2248
- }
2249
- if (action === 'remove' || action === 'toggle') {
2250
- mcpMessage(scope, '完成。新設定在下一次回應生效。', 'ok');
2251
- } else {
2252
- mcpMessage(scope, failed ? '已儲存,但 ' + failed + ' 個 server 連線失敗,見下方錯誤。' : '已儲存並連線成功。', failed ? 'err' : 'ok');
2253
- }
2254
- await loadMcpServers();
2255
- } catch (err) {
2256
- mcpMessage(scope, err.message, 'err');
2257
- }
2258
- }
2259
-
2260
- function openMcpPreset(scope, presetId) {
2261
- const preset = mcpPresets.find((item) => item.id === presetId);
2262
- if (!preset) return;
2263
- pendingMcpInstall = { scope: scope, preset: preset };
2264
- const local = Boolean(preset.server.command);
2265
- const installed = Boolean(mcpServersByScope[scope][preset.serverName]);
2266
- const credentials = preset.credentials.map((credential, index) =>
2267
- '<label class="mcp-credential"><span>' + escHtml(credential.label) + (credential.required ? ' *' : '') + '</span>' +
2268
- '<input data-mcp-credential="' + index + '" type="' + (credential.secret ? 'password' : 'text') + '" autocomplete="off" />' +
2269
- '<small>' + escHtml(credential.description) + '</small></label>'
2270
- ).join('');
2271
- document.getElementById('mcp-dialog-title').textContent = preset.name;
2272
- const dialogError = document.getElementById('mcp-dialog-error');
2273
- dialogError.style.display = 'none';
2274
- dialogError.textContent = '';
2275
- document.getElementById('mcp-dialog-content').innerHTML =
2276
- '<p class="mcp-dialog-desc">' + escHtml(preset.description) + '</p>' +
2277
- '<div class="mcp-install-preview"><span>' + (local ? 'Host command' : 'Remote endpoint') + '</span><code>' + escHtml(mcpTransport(preset.server)) + '</code></div>' +
2278
- '<div class="mcp-security-note ' + (local ? 'local' : 'remote') + '">' +
2279
- (local
2280
- ? '<strong>Host code execution.</strong> This command runs outside the conversation sandbox with the mikan process user permissions.'
2281
- : '<strong>External service.</strong> Tool calls and selected data will be sent to this remote origin.') +
2282
- '</div>' +
2283
- (installed ? '<div class="mcp-replace-note">Installing again replaces the existing <code>' + escHtml(preset.serverName) + '</code> entry in this scope.</div>' : '') +
2284
- (credentials || '<p class="mcp-no-credentials">No credentials required.</p>') +
2285
- '<p class="mcp-secret-note">These values are stored in host-private settings and are not shown to the model or sandbox.</p>' +
2286
- '<a class="mcp-setup-link" href="' + escAttr(preset.setupUrl) + '" target="_blank" rel="noopener">Setup documentation ↗</a>';
2287
- document.getElementById('mcp-install-dialog').showModal();
2288
- }
2289
-
2290
- function closeMcpInstall() {
2291
- pendingMcpInstall = null;
2292
- document.getElementById('mcp-install-dialog').close();
2293
- }
2294
-
2295
- async function installMcpPreset(btn) {
2296
- if (!pendingMcpInstall) return;
2297
- const { scope, preset } = pendingMcpInstall;
2298
- const credentials = {};
2299
- document.querySelectorAll('[data-mcp-credential]').forEach((input) => {
2300
- const descriptor = preset.credentials[Number(input.dataset.mcpCredential)];
2301
- if (descriptor) credentials[descriptor.key] = input.value;
2302
- });
2303
- btn.disabled = true;
2304
- btn.textContent = 'Installing…';
2305
- try {
2306
- const data = await apiPost('/admin/api/mcp-servers/mutate', {
2307
- action: 'install', scope: scope, presetId: preset.id, credentials: credentials, ...scopeBody(),
2308
- });
2309
- closeMcpInstall();
2310
- const result = (Array.isArray(data.results) ? data.results : [])[0];
2311
- if (result && result.error) mcpMessage(scope, '✗ ' + preset.name + ' saved but failed to connect: ' + result.error, 'err');
2312
- else mcpMessage(scope, '✓ ' + preset.name + ' installed, ' + (result ? result.tools : 0) + ' tool(s).', 'ok');
2313
- await loadMcpServers();
2314
- } catch (err) {
2315
- const dialogError = document.getElementById('mcp-dialog-error');
2316
- dialogError.textContent = err.message;
2317
- dialogError.style.display = 'block';
2318
- } finally {
2319
- btn.disabled = false;
2320
- btn.textContent = 'Install preset';
2321
- }
2322
- }
2323
-
2324
- document.addEventListener('click', (event) => {
2325
- const modeBtn = event.target.closest('#mcp-custom-dialog-content [data-mcp-mode]');
2326
- if (modeBtn) { switchMcpCustomMode(modeBtn.dataset.mcpMode); return; }
2327
- const kvAddBtn = event.target.closest('[data-mcp-kv-add]');
2328
- if (kvAddBtn) { addMcpGuidedRow(kvAddBtn.dataset.mcpKvAdd); return; }
2329
- const kvRemoveBtn = event.target.closest('[data-mcp-kv-remove]');
2330
- if (kvRemoveBtn) { kvRemoveBtn.closest('.mcp-kv-row').remove(); return; }
2331
- const transportRadio = event.target.closest('input[name^="mcp-"][name$="-g-transport"]');
2332
- if (transportRadio) {
2333
- const match = /^mcp-(.+)-g-transport$/.exec(transportRadio.name);
2334
- if (match) updateMcpGuidedTransport(match[1]);
2335
- return;
2336
- }
2337
- const btn = event.target.closest('[data-mcp-action]');
2338
- if (!btn) return;
2339
- if (btn.dataset.mcpAction === 'open-custom') { openMcpCustomDialog(btn.dataset.mcpScope); return; }
2340
- if (btn.dataset.mcpAction === 'preset') { openMcpPreset(btn.dataset.mcpScope, btn.dataset.mcpPreset); return; }
2341
- void mutateMcpServer(btn.dataset.mcpScope, btn.dataset.mcpAction, btn.dataset.mcpName);
2342
- });
2343
-
2344
- let skillsCache = [];
2345
-
2346
- function renderSkillsInto(containerId, skills, allowedSource) {
2347
- const container = document.getElementById(containerId);
2348
- if (!container) return;
2349
- const filtered = allowedSource ? skills.filter((s) => s.source === allowedSource) : skills;
2350
- if (filtered.length === 0) {
2351
- container.innerHTML = '<div class="empty-state">No skills available</div>';
2352
- return;
2353
- }
2354
- container.innerHTML = '<div class="skills-list">' +
2355
- filtered.map((s) =>
2356
- '<div class="skill-row">' +
2357
- '<button class="skill-row-btn" data-skill-source="' + escAttr(s.source) + '" data-skill-directory="' + escAttr(s.directory) + '" data-skill-name="' + escAttr(s.name) + '">' +
2358
- '<div class="skill-name">' + escHtml(s.name) + '<span class="skill-source skill-source-' + s.source + '">' + s.source + '</span></div>' +
2359
- (s.description ? '<div class="skill-desc">' + escHtml(s.description) + '</div>' : '') +
2360
- '</button>' +
2361
- '<div class="skill-row-actions">' +
2362
- '<button class="mcp-btn" data-skill-edit-source="' + escAttr(s.source) + '" data-skill-edit-directory="' + escAttr(s.directory) + '">Edit</button>' +
2363
- '<button class="mcp-btn mcp-btn-danger" data-skill-delete-source="' + escAttr(s.source) + '" data-skill-delete-directory="' + escAttr(s.directory) + '" data-skill-delete-name="' + escAttr(s.name) + '">Delete</button>' +
2364
- '</div>' +
2365
- '</div>'
2366
- ).join('') + '</div>';
2367
- }
2368
-
2369
- async function loadSkills() {
2370
- const container = document.getElementById('skills-content');
2371
- const previewEl = document.getElementById('skills-preview');
2372
- container.innerHTML = '<div class="loading-msg">Loading…</div>';
2373
- if (previewEl) previewEl.innerHTML = '<div class="placeholder-msg">Click a skill to preview SKILL.md</div>';
2374
- try {
2375
- const data = await apiGet('/admin/api/skills?' + scopeQuery());
2376
- skillsCache = Array.isArray(data.skills) ? data.skills : [];
2377
- renderSkillsInto('skills-content', skillsCache);
2378
- renderSkillsInto('global-skills-content', skillsCache, 'global');
2379
- } catch (err) {
2380
- container.innerHTML = '<div class="err-msg">' + escHtml(err.message) + '</div>';
2381
- }
2382
- }
2383
-
2384
- const loadGlobalSkills = loadSkills;
2385
-
2386
- async function previewSkillInto(previewId, source, directory, name) {
2387
- const previewEl = document.getElementById(previewId);
2388
- if (!previewEl) return;
2389
- if (!source || !directory) {
2390
- previewEl.innerHTML = '<div class="err-msg">Missing skill source or directory</div>';
2391
- return;
2392
- }
2393
- previewEl.innerHTML = '<div class="loading-msg">Loading ' + escHtml(name || directory) + '…</div>';
2394
- try {
2395
- const data = await apiGet('/admin/api/skills/file?' + scopeQuery() + '&source=' + encodeURIComponent(source) + '&directory=' + encodeURIComponent(directory));
2396
- renderPreviewFileResult(previewEl, source + '/' + directory + '/SKILL.md', data);
2397
- } catch (err) {
2398
- previewEl.innerHTML = '<div class="err-msg">' + escHtml(err.message) + '</div>';
2399
- }
2400
- }
2401
-
2402
- async function deleteSkill(source, directory, name) {
2403
- if (!confirm('Delete skill "' + (name || directory) + '" (' + source + ')?')) return;
2404
- try {
2405
- await apiPost('/admin/api/skills/mutate', { action: 'delete', source: source, directory: directory, ...scopeBody() });
2406
- await loadSkills();
2407
- } catch (err) {
2408
- alert(err.message);
2409
- }
2410
- }
2411
-
2412
- function bindSkillListEvents(containerId, previewId) {
2413
- const el = document.getElementById(containerId);
2414
- if (!el) return;
2415
- el.addEventListener('click', (event) => {
2416
- const previewBtn = event.target.closest('[data-skill-source]');
2417
- if (previewBtn) {
2418
- previewSkillInto(previewId, previewBtn.dataset.skillSource, previewBtn.dataset.skillDirectory, previewBtn.dataset.skillName);
2419
- return;
2420
- }
2421
- const editBtn = event.target.closest('[data-skill-edit-source]');
2422
- if (editBtn) {
2423
- const skill = skillsCache.find((s) => s.source === editBtn.dataset.skillEditSource && s.directory === editBtn.dataset.skillEditDirectory);
2424
- if (skill) void openSkillDialog(skill.source, skill);
2425
- return;
2426
- }
2427
- const deleteBtn = event.target.closest('[data-skill-delete-source]');
2428
- if (deleteBtn) {
2429
- void deleteSkill(deleteBtn.dataset.skillDeleteSource, deleteBtn.dataset.skillDeleteDirectory, deleteBtn.dataset.skillDeleteName);
2430
- }
2431
- });
2432
- }
2433
- bindSkillListEvents('skills-content', 'skills-preview');
2434
- bindSkillListEvents('global-skills-content', 'global-skills-preview');
2435
-
2436
- let editingSkill = null;
2437
- let pendingSkillScope = null;
2438
-
2439
- async function openSkillDialog(scope, existing) {
2440
- editingSkill = existing ? { source: existing.source, directory: existing.directory } : null;
2441
- document.getElementById('skill-dialog-eyebrow').textContent = scope === 'global' ? 'Global skill' : 'Conversation skill';
2442
- document.getElementById('skill-dialog-title').textContent = existing ? 'Edit skill' : 'New skill';
2443
- const dirInput = document.getElementById('skill-dialog-directory');
2444
- dirInput.value = existing ? existing.directory : '';
2445
- dirInput.disabled = Boolean(existing);
2446
- document.getElementById('skill-dialog-name').value = existing ? existing.name : '';
2447
- document.getElementById('skill-dialog-description').value = existing ? existing.description : '';
2448
- const contentEl = document.getElementById('skill-dialog-content');
2449
- contentEl.value = '';
2450
- const dialogError = document.getElementById('skill-dialog-error');
2451
- dialogError.style.display = 'none';
2452
- dialogError.textContent = '';
2453
- pendingSkillScope = scope;
2454
- if (existing) {
2455
- contentEl.value = 'Loading…';
2456
- try {
2457
- const data = await apiGet('/admin/api/skills/file?' + scopeQuery() + '&source=' + encodeURIComponent(existing.source) + '&directory=' + encodeURIComponent(existing.directory));
2458
- contentEl.value = data.binary ? '' : (data.content || '').replace(/^---[\\s\\S]*?---\\n?/, '').trim();
2459
- } catch (err) {
2460
- contentEl.value = '';
2461
- dialogError.textContent = 'Failed to load current content: ' + err.message;
2462
- dialogError.style.display = 'block';
2463
- }
2464
- }
2465
- document.getElementById('skill-dialog').showModal();
2466
- }
2467
-
2468
- function closeSkillDialog() {
2469
- editingSkill = null;
2470
- pendingSkillScope = null;
2471
- document.getElementById('skill-dialog').close();
2472
- }
2473
-
2474
- async function submitSkillDialog(btn) {
2475
- const dialogError = document.getElementById('skill-dialog-error');
2476
- dialogError.style.display = 'none';
2477
- dialogError.textContent = '';
2478
- const directory = document.getElementById('skill-dialog-directory').value.trim();
2479
- const name = document.getElementById('skill-dialog-name').value.trim() || directory;
2480
- const description = document.getElementById('skill-dialog-description').value.trim();
2481
- const content = document.getElementById('skill-dialog-content').value;
2482
- if (!directory) { dialogError.textContent = 'Directory is required'; dialogError.style.display = 'block'; return; }
2483
- if (!description) { dialogError.textContent = 'Description is required'; dialogError.style.display = 'block'; return; }
2484
- const source = editingSkill ? editingSkill.source : pendingSkillScope;
2485
- btn.disabled = true;
2486
- btn.textContent = 'Saving…';
2487
- try {
2488
- await apiPost('/admin/api/skills/mutate', {
2489
- action: 'save', source: source, directory: directory, name: name, description: description, content: content, ...scopeBody(),
2490
- });
2491
- closeSkillDialog();
2492
- await loadSkills();
2493
- } catch (err) {
2494
- dialogError.textContent = err.message;
2495
- dialogError.style.display = 'block';
2496
- } finally {
2497
- btn.disabled = false;
2498
- btn.textContent = 'Save';
2499
- }
2500
- }
2501
-
2502
- // ── Vault (Login link) ───────────────────────────────────────────────────────
2503
-
2504
- const openLogin = (silent) => openPortalLink('vault', 'login', silent);
2505
- const openSessionView = (silent) => openPortalLink('session', 'session', silent);
2506
-
2507
- // Generates a fresh one-time/short-lived link for the other portal and
2508
- // opens it in a new tab — the login and session-view tokens are separate
2509
- // capabilities from the admin token (see docs/portal-auth-model.md), so
2510
- // this always mints a new link rather than reusing anything cached here.
2511
- async function openPortalLink(resultId, kind, silent) {
2512
- const result = document.getElementById(resultId + '-link-result');
2513
- if (silent) { result.style.display = 'none'; return; }
2514
- result.style.display = 'block'; result.className = 'link-result loading'; result.textContent = 'Generating link…';
2515
- try {
2516
- const data = await apiPost('/admin/api/conversations/' + kind + '-link', scopeBody());
2517
- result.className = 'link-result ok';
2518
- result.innerHTML =
2519
- (kind === 'login' ? '<span class="link-vault">vault: <code>' + escHtml(data.vaultId) + '</code></span>' : '') +
2520
- '<a href="' + escAttr(data.url) + '" target="_blank" rel="noopener">' + escHtml(data.url) + '</a>' +
2521
- '<button class="copy-link-btn" data-admin-action="copy-link" data-copy-text="' + escAttr(data.url) + '">Copy</button>';
2522
- window.open(data.url, '_blank', 'noopener');
2523
- } catch (err) {
2524
- result.className = 'link-result err'; result.textContent = err.message;
2525
- }
2526
- }
2527
-
2528
- // ── Events ───────────────────────────────────────────────────────────────────
2529
-
2530
- const loadConversationEvents = () => loadEventList(true);
2531
- const loadEvents = () => loadEventList(false);
2532
-
2533
- async function loadEventList(conversation) {
2534
- const container = document.getElementById(conversation ? 'events-content' : 'global-events-content');
2535
- if (!container) return;
2536
- container.innerHTML = '<div class="loading-msg">Loading…</div>';
2537
- try {
2538
- const data = await apiGet(conversation ? '/admin/api/conversations/events?' + scopeQuery() : '/admin/api/events');
2539
- if (data.events.length === 0) {
2540
- container.innerHTML = '<div class="empty-state">' + (conversation ? '沒有關聯此對話的 event' : 'No events scheduled') + '</div>';
2541
- return;
2542
- }
2543
- container.innerHTML = '<div class="events-list">' +
2544
- data.events.map((e) => renderEventRow(e, conversation)).join('') + '</div>';
2545
- } catch (err) {
2546
- container.innerHTML = '<div class="err-msg">' + escHtml(err.message) + '</div>';
2547
- }
2548
- }
2549
-
2550
- function renderEventRow(e, allowDelete) {
2551
- const meta = [e.type, e.platform, e.conversationId, e.schedule || e.at]
2552
- .filter(Boolean).map(escHtml).join(' · ');
2553
- const preview = e.text ? '<div class="event-text">' + escHtml(e.text.length > 240 ? e.text.slice(0, 237) + '…' : e.text) + '</div>' : '';
2554
- const deleteBtn = allowDelete
2555
- ? '<button class="event-delete-btn" data-admin-action="delete-event" data-event-name="' + escAttr(e.name) + '">Delete</button>'
2556
- : '';
2557
- return '<div class="event-row">' +
2558
- '<div class="event-row-top">' +
2559
- '<div class="event-name"><code>' + escHtml(e.name) + '</code></div>' +
2560
- deleteBtn +
2561
- '</div>' +
2562
- '<div class="event-meta">' + meta + '</div>' +
2563
- preview +
2564
- '</div>';
2565
- }
2566
-
2567
- async function deleteEvent(name, btn) {
2568
- if (!confirm('Delete event "' + name + '"?')) return;
2569
- btn.disabled = true; btn.textContent = 'Deleting…';
2570
- try {
2571
- await apiPost('/admin/api/conversations/events/delete', {
2572
- ...scopeBody(), name,
2573
- });
2574
- await loadConversationEvents();
2575
- } catch (err) {
2576
- btn.disabled = false; btn.textContent = 'Delete';
2577
- alert(err.message);
2578
- }
2579
- }
2580
-
2581
- // ── Global section ──────────────────────────────────────────────────────────
2582
-
2583
- // Switching to the Workspace scope only needs to (re)select its first
2584
- // pane; every pane still loads lazily through paneLoader/ensurePaneLoaded
2585
- // the first time it becomes active, same as the conversation scope.
2586
- function initGlobal() {}
2587
-
2588
- async function loadAllConversations() {
2589
- const container = document.getElementById('all-conv-content');
2590
- container.innerHTML = '<div class="loading-msg">Loading…</div>';
2591
- try {
2592
- const data = await apiGet('/admin/api/conversations');
2593
- if (data.conversations.length === 0) {
2594
- container.innerHTML = '<div class="empty-state">No conversations found</div>';
2595
- return;
2596
- }
2597
- container.innerHTML = '<div class="conv-list">' + data.conversations.map((c) => {
2598
- const last = c.lastActivityAt ? new Date(c.lastActivityAt).toLocaleString() : '—';
2599
- return '<button class="conv-row-btn" data-admin-action="select-conversation" data-conversation-id="' + escAttr(c.conversationId) + '">' +
2600
- '<span class="conv-id">' + escHtml(c.label || c.conversationId) + '</span>' +
2601
- (c.running ? '<span class="status-pill running">running</span>' : '') +
2602
- '<span class="conv-last">' + escHtml(last) + '</span>' +
2603
- '</button>';
2604
- }).join('') + '</div>';
2605
- } catch (err) {
2606
- container.innerHTML = '<div class="err-msg">' + escHtml(err.message) + '</div>';
2607
- }
2608
- }
2609
-
2610
- async function loadSessionUsage() {
2611
- const container = document.getElementById('session-usage-content');
2612
- container.innerHTML = '<div class="loading-msg">Loading…</div>';
2613
- try {
2614
- const data = await apiGet('/admin/api/session-usage');
2615
- if (data.sessions.length === 0) {
2616
- container.innerHTML = '<div class="empty-state">No token usage found</div>';
2617
- return;
2618
- }
2619
- container.innerHTML = '<div class="usage-table-wrap"><table class="usage-table"><thead><tr><th>#</th><th>Channel</th><th>Session</th><th>Updated</th><th>Input</th><th>Output</th><th>Cache Read</th><th>Cache Write</th><th>Total</th><th>Cost</th></tr></thead><tbody>' +
2620
- data.sessions.map((s, i) => '<tr>' +
2621
- '<td>' + (i + 1) + '</td>' +
2622
- '<td>' + escHtml(s.label || s.conversationId) + '</td>' +
2623
- '<td><code>' + escHtml(s.fileName) + '</code></td>' +
2624
- '<td>' + escHtml(new Date(s.updatedAt).toLocaleString()) + '</td>' +
2625
- '<td>' + fmtNum(s.input) + '</td>' +
2626
- '<td>' + fmtNum(s.output) + '</td>' +
2627
- '<td>' + fmtNum(s.cacheRead) + '</td>' +
2628
- '<td>' + fmtNum(s.cacheWrite) + '</td>' +
2629
- '<td><strong>' + fmtNum(s.total) + '</strong></td>' +
2630
- '<td>' + (s.cost > 0 ? '$' + Number(s.cost).toFixed(4) : '—') + '</td>' +
2631
- '</tr>').join('') + '</tbody></table></div>';
2632
- } catch (err) {
2633
- container.innerHTML = '<div class="err-msg">' + escHtml(err.message) + '</div>';
2634
- }
2635
- }
2636
-
2637
- function fmtNum(value) {
2638
- return Number(value || 0).toLocaleString('en-US');
2639
- }
2640
-
2641
- let timelineConvLoaded = false;
2642
- let timelineData = null;
2643
- let timelineFilter = null;
2644
- async function ensureTimelineConvOptions() {
2645
- if (timelineConvLoaded) return;
2646
- const sel = document.getElementById('timeline-conv');
2647
- const prev = sel.value;
2648
- const data = await apiGet('/admin/api/conversations');
2649
- if (!data.conversations.length) {
2650
- sel.innerHTML = '<option value="">No conversations</option>';
2651
- timelineConvLoaded = true;
2652
- return;
2653
- }
2654
- const want = prev || defaultConversationKey;
2655
- sel.innerHTML = data.conversations.map((c) => {
2656
- const key = c.platform + ':' + c.conversationId;
2657
- return '<option value="' + escAttr(key) + '"' +
2658
- (key === want ? ' selected' : '') + '>' +
2659
- escHtml(c.label || c.conversationId) + '</option>';
2660
- }).join('');
2661
- timelineConvLoaded = true;
2662
- }
2663
-
2664
- // Shared refresh for the merged Token Usage section: reloads the session
2665
- // ranking and re-fetches the conversation list (so new sessions appear),
2666
- // preserving the currently selected conversation.
2667
- async function loadTokenUsage() {
2668
- loadSessionUsage();
2669
- timelineConvLoaded = false;
2670
- await loadUsageTimeline();
2671
- }
2672
-
2673
- async function loadUsageTimeline() {
2674
- const container = document.getElementById('usage-timeline-content');
2675
- try {
2676
- await ensureTimelineConvOptions();
2677
- const conv = document.getElementById('timeline-conv').value;
2678
- if (!conv) {
2679
- container.innerHTML = '<div class="empty-state">No conversations found</div>';
2680
- return;
2681
- }
2682
- container.innerHTML = '<div class="loading-msg">Loading…</div>';
2683
- const convScope = scopeOf(conv);
2684
- const data = await apiGet('/admin/api/conversation-usage?conversationId=' +
2685
- encodeURIComponent(convScope.conversationId) +
2686
- '&platform=' + encodeURIComponent(convScope.platform));
2687
- timelineData = data;
2688
- container.innerHTML = renderUsageTimeline(data);
2689
- } catch (err) {
2690
- container.innerHTML = '<div class="err-msg">' + escHtml(err.message) + '</div>';
2691
- }
2692
- }
2693
-
2694
- function tlCard(label, value) {
2695
- return '<div class="tl-card"><div class="tl-card-label">' + label +
2696
- '</div><div class="tl-card-value">' + value + '</div></div>';
2697
- }
2698
-
2699
- const TL_SERIES = [
2700
- { key: 'cacheRead', seg: 'tl-cache-read', sw: 'sw-cache-read', label: 'Cache read' },
2701
- { key: 'cacheWrite', seg: 'tl-cache-write', sw: 'sw-cache-write', label: 'Cache write' },
2702
- { key: 'input', seg: 'tl-input', sw: 'sw-input', label: 'Input' },
2703
- { key: 'output', seg: 'tl-output', sw: 'sw-output', label: 'Output' },
2704
- ];
2705
-
2706
- // Click a legend item to show only that series; click it again for all.
2707
- function toggleTimelineFilter(key) {
2708
- timelineFilter = timelineFilter === key ? null : key;
2709
- if (timelineData) {
2710
- document.getElementById('usage-timeline-content').innerHTML = renderUsageTimeline(timelineData);
2711
- }
2712
- }
2713
-
2714
- function renderUsageTimeline(data) {
2715
- const buckets = data.buckets || [];
2716
- const totals = data.totals || { total: 0, cost: 0, cacheRead: 0 };
2717
- const cacheHit = totals.total > 0 ? Math.round((totals.cacheRead / totals.total) * 100) : 0;
2718
- const cards = '<div class="tl-cards">' +
2719
- tlCard('Total cost', totals.cost > 0 ? '$' + Number(totals.cost).toFixed(4) : '—') +
2720
- tlCard('Total tokens', fmtNum(totals.total)) +
2721
- tlCard('Cache hit', cacheHit + '%') +
2722
- '</div>';
2723
-
2724
- if (totals.total === 0) {
2725
- const emptyNote = data.hasOlder
2726
- ? '<div class="tl-note">No usage in the last 14 days · earlier activity exists</div>'
2727
- : '<div class="empty-state">No token usage in the last 14 days</div>';
2728
- return cards + emptyNote;
2729
- }
2730
-
2731
- const active = TL_SERIES.find((s) => s.key === timelineFilter) || null;
2732
- const valueOf = (b) => active ? (b[active.key] || 0) : b.total;
2733
- const max = Math.max(1, ...buckets.map(valueOf));
2734
- const px = (v) => Math.round((v / max) * 180);
2735
-
2736
- const legend = '<div class="tl-legend">' + TL_SERIES.map((s) => {
2737
- const cls = 'tl-legend-item' +
2738
- (active && active.key === s.key ? ' active' : (active ? ' dim' : ''));
2739
- return '<span class="' + cls + '" data-admin-action="toggle-timeline-filter" data-filter-key="' + escAttr(s.key) + '">' +
2740
- '<i class="sw ' + s.sw + '"></i>' + s.label + '</span>';
2741
- }).join('') + '</div>';
2742
-
2743
- const bars = buckets.map((b) => {
2744
- const val = valueOf(b);
2745
- const tip = active
2746
- ? b.date + ' · ' + active.label + ': ' + fmtNum(b[active.key] || 0) + ' tokens'
2747
- : b.date + ' · ' + fmtNum(b.total) + ' tokens' +
2748
- (b.cost > 0 ? ' · $' + Number(b.cost).toFixed(4) : '');
2749
- let inner;
2750
- if (val <= 0) {
2751
- inner = '<span class="tl-empty"></span>';
2752
- } else if (active) {
2753
- inner = '<span class="tl-seg ' + active.seg + '" style="height:' + px(val) + 'px"></span>';
2754
- } else {
2755
- inner = '<span class="tl-seg tl-output" style="height:' + px(b.output) + 'px"></span>' +
2756
- '<span class="tl-seg tl-input" style="height:' + px(b.input) + 'px"></span>' +
2757
- '<span class="tl-seg tl-cache-write" style="height:' + px(b.cacheWrite) + 'px"></span>' +
2758
- '<span class="tl-seg tl-cache-read" style="height:' + px(b.cacheRead) + 'px"></span>';
2759
- }
2760
- return '<div class="tl-bar">' +
2761
- '<span class="tl-tip">' + escHtml(tip) + '</span>' +
2762
- '<div class="tl-fill">' + inner + '</div>' +
2763
- '</div>';
2764
- }).join('');
2765
-
2766
- const axis = buckets.length
2767
- ? '<div class="tl-axis"><span>' + escHtml(buckets[0].date.slice(5)) +
2768
- '</span><span>' + escHtml(buckets[buckets.length - 1].date.slice(5)) + '</span></div>'
2769
- : '';
2770
- const note = data.hasOlder
2771
- ? '<div class="tl-note">Showing last 14 days · earlier activity not shown</div>'
2772
- : '<div class="tl-note">Showing last 14 days</div>';
2773
- const peak = '<div class="tl-peak" style="bottom:180px"><span class="tl-peak-label">' +
2774
- fmtNum(max) + ' tokens</span></div>';
2775
- return cards + legend + '<div class="tl-chart">' + peak + bars + '</div>' + axis + note;
2776
- }
2777
-
2778
- function renderGlobalSettings(data) {
2779
- const thinkingOpts = renderThinkingOptions(data.thinkingLevel);
2780
- const replyModeOpts = renderReplyOptions(data.slack);
2781
- return [
2782
- '<div class="config-grid">',
2783
- renderConfigCard('Default model', [
2784
- '<div class="config-row config-row-stack"><label>Model</label><select id="g-model-ref">' + renderModelOptions(data.provider, data.model) + '</select></div>',
2785
- '<div class="config-row"><label>Thinking</label><select id="g-thinking">' + thinkingOpts + '</select></div>',
2786
- ].join(''), 'saveGlobalModel', 'Save model', 'g-model-result'),
2787
- renderConfigCard('Sandbox limits', [
2788
- '<div class="config-row"><label>CPUs</label><input id="g-cpus" placeholder="0.5" value="' + escAttr(data.sandboxCpus || '') + '"></div>',
2789
- '<div class="config-row"><label>Memory</label><input id="g-mem" placeholder="1g" value="' + escAttr(data.sandboxMemory || '') + '"></div>',
2790
- '<div class="config-row"><label>Boost CPUs</label><input id="g-bcpus" placeholder="2" value="' + escAttr(data.sandboxBoostCpus || '') + '"></div>',
2791
- '<div class="config-row"><label>Boost Mem</label><input id="g-bmem" placeholder="4g" value="' + escAttr(data.sandboxBoostMemory || '') + '"></div>',
2792
- ].join(''), 'saveGlobalSandbox', 'Save sandbox', 'g-sandbox-result'),
2793
- renderConfigCard('Slack', [
2794
- '<div class="config-row"><label>Reply mode</label><select id="g-slack-reply-mode">' + replyModeOpts + '</select></div>',
2795
- ].join(''), 'saveGlobalSlack', 'Save Slack', 'g-slack-result'),
2796
- '</div>',
2797
- ].join('');
2798
- }
2799
-
2800
- async function saveGlobalModel(btn) {
2801
- const selectedModel = parseModelRef(document.getElementById('g-model-ref').value.trim());
2802
- const provider = selectedModel.provider;
2803
- const model = selectedModel.model;
2804
- const thinkingLevel = document.getElementById('g-thinking').value;
2805
- const result = document.getElementById('g-model-result');
2806
- if (!provider || !model) {
2807
- result.style.display = 'block'; result.className = 'inline-result err';
2808
- result.textContent = 'Provider and model are required'; return;
2809
- }
2810
- btn.disabled = true; btn.textContent = 'Saving…'; result.style.display = 'none';
2811
- try {
2812
- await apiPost('/admin/api/settings/model', { provider, model, thinkingLevel });
2813
- result.style.display = 'block'; result.className = 'inline-result ok'; result.textContent = 'Saved ✓';
2814
- } catch (err) {
2815
- result.style.display = 'block'; result.className = 'inline-result err'; result.textContent = err.message;
2816
- } finally {
2817
- btn.disabled = false; btn.textContent = 'Save model';
2818
- }
2819
- }
2820
-
2821
- async function saveGlobalSandbox(btn) {
2822
- const cpus = document.getElementById('g-cpus').value.trim();
2823
- const memory = document.getElementById('g-mem').value.trim();
2824
- const boostCpus = document.getElementById('g-bcpus').value.trim();
2825
- const boostMemory = document.getElementById('g-bmem').value.trim();
2826
- const result = document.getElementById('g-sandbox-result');
2827
- await saveSetting(btn, result, 'settings/sandbox', { cpus, memory, boostCpus, boostMemory }, 'Save sandbox');
2828
- }
2829
-
2830
- async function saveGlobalSlack(btn) {
2831
- const replyMode = document.getElementById('g-slack-reply-mode').value;
2832
- const result = document.getElementById('g-slack-result');
2833
- await saveSetting(btn, result, 'settings/slack', { replyMode }, 'Save Slack');
2834
- }
2835
-
2836
- // ── Init ─────────────────────────────────────────────────────────────────────
2837
-
2838
- // Panes load on demand (see ensurePaneLoaded); loadSettingsPanel already
2839
- // calls loadModels() itself the first time it's needed.
2840
- initConvSwitcher();
2841
- initRailNav();
2842
- `;
2843
1481
  function renderAdminPage(token) {
2844
1482
  const userLabel = token.platformUserName ?? token.platformUserId;
2845
1483
  const script = `
2846
1484
  const adminToken = ${JSON.stringify(token.token)};
2847
- // Conversation ids never contain ":" (session-key grammar), so
2848
- // "platform:id" is a safe composite scope key for the UI.
2849
1485
  const defaultConversationKey = ${JSON.stringify(`${token.platform}:${token.conversationId}`)};
2850
- ${adminViewScript}`;
1486
+ ${adminViewFunctionsScript}
1487
+ ${adminViewStartupScript}`;
2851
1488
  return renderPortalShell({
2852
1489
  activeView: "admin",
2853
1490
  pageTitle: "Admin",
@@ -2869,544 +1506,4 @@ function renderAdminErrorPage(message) {
2869
1506
  </section>`,
2870
1507
  });
2871
1508
  }
2872
- // ── Styles ─────────────────────────────────────────────────────────────────────
2873
- const adminViewStyles = `
2874
- /* The shared shell caps at 960px for the narrower session/login portals;
2875
- the sidebar + single-pane layout here reads cramped at that width, so
2876
- admin alone widens it. */
2877
- .shell { max-width: 1180px; }
2878
-
2879
- /* ── Settings shell: persistent rail + single active pane ───────────── */
2880
-
2881
- .settings-shell {
2882
- display: grid;
2883
- grid-template-columns: 216px 1fr;
2884
- align-items: start;
2885
- gap: 22px;
2886
- }
2887
-
2888
- .settings-rail {
2889
- position: sticky;
2890
- top: 28px;
2891
- display: flex;
2892
- flex-direction: column;
2893
- gap: 16px;
2894
- }
2895
-
2896
- .rail-scope {
2897
- display: flex;
2898
- padding: 3px;
2899
- border: 1px solid var(--border);
2900
- border-radius: 11px;
2901
- background: rgba(0,0,0,0.03);
2902
- gap: 2px;
2903
- }
2904
- .rail-scope-btn {
2905
- flex: 1;
2906
- padding: 7px 8px;
2907
- border: none;
2908
- border-radius: 8px;
2909
- background: transparent;
2910
- color: var(--muted);
2911
- font: 600 0.72rem/1.2 'DM Sans', sans-serif;
2912
- letter-spacing: 0.01em;
2913
- cursor: pointer;
2914
- transition: background 140ms, color 140ms;
2915
- }
2916
- .rail-scope-btn:hover { color: var(--text); }
2917
- .rail-scope-btn.active { background: var(--surface); color: var(--text); box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
2918
- .rail-scope-btn:focus-visible { outline: 2px solid var(--text); outline-offset: 2px; }
2919
-
2920
- .rail-nav { display: flex; flex-direction: column; gap: 1px; }
2921
- .rail-nav[hidden] { display: none; }
2922
- .rail-link {
2923
- display: flex; align-items: center; gap: 10px;
2924
- padding: 8px 10px; border: none; border-radius: 9px;
2925
- background: transparent; color: var(--muted); text-align: left;
2926
- font: 500 0.86rem/1.2 'DM Sans', sans-serif; cursor: pointer;
2927
- transition: background 120ms, color 120ms;
2928
- }
2929
- .rail-link svg { flex-shrink: 0; opacity: 0.75; }
2930
- .rail-link:hover { background: rgba(0,0,0,0.045); color: var(--text); }
2931
- .rail-link:hover svg { opacity: 1; }
2932
- .rail-link.active {
2933
- background: var(--text); color: #fafafa; font-weight: 600;
2934
- }
2935
- .rail-link.active svg { opacity: 1; }
2936
- .rail-link:focus-visible { outline: 2px solid var(--text); outline-offset: 1px; }
2937
-
2938
- .settings-panels { min-width: 0; }
2939
- .tab-panel { display: none; flex-direction: column; gap: 0; }
2940
- .tab-panel.active { display: flex; }
2941
-
2942
- /* ── Panes: one continuous surface, not stacked cards ────────────────── */
2943
-
2944
- .pane { display: none; padding: 30px 0; border-bottom: 1px solid var(--border); }
2945
- .pane:last-child { border-bottom: none; }
2946
- .pane.active { display: block; animation: pane-in 180ms ease; }
2947
- @keyframes pane-in { from { opacity: 0; transform: translateY(3px); } to { opacity: 1; transform: none; } }
2948
-
2949
- .pane-head {
2950
- display: flex; align-items: flex-start; justify-content: space-between;
2951
- gap: 16px; margin-bottom: 22px; flex-wrap: wrap;
2952
- }
2953
- .pane-title {
2954
- font-family: 'Lora', Georgia, serif; font-size: 1.32rem; font-weight: 600;
2955
- letter-spacing: -0.01em; line-height: 1.25; margin-bottom: 5px;
2956
- }
2957
- .pane-desc { color: var(--muted); font-size: 0.88rem; line-height: 1.55; max-width: 52ch; }
2958
- .pane-actions { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
2959
- .pane-body { display: flex; flex-direction: column; gap: 14px; }
2960
-
2961
- .card-desc { color: var(--muted); font-size: 0.9rem; line-height: 1.55; margin-bottom: 12px; }
2962
-
2963
- .link-result {
2964
- margin-top: 12px; padding: 10px 14px; border-radius: 10px;
2965
- display: flex; gap: 10px; align-items: center; flex-wrap: wrap;
2966
- font-size: 0.84rem;
2967
- }
2968
- .link-result.ok { background: var(--ok-bg); border: 1px solid var(--ok-border); }
2969
- .link-result.err { background: var(--err-bg); border: 1px solid var(--err-border); color: var(--err-text); }
2970
- .link-result.loading { background: rgba(0,0,0,0.025); border: 1px solid var(--border); color: var(--muted); }
2971
- .link-result a {
2972
- color: var(--ok-text);
2973
- font-family: 'JetBrains Mono', ui-monospace, monospace;
2974
- font-size: 0.78rem; word-break: break-all; flex: 1; min-width: 0;
2975
- }
2976
- .link-vault { color: var(--muted); font-size: 0.78rem; flex-shrink: 0; }
2977
- .copy-link-btn {
2978
- padding: 5px 12px; border: 1px solid var(--ok-border); border-radius: 7px;
2979
- background: rgba(255,255,255,0.7); color: var(--ok-text);
2980
- font: 500 0.78rem/1.2 'DM Sans', sans-serif;
2981
- cursor: pointer; flex-shrink: 0;
2982
- }
2983
-
2984
- .config-grid {
2985
- display: grid; grid-template-columns: 1fr 1fr; gap: 18px;
2986
- }
2987
- .config-block { display: flex; flex-direction: column; gap: 10px; }
2988
- /* Config blocks are flex columns, so a plain child button stretches to the
2989
- full column width. Keep the save action a normal-sized button instead. */
2990
- .config-block .primary-action-btn { align-self: flex-start; min-width: 132px; justify-content: center; }
2991
- .config-row { display: grid; grid-template-columns: 110px 1fr; gap: 10px; align-items: center; }
2992
- .config-row.config-row-stack { grid-template-columns: 1fr; }
2993
- .config-row label { font-size: 0.82rem; color: var(--muted); }
2994
- .config-row input, .config-row select, .config-row textarea {
2995
- padding: 7px 10px; border: 1px solid var(--border); border-radius: 8px;
2996
- font-family: inherit; font-size: 0.84rem; width: 100%;
2997
- }
2998
- .config-row textarea {
2999
- font-family: 'JetBrains Mono', ui-monospace, monospace;
3000
- resize: vertical;
3001
- }
3002
- .toggle { display: inline-flex; align-items: center; gap: 8px; font-size: 0.84rem; }
3003
-
3004
- .inline-result {
3005
- padding: 8px 12px; border-radius: 8px; font-size: 0.82rem; margin-top: 4px;
3006
- }
3007
- .inline-result.ok { background: var(--ok-bg); color: var(--ok-text); border: 1px solid var(--ok-border); }
3008
- .inline-result.err { background: var(--err-bg); color: var(--err-text); border: 1px solid var(--err-border); }
3009
-
3010
- .refresh-btn {
3011
- flex-shrink: 0; padding: 6px 12px;
3012
- border: 1px solid var(--border); border-radius: 10px;
3013
- background: rgba(0,0,0,0.025); color: var(--muted);
3014
- font: 500 0.8rem/1.2 'DM Sans', sans-serif; cursor: pointer;
3015
- transition: background 120ms, color 120ms;
3016
- }
3017
- .refresh-btn:hover { background: rgba(0,0,0,0.06); color: var(--text); }
3018
-
3019
- /* ── Workspace ──────────────────────────────────────────────────────── */
3020
-
3021
- .workspace-split {
3022
- display: grid; grid-template-columns: 260px 1fr; gap: 14px;
3023
- min-height: 360px;
3024
- }
3025
- .workspace-tree {
3026
- border: 1px solid var(--border); border-radius: 12px; padding: 10px;
3027
- background: rgba(0,0,0,0.02); overflow: auto; max-height: 480px;
3028
- font-family: 'JetBrains Mono', ui-monospace, monospace;
3029
- font-size: 0.78rem;
3030
- }
3031
- .workspace-tree ul { list-style: none; padding-left: 12px; margin: 0; }
3032
- .workspace-tree .tree-root { padding-left: 0; }
3033
- .workspace-tree details { margin: 1px 0; }
3034
- .workspace-tree summary { cursor: pointer; padding: 2px 4px; border-radius: 4px; }
3035
- .workspace-tree summary:hover { background: rgba(0,0,0,0.05); }
3036
- .tree-dir { color: var(--text); font-weight: 600; }
3037
- .tree-dir.empty { color: var(--subtle); font-weight: 400; }
3038
- .tree-file {
3039
- display: block; width: 100%; text-align: left;
3040
- background: transparent; border: none; cursor: pointer;
3041
- padding: 2px 4px; border-radius: 4px;
3042
- font-family: inherit; font-size: inherit; color: var(--muted);
3043
- }
3044
- .tree-file:hover { background: rgba(0,0,0,0.05); color: var(--text); }
3045
-
3046
- .workspace-preview {
3047
- border: 1px solid var(--border); border-radius: 12px;
3048
- background: #fff; padding: 12px; overflow: auto; max-height: 480px;
3049
- }
3050
- .preview-meta {
3051
- font-size: 0.74rem; color: var(--subtle);
3052
- margin-bottom: 8px; padding-bottom: 8px; border-bottom: 1px solid var(--border);
3053
- font-family: 'JetBrains Mono', ui-monospace, monospace;
3054
- }
3055
- .preview-body {
3056
- margin: 0; white-space: pre-wrap; word-break: break-word;
3057
- font-family: 'JetBrains Mono', ui-monospace, monospace;
3058
- font-size: 0.78rem; color: var(--text);
3059
- }
3060
- .placeholder-msg { color: var(--subtle); font-size: 0.86rem; padding: 24px 8px; text-align: center; }
3061
-
3062
- /* ── Skills ─────────────────────────────────────────────────────────── */
3063
-
3064
- .skills-list { display: flex; flex-direction: column; gap: 8px; }
3065
- .skill-row {
3066
- padding: 10px 12px; border: 1px solid var(--border); border-radius: 10px;
3067
- background: rgba(0,0,0,0.02);
3068
- display: flex; flex-direction: column; align-items: stretch; gap: 6px;
3069
- }
3070
- .skill-row-btn {
3071
- flex: 1 1 auto; min-width: 0; text-align: left; cursor: pointer; font-family: inherit;
3072
- background: transparent; border: none; padding: 0;
3073
- }
3074
- .skill-row-btn:hover .skill-name { color: var(--accent); }
3075
- .skill-row-actions { display: flex; gap: 6px; flex-shrink: 0; justify-content: flex-end; }
3076
- .skill-name {
3077
- font-weight: 650; font-size: 0.9rem; color: var(--text);
3078
- display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
3079
- }
3080
- .skill-source {
3081
- padding: 1px 8px; border-radius: 999px; font-size: 0.7rem;
3082
- font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase;
3083
- }
3084
- .skill-source-global { background: rgba(59,130,246,0.1); color: #1d4ed8; }
3085
- .skill-source-conversation { background: rgba(217,119,6,0.1); color: var(--accent); }
3086
- .skill-desc { color: var(--muted); font-size: 0.82rem; margin-top: 4px; line-height: 1.5; }
3087
-
3088
- /* ── MCP marketplace ───────────────────────────────────────────────── */
3089
-
3090
- .form-input {
3091
- flex: 1 1 240px; min-width: 0; padding: 8px 10px;
3092
- border: 1px solid var(--border); border-radius: 8px; background: var(--card);
3093
- font-family: 'JetBrains Mono', ui-monospace, monospace; font-size: 0.8rem; color: var(--text);
3094
- }
3095
- .status-msg {
3096
- padding: 8px 12px; border-radius: 8px; margin-bottom: 12px;
3097
- font-size: 0.82rem; line-height: 1.5; word-break: break-word;
3098
- }
3099
- .status-msg-ok { background: rgba(22,163,74,0.1); color: #15803d; }
3100
- .status-msg-err { background: rgba(220,38,38,0.1); color: #b91c1c; }
3101
- .status-msg-busy { background: rgba(0,0,0,0.05); color: var(--muted); }
3102
- .mcp-btn {
3103
- padding: 4px 10px; border: 1px solid var(--border); border-radius: 7px;
3104
- background: var(--card); color: var(--text); font-size: 0.76rem; cursor: pointer;
3105
- }
3106
- .mcp-btn:hover { background: rgba(0,0,0,0.05); }
3107
- .mcp-btn-danger { color: #b91c1c; }
3108
- .mcp-badge {
3109
- padding: 1px 8px; border-radius: 999px; font-size: 0.7rem;
3110
- font-weight: 600; letter-spacing: 0.03em;
3111
- }
3112
- .mcp-badge-ok { background: rgba(22,163,74,0.1); color: #15803d; }
3113
- .mcp-badge-warn { background: rgba(217,119,6,0.12); color: var(--accent); }
3114
-
3115
- .mcp-market-head {
3116
- display: flex; align-items: end; justify-content: space-between; gap: 16px;
3117
- margin-bottom: 12px;
3118
- }
3119
- .mcp-market-head h3 { margin: 0; font-size: 1rem; }
3120
- .mcp-market-head p { margin: 3px 0 0; color: var(--muted); font-size: 0.8rem; }
3121
- .mcp-market-head > span {
3122
- color: var(--subtle); font: 500 0.72rem/1.2 'JetBrains Mono', ui-monospace, monospace;
3123
- }
3124
- .mcp-preset-grid {
3125
- display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px;
3126
- }
3127
- .mcp-preset {
3128
- min-width: 0; padding: 14px; border: 1px solid var(--border); border-radius: 12px;
3129
- background:
3130
- linear-gradient(145deg, rgba(255,255,255,0.85), rgba(247,245,238,0.72)),
3131
- var(--card);
3132
- display: flex; flex-direction: column; min-height: 190px;
3133
- }
3134
- .mcp-preset-top { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
3135
- .mcp-preset-category {
3136
- color: var(--accent); font-size: 0.66rem; font-weight: 700;
3137
- letter-spacing: 0.09em; text-transform: uppercase;
3138
- }
3139
- .mcp-preset h3 { margin: 14px 0 5px; font-size: 1.06rem; letter-spacing: -0.02em; }
3140
- .mcp-preset p { margin: 0; color: var(--muted); font-size: 0.8rem; line-height: 1.5; }
3141
- .mcp-preset-meta {
3142
- margin-top: 12px; color: var(--subtle); font-size: 0.72rem;
3143
- }
3144
- .mcp-preset-meta code { color: var(--text); background: transparent; }
3145
- .mcp-preset-actions {
3146
- margin-top: auto; padding-top: 16px; display: flex; align-items: center;
3147
- justify-content: space-between; gap: 10px;
3148
- }
3149
- .mcp-preset-actions a, .mcp-setup-link {
3150
- color: var(--muted); font-size: 0.76rem; text-decoration: none;
3151
- }
3152
- .mcp-preset-actions a:hover, .mcp-setup-link:hover { color: var(--text); text-decoration: underline; }
3153
- .mcp-preset-actions .primary-action-btn { padding: 6px 12px; }
3154
- .mcp-installed {
3155
- margin-top: 14px; border-top: 1px solid var(--border); padding-top: 12px;
3156
- }
3157
- .mcp-installed > summary {
3158
- cursor: pointer; color: var(--muted); font-size: 0.8rem; font-weight: 600;
3159
- margin-bottom: 10px;
3160
- }
3161
- .mcp-installed-card { min-height: 150px; }
3162
- .mcp-installed-transport {
3163
- font-family: 'JetBrains Mono', ui-monospace, monospace; word-break: break-all;
3164
- }
3165
- .mcp-add-card {
3166
- min-width: 0; min-height: 190px; border: 1.5px dashed var(--border); border-radius: 12px;
3167
- background: transparent; color: var(--muted); font-size: 0.86rem; font-weight: 600;
3168
- display: flex; flex-direction: column; align-items: center; justify-content: center;
3169
- gap: 8px; cursor: pointer;
3170
- }
3171
- .mcp-add-card:hover { background: rgba(0,0,0,0.03); color: var(--text); border-color: var(--text); }
3172
- .mcp-add-card-plus { font-size: 1.8rem; line-height: 1; font-weight: 400; }
3173
- .mcp-manual-hint { color: var(--muted); font-size: 0.8rem; margin: 0 0 10px; line-height: 1.5; }
3174
- .mcp-add-mode { display: flex; gap: 8px; margin: 0 0 12px; }
3175
- .mcp-mode-btn {
3176
- padding: 6px 12px; border: 1px solid var(--border); border-radius: 999px;
3177
- background: var(--card); font-size: 0.8rem; cursor: pointer; color: var(--muted);
3178
- }
3179
- .mcp-mode-btn.active { background: var(--text); color: #fafafa; border-color: var(--text); }
3180
- .mcp-guided-grid { display: grid; gap: 12px; }
3181
- .mcp-field { display: grid; gap: 5px; }
3182
- .mcp-field span { font-size: 0.78rem; font-weight: 650; color: var(--muted); }
3183
- .mcp-transport-toggle { display: flex; gap: 16px; font-size: 0.85rem; align-items: center; }
3184
- .mcp-transport-toggle label { display: flex; align-items: center; gap: 6px; cursor: pointer; }
3185
- .mcp-transport-fields { display: grid; gap: 12px; }
3186
- .mcp-kv-block { display: grid; gap: 8px; }
3187
- .mcp-kv-head { display: flex; align-items: center; justify-content: space-between; }
3188
- .mcp-kv-head span { font-size: 0.78rem; font-weight: 650; color: var(--muted); }
3189
- .mcp-kv-rows { display: grid; gap: 6px; }
3190
- .mcp-kv-row { display: flex; gap: 8px; }
3191
- .mcp-kv-row .form-input { flex: 1 1 auto; }
3192
- .mcp-json {
3193
- width: 100%; box-sizing: border-box; font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
3194
- font-size: 0.8rem; line-height: 1.45; resize: vertical;
3195
- }
3196
- .mcp-verify { margin-top: 10px; display: grid; gap: 6px; }
3197
- .mcp-verify .inline-result { word-break: break-word; }
3198
-
3199
- .mcp-dialog {
3200
- width: min(620px, calc(100vw - 28px)); max-height: calc(100vh - 40px);
3201
- border: 1px solid var(--border); border-radius: 18px; padding: 22px;
3202
- color: var(--text); background: #fbfaf6; box-shadow: 0 28px 90px rgba(18,18,16,0.24);
3203
- /* The shell's global "* { margin: 0 }" reset overrides the UA stylesheet's
3204
- dialog[open] { margin: auto } centering rule, so pin it back explicitly. */
3205
- position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); margin: 0;
3206
- }
3207
- .mcp-dialog::backdrop { background: rgba(20,20,18,0.5); backdrop-filter: blur(3px); }
3208
- .mcp-dialog-head {
3209
- display: flex; align-items: flex-start; justify-content: space-between; gap: 18px;
3210
- padding-bottom: 14px; border-bottom: 1px solid var(--border);
3211
- }
3212
- .mcp-dialog-head .card-title { margin: 4px 0 0; }
3213
- .mcp-dialog-close {
3214
- border: 0; background: transparent; color: var(--muted); cursor: pointer;
3215
- font-size: 1.6rem; line-height: 1; padding: 2px 5px;
3216
- }
3217
- .mcp-dialog-desc { color: var(--muted); line-height: 1.55; font-size: 0.88rem; }
3218
- .mcp-install-preview {
3219
- display: grid; gap: 6px; padding: 12px; border-radius: 10px;
3220
- background: #1d211f; color: #f5f2e9;
3221
- }
3222
- .mcp-install-preview span {
3223
- color: #aeb8b1; font-size: 0.66rem; font-weight: 700;
3224
- letter-spacing: 0.08em; text-transform: uppercase;
3225
- }
3226
- .mcp-install-preview code {
3227
- color: #f5f2e9; background: transparent; word-break: break-all; font-size: 0.78rem;
3228
- }
3229
- .mcp-security-note, .mcp-replace-note {
3230
- margin-top: 12px; padding: 10px 12px; border-radius: 9px;
3231
- font-size: 0.78rem; line-height: 1.5;
3232
- }
3233
- .mcp-security-note.local {
3234
- background: rgba(220,38,38,0.08); border: 1px solid rgba(185,28,28,0.18); color: #991b1b;
3235
- }
3236
- .mcp-security-note.remote {
3237
- background: rgba(59,130,246,0.08); border: 1px solid rgba(29,78,216,0.16); color: #1e40af;
3238
- }
3239
- .mcp-replace-note { background: rgba(217,119,6,0.1); color: #92400e; }
3240
- .mcp-credential { display: grid; gap: 5px; margin-top: 14px; }
3241
- .mcp-credential span { font-size: 0.78rem; font-weight: 650; }
3242
- .mcp-credential input {
3243
- width: 100%; padding: 9px 10px; border: 1px solid var(--border); border-radius: 8px;
3244
- background: #fff; color: var(--text); font: 0.8rem/1.3 'JetBrains Mono', ui-monospace, monospace;
3245
- }
3246
- .mcp-credential small, .mcp-secret-note, .mcp-no-credentials {
3247
- color: var(--subtle); font-size: 0.72rem; line-height: 1.45;
3248
- }
3249
- .mcp-secret-note { margin: 14px 0 8px; }
3250
- .mcp-no-credentials { margin: 14px 0 0; }
3251
- #mcp-dialog-error, #mcp-custom-dialog-error { margin-top: 14px; }
3252
- .mcp-dialog-actions {
3253
- display: flex; justify-content: flex-end; gap: 8px; margin-top: 20px;
3254
- padding-top: 14px; border-top: 1px solid var(--border);
3255
- }
3256
-
3257
- /* ── Events ─────────────────────────────────────────────────────────── */
3258
-
3259
- .events-list { display: flex; flex-direction: column; gap: 8px; }
3260
- .event-row {
3261
- padding: 10px 12px; border: 1px solid var(--border); border-radius: 10px;
3262
- background: rgba(0,0,0,0.02);
3263
- }
3264
- .event-row-top {
3265
- display: flex; align-items: center; justify-content: space-between;
3266
- gap: 10px;
3267
- }
3268
- .event-name { min-width: 0; flex: 1; word-break: break-all; }
3269
- .event-name code { font-size: 0.82rem; background: transparent; padding: 0; }
3270
- .event-meta { font-size: 0.74rem; color: var(--muted); margin-top: 3px; }
3271
- .event-text {
3272
- font-size: 0.82rem; color: var(--text); margin-top: 6px;
3273
- font-family: 'JetBrains Mono', ui-monospace, monospace;
3274
- white-space: pre-wrap; word-break: break-word;
3275
- }
3276
- .event-delete-btn {
3277
- flex-shrink: 0; padding: 4px 10px;
3278
- border-radius: 7px; border: 1px solid rgba(185, 28, 28, 0.18);
3279
- background: rgba(0,0,0,0.03); color: var(--err-text);
3280
- font: 500 0.76rem/1.2 'DM Sans', sans-serif; cursor: pointer;
3281
- }
3282
- .event-delete-btn:hover:not(:disabled) {
3283
- background: var(--err-bg); border-color: rgba(185, 28, 28, 0.28);
3284
- }
3285
- .event-delete-btn:disabled { opacity: 0.5; cursor: wait; }
3286
-
3287
- /* ── All Conversations list ─────────────────────────────────────────── */
3288
-
3289
- .conv-list { display: flex; flex-direction: column; gap: 6px; }
3290
- .conv-row-btn {
3291
- display: flex; align-items: center; gap: 12px;
3292
- padding: 10px 14px; border: 1px solid var(--border); border-radius: 10px;
3293
- background: rgba(0,0,0,0.02); cursor: pointer; text-align: left;
3294
- transition: background 120ms, border-color 120ms;
3295
- }
3296
- .conv-row-btn:hover { background: rgba(0,0,0,0.05); border-color: rgba(0,0,0,0.14); }
3297
- .conv-id { flex: 1; font-family: 'JetBrains Mono', ui-monospace, monospace; font-size: 0.84rem; }
3298
- .conv-last { color: var(--subtle); font-size: 0.78rem; }
3299
-
3300
- .usage-table-wrap { overflow-x: auto; }
3301
- .usage-table { width: 100%; border-collapse: collapse; font-size: 0.78rem; }
3302
- .usage-table th, .usage-table td {
3303
- padding: 8px 10px; border-bottom: 1px solid var(--border);
3304
- text-align: left; white-space: nowrap;
3305
- }
3306
- .usage-table th {
3307
- color: var(--subtle); font-size: 0.68rem;
3308
- text-transform: uppercase; letter-spacing: 0.08em;
3309
- }
3310
- .usage-table code { font-size: 0.72rem; }
3311
-
3312
- .timeline-controls {
3313
- display: flex; flex-wrap: wrap; gap: 18px; align-items: center; margin-bottom: 16px;
3314
- }
3315
- .timeline-controls label {
3316
- display: flex; align-items: center; gap: 8px;
3317
- font-size: 0.76rem; color: var(--muted);
3318
- }
3319
- .timeline-controls select {
3320
- padding: 6px 10px; border: 1px solid var(--border); border-radius: 8px;
3321
- background: #fff; color: var(--text); font-size: 0.8rem; max-width: 240px;
3322
- }
3323
- .tl-note { margin-top: 8px; font-size: 0.72rem; color: var(--subtle); }
3324
- .tl-cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin-bottom: 16px; }
3325
- .tl-card { background: rgba(0,0,0,0.025); border-radius: 10px; padding: 10px 12px; }
3326
- .tl-card-label { font-size: 0.73rem; color: var(--muted); margin-bottom: 4px; }
3327
- .tl-card-value { font-size: 1.35rem; font-weight: 600; color: var(--text); }
3328
- .tl-legend { display: flex; gap: 16px; font-size: 0.73rem; color: var(--muted); margin-bottom: 10px; }
3329
- .tl-legend span { display: inline-flex; align-items: center; gap: 6px; }
3330
- .tl-legend-item { cursor: pointer; transition: opacity 100ms; }
3331
- .tl-legend-item:hover { text-decoration: underline; }
3332
- .tl-legend-item.active { color: var(--text); font-weight: 600; text-decoration: underline; }
3333
- .tl-legend-item.dim { opacity: 0.4; }
3334
- .sw { width: 10px; height: 10px; border-radius: 2px; display: inline-block; }
3335
- .sw-cache-read { background: rgba(0,0,0,0.18); }
3336
- .sw-cache-write { background: #3b6fb0; }
3337
- .sw-input { background: var(--accent); }
3338
- .sw-output { background: var(--ok-text); }
3339
- .tl-chart {
3340
- position: relative;
3341
- display: flex; align-items: flex-end; gap: 6px;
3342
- height: 216px; border-bottom: 1px solid var(--border);
3343
- }
3344
- .tl-peak {
3345
- position: absolute; left: 0; right: 0; height: 0;
3346
- border-top: 1px dashed var(--accent); pointer-events: none;
3347
- }
3348
- .tl-peak-label {
3349
- position: absolute; left: 0; top: -15px;
3350
- font-size: 0.7rem; color: var(--accent); white-space: nowrap;
3351
- }
3352
- .tl-bar {
3353
- position: relative; flex: 1; min-width: 0; height: 180px;
3354
- display: flex; flex-direction: column; justify-content: flex-end;
3355
- border-radius: 6px 6px 0 0; transition: background 80ms ease;
3356
- }
3357
- .tl-bar:hover { background: rgba(0,0,0,0.06); }
3358
- .tl-bar:hover .tl-seg { opacity: 0.55; }
3359
- .tl-fill {
3360
- display: flex; flex-direction: column; justify-content: flex-end;
3361
- border-radius: 3px 3px 0 0; overflow: hidden;
3362
- }
3363
- .tl-tip {
3364
- position: absolute; bottom: 100%; left: 50%; transform: translateX(-50%);
3365
- margin-bottom: 6px; padding: 5px 9px; border-radius: 6px;
3366
- background: var(--text); color: #fafafa; font-size: 0.7rem; line-height: 1.35;
3367
- white-space: nowrap; opacity: 0; pointer-events: none; z-index: 5;
3368
- }
3369
- .tl-bar:hover .tl-tip { opacity: 1; }
3370
- .tl-seg { display: block; width: 100%; }
3371
- .tl-seg.tl-output { background: var(--ok-text); }
3372
- .tl-seg.tl-input { background: var(--accent); }
3373
- .tl-seg.tl-cache-write { background: #3b6fb0; }
3374
- .tl-seg.tl-cache-read { background: rgba(0,0,0,0.18); }
3375
- .tl-empty { display: block; width: 100%; height: 3px; background: var(--border); }
3376
- .tl-axis { display: flex; justify-content: space-between; margin-top: 6px; font-size: 0.7rem; color: var(--subtle); }
3377
-
3378
- .status-pill {
3379
- display: inline-flex; padding: 2px 9px; border-radius: 999px;
3380
- font-size: 0.7rem; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase;
3381
- }
3382
- .status-pill.running { background: var(--ok-bg); color: var(--ok-text); border: 1px solid var(--ok-border); }
3383
-
3384
- @media (max-width: 860px) {
3385
- .settings-shell { grid-template-columns: 1fr; }
3386
- .settings-rail { position: static; }
3387
- /* Keep labels on narrow screens: a scrollable chip row is far easier to
3388
- scan than a row of bare icons with no text. */
3389
- .rail-nav {
3390
- flex-direction: row; flex-wrap: nowrap; gap: 6px;
3391
- overflow-x: auto; padding-bottom: 6px;
3392
- /* Without min-width:0 a nowrap flex row refuses to shrink and blows the
3393
- page width out instead of scrolling. */
3394
- min-width: 0; max-width: 100%;
3395
- scrollbar-width: thin; -webkit-overflow-scrolling: touch;
3396
- }
3397
- .settings-rail { min-width: 0; }
3398
- .rail-nav[hidden] { display: none; }
3399
- .rail-link { flex: 0 0 auto; padding: 7px 11px; }
3400
- .rail-link span { white-space: nowrap; }
3401
- }
3402
-
3403
- @media (max-width: 640px) {
3404
- .config-grid { grid-template-columns: 1fr; }
3405
- .config-row { grid-template-columns: 1fr; gap: 4px; }
3406
- .workspace-split { grid-template-columns: 1fr; }
3407
- .workspace-tree, .workspace-preview { max-height: 260px; }
3408
- .mcp-preset-grid { grid-template-columns: 1fr; }
3409
- .mcp-dialog { padding: 18px; }
3410
- }
3411
- `;
3412
1509
  //# sourceMappingURL=portal.js.map